using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security; using System.Security.Cryptography; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using RunicPermissions.Contracts; using RunicPermissions.Groups; using RunicPortals.Api; using RunicPortals.Core; using RunicPortals.Integration; using Splatform; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: AssemblyTitle("Runic Portals")] [assembly: AssemblyDescription("Permission-aware deterministic portal directories and fail-closed universal routing.")] [assembly: AssemblyCompany("Chazman")] [assembly: AssemblyProduct("Runic Portals")] [assembly: AssemblyCopyright("Copyright © 2026 Chazman")] [assembly: ComVisible(false)] [assembly: Guid("10c99bf6-a049-48d3-bcb3-4582c10180db")] [assembly: AssemblyFileVersion("1.1.3.0")] [assembly: AssemblyInformationalVersion("1.1.3")] [assembly: InternalsVisibleTo("RunicPortals.Tests")] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyVersion("1.1.3.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace RunicPermissions.Contracts { public sealed class StableIdentity : IEquatable, IComparable { public const int MaximumAuthorityLength = 64; public const int MaximumSubjectIdLength = 256; public string Authority { get; } public string SubjectId { get; } public string CanonicalKey { get; } public StableIdentity(string authority, string subjectId) { string value = NormalizeAuthority(authority); string value2 = NormalizeSubject(subjectId); if (!IsValidAuthority(value)) { throw new ArgumentException("Identity authority must contain only ASCII letters, digits, '.', '_', or '-'.", "authority"); } if (!IsValidSubject(value2)) { throw new ArgumentException("Identity subject ID is empty, too long, or contains control characters.", "subjectId"); } Authority = value; SubjectId = value2; CanonicalKey = Authority + ":" + Uri.EscapeDataString(SubjectId); } public static bool TryCreate(string authority, string subjectId, out StableIdentity identity) { try { identity = new StableIdentity(authority, subjectId); return true; } catch (ArgumentException) { identity = null; return false; } } public bool Equals(StableIdentity other) { if (other != null && string.Equals(Authority, other.Authority, StringComparison.Ordinal)) { return string.Equals(SubjectId, other.SubjectId, StringComparison.Ordinal); } return false; } public override bool Equals(object obj) { return Equals(obj as StableIdentity); } public override int GetHashCode() { return (StringComparer.Ordinal.GetHashCode(Authority) * 397) ^ StringComparer.Ordinal.GetHashCode(SubjectId); } public int CompareTo(StableIdentity other) { if (other == null) { return 1; } int num = string.Compare(Authority, other.Authority, StringComparison.Ordinal); if (num == 0) { return string.Compare(SubjectId, other.SubjectId, StringComparison.Ordinal); } return num; } public override string ToString() { return CanonicalKey; } private static string NormalizeAuthority(string value) { return (value ?? string.Empty).Trim().ToLowerInvariant(); } private static string NormalizeSubject(string value) { return (value ?? string.Empty).Trim(); } private static bool IsValidAuthority(string value) { if (value.Length == 0 || value.Length > 64) { return false; } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { return false; } } return true; } private static bool IsValidSubject(string value) { if (value.Length == 0 || value.Length > 256) { return false; } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { return false; } } return true; } } public enum IdentityResolutionStatus { Verified, Missing, Ambiguous, Stale } public sealed class IdentityClaim { public StableIdentity Identity { get; } public string DisplayNameSnapshot { get; } public IdentityResolutionStatus Status { get; } public bool IsVerified { get { if (Status == IdentityResolutionStatus.Verified) { return Identity != null; } return false; } } public IdentityClaim(StableIdentity identity, string displayNameSnapshot, IdentityResolutionStatus status) { Identity = identity; DisplayNameSnapshot = displayNameSnapshot ?? string.Empty; Status = status; } public static IdentityClaim Verified(StableIdentity identity, string displayNameSnapshot = "") { return new IdentityClaim(identity, displayNameSnapshot, IdentityResolutionStatus.Verified); } } } namespace RunicPermissions.Groups { public enum GroupWorldReadState { Missing, Ready, Corrupt, EvidenceConflict, Unavailable } public enum GroupWorldCommitState { Committed, RevisionConflict, Corrupt, EvidenceConflict, Unavailable, InvalidReplacement } public sealed class GroupWorldReadResult { public GroupWorldReadState State { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public string ExactSha256 { get; } internal GroupWorldReadResult(GroupWorldReadState state, string reasonCode, GroupCatalog catalog, string exactSha256) { State = state; ReasonCode = reasonCode ?? string.Empty; Catalog = catalog; ExactSha256 = exactSha256 ?? string.Empty; } } public sealed class GroupWorldCommitResult { public GroupWorldCommitState State { get; } public string ReasonCode { get; } public GroupWorldReadResult Current { get; } public bool Success => State == GroupWorldCommitState.Committed; internal GroupWorldCommitResult(GroupWorldCommitState state, string reasonCode, GroupWorldReadResult current) { State = state; ReasonCode = reasonCode ?? string.Empty; Current = current; } } public interface IGroupWorldStore { GroupWorldReadResult Read(string worldScope); GroupWorldCommitResult TryCommit(string worldScope, long expectedCatalogRevision, GroupCatalog replacement); } internal sealed class CompatibleGroupWorldStore : IGroupWorldStore { private readonly struct Paths { internal string Primary { get; } internal string Temporary { get; } internal string Backup { get; } internal string Lock { get; } internal Paths(string primary, string temporary, string backup, string @lock) { Primary = primary; Temporary = temporary; Backup = backup; Lock = @lock; } } private readonly string _root; internal CompatibleGroupWorldStore(string root) { if (string.IsNullOrWhiteSpace(root)) { throw new ArgumentException("A storage root is required.", "root"); } _root = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (string.Equals(_root, Path.GetPathRoot(_root), PathComparison())) { throw new ArgumentException("A filesystem root cannot be used.", "root"); } } public GroupWorldReadResult Read(string worldScope) { try { Paths paths = Resolve(worldScope); if (!Directory.Exists(_root)) { return Missing(); } using (Acquire(paths.Lock)) { return ReadLocked(paths, worldScope); } } catch (ArgumentException) { return Unavailable("group-world-scope-invalid"); } catch (Exception exception) when (StorageFailure(exception)) { return Unavailable("group-store-read-unavailable"); } } public GroupWorldCommitResult TryCommit(string worldScope, long expectedCatalogRevision, GroupCatalog replacement) { if (expectedCatalogRevision < 0 || replacement == null || replacement.Revision != expectedCatalogRevision + 1) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-replacement-invalid", null); } try { Paths paths = Resolve(worldScope); Directory.CreateDirectory(_root); using (Acquire(paths.Lock)) { GroupWorldReadResult groupWorldReadResult = ReadLocked(paths, worldScope); if (groupWorldReadResult.State == GroupWorldReadState.Corrupt) { return Commit(GroupWorldCommitState.Corrupt, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { return Commit(GroupWorldCommitState.EvidenceConflict, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (groupWorldReadResult.State == GroupWorldReadState.Unavailable) { return Commit(GroupWorldCommitState.Unavailable, groupWorldReadResult.ReasonCode, groupWorldReadResult); } if (((groupWorldReadResult.State == GroupWorldReadState.Missing) ? 0 : groupWorldReadResult.Catalog.Revision) != expectedCatalogRevision) { return Commit(GroupWorldCommitState.RevisionConflict, "group-store-revision-conflict", groupWorldReadResult); } if (!SameLedger((groupWorldReadResult.State == GroupWorldReadState.Missing) ? GroupCommandLedger.Empty : groupWorldReadResult.Catalog.CommandLedger, replacement.CommandLedger)) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-command-ledger-mismatch", groupWorldReadResult); } return Publish(paths, worldScope, replacement, groupWorldReadResult); } } catch (ArgumentException) { return Commit(GroupWorldCommitState.InvalidReplacement, "group-store-replacement-invalid", null); } catch (Exception exception) when (StorageFailure(exception)) { return Commit(GroupWorldCommitState.Unavailable, "group-store-commit-unavailable", null); } } private GroupWorldCommitResult Publish(Paths paths, string worldScope, GroupCatalog replacement, GroupWorldReadResult current) { if (File.Exists(paths.Temporary) || File.Exists(paths.Backup)) { return Commit(GroupWorldCommitState.EvidenceConflict, "group-store-nonprimary-evidence", current); } byte[] array = GroupCatalogCodec.Encode(worldScope, replacement); using (FileStream fileStream = new FileStream(paths.Temporary, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } byte[] array2 = ReadBounded(paths.Temporary); if (!Exact(array, array2) || !GroupCatalogCodec.TryDecode(array2, worldScope, out var catalog, out var _) || catalog.Revision != replacement.Revision) { return Commit(GroupWorldCommitState.Unavailable, "group-store-staged-readback-failed", current); } if (File.Exists(paths.Primary)) { File.Replace(paths.Temporary, paths.Primary, null, ignoreMetadataErrors: true); } else { File.Move(paths.Temporary, paths.Primary); } GroupWorldReadResult groupWorldReadResult = ReadPrimary(paths.Primary, worldScope); if (groupWorldReadResult.State != GroupWorldReadState.Ready || groupWorldReadResult.Catalog.Revision != replacement.Revision) { return Commit(GroupWorldCommitState.Unavailable, "group-store-commit-readback-failed", groupWorldReadResult); } return Commit(GroupWorldCommitState.Committed, "group-store-committed", groupWorldReadResult); } private GroupWorldReadResult ReadLocked(Paths paths, string worldScope) { if (File.Exists(paths.Temporary) || File.Exists(paths.Backup)) { return new GroupWorldReadResult(GroupWorldReadState.EvidenceConflict, "group-store-nonprimary-evidence", null, string.Empty); } if (!File.Exists(paths.Primary)) { return Missing(); } return ReadPrimary(paths.Primary, worldScope); } private static GroupWorldReadResult ReadPrimary(string path, string worldScope) { byte[] bytes = ReadBounded(path); if (!GroupCatalogCodec.TryDecode(bytes, worldScope, out var catalog, out var reason)) { return new GroupWorldReadResult(GroupWorldReadState.Corrupt, reason, null, GroupCatalogCodec.ComputeSha256(bytes)); } return new GroupWorldReadResult(GroupWorldReadState.Ready, "group-store-ready", catalog, GroupCatalogCodec.ComputeSha256(bytes)); } private Paths Resolve(string worldScope) { string s = GroupIdentity.RequireWorldScope(worldScope); byte[] array; using (SHA256 sHA = SHA256.Create()) { array = sHA.ComputeHash(Encoding.UTF8.GetBytes(s)); } StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } string text = Path.Combine(_root, stringBuilder?.ToString() + ".groups"); if (!string.Equals(Path.GetDirectoryName(text), _root, PathComparison())) { throw new ArgumentException("The group path escaped its root.", "worldScope"); } return new Paths(text, text + ".tmp", text + ".bak", text + ".lock"); } private static FileStream Acquire(string path) { return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1, FileOptions.WriteThrough); } private static byte[] ReadBounded(string path) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.SequentialScan); if (fileStream.Length < 1 || fileStream.Length > 8388608) { throw new InvalidDataException("The group catalog length is invalid."); } byte[] array = new byte[(int)fileStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { throw new EndOfStreamException(); } } return array; } private static bool SameLedger(GroupCommandLedger left, GroupCommandLedger right) { if (left != null && right != null && left.Epoch == right.Epoch && left.NextSequence == right.NextSequence && left.MinimumAcceptedSequence == right.MinimumAcceptedSequence && left.Issues.Count == right.Issues.Count) { return left.Receipts.Count == right.Receipts.Count; } return false; } private static bool Exact(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } private static bool StorageFailure(Exception exception) { if (!(exception is IOException) && !(exception is UnauthorizedAccessException) && !(exception is NotSupportedException)) { return exception is SecurityException; } return true; } private static StringComparison PathComparison() { if (Path.DirectorySeparatorChar != '\\') { return StringComparison.Ordinal; } return StringComparison.OrdinalIgnoreCase; } private static GroupWorldReadResult Missing() { return new GroupWorldReadResult(GroupWorldReadState.Missing, "group-store-missing", GroupCatalog.Empty, string.Empty); } private static GroupWorldReadResult Unavailable(string reason) { return new GroupWorldReadResult(GroupWorldReadState.Unavailable, reason, null, string.Empty); } private static GroupWorldCommitResult Commit(GroupWorldCommitState state, string reason, GroupWorldReadResult current) { return new GroupWorldCommitResult(state, reason, current); } } public enum GroupRole : byte { Member = 1, Officer, Owner } public static class GroupLimits { public const int MaximumGroups = 256; public const int MaximumMembersPerGroup = 256; public const int MaximumInvitationsPerGroup = 256; public const int MaximumGroupsPerIdentity = 64; public const int MaximumRetiredGroupIds = 4096; public const int MaximumDisplayNameUtf8Bytes = 64; public const int MaximumWorldScopeUtf8Bytes = 128; public const int MaximumCatalogBytes = 8388608; public static readonly TimeSpan MaximumInvitationLifetime = TimeSpan.FromDays(30.0); } public static class GroupIdentity { public static string ToCanonicalId(Guid groupId) { if (groupId == Guid.Empty) { throw new ArgumentException("A nonempty group UUID is required.", "groupId"); } return groupId.ToString("N"); } public static bool TryParseCanonicalId(string value, out Guid groupId) { groupId = Guid.Empty; if (value != null && value.Length == 32 && Guid.TryParseExact(value, "N", out groupId) && groupId != Guid.Empty) { return string.Equals(value, groupId.ToString("N"), StringComparison.Ordinal); } return false; } public static bool IsCanonicalId(string value) { Guid groupId; return TryParseCanonicalId(value, out groupId); } internal static string RequireDisplayName(string value) { if (value == null || value.Length == 0 || !string.Equals(value, value.Trim(), StringComparison.Ordinal) || !value.IsNormalized(NormalizationForm.FormC) || Encoding.UTF8.GetByteCount(value) > 64) { throw new ArgumentException("A nonempty, trimmed, NFC group display name within 64 UTF-8 bytes is required.", "value"); } for (int i = 0; i < value.Length; i++) { if (char.IsControl(value[i])) { throw new ArgumentException("Group display names cannot contain control characters.", "value"); } } return value; } internal static string RequireWorldScope(string value) { if (value == null || value.Length == 0 || value.Length > 128 || Encoding.UTF8.GetByteCount(value) > 128) { throw new ArgumentException("A bounded canonical world scope is required.", "value"); } foreach (char c in value) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '_' && c != '-') { throw new ArgumentException("The world scope is not canonical.", "value"); } } return value; } } public sealed class GroupMember { public StableIdentity Identity { get; } public GroupRole Role { get; } public long JoinedRevision { get; } public GroupMember(StableIdentity identity, GroupRole role, long joinedRevision) { Identity = identity ?? throw new ArgumentNullException("identity"); if (!Enum.IsDefined(typeof(GroupRole), role)) { throw new ArgumentOutOfRangeException("role"); } if (joinedRevision < 1) { throw new ArgumentOutOfRangeException("joinedRevision"); } Role = role; JoinedRevision = joinedRevision; } } public sealed class GroupInvitation { public StableIdentity Invitee { get; } public StableIdentity InvitedBy { get; } public long IssuedRevision { get; } public long ExpiresUtcTicks { get; } public GroupInvitation(StableIdentity invitee, StableIdentity invitedBy, long issuedRevision, long expiresUtcTicks) { Invitee = invitee ?? throw new ArgumentNullException("invitee"); InvitedBy = invitedBy ?? throw new ArgumentNullException("invitedBy"); if (issuedRevision < 1) { throw new ArgumentOutOfRangeException("issuedRevision"); } if (expiresUtcTicks <= 0) { throw new ArgumentOutOfRangeException("expiresUtcTicks"); } IssuedRevision = issuedRevision; ExpiresUtcTicks = expiresUtcTicks; } public bool IsExpired(long nowUtcTicks) { if (nowUtcTicks > 0) { return nowUtcTicks >= ExpiresUtcTicks; } return true; } } public sealed class GroupRecord { private readonly GroupMember[] _members; private readonly GroupInvitation[] _invitations; private readonly ReadOnlyCollection _memberView; private readonly ReadOnlyCollection _invitationView; public Guid Id { get; } public string IdText { get; } public string DisplayName { get; } public long Revision { get; } public IReadOnlyList Members => _memberView; public IReadOnlyList Invitations => _invitationView; public GroupRecord(Guid id, string displayName, long revision, IEnumerable members, IEnumerable invitations = null) { Id = id; IdText = GroupIdentity.ToCanonicalId(id); DisplayName = GroupIdentity.RequireDisplayName(displayName); if (revision < 1) { throw new ArgumentOutOfRangeException("revision"); } Revision = revision; _members = CopyMembers(members, revision); _invitations = CopyInvitations(invitations, revision, _members); _memberView = Array.AsReadOnly(_members); _invitationView = Array.AsReadOnly(_invitations); } public bool TryGetMember(StableIdentity identity, out GroupMember member) { member = null; if (identity == null) { return false; } int num = FindMember(_members, identity); if (num < 0) { return false; } member = _members[num]; return true; } public bool TryGetInvitation(StableIdentity identity, out GroupInvitation invitation) { invitation = null; if (identity == null) { return false; } int num = FindInvitation(_invitations, identity); if (num < 0) { return false; } invitation = _invitations[num]; return true; } internal GroupRecord Rename(string displayName) { return new GroupRecord(Id, displayName, checked(Revision + 1), _members, _invitations); } internal GroupRecord Invite(StableIdentity actor, StableIdentity invitee, long expiresUtcTicks) { checked { List invitations = new List(_invitations) { new GroupInvitation(invitee, actor, Revision + 1, expiresUtcTicks) }; return new GroupRecord(Id, DisplayName, Revision + 1, _members, invitations); } } internal GroupRecord CancelInvitation(StableIdentity invitee) { GroupInvitation[] invitations = _invitations.Where((GroupInvitation value) => !value.Invitee.Equals(invitee)).ToArray(); return new GroupRecord(Id, DisplayName, checked(Revision + 1), _members, invitations); } internal GroupRecord Accept(StableIdentity invitee) { long num = checked(Revision + 1); List members = new List(_members) { new GroupMember(invitee, GroupRole.Member, num) }; GroupInvitation[] invitations = _invitations.Where((GroupInvitation value) => !value.Invitee.Equals(invitee)).ToArray(); return new GroupRecord(Id, DisplayName, num, members, invitations); } internal GroupRecord RemoveMember(StableIdentity identity) { GroupMember[] members = _members.Where((GroupMember value) => !value.Identity.Equals(identity)).ToArray(); return new GroupRecord(Id, DisplayName, checked(Revision + 1), members, _invitations); } internal GroupRecord SetRole(StableIdentity identity, GroupRole role) { GroupMember[] array = new GroupMember[_members.Length]; for (int i = 0; i < _members.Length; i++) { GroupMember groupMember = _members[i]; array[i] = (groupMember.Identity.Equals(identity) ? new GroupMember(groupMember.Identity, role, groupMember.JoinedRevision) : groupMember); } return new GroupRecord(Id, DisplayName, checked(Revision + 1), array, _invitations); } internal GroupRecord TransferOwnership(StableIdentity owner, StableIdentity successor) { GroupMember[] array = new GroupMember[_members.Length]; for (int i = 0; i < _members.Length; i++) { GroupMember groupMember = _members[i]; GroupRole groupRole = (groupMember.Identity.Equals(owner) ? GroupRole.Officer : (groupMember.Identity.Equals(successor) ? GroupRole.Owner : groupMember.Role)); array[i] = ((groupRole == groupMember.Role) ? groupMember : new GroupMember(groupMember.Identity, groupRole, groupMember.JoinedRevision)); } return new GroupRecord(Id, DisplayName, checked(Revision + 1), array, _invitations); } internal GroupRecord PruneExpired(long nowUtcTicks) { GroupInvitation[] array = _invitations.Where((GroupInvitation value) => !value.IsExpired(nowUtcTicks)).ToArray(); if (array.Length != _invitations.Length) { return new GroupRecord(Id, DisplayName, checked(Revision + 1), _members, array); } return this; } private static GroupMember[] CopyMembers(IEnumerable source, long revision) { if (source == null) { throw new ArgumentNullException("source"); } GroupMember[] array = source.ToArray(); if (array.Length < 1 || array.Length > 256 || array.Any((GroupMember value) => value == null || value.JoinedRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupMember left, GroupMember right) => left.Identity.CompareTo(right.Identity)); int num = 0; for (int num2 = 0; num2 < array.Length; num2++) { if (num2 > 0 && array[num2 - 1].Identity.Equals(array[num2].Identity)) { throw new ArgumentException("Group member identities must be unique.", "source"); } if (array[num2].Role == GroupRole.Owner) { num++; } } if (num != 1) { throw new ArgumentException("A group requires exactly one Owner.", "source"); } return array; } private static GroupInvitation[] CopyInvitations(IEnumerable source, long revision, IReadOnlyList members) { GroupInvitation[] array = (source ?? Array.Empty()).ToArray(); if (array.Length > 256 || array.Any((GroupInvitation value) => value == null || value.IssuedRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupInvitation left, GroupInvitation right) => left.Invitee.CompareTo(right.Invitee)); for (int num = 0; num < array.Length; num++) { if (num > 0 && array[num - 1].Invitee.Equals(array[num].Invitee)) { throw new ArgumentException("Group invitation identities must be unique.", "source"); } if (FindMember(members, array[num].Invitee) >= 0) { throw new ArgumentException("Group invitation membership evidence is invalid.", "source"); } } return array; } internal static int FindMember(IReadOnlyList values, StableIdentity identity) { int num = 0; int num2 = values.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = values[num3].Identity.CompareTo(identity); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } private static int FindInvitation(IReadOnlyList values, StableIdentity identity) { int num = 0; int num2 = values.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = values[num3].Invitee.CompareTo(identity); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } } public sealed class RetiredGroupId { public Guid Id { get; } public string IdText { get; } public long DeletedCatalogRevision { get; } public RetiredGroupId(Guid id, long deletedCatalogRevision) { Id = id; IdText = GroupIdentity.ToCanonicalId(id); if (deletedCatalogRevision < 1) { throw new ArgumentOutOfRangeException("deletedCatalogRevision"); } DeletedCatalogRevision = deletedCatalogRevision; } } internal static class GroupCommandDurabilityLimits { internal const int MaximumOutstandingIssues = 256; internal const int MaximumRetainedReceipts = 4096; internal const int MaximumReasonCharacters = 96; internal static readonly TimeSpan MaximumIssueLifetime = TimeSpan.FromMinutes(10.0); } internal sealed class GroupCommandIssue { internal Guid Epoch { get; } internal long Sequence { get; } internal StableIdentity Actor { get; } internal string RequestSha256 { get; } internal Guid GroupId { get; } internal long ExpectedCatalogRevision { get; } internal long ExpectedGroupRevision { get; } internal long ExpiresUtcTicks { get; } internal string Token => GroupCommandToken.Format(Epoch, Sequence); internal GroupCommandIssue(Guid epoch, long sequence, StableIdentity actor, string requestSha256, Guid groupId, long expectedCatalogRevision, long expectedGroupRevision, long expiresUtcTicks) { if (epoch == Guid.Empty || sequence < 1 || actor == null || groupId == Guid.Empty || expectedCatalogRevision < 0 || expectedGroupRevision < -1 || expiresUtcTicks <= 0) { throw new ArgumentException("The Group command issue is invalid."); } Epoch = epoch; Sequence = sequence; Actor = actor; RequestSha256 = RequireSha256(requestSha256); GroupId = groupId; ExpectedCatalogRevision = expectedCatalogRevision; ExpectedGroupRevision = expectedGroupRevision; ExpiresUtcTicks = expiresUtcTicks; } internal bool Matches(StableIdentity actor, string requestSha256) { if (actor != null && Actor.Equals(actor)) { return string.Equals(RequestSha256, requestSha256, StringComparison.Ordinal); } return false; } internal static string RequireSha256(string value) { if (value == null || value.Length != 64) { throw new ArgumentException("SHA-256 is invalid."); } foreach (char c in value) { if ((c < '0' || c > '9') && (c < 'a' || c > 'f')) { throw new ArgumentException("SHA-256 is not canonical lowercase hexadecimal."); } } return value; } } internal sealed class GroupCommandReceipt { internal Guid Epoch { get; } internal long Sequence { get; } internal StableIdentity Actor { get; } internal string RequestSha256 { get; } internal GroupMutationCode Code { get; } internal string ReasonCode { get; } internal long ExpectedCatalogRevision { get; } internal long ExpectedGroupRevision { get; } internal long CatalogRevision { get; } internal long GroupRevision { get; } internal string Token => GroupCommandToken.Format(Epoch, Sequence); internal bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupCommandReceipt(Guid epoch, long sequence, StableIdentity actor, string requestSha256, GroupMutationCode code, string reasonCode, long expectedCatalogRevision, long expectedGroupRevision, long catalogRevision, long groupRevision) { if (epoch == Guid.Empty || sequence < 1 || actor == null || !Enum.IsDefined(typeof(GroupMutationCode), code) || expectedCatalogRevision < 0 || expectedGroupRevision < -1 || catalogRevision < 0 || groupRevision < -1) { throw new ArgumentException("The Group command receipt is invalid."); } string text = reasonCode ?? string.Empty; if (text.Length < 1 || text.Length > 96) { throw new ArgumentOutOfRangeException("reasonCode"); } foreach (char c in text) { if ((c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '.' && c != '-' && c != '_') { throw new ArgumentException("The Group receipt reason is not canonical."); } } Epoch = epoch; Sequence = sequence; Actor = actor; RequestSha256 = GroupCommandIssue.RequireSha256(requestSha256); Code = code; ReasonCode = text; ExpectedCatalogRevision = expectedCatalogRevision; ExpectedGroupRevision = expectedGroupRevision; CatalogRevision = catalogRevision; GroupRevision = groupRevision; } internal bool Matches(StableIdentity actor, string requestSha256) { if (actor != null && Actor.Equals(actor)) { return string.Equals(RequestSha256, requestSha256, StringComparison.Ordinal); } return false; } } internal static class GroupCommandToken { internal static string Format(Guid epoch, long sequence) { if (epoch == Guid.Empty || sequence < 1) { throw new ArgumentException("Token identity is invalid."); } return epoch.ToString("N") + ":" + sequence.ToString("x16", CultureInfo.InvariantCulture); } internal static bool TryParse(string value, out Guid epoch, out long sequence) { epoch = Guid.Empty; sequence = 0L; if (value == null || value.Length != 49 || value[32] != ':' || !Guid.TryParseExact(value.Substring(0, 32), "N", out epoch) || epoch == Guid.Empty || !long.TryParse(value.Substring(33), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out sequence) || sequence < 1) { epoch = Guid.Empty; sequence = 0L; return false; } return string.Equals(value, Format(epoch, sequence), StringComparison.Ordinal); } } internal sealed class GroupCommandLedger { private readonly GroupCommandIssue[] _issues; private readonly GroupCommandReceipt[] _receipts; private readonly ReadOnlyCollection _issueView; private readonly ReadOnlyCollection _receiptView; internal static GroupCommandLedger Empty { get; } = new GroupCommandLedger(Guid.Empty, 0L, 0L); internal Guid Epoch { get; } internal long NextSequence { get; } internal long MinimumAcceptedSequence { get; } internal IReadOnlyList Issues => _issueView; internal IReadOnlyList Receipts => _receiptView; internal GroupCommandLedger(Guid epoch, long nextSequence, long minimumAcceptedSequence, IEnumerable issues = null, IEnumerable receipts = null) { if (nextSequence < 0 || minimumAcceptedSequence < 0 || minimumAcceptedSequence > nextSequence || epoch == Guid.Empty != (nextSequence == 0)) { throw new ArgumentException("The Group command ledger frontier is invalid."); } Epoch = epoch; NextSequence = nextSequence; MinimumAcceptedSequence = minimumAcceptedSequence; _issues = SortedIssues(issues, epoch, minimumAcceptedSequence, nextSequence); _receipts = SortedReceipts(receipts, epoch, minimumAcceptedSequence, nextSequence); if (_issues.Length > 256 || _receipts.Length > 4096) { throw new ArgumentOutOfRangeException("issues"); } int num = 0; int num2 = 0; for (long num3 = minimumAcceptedSequence + 1; num3 <= nextSequence; num3++) { bool num4 = num < _issues.Length && _issues[num].Sequence == num3; bool flag = num2 < _receipts.Length && _receipts[num2].Sequence == num3; if (num4 == flag) { throw new ArgumentException("The Group command ledger has a gap or duplicate."); } if (num4) { num++; } else { num2++; } } _issueView = Array.AsReadOnly(_issues); _receiptView = Array.AsReadOnly(_receipts); } internal GroupCommandLedger Issue(StableIdentity actor, string requestSha256, Guid groupId, long expectedCatalogRevision, long expectedGroupRevision, long nowUtcTicks, long expiresUtcTicks, out GroupCommandIssue issue) { if (actor == null || nowUtcTicks <= 0 || expiresUtcTicks <= nowUtcTicks || expiresUtcTicks - nowUtcTicks > GroupCommandDurabilityLimits.MaximumIssueLifetime.Ticks) { throw new ArgumentException("The Group command issue lifetime is invalid."); } GroupCommandLedger groupCommandLedger = PruneExpired(nowUtcTicks); GroupCommandIssue[] issues = groupCommandLedger._issues; foreach (GroupCommandIssue groupCommandIssue in issues) { if (groupCommandIssue.Matches(actor, requestSha256) && groupCommandIssue.GroupId == groupId && groupCommandIssue.ExpectedCatalogRevision == expectedCatalogRevision && groupCommandIssue.ExpectedGroupRevision == expectedGroupRevision) { issue = groupCommandIssue; return groupCommandLedger; } } if (groupCommandLedger._issues.Length >= 256 || groupCommandLedger.NextSequence == long.MaxValue) { throw new InvalidOperationException("group-command-issue-capacity"); } Guid epoch = ((groupCommandLedger.Epoch == Guid.Empty) ? Guid.NewGuid() : groupCommandLedger.Epoch); long num = checked(groupCommandLedger.NextSequence + 1); issue = new GroupCommandIssue(epoch, num, actor, requestSha256, groupId, expectedCatalogRevision, expectedGroupRevision, expiresUtcTicks); List issues2 = new List(groupCommandLedger._issues) { issue }; return new GroupCommandLedger(epoch, num, groupCommandLedger.MinimumAcceptedSequence, issues2, groupCommandLedger._receipts); } internal GroupCommandLedger Complete(GroupCommandIssue issue, GroupMutationCode code, string reasonCode, long catalogRevision, long groupRevision, out GroupCommandReceipt receipt) { if (issue == null || issue.Epoch != Epoch || !TryGetIssue(issue.Token, out var issue2) || (issue2 != issue && !SameIssue(issue2, issue))) { throw new ArgumentException("The Group command issue is not current.", "issue"); } receipt = new GroupCommandReceipt(Epoch, issue.Sequence, issue.Actor, issue.RequestSha256, code, reasonCode, issue.ExpectedCatalogRevision, issue.ExpectedGroupRevision, catalogRevision, groupRevision); List issues = _issues.Where((GroupCommandIssue value) => value.Sequence != issue.Sequence).ToList(); List list = new List(_receipts) { receipt }; list.Sort((GroupCommandReceipt left, GroupCommandReceipt right) => left.Sequence.CompareTo(right.Sequence)); return Compact(Epoch, NextSequence, MinimumAcceptedSequence, issues, list); } internal GroupCommandLedger PruneExpired(long nowUtcTicks) { if (nowUtcTicks <= 0) { throw new ArgumentOutOfRangeException("nowUtcTicks"); } List list = new List(_issues.Length); List list2 = new List(_receipts); bool flag = false; GroupCommandIssue[] issues = _issues; foreach (GroupCommandIssue groupCommandIssue in issues) { if (groupCommandIssue.ExpiresUtcTicks >= nowUtcTicks) { list.Add(groupCommandIssue); continue; } list2.Add(new GroupCommandReceipt(Epoch, groupCommandIssue.Sequence, groupCommandIssue.Actor, groupCommandIssue.RequestSha256, GroupMutationCode.RevisionConflict, "group-command-token-expired", groupCommandIssue.ExpectedCatalogRevision, groupCommandIssue.ExpectedGroupRevision, groupCommandIssue.ExpectedCatalogRevision, groupCommandIssue.ExpectedGroupRevision)); flag = true; } if (!flag) { return this; } list2.Sort((GroupCommandReceipt left, GroupCommandReceipt right) => left.Sequence.CompareTo(right.Sequence)); return Compact(Epoch, NextSequence, MinimumAcceptedSequence, list, list2); } internal bool TryGetIssue(string token, out GroupCommandIssue issue) { issue = null; if (!GroupCommandToken.TryParse(token, out var epoch, out var sequence) || epoch != Epoch || sequence <= MinimumAcceptedSequence || sequence > NextSequence) { return false; } for (int i = 0; i < _issues.Length; i++) { if (_issues[i].Sequence == sequence) { issue = _issues[i]; return true; } } return false; } internal bool TryGetReceipt(string token, out GroupCommandReceipt receipt) { receipt = null; if (!GroupCommandToken.TryParse(token, out var epoch, out var sequence) || epoch != Epoch || sequence <= MinimumAcceptedSequence || sequence > NextSequence) { return false; } for (int i = 0; i < _receipts.Length; i++) { if (_receipts[i].Sequence == sequence) { receipt = _receipts[i]; return true; } } return false; } private static GroupCommandLedger Compact(Guid epoch, long nextSequence, long minimum, List issues, List receipts) { while (receipts.Count > 4096) { long next = minimum + 1; int num = receipts.FindIndex((GroupCommandReceipt value) => value.Sequence == next); if (num < 0) { throw new InvalidOperationException("group-command-receipt-capacity"); } receipts.RemoveAt(num); minimum = next; } return new GroupCommandLedger(epoch, nextSequence, minimum, issues, receipts); } private static GroupCommandIssue[] SortedIssues(IEnumerable values, Guid epoch, long minimum, long maximum) { GroupCommandIssue[] array = (values ?? Array.Empty()).OrderBy((GroupCommandIssue value) => value?.Sequence ?? 0).ToArray(); long num = minimum; GroupCommandIssue[] array2 = array; foreach (GroupCommandIssue groupCommandIssue in array2) { if (groupCommandIssue == null || groupCommandIssue.Epoch != epoch || groupCommandIssue.Sequence <= num || groupCommandIssue.Sequence > maximum) { throw new ArgumentException("The Group command issue set is invalid."); } num = groupCommandIssue.Sequence; } return array; } private static GroupCommandReceipt[] SortedReceipts(IEnumerable values, Guid epoch, long minimum, long maximum) { GroupCommandReceipt[] array = (values ?? Array.Empty()).OrderBy((GroupCommandReceipt value) => value?.Sequence ?? 0).ToArray(); long num = minimum; GroupCommandReceipt[] array2 = array; foreach (GroupCommandReceipt groupCommandReceipt in array2) { if (groupCommandReceipt == null || groupCommandReceipt.Epoch != epoch || groupCommandReceipt.Sequence <= num || groupCommandReceipt.Sequence > maximum) { throw new ArgumentException("The Group command receipt set is invalid."); } num = groupCommandReceipt.Sequence; } return array; } private static bool SameIssue(GroupCommandIssue left, GroupCommandIssue right) { if (left != null && right != null && left.Epoch == right.Epoch && left.Sequence == right.Sequence && left.Actor.Equals(right.Actor) && string.Equals(left.RequestSha256, right.RequestSha256, StringComparison.Ordinal) && left.GroupId == right.GroupId && left.ExpectedCatalogRevision == right.ExpectedCatalogRevision && left.ExpectedGroupRevision == right.ExpectedGroupRevision) { return left.ExpiresUtcTicks == right.ExpiresUtcTicks; } return false; } } public enum GroupMutationCode { Created, Renamed, Invited, InvitationCancelled, Accepted, Left, Removed, RoleChanged, OwnershipTransferred, Deleted, ExpiredInvitationsPruned, NoChange, RevisionConflict, InvalidRequest, GroupMissing, NameConflict, Unauthorized, AlreadyMember, InvitationMissing, InvitationExpired, CapacityReached, RetiredIdentity } public sealed class GroupMutationResult { public GroupMutationCode Code { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public GroupRecord Group { get; } public bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupMutationResult(GroupMutationCode code, string reasonCode, GroupCatalog catalog, GroupRecord group) { Code = code; ReasonCode = reasonCode ?? throw new ArgumentNullException("reasonCode"); Catalog = catalog ?? throw new ArgumentNullException("catalog"); Group = group; } } public sealed class GroupMembership { public Guid GroupId { get; } public string GroupIdText { get; } public string DisplayName { get; } public GroupRole Role { get; } public long GroupRevision { get; } internal GroupMembership(Guid groupId, string displayName, GroupRole role, long groupRevision) { GroupId = groupId; GroupIdText = GroupIdentity.ToCanonicalId(groupId); DisplayName = displayName; Role = role; GroupRevision = groupRevision; } } public sealed class GroupCatalog { private readonly GroupRecord[] _groups; private readonly RetiredGroupId[] _retired; private readonly ReadOnlyCollection _groupView; private readonly ReadOnlyCollection _retiredView; public long Revision { get; } internal GroupCommandLedger CommandLedger { get; } public IReadOnlyList Groups => _groupView; public IReadOnlyList RetiredGroupIds => _retiredView; public static GroupCatalog Empty { get; } = new GroupCatalog(0L); public GroupCatalog(long revision, IEnumerable groups = null, IEnumerable retiredGroupIds = null) : this(revision, groups, retiredGroupIds, GroupCommandLedger.Empty) { } internal GroupCatalog(long revision, IEnumerable groups, IEnumerable retiredGroupIds, GroupCommandLedger commandLedger) { if (revision < 0) { throw new ArgumentOutOfRangeException("revision"); } Revision = revision; CommandLedger = commandLedger ?? throw new ArgumentNullException("commandLedger"); _groups = CopyGroups(groups, revision); _retired = CopyRetired(retiredGroupIds, revision, _groups); ValidateMembershipBounds(_groups); _groupView = Array.AsReadOnly(_groups); _retiredView = Array.AsReadOnly(_retired); } internal GroupCatalog WithCommandLedger(GroupCommandLedger ledger) { return new GroupCatalog(Revision, _groups, _retired, ledger); } public bool TryGetGroup(Guid id, out GroupRecord group) { group = null; if (id == Guid.Empty) { return false; } int num = FindGroup(_groups, GroupIdentity.ToCanonicalId(id)); if (num < 0) { return false; } group = _groups[num]; return true; } public IReadOnlyList GetMemberships(StableIdentity identity) { if (identity == null) { return Array.Empty(); } List list = new List(); GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (groupRecord.TryGetMember(identity, out var member)) { list.Add(new GroupMembership(groupRecord.Id, groupRecord.DisplayName, member.Role, groupRecord.Revision)); } } return list.AsReadOnly(); } public GroupMutationResult Create(long expectedCatalogRevision, Guid id, string displayName, StableIdentity owner) { if (!Expected(expectedCatalogRevision)) { return Conflict(); } if (id == Guid.Empty || owner == null || !TryDisplayName(displayName)) { return Invalid(); } string idText = GroupIdentity.ToCanonicalId(id); if (FindGroup(_groups, idText) >= 0) { return Fail(GroupMutationCode.InvalidRequest, "group-id-exists"); } if (FindRetired(_retired, idText) >= 0) { return Fail(GroupMutationCode.RetiredIdentity, "group-id-retired"); } if (_groups.Length >= 256 || CountMemberships(owner) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-capacity-reached"); } if (NameExists(displayName, null)) { return Fail(GroupMutationCode.NameConflict, "group-name-conflict"); } GroupRecord replacement = new GroupRecord(id, displayName, 1L, new GroupMember[1] { new GroupMember(owner, GroupRole.Owner, 1L) }); return Replace(null, replacement, GroupMutationCode.Created, "group-created"); } public GroupMutationResult Rename(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, string displayName) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (!TryDisplayName(displayName) || actor == null) { return Invalid(group); } if (!IsOwner(group, actor)) { return Unauthorized(group); } if (NameExists(displayName, group.IdText)) { return Fail(GroupMutationCode.NameConflict, "group-name-conflict", group); } if (string.Equals(group.DisplayName, displayName, StringComparison.Ordinal)) { return Ok(GroupMutationCode.NoChange, "group-name-unchanged", group); } return Replace(group, group.Rename(displayName), GroupMutationCode.Renamed, "group-renamed"); } public GroupMutationResult Invite(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity invitee, long nowUtcTicks, long expiresUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || invitee == null || nowUtcTicks <= 0 || expiresUtcTicks <= nowUtcTicks || expiresUtcTicks - nowUtcTicks > GroupLimits.MaximumInvitationLifetime.Ticks) { return Invalid(group); } if (!IsOfficerOrOwner(group, actor)) { return Unauthorized(group); } if (group.TryGetMember(invitee, out var _)) { return Fail(GroupMutationCode.AlreadyMember, "group-already-member", group); } if (CountMemberships(invitee) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-membership-capacity", group); } if (group.TryGetInvitation(invitee, out var invitation)) { if (invitation.InvitedBy.Equals(actor) && invitation.ExpiresUtcTicks == expiresUtcTicks) { return Ok(GroupMutationCode.NoChange, "group-invitation-unchanged", group); } return Fail(GroupMutationCode.InvalidRequest, "group-invitation-exists", group); } if (group.Invitations.Count >= 256) { return Fail(GroupMutationCode.CapacityReached, "group-invitation-capacity", group); } return Replace(group, group.Invite(actor, invitee, expiresUtcTicks), GroupMutationCode.Invited, "group-invited"); } public GroupMutationResult CancelInvitation(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity invitee) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || invitee == null) { return Invalid(group); } if (!IsOfficerOrOwner(group, actor)) { return Unauthorized(group); } if (!group.TryGetInvitation(invitee, out var _)) { return Fail(GroupMutationCode.InvitationMissing, "group-invitation-missing", group); } return Replace(group, group.CancelInvitation(invitee), GroupMutationCode.InvitationCancelled, "group-invitation-cancelled"); } public GroupMutationResult Accept(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity invitee, long nowUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (invitee == null || nowUtcTicks <= 0) { return Invalid(group); } if (group.TryGetMember(invitee, out var _)) { return Fail(GroupMutationCode.AlreadyMember, "group-already-member", group); } if (!group.TryGetInvitation(invitee, out var invitation)) { return Fail(GroupMutationCode.InvitationMissing, "group-invitation-missing", group); } if (!invitation.Invitee.Equals(invitee)) { return Fail(GroupMutationCode.Unauthorized, "group-invitation-identity-mismatch", group); } if (invitation.IsExpired(nowUtcTicks)) { return Fail(GroupMutationCode.InvitationExpired, "group-invitation-expired", group); } if (group.Members.Count >= 256 || CountMemberships(invitee) >= 64) { return Fail(GroupMutationCode.CapacityReached, "group-membership-capacity", group); } return Replace(group, group.Accept(invitee), GroupMutationCode.Accepted, "group-invitation-accepted"); } public GroupMutationResult Leave(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || !group.TryGetMember(actor, out var member)) { return Unauthorized(group); } if (member.Role == GroupRole.Owner) { return Fail(GroupMutationCode.Unauthorized, "group-owner-transfer-required", group); } return Replace(group, group.RemoveMember(actor), GroupMutationCode.Left, "group-left"); } public GroupMutationResult Remove(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity target) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || target == null || actor.Equals(target) || !group.TryGetMember(actor, out var member) || !group.TryGetMember(target, out var member2)) { return Unauthorized(group); } if (member2.Role == GroupRole.Owner || member.Role == GroupRole.Member || (member.Role == GroupRole.Officer && member2.Role != GroupRole.Member)) { return Unauthorized(group); } return Replace(group, group.RemoveMember(target), GroupMutationCode.Removed, "group-member-removed"); } public GroupMutationResult SetRole(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity target, GroupRole role) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || target == null || role == GroupRole.Owner || !Enum.IsDefined(typeof(GroupRole), role) || !IsOwner(group, actor) || !group.TryGetMember(target, out var member) || member.Role == GroupRole.Owner) { return Unauthorized(group); } if (member.Role == role) { return Ok(GroupMutationCode.NoChange, "group-role-unchanged", group); } return Replace(group, group.SetRole(target, role), GroupMutationCode.RoleChanged, "group-role-changed"); } public GroupMutationResult TransferOwnership(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor, StableIdentity successor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || successor == null || actor.Equals(successor) || !IsOwner(group, actor) || !group.TryGetMember(successor, out var member) || member.Role == GroupRole.Owner) { return Unauthorized(group); } return Replace(group, group.TransferOwnership(actor, successor), GroupMutationCode.OwnershipTransferred, "group-ownership-transferred"); } public GroupMutationResult Delete(long expectedCatalogRevision, Guid id, long expectedGroupRevision, StableIdentity actor) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (actor == null || !IsOwner(group, actor)) { return Unauthorized(group); } if (_retired.Length >= 4096) { return Fail(GroupMutationCode.CapacityReached, "group-retired-id-capacity", group); } long num = checked(Revision + 1); GroupRecord[] groups = _groups.Where((GroupRecord value) => value.Id != id).ToArray(); List retiredGroupIds = new List(_retired) { new RetiredGroupId(id, num) }; GroupCatalog catalog = new GroupCatalog(num, groups, retiredGroupIds, CommandLedger); return new GroupMutationResult(GroupMutationCode.Deleted, "group-deleted", catalog, null); } public GroupMutationResult PruneExpiredInvitations(long expectedCatalogRevision, Guid id, long expectedGroupRevision, long nowUtcTicks) { if (!TryMutation(expectedCatalogRevision, id, expectedGroupRevision, out var group, out var failure)) { return failure; } if (nowUtcTicks <= 0) { return Invalid(group); } GroupRecord groupRecord = group.PruneExpired(nowUtcTicks); if (group != groupRecord) { return Replace(group, groupRecord, GroupMutationCode.ExpiredInvitationsPruned, "group-expired-invitations-pruned"); } return Ok(GroupMutationCode.NoChange, "group-no-expired-invitations", group); } private bool TryMutation(long expectedCatalogRevision, Guid id, long expectedGroupRevision, out GroupRecord group, out GroupMutationResult failure) { group = null; failure = null; if (!Expected(expectedCatalogRevision)) { failure = Conflict(); return false; } if (id == Guid.Empty || expectedGroupRevision < 1) { failure = Invalid(); return false; } if (!TryGetGroup(id, out group)) { failure = Fail(GroupMutationCode.GroupMissing, "group-missing"); return false; } if (group.Revision != expectedGroupRevision) { failure = Fail(GroupMutationCode.RevisionConflict, "group-revision-conflict", group); return false; } return true; } private GroupMutationResult Replace(GroupRecord current, GroupRecord replacement, GroupMutationCode code, string reason) { List list = new List(_groups.Length + ((current == null) ? 1 : 0)); GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (current == null || groupRecord.Id != current.Id) { list.Add(groupRecord); } } list.Add(replacement); GroupCatalog catalog = new GroupCatalog(checked(Revision + 1), list, _retired, CommandLedger); return new GroupMutationResult(code, reason, catalog, replacement); } private bool NameExists(string name, string exceptId) { GroupRecord[] groups = _groups; foreach (GroupRecord groupRecord in groups) { if (!string.Equals(groupRecord.IdText, exceptId, StringComparison.Ordinal) && string.Equals(groupRecord.DisplayName, name, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private int CountMemberships(StableIdentity identity) { int num = 0; GroupRecord[] groups = _groups; for (int i = 0; i < groups.Length; i++) { if (groups[i].TryGetMember(identity, out var _)) { num++; } } return num; } private static bool IsOwner(GroupRecord group, StableIdentity identity) { if (group.TryGetMember(identity, out var member)) { return member.Role == GroupRole.Owner; } return false; } private static bool IsOfficerOrOwner(GroupRecord group, StableIdentity identity) { if (group.TryGetMember(identity, out var member)) { return (int)member.Role >= 2; } return false; } private bool Expected(long revision) { if (revision >= 0) { return revision == Revision; } return false; } private GroupMutationResult Conflict() { return Fail(GroupMutationCode.RevisionConflict, "group-catalog-revision-conflict"); } private GroupMutationResult Invalid(GroupRecord group = null) { return Fail(GroupMutationCode.InvalidRequest, "group-request-invalid", group); } private GroupMutationResult Unauthorized(GroupRecord group) { return Fail(GroupMutationCode.Unauthorized, "group-actor-unauthorized", group); } private GroupMutationResult Ok(GroupMutationCode code, string reason, GroupRecord group) { return new GroupMutationResult(code, reason, this, group); } private GroupMutationResult Fail(GroupMutationCode code, string reason, GroupRecord group = null) { return new GroupMutationResult(code, reason, this, group); } private static bool TryDisplayName(string value) { try { GroupIdentity.RequireDisplayName(value); return true; } catch (ArgumentException) { return false; } } private static GroupRecord[] CopyGroups(IEnumerable source, long revision) { GroupRecord[] array = (source ?? Array.Empty()).ToArray(); if (array.Length > 256 || array.Any((GroupRecord value) => value == null || value.Revision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (GroupRecord left, GroupRecord right) => string.Compare(left.IdText, right.IdText, StringComparison.Ordinal)); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int num = 0; num < array.Length; num++) { if (num > 0 && string.Equals(array[num - 1].IdText, array[num].IdText, StringComparison.Ordinal)) { throw new ArgumentException("Group UUIDs must be unique.", "source"); } if (!hashSet.Add(array[num].DisplayName)) { throw new ArgumentException("Group display names must be unique.", "source"); } } return array; } private static RetiredGroupId[] CopyRetired(IEnumerable source, long revision, IReadOnlyList groups) { RetiredGroupId[] array = (source ?? Array.Empty()).ToArray(); if (array.Length > 4096 || array.Any((RetiredGroupId value) => value == null || value.DeletedCatalogRevision > revision)) { throw new ArgumentOutOfRangeException("source"); } Array.Sort(array, (RetiredGroupId left, RetiredGroupId right) => string.Compare(left.IdText, right.IdText, StringComparison.Ordinal)); for (int num = 0; num < array.Length; num++) { if (num > 0 && string.Equals(array[num - 1].IdText, array[num].IdText, StringComparison.Ordinal)) { throw new ArgumentException("Retired group UUIDs must be unique.", "source"); } if (FindGroup(groups, array[num].IdText) >= 0) { throw new ArgumentException("A current group UUID cannot also be retired.", "source"); } } return array; } private static void ValidateMembershipBounds(IEnumerable groups) { Dictionary dictionary = new Dictionary(); foreach (GroupRecord group in groups) { foreach (GroupMember member in group.Members) { dictionary.TryGetValue(member.Identity, out var value); value++; if (value > 64) { throw new ArgumentOutOfRangeException("groups"); } dictionary[member.Identity] = value; } } } private static int FindGroup(IReadOnlyList groups, string idText) { int num = 0; int num2 = groups.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = string.Compare(groups[num3].IdText, idText, StringComparison.Ordinal); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } private static int FindRetired(IReadOnlyList retired, string idText) { int num = 0; int num2 = retired.Count - 1; while (num <= num2) { int num3 = num + (num2 - num) / 2; int num4 = string.Compare(retired[num3].IdText, idText, StringComparison.Ordinal); if (num4 == 0) { return num3; } if (num4 < 0) { num = num3 + 1; } else { num2 = num3 - 1; } } return -1; } } internal static class GroupCatalogCodec { private const uint Magic = 1196446279u; private const ushort SchemaVersion = 2; private const int DigestBytes = 32; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] Encode(string worldScope, GroupCatalog catalog) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (catalog == null) { throw new ArgumentNullException("catalog"); } byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(1196446279u); binaryWriter.Write((ushort)2); WriteText(binaryWriter, worldScope, 128); binaryWriter.Write(catalog.Revision); binaryWriter.Write(catalog.Groups.Count); foreach (GroupRecord group in catalog.Groups) { WriteText(binaryWriter, group.IdText, 32); WriteText(binaryWriter, group.DisplayName, 64); binaryWriter.Write(group.Revision); binaryWriter.Write(group.Members.Count); foreach (GroupMember member in group.Members) { WriteIdentity(binaryWriter, member.Identity); binaryWriter.Write((byte)member.Role); binaryWriter.Write(member.JoinedRevision); } binaryWriter.Write(group.Invitations.Count); foreach (GroupInvitation invitation in group.Invitations) { WriteIdentity(binaryWriter, invitation.Invitee); WriteIdentity(binaryWriter, invitation.InvitedBy); binaryWriter.Write(invitation.IssuedRevision); binaryWriter.Write(invitation.ExpiresUtcTicks); } } binaryWriter.Write(catalog.RetiredGroupIds.Count); foreach (RetiredGroupId retiredGroupId in catalog.RetiredGroupIds) { WriteText(binaryWriter, retiredGroupId.IdText, 32); binaryWriter.Write(retiredGroupId.DeletedCatalogRevision); } WriteCommandLedger(binaryWriter, catalog.CommandLedger); binaryWriter.Flush(); array = memoryStream.ToArray(); } byte[] array2 = Sha256(array); if (array.Length > 8388608 - array2.Length) { throw new InvalidOperationException("The group catalog exceeds its hard byte limit."); } byte[] array3 = new byte[array.Length + array2.Length]; Buffer.BlockCopy(array, 0, array3, 0, array.Length); Buffer.BlockCopy(array2, 0, array3, array.Length, array2.Length); return array3; } internal static bool TryDecode(byte[] bytes, string expectedWorldScope, out GroupCatalog catalog, out string reason) { catalog = null; reason = "group-catalog-corrupt"; try { expectedWorldScope = GroupIdentity.RequireWorldScope(expectedWorldScope); if (bytes == null || bytes.Length < 55 || bytes.Length > 8388608) { return false; } int num = bytes.Length - 32; if (!FixedEquals(Sha256(bytes, 0, num), bytes, num)) { reason = "group-catalog-digest-mismatch"; return false; } using (MemoryStream memoryStream = new MemoryStream(bytes, 0, num, writable: false, publiclyVisible: true)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 1196446279) { reason = "group-catalog-schema-unsupported"; return false; } ushort num2 = binaryReader.ReadUInt16(); if (num2 != 1 && num2 != 2) { reason = "group-catalog-schema-unsupported"; return false; } if (!string.Equals(ReadText(binaryReader, 128), expectedWorldScope, StringComparison.Ordinal)) { reason = "group-catalog-world-mismatch"; return false; } long revision = binaryReader.ReadInt64(); int num3 = ReadCount(binaryReader, 256); List list = new List(num3); for (int i = 0; i < num3; i++) { if (!GroupIdentity.TryParseCanonicalId(ReadText(binaryReader, 32), out var groupId)) { return false; } string displayName = ReadText(binaryReader, 64); long revision2 = binaryReader.ReadInt64(); int num4 = ReadCount(binaryReader, 256); if (num4 < 1) { return false; } List list2 = new List(num4); for (int j = 0; j < num4; j++) { list2.Add(new GroupMember(ReadIdentity(binaryReader), (GroupRole)binaryReader.ReadByte(), binaryReader.ReadInt64())); } int num5 = ReadCount(binaryReader, 256); List list3 = new List(num5); for (int k = 0; k < num5; k++) { list3.Add(new GroupInvitation(ReadIdentity(binaryReader), ReadIdentity(binaryReader), binaryReader.ReadInt64(), binaryReader.ReadInt64())); } list.Add(new GroupRecord(groupId, displayName, revision2, list2, list3)); } int num6 = ReadCount(binaryReader, 4096); List list4 = new List(num6); for (int l = 0; l < num6; l++) { if (!GroupIdentity.TryParseCanonicalId(ReadText(binaryReader, 32), out var groupId2)) { return false; } list4.Add(new RetiredGroupId(groupId2, binaryReader.ReadInt64())); } GroupCommandLedger commandLedger = ((num2 == 1) ? GroupCommandLedger.Empty : ReadCommandLedger(binaryReader)); if (memoryStream.Position != num) { reason = "group-catalog-trailing-data"; return false; } catalog = new GroupCatalog(revision, list, list4, commandLedger); byte[] array = ((num2 == 2) ? Encode(expectedWorldScope, catalog) : null); if (array != null && !ExactEquals(bytes, array)) { catalog = null; reason = "group-catalog-noncanonical"; return false; } } reason = "group-catalog-ready"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is EndOfStreamException || ex is DecoderFallbackException || ex is OverflowException) { catalog = null; return false; } } internal static string ComputeSha256(byte[] bytes) { byte[] array = Sha256(bytes ?? Array.Empty()); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); byte[] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("x2")); } return stringBuilder.ToString(); } private static void WriteIdentity(BinaryWriter writer, StableIdentity identity) { if (identity == null) { throw new ArgumentNullException("identity"); } WriteText(writer, identity.Authority, 64); WriteText(writer, identity.SubjectId, 1024); } private static StableIdentity ReadIdentity(BinaryReader reader) { return new StableIdentity(ReadText(reader, 64), ReadText(reader, 1024)); } private static void WriteCommandLedger(BinaryWriter writer, GroupCommandLedger ledger) { ledger = ledger ?? GroupCommandLedger.Empty; writer.Write(ledger.Epoch.ToByteArray()); writer.Write(ledger.NextSequence); writer.Write(ledger.MinimumAcceptedSequence); writer.Write(ledger.Issues.Count); foreach (GroupCommandIssue issue in ledger.Issues) { writer.Write(issue.Sequence); WriteIdentity(writer, issue.Actor); WriteText(writer, issue.RequestSha256, 64); WriteText(writer, GroupIdentity.ToCanonicalId(issue.GroupId), 32); writer.Write(issue.ExpectedCatalogRevision); writer.Write(issue.ExpectedGroupRevision); writer.Write(issue.ExpiresUtcTicks); } writer.Write(ledger.Receipts.Count); foreach (GroupCommandReceipt receipt in ledger.Receipts) { writer.Write(receipt.Sequence); WriteIdentity(writer, receipt.Actor); WriteText(writer, receipt.RequestSha256, 64); writer.Write((int)receipt.Code); WriteText(writer, receipt.ReasonCode, 96); writer.Write(receipt.ExpectedCatalogRevision); writer.Write(receipt.ExpectedGroupRevision); writer.Write(receipt.CatalogRevision); writer.Write(receipt.GroupRevision); } } private static GroupCommandLedger ReadCommandLedger(BinaryReader reader) { byte[] array = reader.ReadBytes(16); if (array.Length != 16) { throw new EndOfStreamException(); } Guid epoch = new Guid(array); long nextSequence = reader.ReadInt64(); long minimumAcceptedSequence = reader.ReadInt64(); int num = ReadCount(reader, 256); List list = new List(num); for (int i = 0; i < num; i++) { long sequence = reader.ReadInt64(); StableIdentity actor = ReadIdentity(reader); string requestSha = ReadText(reader, 64); if (!GroupIdentity.TryParseCanonicalId(ReadText(reader, 32), out var groupId)) { throw new InvalidDataException(); } list.Add(new GroupCommandIssue(epoch, sequence, actor, requestSha, groupId, reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64())); } int num2 = ReadCount(reader, 4096); List list2 = new List(num2); for (int j = 0; j < num2; j++) { list2.Add(new GroupCommandReceipt(epoch, reader.ReadInt64(), ReadIdentity(reader), ReadText(reader, 64), (GroupMutationCode)reader.ReadInt32(), ReadText(reader, 96), reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64(), reader.ReadInt64())); } return new GroupCommandLedger(epoch, nextSequence, minimumAcceptedSequence, list, list2); } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length < 1 || bytes.Length > maximumBytes) { throw new ArgumentOutOfRangeException("value"); } writer.Write(bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadInt32(); if (num < 1 || num > maximumBytes) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } return StrictUtf8.GetString(array); } private static int ReadCount(BinaryReader reader, int maximum) { int num = reader.ReadInt32(); if (num < 0 || num > maximum) { throw new InvalidDataException(); } return num; } private static byte[] Sha256(byte[] bytes) { return Sha256(bytes, 0, bytes.Length); } private static byte[] Sha256(byte[] bytes, int offset, int count) { using SHA256 sHA = SHA256.Create(); return sHA.ComputeHash(bytes, offset, count); } private static bool FixedEquals(byte[] expected, byte[] source, int offset) { if (expected.Length != source.Length - offset) { return false; } int num = 0; for (int i = 0; i < expected.Length; i++) { num |= expected[i] ^ source[offset + i]; } return num == 0; } private static bool ExactEquals(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } } public enum GroupCommandKind : byte { Create = 1, Rename, Invite, CancelInvitation, Accept, Leave, Remove, SetRole, TransferOwnership, Delete } public sealed class GroupCommand { public GroupCommandKind Kind { get; } public Guid GroupId { get; } public string DisplayName { get; } public StableIdentity Target { get; } public GroupRole Role { get; } public long InvitationExpiresUtcTicks { get; } public GroupCommand(GroupCommandKind kind, Guid groupId, string displayName = "", StableIdentity target = null, GroupRole role = GroupRole.Member, long invitationExpiresUtcTicks = 0L) { Kind = kind; GroupId = groupId; DisplayName = displayName ?? string.Empty; Target = target; Role = role; InvitationExpiresUtcTicks = invitationExpiresUtcTicks; Validate(); } private void Validate() { if (!Enum.IsDefined(typeof(GroupCommandKind), Kind) || GroupId == Guid.Empty) { throw new ArgumentException("The group command kind or ID is invalid."); } if (!Enum.IsDefined(typeof(GroupRole), Role)) { throw new ArgumentOutOfRangeException("Role"); } bool num = Kind == GroupCommandKind.Create || Kind == GroupCommandKind.Rename; bool flag = Kind == GroupCommandKind.Invite || Kind == GroupCommandKind.CancelInvitation || Kind == GroupCommandKind.Remove || Kind == GroupCommandKind.SetRole || Kind == GroupCommandKind.TransferOwnership; if (num) { GroupIdentity.RequireDisplayName(DisplayName); } else if (DisplayName.Length != 0) { throw new ArgumentException("This group command does not accept a display name."); } if (flag != (Target != null)) { throw new ArgumentException("The group command target shape is invalid."); } if (Kind == GroupCommandKind.SetRole) { if (Role != GroupRole.Member && Role != GroupRole.Officer) { throw new ArgumentException("SetRole accepts Member or Officer only."); } } else if (Role != GroupRole.Member) { throw new ArgumentException("This group command does not accept a role."); } if (Kind == GroupCommandKind.Invite) { if (InvitationExpiresUtcTicks <= 0) { throw new ArgumentOutOfRangeException("InvitationExpiresUtcTicks"); } } else if (InvitationExpiresUtcTicks != 0L) { throw new ArgumentException("This group command does not accept an expiry."); } } } public static class GroupCommandCodec { private const uint Magic = 1129337415u; private const byte Schema = 1; public const int MaximumPayloadBytes = 1024; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); public static byte[] Encode(GroupCommand command) { if (command == null) { throw new ArgumentNullException("command"); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(1129337415u); binaryWriter.Write((byte)1); binaryWriter.Write((byte)command.Kind); binaryWriter.Write(command.GroupId.ToByteArray()); WriteText(binaryWriter, command.DisplayName, 64); WriteText(binaryWriter, command.Target?.Authority ?? string.Empty, 64); WriteText(binaryWriter, command.Target?.SubjectId ?? string.Empty, 1024); binaryWriter.Write((byte)command.Role); binaryWriter.Write(command.InvitationExpiresUtcTicks); binaryWriter.Flush(); if (memoryStream.Length > 1024) { throw new InvalidOperationException("The group command exceeds its wire bound."); } return memoryStream.ToArray(); } public static bool TryDecode(byte[] payload, out GroupCommand command, out string failureCode) { command = null; failureCode = "group-command-invalid"; if (payload == null || payload.Length < 33 || payload.Length > 1024) { return false; } try { using (MemoryStream memoryStream = new MemoryStream(payload, writable: false)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 1129337415 || binaryReader.ReadByte() != 1) { failureCode = "group-command-schema-invalid"; return false; } GroupCommandKind kind = (GroupCommandKind)binaryReader.ReadByte(); byte[] array = binaryReader.ReadBytes(16); if (array.Length != 16) { failureCode = "group-command-id-truncated"; return false; } Guid groupId = new Guid(array); string displayName = ReadText(binaryReader, 64); string text = ReadText(binaryReader, 64); string text2 = ReadText(binaryReader, 1024); GroupRole role = (GroupRole)binaryReader.ReadByte(); long invitationExpiresUtcTicks = binaryReader.ReadInt64(); if (memoryStream.Position != memoryStream.Length) { failureCode = "group-command-trailing-data"; return false; } StableIdentity target = ((text.Length == 0 && text2.Length == 0) ? null : new StableIdentity(text, text2)); if (text.Length == 0 != (text2.Length == 0)) { failureCode = "group-command-target-invalid"; return false; } command = new GroupCommand(kind, groupId, displayName, target, role, invitationExpiresUtcTicks); } if (!payload.SequenceEqual(Encode(command))) { command = null; failureCode = "group-command-noncanonical"; return false; } failureCode = "ok"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is EndOfStreamException || ex is IOException || ex is DecoderFallbackException || ex is OverflowException) { command = null; failureCode = "group-command-invalid"; return false; } } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > maximumBytes || bytes.Length > 65535) { throw new ArgumentOutOfRangeException("value"); } writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadUInt16(); if (num > maximumBytes || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException("The group command text length is invalid."); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } string text = StrictUtf8.GetString(array); if (!StrictUtf8.GetBytes(text).SequenceEqual(array)) { throw new InvalidDataException("The group command text is not canonical UTF-8."); } return text; } } public sealed class GroupCommandExecutionResult { public GroupMutationCode Code { get; } public string ReasonCode { get; } public GroupCatalog Catalog { get; } public GroupRecord Group { get; } public bool Success { get { if (Code >= GroupMutationCode.Created) { return Code <= GroupMutationCode.NoChange; } return false; } } internal GroupCommandExecutionResult(GroupMutationCode code, string reasonCode, GroupCatalog catalog, GroupRecord group) { Code = code; ReasonCode = reasonCode ?? string.Empty; Catalog = catalog; Group = group; } } public sealed class GroupCommandProcessor { private readonly IGroupWorldStore _store; private readonly Func _worldScopeProvider; private readonly Func _utcTicksProvider; public GroupCommandProcessor(IGroupWorldStore store, Func worldScopeProvider, Func utcTicksProvider = null) { _store = store ?? throw new ArgumentNullException("store"); _worldScopeProvider = worldScopeProvider ?? throw new ArgumentNullException("worldScopeProvider"); _utcTicksProvider = utcTicksProvider ?? ((Func)(() => DateTime.UtcNow.Ticks)); } public GroupCommandExecutionResult Execute(StableIdentity actor, GroupCommand command) { if (actor == null || command == null) { return Fail(GroupMutationCode.InvalidRequest, "group-command-invalid", null); } string text; try { text = _worldScopeProvider() ?? string.Empty; } catch { return Fail(GroupMutationCode.RevisionConflict, "group-world-unavailable", null); } if (text.Length == 0) { return Fail(GroupMutationCode.RevisionConflict, "group-world-unavailable", null); } GroupWorldReadResult groupWorldReadResult; try { groupWorldReadResult = _store.Read(text); } catch { return Fail(GroupMutationCode.RevisionConflict, "group-store-unavailable", null); } if (groupWorldReadResult == null || groupWorldReadResult.State == GroupWorldReadState.Corrupt || groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { return Fail(GroupMutationCode.RevisionConflict, "group-store-evidence-conflict", groupWorldReadResult?.Catalog); } if (groupWorldReadResult.State == GroupWorldReadState.Unavailable) { return Fail(GroupMutationCode.RevisionConflict, "group-store-unavailable", groupWorldReadResult.Catalog); } GroupCatalog groupCatalog = ((groupWorldReadResult.State == GroupWorldReadState.Missing) ? GroupCatalog.Empty : groupWorldReadResult.Catalog); if (groupCatalog == null) { return Fail(GroupMutationCode.RevisionConflict, "group-store-invalid", null); } GroupCommandExecutionResult groupCommandExecutionResult = TryExactDesiredReplay(groupCatalog, actor, command); if (groupCommandExecutionResult != null) { return groupCommandExecutionResult; } GroupMutationResult groupMutationResult; if (command.Kind == GroupCommandKind.Create) { groupMutationResult = groupCatalog.Create(groupCatalog.Revision, command.GroupId, command.DisplayName, actor); } else { if (!groupCatalog.TryGetGroup(command.GroupId, out var group)) { return Fail(GroupMutationCode.GroupMissing, "group-missing", groupCatalog); } long now = _utcTicksProvider(); groupMutationResult = Apply(groupCatalog, group, actor, command, now); } if (!groupMutationResult.Success) { return From(groupMutationResult); } if (groupMutationResult.Code == GroupMutationCode.NoChange || groupMutationResult.Catalog == groupCatalog) { return From(groupMutationResult); } GroupWorldCommitResult groupWorldCommitResult; try { groupWorldCommitResult = _store.TryCommit(text, groupCatalog.Revision, groupMutationResult.Catalog); } catch { return Fail(GroupMutationCode.RevisionConflict, "group-store-commit-failed", groupCatalog); } if (groupWorldCommitResult == null || !groupWorldCommitResult.Success) { return Fail(GroupMutationCode.RevisionConflict, groupWorldCommitResult?.ReasonCode ?? "group-store-commit-failed", groupWorldCommitResult?.Current?.Catalog ?? groupCatalog); } return From(groupMutationResult); } internal static GroupCommandExecutionResult EvaluateIssued(GroupCatalog current, StableIdentity actor, GroupCommand command, long expectedCatalogRevision, long expectedGroupRevision, long nowUtcTicks) { if (current == null || actor == null || command == null || nowUtcTicks <= 0) { return Fail(GroupMutationCode.InvalidRequest, "group-command-invalid", current); } if (current.Revision != expectedCatalogRevision) { return Fail(GroupMutationCode.RevisionConflict, "group-catalog-revision-conflict", current); } GroupMutationResult result; if (command.Kind == GroupCommandKind.Create) { if (expectedGroupRevision != -1 || current.TryGetGroup(command.GroupId, out var _)) { return Fail(GroupMutationCode.RevisionConflict, "group-create-revision-conflict", current); } result = current.Create(expectedCatalogRevision, command.GroupId, command.DisplayName, actor); } else { if (expectedGroupRevision < 1 || !current.TryGetGroup(command.GroupId, out var group2) || group2.Revision != expectedGroupRevision) { return Fail(GroupMutationCode.RevisionConflict, "group-revision-conflict", current); } result = Apply(current, group2, actor, command, nowUtcTicks); } return From(result); } private static GroupMutationResult Apply(GroupCatalog catalog, GroupRecord group, StableIdentity actor, GroupCommand command, long now) { return command.Kind switch { GroupCommandKind.Rename => catalog.Rename(catalog.Revision, group.Id, group.Revision, actor, command.DisplayName), GroupCommandKind.Invite => catalog.Invite(catalog.Revision, group.Id, group.Revision, actor, command.Target, now, command.InvitationExpiresUtcTicks), GroupCommandKind.CancelInvitation => catalog.CancelInvitation(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.Accept => catalog.Accept(catalog.Revision, group.Id, group.Revision, actor, now), GroupCommandKind.Leave => catalog.Leave(catalog.Revision, group.Id, group.Revision, actor), GroupCommandKind.Remove => catalog.Remove(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.SetRole => catalog.SetRole(catalog.Revision, group.Id, group.Revision, actor, command.Target, command.Role), GroupCommandKind.TransferOwnership => catalog.TransferOwnership(catalog.Revision, group.Id, group.Revision, actor, command.Target), GroupCommandKind.Delete => catalog.Delete(catalog.Revision, group.Id, group.Revision, actor), _ => new GroupMutationResult(GroupMutationCode.InvalidRequest, "group-command-kind-invalid", catalog, group), }; } private static GroupCommandExecutionResult TryExactDesiredReplay(GroupCatalog catalog, StableIdentity actor, GroupCommand command) { if (command.Kind == GroupCommandKind.Create && catalog.TryGetGroup(command.GroupId, out var group) && string.Equals(group.DisplayName, command.DisplayName, StringComparison.Ordinal) && group.TryGetMember(actor, out var member) && member.Role == GroupRole.Owner) { return NoChange("group-create-already-applied", catalog, group); } if (command.Kind == GroupCommandKind.Delete && catalog.RetiredGroupIds.Any((RetiredGroupId value) => value.Id == command.GroupId)) { return NoChange("group-delete-already-applied", catalog, null); } if (!catalog.TryGetGroup(command.GroupId, out var group2)) { return null; } if (command.Kind == GroupCommandKind.Accept && group2.TryGetMember(actor, out var member2)) { return NoChange("group-accept-already-applied", catalog, group2); } if (command.Kind == GroupCommandKind.Leave && !group2.TryGetMember(actor, out member2)) { return NoChange("group-leave-already-applied", catalog, group2); } if (command.Kind == GroupCommandKind.TransferOwnership && command.Target != null && group2.TryGetMember(command.Target, out var member3) && member3.Role == GroupRole.Owner && group2.TryGetMember(actor, out member2)) { return NoChange("group-transfer-already-applied", catalog, group2); } return null; } private static GroupCommandExecutionResult From(GroupMutationResult result) { return new GroupCommandExecutionResult(result.Code, result.ReasonCode, result.Catalog, result.Group); } private static GroupCommandExecutionResult NoChange(string reason, GroupCatalog catalog, GroupRecord group) { return new GroupCommandExecutionResult(GroupMutationCode.NoChange, reason, catalog, group); } private static GroupCommandExecutionResult Fail(GroupMutationCode code, string reason, GroupCatalog catalog) { return new GroupCommandExecutionResult(code, reason, catalog, null); } } public enum ActiveGroupSelectionStatus : byte { Available = 1, NoneSelected, Stale, Ambiguous, GroupMissing, NotMember } public sealed class ActiveGroupSelection { public ActiveGroupSelectionStatus Status { get; } public string GroupId { get; } public string DisplayName { get; } public bool IsAvailable => Status == ActiveGroupSelectionStatus.Available; internal static ActiveGroupSelection None { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.NoneSelected); internal static ActiveGroupSelection Stale { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.Stale); internal static ActiveGroupSelection Ambiguous { get; } = new ActiveGroupSelection(ActiveGroupSelectionStatus.Ambiguous); internal ActiveGroupSelection(ActiveGroupSelectionStatus status, string groupId = "", string displayName = "") { if (!Enum.IsDefined(typeof(ActiveGroupSelectionStatus), status)) { throw new ArgumentOutOfRangeException("status"); } if (status == ActiveGroupSelectionStatus.Available && !GroupIdentity.IsCanonicalId(groupId)) { throw new ArgumentException("An available active Group requires an exact Group UUID.", "groupId"); } if (status != ActiveGroupSelectionStatus.Available && !string.IsNullOrEmpty(groupId)) { throw new ArgumentException("An unavailable active Group cannot expose an ID.", "groupId"); } Status = status; GroupId = groupId ?? string.Empty; DisplayName = displayName ?? string.Empty; } } public interface IActiveGroupSelectionService { ActiveGroupSelection Resolve(StableIdentity identity); } public enum GroupActiveSelectionReadState { Missing, Ready, Corrupt, Ambiguous, Unavailable } public sealed class GroupActiveSelectionReadResult { public GroupActiveSelectionReadState State { get; } public string ReasonCode { get; } public Guid GroupId { get; } public bool HasSelection { get { if (State == GroupActiveSelectionReadState.Ready) { return GroupId != Guid.Empty; } return false; } } internal GroupActiveSelectionReadResult(GroupActiveSelectionReadState state, string reasonCode, Guid groupId) { State = state; ReasonCode = reasonCode ?? string.Empty; GroupId = groupId; } } public interface IGroupActiveSelectionStore { GroupActiveSelectionReadResult Read(string worldScope, StableIdentity identity); bool TrySet(string worldScope, StableIdentity identity, Guid groupId, out string reasonCode); } public sealed class FileGroupActiveSelectionStore : IGroupActiveSelectionStore { private readonly struct Paths { internal string Primary { get; } internal string Temporary { get; } internal string Lock { get; } internal Paths(string primary, string temporary, string @lock) { Primary = primary; Temporary = temporary; Lock = @lock; } } private const uint Magic = 826361682u; private const ushort Schema = 1; private const int MaximumFileBytes = 2048; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); private readonly string _root; public FileGroupActiveSelectionStore(string root) { if (string.IsNullOrWhiteSpace(root)) { throw new ArgumentException("A storage root is required.", "root"); } _root = TrimTrailingSeparators(Path.GetFullPath(root)); if (string.Equals(_root, Path.GetPathRoot(_root), PathComparison())) { throw new ArgumentException("A filesystem root cannot be the active Group storage root.", "root"); } } public GroupActiveSelectionReadResult Read(string worldScope, StableIdentity identity) { if (identity == null) { return Unavailable("group-active-identity-missing"); } try { Paths paths = Resolve(worldScope, identity); if (!Directory.Exists(_root)) { return Missing(); } using (Acquire(paths.Lock)) { if (File.Exists(paths.Temporary)) { return Ambiguous("group-active-temporary-evidence"); } if (!File.Exists(paths.Primary)) { return Missing(); } Guid groupId; return TryDecode(ReadBounded(paths.Primary), worldScope, identity, out groupId) ? new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Ready, "group-active-ready", groupId) : Corrupt("group-active-corrupt"); } } catch (ArgumentException) { return Unavailable("group-active-request-invalid"); } catch (Exception exception) when (IsStorageFailure(exception)) { return Unavailable("group-active-read-unavailable"); } } public bool TrySet(string worldScope, StableIdentity identity, Guid groupId, out string reasonCode) { reasonCode = "group-active-write-unavailable"; if (identity == null) { reasonCode = "group-active-identity-missing"; return false; } try { Paths paths = Resolve(worldScope, identity); Directory.CreateDirectory(_root); using (Acquire(paths.Lock)) { byte[] array = Encode(worldScope, identity, groupId); using (FileStream fileStream = new FileStream(paths.Temporary, FileMode.Create, FileAccess.Write, FileShare.None, 4096, FileOptions.WriteThrough)) { fileStream.Write(array, 0, array.Length); fileStream.Flush(flushToDisk: true); } byte[] array2 = ReadBounded(paths.Temporary); if (!ExactEquals(array, array2) || !TryDecode(array2, worldScope, identity, out var groupId2) || groupId2 != groupId) { reasonCode = "group-active-staged-readback-failed"; return false; } if (File.Exists(paths.Primary)) { File.Replace(paths.Temporary, paths.Primary, null, ignoreMetadataErrors: true); } else { File.Move(paths.Temporary, paths.Primary); } if (!TryDecode(ReadBounded(paths.Primary), worldScope, identity, out var groupId3) || groupId3 != groupId || File.Exists(paths.Temporary)) { reasonCode = "group-active-commit-readback-failed"; return false; } reasonCode = ((groupId == Guid.Empty) ? "group-active-cleared" : "group-active-selected"); return true; } } catch (ArgumentException) { reasonCode = "group-active-request-invalid"; return false; } catch (Exception exception) when (IsStorageFailure(exception)) { reasonCode = "group-active-write-unavailable"; return false; } } internal string GetPrimaryPath(string worldScope, StableIdentity identity) { return Resolve(worldScope, identity).Primary; } private Paths Resolve(string worldScope, StableIdentity identity) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (identity == null) { throw new ArgumentNullException("identity"); } byte[] bytes = StrictUtf8.GetBytes(worldScope + "\n" + identity.CanonicalKey); string text; using (SHA256 sHA = SHA256.Create()) { byte[] array = sHA.ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(array.Length * 2); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2")); } text = stringBuilder.ToString(); } string text2 = Path.Combine(_root, text + ".active"); if (!IsExactChild(text2, _root)) { throw new ArgumentException("The active Group path escaped its trusted root."); } return new Paths(text2, text2 + ".tmp", text2 + ".lock"); } private static byte[] Encode(string worldScope, StableIdentity identity, Guid groupId) { worldScope = GroupIdentity.RequireWorldScope(worldScope); if (identity == null) { throw new ArgumentNullException("identity"); } byte[] array; using (MemoryStream memoryStream = new MemoryStream()) { using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(826361682u); binaryWriter.Write((ushort)1); WriteText(binaryWriter, worldScope, 128); WriteText(binaryWriter, identity.Authority, 64); WriteText(binaryWriter, identity.SubjectId, 1024); binaryWriter.Write(groupId.ToByteArray()); binaryWriter.Flush(); array = memoryStream.ToArray(); } byte[] array2; using (SHA256 sHA = SHA256.Create()) { array2 = sHA.ComputeHash(array); } byte[] array3 = new byte[array.Length + array2.Length]; Buffer.BlockCopy(array, 0, array3, 0, array.Length); Buffer.BlockCopy(array2, 0, array3, array.Length, array2.Length); if (array3.Length > 2048) { throw new InvalidDataException(); } return array3; } private static bool TryDecode(byte[] exact, string expectedWorld, StableIdentity expectedIdentity, out Guid groupId) { groupId = Guid.Empty; if (exact == null || exact.Length < 64 || exact.Length > 2048 || expectedIdentity == null) { return false; } int num = exact.Length - 32; byte[] array = new byte[num]; byte[] array2 = new byte[32]; Buffer.BlockCopy(exact, 0, array, 0, num); Buffer.BlockCopy(exact, num, array2, 0, array2.Length); byte[] right; using (SHA256 sHA = SHA256.Create()) { right = sHA.ComputeHash(array); } if (!ExactEquals(array2, right)) { return false; } try { using MemoryStream memoryStream = new MemoryStream(array, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 826361682 || binaryReader.ReadUInt16() != 1) { return false; } string a = ReadText(binaryReader, 128); string authority = ReadText(binaryReader, 64); string subjectId = ReadText(binaryReader, 1024); byte[] array3 = binaryReader.ReadBytes(16); if (array3.Length != 16 || memoryStream.Position != memoryStream.Length) { return false; } StableIdentity stableIdentity = new StableIdentity(authority, subjectId); if (!string.Equals(a, GroupIdentity.RequireWorldScope(expectedWorld), StringComparison.Ordinal) || !stableIdentity.Equals(expectedIdentity)) { return false; } groupId = new Guid(array3); return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is DecoderFallbackException) { return false; } } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length < 1 || bytes.Length > maximumBytes || bytes.Length > 65535) { throw new ArgumentOutOfRangeException("value"); } writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadUInt16(); if (num < 1 || num > maximumBytes || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); string text = StrictUtf8.GetString(array); if (!ExactEquals(array, StrictUtf8.GetBytes(text))) { throw new InvalidDataException(); } return text; } private static FileStream Acquire(string path) { return new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None, 1, FileOptions.WriteThrough); } private static byte[] ReadBounded(string path) { using FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.SequentialScan); if (fileStream.Length < 1 || fileStream.Length > 2048) { throw new InvalidDataException(); } byte[] array = new byte[(int)fileStream.Length]; int num; for (int i = 0; i < array.Length; i += num) { num = fileStream.Read(array, i, array.Length - i); if (num <= 0) { throw new EndOfStreamException(); } } return array; } private static bool ExactEquals(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } private static bool IsExactChild(string fullPath, string fullRoot) { if (Path.IsPathRooted(fullPath) && Path.IsPathRooted(fullRoot) && string.Equals(Path.GetDirectoryName(fullPath), fullRoot, PathComparison())) { return !string.IsNullOrEmpty(Path.GetFileName(fullPath)); } return false; } private static string TrimTrailingSeparators(string path) { string text = Path.GetPathRoot(path) ?? string.Empty; int num = path.Length; while (num > text.Length && (path[num - 1] == Path.DirectorySeparatorChar || path[num - 1] == Path.AltDirectorySeparatorChar)) { num--; } if (num != path.Length) { return path.Substring(0, num); } return path; } private static StringComparison PathComparison() { if (Path.DirectorySeparatorChar != '\\') { return StringComparison.Ordinal; } return StringComparison.OrdinalIgnoreCase; } private static bool IsStorageFailure(Exception exception) { if (!(exception is IOException) && !(exception is UnauthorizedAccessException) && !(exception is NotSupportedException)) { return exception is SecurityException; } return true; } private static GroupActiveSelectionReadResult Missing() { return new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Missing, "group-active-missing", Guid.Empty); } private static GroupActiveSelectionReadResult Corrupt(string reason) { return new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Corrupt, reason, Guid.Empty); } private static GroupActiveSelectionReadResult Ambiguous(string reason) { return new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Ambiguous, reason, Guid.Empty); } private static GroupActiveSelectionReadResult Unavailable(string reason) { return new GroupActiveSelectionReadResult(GroupActiveSelectionReadState.Unavailable, reason, Guid.Empty); } } public sealed class GroupActiveSelectionService : IActiveGroupSelectionService { private readonly object _gate = new object(); private readonly IGroupWorldStore _catalogs; private readonly IGroupActiveSelectionStore _selections; private readonly Func _worldScopeProvider; private StableIdentity _cachedIdentity; private ActiveGroupSelection _cached = ActiveGroupSelection.Stale; public GroupActiveSelectionService(IGroupWorldStore catalogs, IGroupActiveSelectionStore selections, Func worldScopeProvider) { _catalogs = catalogs ?? throw new ArgumentNullException("catalogs"); _selections = selections ?? throw new ArgumentNullException("selections"); _worldScopeProvider = worldScopeProvider ?? throw new ArgumentNullException("worldScopeProvider"); } public ActiveGroupSelection Resolve(StableIdentity identity) { if (identity == null) { return ActiveGroupSelection.Ambiguous; } string text; try { text = _worldScopeProvider() ?? string.Empty; } catch { return ActiveGroupSelection.Stale; } if (text.Length == 0) { lock (_gate) { return (_cachedIdentity != null && _cachedIdentity.Equals(identity)) ? _cached : ActiveGroupSelection.Stale; } } GroupActiveSelectionReadResult groupActiveSelectionReadResult = _selections.Read(text, identity); if (groupActiveSelectionReadResult.State == GroupActiveSelectionReadState.Missing || (groupActiveSelectionReadResult.State == GroupActiveSelectionReadState.Ready && !groupActiveSelectionReadResult.HasSelection)) { return ActiveGroupSelection.None; } if (groupActiveSelectionReadResult.State == GroupActiveSelectionReadState.Corrupt || groupActiveSelectionReadResult.State == GroupActiveSelectionReadState.Ambiguous) { return ActiveGroupSelection.Ambiguous; } if (groupActiveSelectionReadResult.State != GroupActiveSelectionReadState.Ready) { return ActiveGroupSelection.Stale; } GroupWorldReadResult groupWorldReadResult = _catalogs.Read(text); if (groupWorldReadResult == null || groupWorldReadResult.State == GroupWorldReadState.Unavailable) { return ActiveGroupSelection.Stale; } if (groupWorldReadResult.State == GroupWorldReadState.Corrupt || groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { return ActiveGroupSelection.Ambiguous; } if (groupWorldReadResult.State != GroupWorldReadState.Ready || groupWorldReadResult.Catalog == null || !groupWorldReadResult.Catalog.TryGetGroup(groupActiveSelectionReadResult.GroupId, out var group)) { return new ActiveGroupSelection(ActiveGroupSelectionStatus.GroupMissing); } if (!group.TryGetMember(identity, out var _)) { return new ActiveGroupSelection(ActiveGroupSelectionStatus.NotMember); } return new ActiveGroupSelection(ActiveGroupSelectionStatus.Available, group.IdText, group.DisplayName); } internal bool TrySetAuthoritative(StableIdentity identity, Guid groupId, out ActiveGroupSelection selection, out string reasonCode) { selection = ActiveGroupSelection.Stale; reasonCode = "group-world-unavailable"; string text; try { text = _worldScopeProvider() ?? string.Empty; } catch { reasonCode = "group-world-unavailable"; return false; } if (text.Length == 0 || !_selections.TrySet(text, identity, groupId, out reasonCode)) { return false; } selection = Resolve(identity); if (!(groupId == Guid.Empty)) { if (selection.IsAvailable) { return string.Equals(selection.GroupId, groupId.ToString("N"), StringComparison.Ordinal); } return false; } return selection.Status == ActiveGroupSelectionStatus.NoneSelected; } internal void SetClientCache(StableIdentity identity, ActiveGroupSelection selection) { lock (_gate) { _cachedIdentity = identity; _cached = selection ?? ActiveGroupSelection.Stale; } } internal void ClearClientCache() { lock (_gate) { _cachedIdentity = null; _cached = ActiveGroupSelection.Stale; } } } internal static class GroupQueryProtocol { internal const int MaximumWireBytes = 65536; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static bool TryDecodeRequest(byte[] payload, out byte kind, out Guid groupId, out string failureCode) { kind = 0; groupId = Guid.Empty; if (payload == null || (payload.Length != 1 && payload.Length != 17)) { failureCode = "group-query-shape-invalid"; return false; } kind = payload[0]; if (((kind == 1 || kind == 3) && payload.Length != 1) || (kind == 2 && payload.Length != 17) || kind < 1 || kind > 3) { failureCode = "group-query-kind-invalid"; return false; } if (kind == 2) { groupId = new Guid(payload.Skip(1).Take(16).ToArray()); } failureCode = "ok"; return true; } internal static byte[] EncodeResponse(string text) { byte[] bytes = StrictUtf8.GetBytes(text ?? string.Empty); if (bytes.Length > 65536) { throw new InvalidOperationException("The Group response exceeds its wire bound."); } return bytes; } } internal enum GroupFriendlyOperation : byte { List = 1, Active = 2, Members = 3, WhoAmI = 4, Create = 16, Select = 17, Invite = 18, Accept = 19, Leave = 20, Rename = 21, CancelInvitation = 22, Remove = 23, SetRole = 24, TransferOwnership = 25, Delete = 26 } internal sealed class GroupFriendlyRequest { internal GroupFriendlyOperation Operation { get; } internal string Primary { get; } internal string Secondary { get; } internal int Number { get; } internal Guid ProposedGroupId { get; } internal bool IsQuery => (int)Operation < 16; internal GroupFriendlyRequest(GroupFriendlyOperation operation, string primary = "", string secondary = "", int number = 0, Guid proposedGroupId = default(Guid)) { if (!Enum.IsDefined(typeof(GroupFriendlyOperation), operation)) { throw new ArgumentOutOfRangeException("operation"); } Operation = operation; Primary = FriendlyText(primary, 256, "primary"); Secondary = FriendlyText(secondary, 64, "secondary"); if (number < 0 || number > 720) { throw new ArgumentOutOfRangeException("number"); } Number = number; ProposedGroupId = proposedGroupId; ValidateShape(); } private void ValidateShape() { bool flag = Primary.Length != 0; bool flag2 = Secondary.Length != 0; switch (Operation) { case GroupFriendlyOperation.List: case GroupFriendlyOperation.Active: case GroupFriendlyOperation.Members: case GroupFriendlyOperation.WhoAmI: case GroupFriendlyOperation.Leave: case GroupFriendlyOperation.Delete: if (flag || flag2 || Number != 0 || ProposedGroupId != Guid.Empty) { throw new ArgumentException("This Group operation does not accept arguments."); } break; case GroupFriendlyOperation.Create: if (!flag || flag2 || Number != 0 || ProposedGroupId == Guid.Empty) { throw new ArgumentException("Create requires a name and proposed UUID."); } GroupIdentity.RequireDisplayName(Primary); break; case GroupFriendlyOperation.Select: case GroupFriendlyOperation.Accept: case GroupFriendlyOperation.Rename: case GroupFriendlyOperation.CancelInvitation: case GroupFriendlyOperation.Remove: case GroupFriendlyOperation.TransferOwnership: if (!flag || flag2 || Number != 0 || ProposedGroupId != Guid.Empty) { throw new ArgumentException("This Group operation requires one text argument."); } if (Operation == GroupFriendlyOperation.Rename) { GroupIdentity.RequireDisplayName(Primary); } break; case GroupFriendlyOperation.Invite: if (!flag || flag2 || Number < 1 || ProposedGroupId != Guid.Empty) { throw new ArgumentException("Invite requires a player and bounded lifetime."); } break; case GroupFriendlyOperation.SetRole: if (!flag || !flag2 || Number != 0 || ProposedGroupId != Guid.Empty || (!string.Equals(Secondary, "member", StringComparison.Ordinal) && !string.Equals(Secondary, "officer", StringComparison.Ordinal))) { throw new ArgumentException("Role requires a player and member/officer."); } break; default: throw new ArgumentOutOfRangeException("Operation"); } } private static string FriendlyText(string value, int maximumBytes, string parameter) { string text = value ?? string.Empty; if (text.Length == 0) { return string.Empty; } if (!string.Equals(text, text.Trim(), StringComparison.Ordinal) || !text.IsNormalized(NormalizationForm.FormC) || Encoding.UTF8.GetByteCount(text) > maximumBytes) { throw new ArgumentException("Group command text is not canonical.", parameter); } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { throw new ArgumentException("Group command text contains a control character.", parameter); } } return text; } } internal sealed class GroupFriendlyResponse { internal string Text { get; } internal ActiveGroupSelection Active { get; } internal GroupFriendlyResponse(string text, ActiveGroupSelection active) { Text = text ?? string.Empty; if (Encoding.UTF8.GetByteCount(Text) > 65280) { throw new ArgumentOutOfRangeException("text"); } Active = active ?? ActiveGroupSelection.Stale; } } internal static class GroupFriendlyProtocol { internal const int MaximumRequestBytes = 1024; internal const int MaximumResponseBytes = 65536; private const uint RequestMagic = 827410002u; private const uint ResponseMagic = 827541074u; private const ushort Schema = 1; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal static byte[] EncodeRequest(GroupFriendlyRequest request) { if (request == null) { throw new ArgumentNullException("request"); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(827410002u); binaryWriter.Write((ushort)1); binaryWriter.Write((byte)request.Operation); WriteText(binaryWriter, request.Primary, 256); WriteText(binaryWriter, request.Secondary, 64); binaryWriter.Write(request.Number); binaryWriter.Write(request.ProposedGroupId.ToByteArray()); binaryWriter.Flush(); byte[] array = memoryStream.ToArray(); if (array.Length > 1024) { throw new InvalidOperationException(); } return array; } internal static bool TryDecodeRequest(byte[] payload, out GroupFriendlyRequest request, out string failure) { request = null; failure = "group-friendly-request-invalid"; if (payload == null || payload.Length < 31 || payload.Length > 1024) { return false; } try { using MemoryStream memoryStream = new MemoryStream(payload, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 827410002 || binaryReader.ReadUInt16() != 1) { return false; } GroupFriendlyOperation operation = (GroupFriendlyOperation)binaryReader.ReadByte(); string primary = ReadText(binaryReader, 256); string secondary = ReadText(binaryReader, 64); int number = binaryReader.ReadInt32(); byte[] array = binaryReader.ReadBytes(16); if (array.Length != 16 || memoryStream.Position != memoryStream.Length) { return false; } request = new GroupFriendlyRequest(operation, primary, secondary, number, new Guid(array)); failure = "ok"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is DecoderFallbackException || ex is OverflowException) { request = null; return false; } } internal static byte[] EncodeResponse(GroupFriendlyResponse response) { if (response == null) { throw new ArgumentNullException("response"); } using MemoryStream memoryStream = new MemoryStream(); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(827541074u); binaryWriter.Write((ushort)1); binaryWriter.Write((byte)response.Active.Status); WriteText(binaryWriter, response.Active.GroupId, 32); WriteText(binaryWriter, response.Active.DisplayName, 64); WriteText(binaryWriter, response.Text, 65408); binaryWriter.Flush(); byte[] array = memoryStream.ToArray(); if (array.Length > 65536) { throw new InvalidOperationException(); } return array; } internal static bool TryDecodeResponse(byte[] payload, out GroupFriendlyResponse response, out string failure) { response = null; failure = "group-friendly-response-invalid"; if (payload == null || payload.Length < 13 || payload.Length > 65536) { return false; } try { using MemoryStream memoryStream = new MemoryStream(payload, writable: false); using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 827541074 || binaryReader.ReadUInt16() != 1) { return false; } ActiveGroupSelectionStatus status = (ActiveGroupSelectionStatus)binaryReader.ReadByte(); string groupId = ReadText(binaryReader, 32); string displayName = ReadText(binaryReader, 64); string text = ReadText(binaryReader, 65408); if (memoryStream.Position != memoryStream.Length) { return false; } response = new GroupFriendlyResponse(text, new ActiveGroupSelection(status, groupId, displayName)); failure = "ok"; return true; } catch (Exception ex) when (ex is ArgumentException || ex is IOException || ex is DecoderFallbackException || ex is OverflowException) { response = null; return false; } } private static void WriteText(BinaryWriter writer, string value, int maximumBytes) { byte[] bytes = StrictUtf8.GetBytes(value ?? string.Empty); if (bytes.Length > maximumBytes || bytes.Length > 65535) { throw new ArgumentOutOfRangeException("value"); } writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximumBytes) { int num = reader.ReadUInt16(); if (num > maximumBytes || num > reader.BaseStream.Length - reader.BaseStream.Position) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } string text = StrictUtf8.GetString(array); if (!StrictUtf8.GetBytes(text).SequenceEqual(array)) { throw new InvalidDataException(); } return text; } } } namespace RunicPortals { internal static class PortalConfig { private static ConfigFile _file; internal static ConfigEntry Enabled { get; private set; } internal static ConfigEntry UniversalRouting { get; private set; } internal static ConfigEntry ShowSetupPanel { get; private set; } internal static ConfigEntry PanelScale { get; private set; } internal static ConfigEntry EditRangeMeters { get; private set; } internal static ConfigEntry IndexRefreshSeconds { get; private set; } internal static ConfigEntry MaximumEndpoints { get; private set; } internal static ConfigEntry DirectoryPageSize { get; private set; } internal static ConfigEntry ReturnRouteMinutes { get; private set; } internal static ConfigEntry OneWayAcknowledgementSeconds { get; private set; } internal static ConfigEntry VerboseLogging { get; private set; } internal static event Action Changed; internal static void Bind(ConfigFile file) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Expected O, but got Unknown //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Expected O, but got Unknown //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_013e: Expected O, but got Unknown //IL_0165: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Expected O, but got Unknown //IL_0193: Unknown result type (might be due to invalid IL or missing references) //IL_019d: Expected O, but got Unknown //IL_01c1: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Expected O, but got Unknown _file = file ?? throw new ArgumentNullException("file"); Enabled = file.Bind("General", "Enabled", true, "Master switch. Standard Pair portals always yield to vanilla behavior."); UniversalRouting = file.Bind("Features", "UniversalRouting", true, "Enable authenticated public/private/Group network routing for solo, local-host, and compatible dedicated-server sessions."); ShowSetupPanel = file.Bind("Display", "ShowSetupPanel", true, "Show the complete state-aware setup guide while aiming at a portal and while the Runic editor is open."); PanelScale = file.Bind("Display", "PanelScale", 1f, new ConfigDescription("Scale of the portal setup guide.", (AcceptableValueBase)(object)new AcceptableValueRange(0.75f, 1.5f), Array.Empty())); EditRangeMeters = file.Bind("Authority", "EditRangeMeters", 5f, new ConfigDescription("Maximum range for a portal-mode mutation.", (AcceptableValueBase)(object)new AcceptableValueRange(2f, 5f), Array.Empty())); IndexRefreshSeconds = file.Bind("Performance", "IndexRefreshSeconds", 2f, new ConfigDescription("Server-side interval for a bounded portal snapshot refresh.", (AcceptableValueBase)(object)new AcceptableValueRange(0.5f, 30f), Array.Empty())); MaximumEndpoints = file.Bind("Performance", "MaximumNetworkEndpoints", 1024, new ConfigDescription("Fail-closed graph cap. No partial graph is published when exceeded.", (AcceptableValueBase)(object)new AcceptableValueRange(16, 2048), Array.Empty())); DirectoryPageSize = file.Bind("Directory", "CyclePageSize", 32, new ConfigDescription("Maximum authorized destinations returned by a bounded non-map directory request; the walk-in map picker uses the protocol cap.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 128), Array.Empty())); ReturnRouteMinutes = file.Bind("Routes", "ReturnRouteMinutes", 15, new ConfigDescription("Session-only per-traveler Return option lifetime.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 120), Array.Empty())); OneWayAcknowledgementSeconds = file.Bind("Routes", "OneWayAcknowledgementSeconds", 10, new ConfigDescription("Fallback one-way confirmation window. Clicking a one-way destination marker acknowledges it immediately.", (AcceptableValueBase)(object)new AcceptableValueRange(3, 30), Array.Empty())); VerboseLogging = file.Bind("Diagnostics", "VerboseLogging", false, "Log bounded decision codes. Portal names, coordinates, and inventory contents are never logged."); file.SettingChanged += OnSettingChanged; } internal static void Unbind() { if (_file != null) { _file.SettingChanged -= OnSettingChanged; } _file = null; Enabled = null; UniversalRouting = null; ShowSetupPanel = null; PanelScale = null; EditRangeMeters = null; IndexRefreshSeconds = null; MaximumEndpoints = null; DirectoryPageSize = null; ReturnRouteMinutes = null; OneWayAcknowledgementSeconds = null; VerboseLogging = null; } private static void OnSettingChanged(object sender, SettingChangedEventArgs args) { PortalConfig.Changed?.Invoke(); } } internal static class Diagnostics { private static ManualLogSource _log; internal static void Initialize(ManualLogSource log) { _log = log; } internal static void Info(string text) { ManualLogSource log = _log; if (log != null) { log.LogInfo((object)text); } } internal static void Warning(string text) { ManualLogSource log = _log; if (log != null) { log.LogWarning((object)text); } } internal static void Error(string text) { ManualLogSource log = _log; if (log != null) { log.LogError((object)text); } } internal static void Error(Exception exception, string text) { ManualLogSource log = _log; if (log != null) { log.LogError((object)(text + " " + exception.GetType().Name + ": " + exception.Message)); } } internal static void Trace(string text) { if (PortalConfig.VerboseLogging != null && PortalConfig.VerboseLogging.Value) { ManualLogSource log = _log; if (log != null) { log.LogDebug((object)text); } } } } [BepInPlugin("chazman.RunicPortals", "Runic Portals", "1.1.3")] public sealed class Plugin : BaseUnityPlugin { public const string Guid = "chazman.RunicPortals"; public const string Name = "Runic Portals"; public const string Version = "1.1.3"; private Harmony _harmony; private CorrelatedDiagnosticBuffer _diagnostics; private PortalGroupRuntime _groups; private bool _configurationSubscribed; private bool _shuttingDown; internal static bool RuntimeReady { get; private set; } internal static PortalRuntime CurrentRuntime { get; private set; } internal static void DisableAfterPatchFault(Exception exception, string context) { if (!RuntimeReady) { return; } RuntimeReady = false; Diagnostics.Error(exception, "Runic Portals " + context + " hook faulted and was disabled for this session."); try { CurrentRuntime?.Shutdown(); } catch (Exception exception2) { Diagnostics.Error(exception2, "Portal runtime fault cleanup failed."); } } private void Awake() { //IL_00a5: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Expected O, but got Unknown Diagnostics.Initialize(((BaseUnityPlugin)this).Logger); PortalConfig.Bind(((BaseUnityPlugin)this).Config); PortalConfig.Changed += OnConfigurationChanged; _configurationSubscribed = true; try { if (!ValheimContracts.Initialize(out var problem)) { throw new MissingMethodException(problem); } _diagnostics = new CorrelatedDiagnosticBuffer(256); _groups = new PortalGroupRuntime(((BaseUnityPlugin)this).Logger); _groups.Initialize(); GroupIntegrationApi.Attach(_groups); CurrentRuntime = new PortalRuntime(_groups, new PortalAuthorityGate(), _diagnostics, new PortalOverwriteConfirmationGate()); CurrentRuntime.Initialize(); _harmony = new Harmony("chazman.RunicPortals"); _harmony.PatchAll(typeof(Plugin).Assembly); RuntimeReady = true; ((BaseUnityPlugin)this).Logger.LogInfo((object)"Runic Portals v1.1.3 ready for Valheim 0.221.12. Network portals use native local ownership; Group commands use one bounded session channel."); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Portals startup failed closed; vanilla portals remain available. " + ex.GetType().Name + ": " + ex.Message)); ShutdownRuntime(); } } private void Update() { if (!RuntimeReady || CurrentRuntime == null) { return; } try { _groups?.Tick(); CurrentRuntime.Tick(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogError((object)("Runic Portals was disabled for this session after a runtime fault: " + ex.GetType().Name + ": " + ex.Message)); ShutdownRuntime(); } } private void OnGUI() { if (!RuntimeReady || CurrentRuntime == null || Application.isBatchMode) { return; } try { CurrentRuntime.DrawHoverPanel(); } catch (Exception exception) { CurrentRuntime.DisableHoverPanel(exception); } try { CurrentRuntime.DrawMapPickerOverlay(); } catch (Exception exception2) { CurrentRuntime.FailMapPickerUi(exception2); } } private void OnConfigurationChanged() { if (!RuntimeReady || CurrentRuntime == null) { return; } try { CurrentRuntime.OnConfigurationChanged(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Portal configuration refresh failed; the previous bounded settings remain active. " + ex.GetType().Name + ": " + ex.Message)); } } private void OnDestroy() { ShutdownRuntime(); if (_configurationSubscribed) { PortalConfig.Changed -= OnConfigurationChanged; _configurationSubscribed = false; } PortalConfig.Unbind(); Diagnostics.Initialize(null); } private void ShutdownRuntime() { if (_shuttingDown) { return; } _shuttingDown = true; RuntimeReady = false; try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Portal patch cleanup failed: " + ex.Message)); } _harmony = null; try { CurrentRuntime?.Shutdown(); } catch (Exception ex2) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Portal runtime cleanup failed: " + ex2.Message)); } CurrentRuntime = null; GroupIntegrationApi.Detach(_groups); try { _groups?.Dispose(); } catch (Exception ex3) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Group runtime cleanup failed: " + ex3.Message)); } _groups = null; _diagnostics = null; _shuttingDown = false; } } } namespace RunicPortals.Integration { [HarmonyPatch(typeof(TeleportWorld), "Awake")] internal static class TeleportWorldAwakePatch { private static void Postfix(TeleportWorld __instance) { try { if (Plugin.RuntimeReady) { Plugin.CurrentRuntime?.Observe(__instance); } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "portal-observation"); } } } [HarmonyPatch(typeof(TeleportWorld), "Interact", new Type[] { typeof(Humanoid), typeof(bool), typeof(bool) })] internal static class TeleportWorldInteractPatch { private static bool Prefix(TeleportWorld __instance, Humanoid human, bool hold, bool alt, ref bool __result) { try { if (!Plugin.RuntimeReady || Plugin.CurrentRuntime == null || !Plugin.CurrentRuntime.TryHandleInteract(__instance, human, hold, alt, out var result)) { return true; } __result = result; return false; } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "interaction"); __result = false; return false; } } } [HarmonyPatch(typeof(TeleportWorld), "GetHoverText")] internal static class TeleportWorldHoverPatch { private static void Postfix(TeleportWorld __instance, ref string __result) { try { if (Plugin.RuntimeReady && Plugin.CurrentRuntime != null) { Plugin.CurrentRuntime.NoteHoveredPortal(__instance); if (Plugin.CurrentRuntime.TryGetHoverText(__instance, out var text)) { __result = text; } } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "hover-text"); } } } [HarmonyPatch(typeof(TeleportWorld), "HaveTarget")] internal static class TeleportWorldHaveTargetPatch { private static void Postfix(TeleportWorld __instance, ref bool __result) { try { if (Plugin.RuntimeReady && Plugin.CurrentRuntime != null) { __result = Plugin.CurrentRuntime.ResolvePortalVisualState(__instance, __result, requireResolvedTarget: false); } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "target-presence"); } } } [HarmonyPatch(typeof(TeleportWorld), "TargetFound")] internal static class TeleportWorldTargetFoundPatch { private static void Postfix(TeleportWorld __instance, ref bool __result) { try { if (Plugin.RuntimeReady && Plugin.CurrentRuntime != null) { __result = Plugin.CurrentRuntime.ResolvePortalVisualState(__instance, __result, requireResolvedTarget: true); } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "target-resolution"); } } } [HarmonyPatch(typeof(Game), "FindRandomUnconnectedPortal", new Type[] { typeof(List), typeof(ZDO), typeof(string) })] internal static class VanillaPortalCandidateBoundaryPatch { [HarmonyPrefix] [HarmonyPriority(800)] private static bool Prefix(ref List __0, ZDO __1) { try { if (Plugin.RuntimeReady && Plugin.CurrentRuntime != null && Plugin.CurrentRuntime.TryFilterVanillaPortalCandidates(__1, __0, out var filtered)) { __0 = filtered; } return true; } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "vanilla-pair-boundary"); __0 = new List(); return true; } } } [HarmonyPatch(typeof(TeleportWorld), "Teleport", new Type[] { typeof(Player) })] internal static class TeleportWorldTeleportPatch { private static bool Prefix(TeleportWorld __instance, Player player) { try { if (!Plugin.RuntimeReady || Plugin.CurrentRuntime == null || !Plugin.CurrentRuntime.TryHandleTeleport(__instance, player, out var handled)) { return true; } return !handled; } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "travel-commit"); return false; } } } [HarmonyPatch(typeof(Player), "CanMove")] internal static class PortalMapPickerPlayerMovementPatch { private static void Postfix(Player __instance, ref bool __result) { try { if (Plugin.RuntimeReady) { PortalRuntime currentRuntime = Plugin.CurrentRuntime; if (currentRuntime != null && currentRuntime.BlocksPlayerMovement(__instance)) { __result = false; } } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "map-picker-movement"); __result = false; } } } [HarmonyPatch(typeof(Minimap), "OnMapLeftClick", new Type[] { })] [HarmonyAfter(new string[] { "chazman.RunicExploration" })] internal static class PortalMapPickerLeftClickPatch { private static bool Prefix() { try { return !PortalMapPickerHarmonyGuard.TryClick(); } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "map-picker-left-click"); return false; } } } [HarmonyPatch(typeof(Minimap), "OnMapDblClick", new Type[] { })] internal static class PortalMapPickerDoubleClickPatch { private static bool Prefix() { return !PortalMapPickerHarmonyGuard.Blocks("double-click"); } } [HarmonyPatch(typeof(Minimap), "OnMapMiddleClick", new Type[] { typeof(UIInputHandler) })] internal static class PortalMapPickerMiddleClickPatch { private static bool Prefix() { return !PortalMapPickerHarmonyGuard.Blocks("middle-click"); } } [HarmonyPatch(typeof(Minimap), "OnMapRightClick", new Type[] { typeof(UIInputHandler) })] internal static class PortalMapPickerRightClickPatch { private static bool Prefix() { return !PortalMapPickerHarmonyGuard.Blocks("right-click"); } } [HarmonyPatch(typeof(Minimap), "ShowPinNameInput", new Type[] { typeof(Vector3) })] internal static class PortalMapPickerShowPinNamePatch { private static bool Prefix() { //IL_001d: Unknown result type (might be due to invalid IL or missing references) return !PortalMapPickerHarmonyGuard.TryClick(new Vector3((float)Screen.width * 0.5f, (float)Screen.height * 0.5f, 0f)); } } [HarmonyPatch(typeof(Minimap), "RemovePin", new Type[] { typeof(Vector3), typeof(float) })] internal static class PortalMapPickerRemoveAtPositionPatch { private static bool Prefix(ref bool __result) { if (!PortalMapPickerHarmonyGuard.Blocks("remove-pin")) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(Minimap), "GetClosestPin", new Type[] { typeof(Vector3), typeof(float), typeof(bool) })] internal static class PortalMapPickerClosestPinPatch { private static bool Prefix(ref PinData __result) { if (!PortalMapPickerHarmonyGuard.Blocks("closest-pin")) { return true; } __result = null; return false; } } internal static class PortalMapPickerHarmonyGuard { internal static bool Blocks(string operation) { try { return Plugin.RuntimeReady && (Plugin.CurrentRuntime?.BlocksModalMapMutation ?? false); } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "map-picker-" + operation); return true; } } internal static bool TryClick() { try { return Plugin.RuntimeReady && (Plugin.CurrentRuntime?.TryHandleMapPickerClick() ?? false); } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "map-picker-left-click"); return true; } } internal static bool TryClick(Vector3 screenPoint) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) try { return Plugin.RuntimeReady && (Plugin.CurrentRuntime?.TryHandleMapPickerClick(screenPoint) ?? false); } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "map-picker-gamepad-click"); return true; } } } [HarmonyPatch(typeof(TextInput), "Hide")] internal static class TextInputHidePatch { private static void Postfix(TextInput __instance) { try { if (Plugin.RuntimeReady) { Plugin.CurrentRuntime?.OnTextInputHidden(__instance); } } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "text-cancel"); } } } internal sealed class PortalRuntime : IPortalDirectoryService, IPortalRoutePlanner, IPortalStatusService { private sealed class PickerCandidate { internal string PortalId; internal string DisplayName; internal string Label; internal long Revision; internal Vector3 Position; internal bool OneWay; internal PinData Pin; } private sealed class PickerSession { internal TeleportWorld Source; internal int SourceInstanceId; internal string SourcePortalId; internal long SourceRevision; internal string NetworkId; internal Player Player; internal long PlayerId; internal Rigidbody Body; internal RigidbodyConstraints OriginalConstraints; internal Minimap Map; internal MapMode OriginalMapMode; internal bool MapViewCaptured; internal float OriginalLargeZoom; internal Vector3 OriginalMapOffset; internal bool IconFilterCaptured; internal bool IconFilterWasVisible; internal PickerCandidate Pending; internal string DirectoryRequestId = string.Empty; internal ZDOID DirectorySourceZdoId; internal long DirectorySourceRevision; internal float DirectoryDeadline; internal float DirectoryNextAttempt; internal int DirectoryAttempts; internal bool DirectoryResponseReceived; internal int DirectoryExpectedCount; internal string DirectoryFailure = string.Empty; internal List DirectoryLocallyKnown; internal bool DirectoryLocallyTruncated; internal readonly List Candidates = new List(); } private sealed class EditSession { internal TeleportWorld Portal; internal int InstanceId; internal long ActorId; internal ZDOID PortalId; internal int Schema; internal int Mode; internal int Revision; internal PortalEditEvidence Evidence; internal long Token; } private sealed class PortalEditReceiver : TextReceiver { private readonly PortalRuntime _runtime; private readonly long _token; internal PortalEditReceiver(PortalRuntime runtime, long token) { _runtime = runtime; _token = token; } public string GetText() { return string.Empty; } public void SetText(string text) { try { _runtime?.ConsumeEdit(_token, text); } catch (Exception exception) { Plugin.DisableAfterPatchFault(exception, "text-commit"); } } } private sealed class CycleCandidate { internal string PortalId; internal string Label; internal string DisplayName; internal long SourceRevision; internal long DestinationRevision; internal bool IsReturn; } private const string DirectoryRequestRpc = "RunicPortals.Directory.Request.v1"; private const string DirectoryResponseRpc = "RunicPortals.Directory.Response.v1"; private const string MapDirectoryRequestRpc = "RunicPortals.MapDirectory.Request.v1"; private const string MapDirectoryResponseRpc = "RunicPortals.MapDirectory.Response.v1"; private const int LegacyDirectoryWireSchema = 1; private const int DirectoryWireSchema = 2; private const int MapDirectoryWireSchema = 1; private const int DirectoryTerminalMarker = 1347568689; private const int MapDirectoryTerminalMarker = 1347570993; private const int MaximumDirectoryEnvelopeBytes = 2048; private const int MaximumDirectoryEndpointsSent = 512; private const int MaximumDirectoryAttempts = 8; private const float MaximumDirectoryRequestDistanceMeters = 16f; private const float DirectoryRequestTimeoutSeconds = 8f; private const float DirectoryRetrySeconds = 0.75f; private const float DirectoryArrivalPollSeconds = 0.1f; private const float MapDirectoryRefreshSeconds = 15f; private ZRoutedRpc _directoryRegisteredRpc; private string _mapDirectoryContext = string.Empty; private string _mapDirectoryRequestId = string.Empty; private float _nextMapDirectoryRequest; private PortalHoverPanel _hoverPanel; private static readonly PortalMapCandidate[] EmptyMapCandidates = Array.Empty(); private PortalMapOverlayRuntime _mapOverlay; private string _mapOverlayContext = string.Empty; private float _nextMapOverlayRefresh; private const float MinimumPinHitRadiusPixels = 22f; private static readonly FieldInfo LargeZoomField = AccessTools.Field(typeof(Minimap), "m_largeZoom"); private static readonly FieldInfo MaximumZoomField = AccessTools.Field(typeof(Minimap), "m_maxZoom"); private static readonly FieldInfo MapOffsetField = AccessTools.Field(typeof(Minimap), "m_mapOffset"); private static readonly FieldInfo VisibleIconTypesField = AccessTools.Field(typeof(Minimap), "m_visibleIconTypes"); private static readonly MethodInfo ToggleIconFilterMethod = AccessTools.Method(typeof(Minimap), "ToggleIconFilter", new Type[1] { typeof(PinType) }, (Type[])null); private PickerSession _mapPicker; private GUIStyle _pickerBoxStyle; private GUIStyle _pickerTitleStyle; private GUIStyle _pickerBodyStyle; private readonly PortalPermissionAdapter _permissions; private readonly PortalGroupRuntime _groups; private readonly PortalAuthorityGate _authority; private readonly CorrelatedDiagnosticBuffer _diagnostics; private readonly PortalGraphService _graph; private readonly PortalIndex _index; private readonly ReturnRouteStore _returns; private readonly PortalSelectionStore _selections; private readonly PortalOverwriteConfirmationGate _confirmations; private readonly OneWayAcknowledgementStore _oneWay = new OneWayAcknowledgementStore(); private readonly PortalArrivalSuppression _arrivalSuppression = new PortalArrivalSuppression(); private readonly Dictionary _instancePortalIds = new Dictionary(); private readonly Dictionary _hoverCache = new Dictionary(); private EditSession _edit; private bool _ready; private string _disabledReason = string.Empty; private float _nextReturnPrune; private long _editSequence; internal TeleportWorld ActiveEditPortalForPanel => _edit?.Portal; internal bool MapPickerActive => _mapPicker != null; internal bool BlocksModalMapMutation => _mapPicker != null; public bool FeatureEnabled { get { if (_ready && PortalConfig.Enabled != null && PortalConfig.Enabled.Value && PortalConfig.UniversalRouting != null) { return PortalConfig.UniversalRouting.Value; } return false; } } public bool RuntimeReady => _ready; public bool LocalHostMutationAvailable { get { if (FeatureEnabled) { return ValheimContracts.HasLocalPlayerAuthority; } return false; } } public bool DedicatedMutationTransportAvailable => false; public int IndexedEndpointCount => _graph.Count; public string DisabledReason => _disabledReason; private void TickDirectorySyncTransport() { ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null && _directoryRegisteredRpc != instance) { instance.Register("RunicPortals.Directory.Request.v1", (Action)ReceiveDirectoryRequest); instance.Register("RunicPortals.Directory.Response.v1", (Action)ReceiveDirectoryResponse); instance.Register("RunicPortals.MapDirectory.Request.v1", (Action)ReceiveMapDirectoryRequest); instance.Register("RunicPortals.MapDirectory.Response.v1", (Action)ReceiveMapDirectoryResponse); _directoryRegisteredRpc = instance; } } private void ShutdownDirectorySyncTransport() { _directoryRegisteredRpc = null; _mapDirectoryContext = string.Empty; _mapDirectoryRequestId = string.Empty; _nextMapDirectoryRequest = 0f; } private bool BeginDirectorySync(PickerSession session, PortalEndpoint source, List locallyKnown, bool locallyTruncated) { //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if (session == null || source == null || (Object)(object)instance == (Object)null || instance.IsServer()) { return false; } TickDirectorySyncTransport(); ZNetPeer serverPeer = instance.GetServerPeer(); if (_directoryRegisteredRpc == null || serverPeer == null || !serverPeer.IsReady()) { return false; } ZNetView val = (((Object)(object)session.Source == (Object)null) ? null : ((Component)session.Source).GetComponent()); ZDO val2 = (((Object)(object)val != (Object)null && val.IsValid()) ? val.GetZDO() : null); if (val2 == null || !val2.IsValid() || ((ZDOID)(ref val2.m_uid)).IsNone()) { return false; } session.DirectoryRequestId = Guid.NewGuid().ToString("N"); session.DirectorySourceZdoId = val2.m_uid; session.DirectorySourceRevision = source.Revision; session.DirectoryDeadline = Time.realtimeSinceStartup + 8f; session.DirectoryNextAttempt = Time.realtimeSinceStartup; session.DirectoryAttempts = 0; session.DirectoryResponseReceived = false; session.DirectoryExpectedCount = 0; session.DirectoryFailure = string.Empty; session.DirectoryLocallyKnown = locallyKnown ?? new List(); session.DirectoryLocallyTruncated = locallyTruncated; SendDirectoryRequest(session, instance, serverPeer); Message(session.Player, "Loading authorized portals from the server..."); return true; } private void SendDirectoryRequest(PickerSession session, ZNet network, ZNetPeer server) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) if (session != null && !((Object)(object)network == (Object)null) && server != null && server.IsReady() && _directoryRegisteredRpc != null && session.DirectoryAttempts < 8) { ZPackage val = WriteDirectoryRequest(session.DirectoryRequestId, session.DirectorySourceZdoId, session.SourcePortalId, session.DirectorySourceRevision, session.NetworkId); if (val.Size() <= 2048) { _directoryRegisteredRpc.InvokeRoutedRPC(server.m_uid, "RunicPortals.Directory.Request.v1", new object[1] { val }); session.DirectoryAttempts++; session.DirectoryNextAttempt = Time.realtimeSinceStartup + 0.75f; } } } private void ReceiveDirectoryRequest(long sender, ZPackage package) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Expected O, but got Unknown //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0118: 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_0174: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_017b: Unknown result type (might be due to invalid IL or missing references) //IL_0180: Unknown result type (might be due to invalid IL or missing references) //IL_019e: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_02e4: Unknown result type (might be due to invalid IL or missing references) //IL_0323: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if (!_ready || (Object)(object)instance == (Object)null || !instance.IsServer() || ZRoutedRpc.instance == null) { return; } byte[] array = null; try { array = ((package != null) ? package.GetArray() : null); } catch { } if (!TryReadDirectoryRequest((ZPackage)((array == null) ? ((object)package) : ((object)new ZPackage(array))), out var requestId, out var sourcePortalId, out var sourceZdoId, out var sourceRevision, out var networkId)) { if (array != null && TryReadLegacyDirectoryRequest(new ZPackage(array), out var requestId2)) { SentinelSecurityBridge.Report(sender, "portal-directory-protocol-outdated", requestId2, 2, "The server rejected a valid legacy portal-directory protocol; the client must update Runic Portals."); ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RunicPortals.Directory.Response.v1", new object[1] { WriteDirectoryResponse(1, requestId2, accepted: false, 0, "client-update-required") }); } else { SentinelSecurityBridge.Report(sender, "portal-directory-envelope-invalid", "portal-directory-envelope", 3, "The server rejected a malformed bounded portal-directory request."); } return; } if (!TryResolvePeerPlayer(sender, out var playerId, out var position)) { SentinelSecurityBridge.Report(sender, "portal-directory-identity-unbound", requestId, 2, "The directory request had no exact current transport-owned player."); return; } bool flag = false; string text = "source-unavailable"; int num = 0; _index.MarkDirty(); ZDOMan instance2 = ZDOMan.instance; ZDO val = ((instance2 != null) ? instance2.GetZDO(sourceZdoId) : null); PortalEndpoint endpoint; string failure; if (val == null || !val.IsValid() || val.m_uid != sourceZdoId) { text = "source-object-missing"; } else if (!string.Equals(((object)Unsafe.As(ref val.m_uid)/*cast due to .constrained prefix*/).ToString(), sourcePortalId, StringComparison.Ordinal)) { text = "source-id-mismatch"; } else if (!PortalZdoCodec.TryRead(val, out endpoint, out failure)) { text = "source-record-invalid"; } else { Vector3 val2 = val.GetPosition() - position; bool evidencePending; PortalEndpoint endpoint2; if (((Vector3)(ref val2)).sqrMagnitude > 256f) { text = "source-too-far"; } else if (!ValheimContracts.SourceWardAllows(val.GetPosition(), playerId, out evidencePending)) { text = (evidencePending ? "source-ward-unavailable" : "source-ward-denied"); } else if (!_index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f)) { text = "directory-index-unavailable"; } else if (!_graph.TryGetEndpoint(endpoint.PortalId, out endpoint2)) { text = "source-not-indexed"; } else { if (endpoint2.Revision != sourceRevision || !string.Equals(endpoint2.NetworkId, networkId, StringComparison.Ordinal)) { ZDOMan.instance.ForceSendZDO(sender, val.m_uid); } string travelerStableId = PortalPermissionAdapter.Identity(playerId); PortalDirectoryResult portalDirectoryResult = _graph.Query(new PortalDirectoryQuery(travelerStableId, endpoint2.PortalId, endpoint2.NetworkId, string.Empty, Math.Min(128, 512))); if (portalDirectoryResult.StopCode == RouteStopCode.Ready) { flag = true; text = "ok"; for (int i = 0; i < portalDirectoryResult.Entries.Count; i++) { if (num >= 512) { break; } PortalDirectoryEntry portalDirectoryEntry = portalDirectoryResult.Entries[i]; if (_graph.TryGetEndpoint(portalDirectoryEntry.PortalId, out var endpoint3) && _index.TryGetZdo(portalDirectoryEntry.PortalId, out var zdo) && ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(zdo.GetPosition(), playerId)) && _permissions.Allows(endpoint3, travelerStableId, PortalAccessAction.ViewDiscover) && _permissions.Allows(endpoint3, travelerStableId, PortalAccessAction.Arrive)) { ZDOMan.instance.ForceSendZDO(sender, zdo.m_uid); num++; } } if (num == 0) { text = "none-authorized"; } } else { text = "directory-" + portalDirectoryResult.StopCode.ToString().ToLowerInvariant(); } } } if (!flag) { Diagnostics.Trace("Portal directory request rejected: " + text + "."); } ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RunicPortals.Directory.Response.v1", new object[1] { WriteDirectoryResponse(requestId, flag, num, text) }); } private void ReceiveDirectoryResponse(long sender, ZPackage package) { ZNet instance = ZNet.instance; PickerSession mapPicker = _mapPicker; if (!_ready || (Object)(object)instance == (Object)null || instance.IsServer() || mapPicker == null || !TryReadDirectoryResponse(package, out var requestId, out var accepted, out var count, out var reason) || !string.Equals(requestId, mapPicker.DirectoryRequestId, StringComparison.Ordinal)) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady() && serverPeer.m_uid == sender) { float realtimeSinceStartup = Time.realtimeSinceStartup; if (!accepted && string.Equals(reason, "source-ward-unavailable", StringComparison.Ordinal) && mapPicker.DirectoryAttempts < 8 && realtimeSinceStartup < mapPicker.DirectoryDeadline) { mapPicker.DirectoryResponseReceived = false; mapPicker.DirectoryExpectedCount = 0; mapPicker.DirectoryFailure = string.Empty; mapPicker.DirectoryNextAttempt = realtimeSinceStartup + 0.75f; } else { mapPicker.DirectoryResponseReceived = true; mapPicker.DirectoryExpectedCount = (accepted ? Math.Max(0, count) : 0); mapPicker.DirectoryFailure = (accepted ? string.Empty : reason); mapPicker.DirectoryNextAttempt = realtimeSinceStartup; } } } private void TickDirectoryPicker(PickerSession session) { if (session == null || string.IsNullOrEmpty(session.DirectoryRequestId)) { return; } float realtimeSinceStartup = Time.realtimeSinceStartup; if (!session.DirectoryResponseReceived) { if (realtimeSinceStartup >= session.DirectoryDeadline) { CompleteOrCancelDirectoryFallback(session, "Portal directory request timed out before the server responded."); } else if (realtimeSinceStartup >= session.DirectoryNextAttempt) { ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetServerPeer() : null); if ((Object)(object)instance != (Object)null && val != null) { SendDirectoryRequest(session, instance, val); } } } else if (!string.IsNullOrEmpty(session.DirectoryFailure)) { CompleteOrCancelDirectoryFallback(session, "Portal directory is unavailable (" + session.DirectoryFailure + ")."); } else if (session.DirectoryExpectedCount == 0) { CompleteOrCancelDirectoryFallback(session, "No authorized online arrival portals are available in network '" + session.NetworkId + "'."); } else { if (realtimeSinceStartup < session.DirectoryNextAttempt) { return; } session.DirectoryNextAttempt = realtimeSinceStartup + 0.1f; string failure = "The source portal changed while its directory was loading."; if (!TryReadVisibleEndpoint(session.Source, out var endpoint) || !TryBuildPickerCandidates(endpoint, session.Player, out var candidates, out var truncated, out failure)) { CompleteOrCancelDirectoryFallback(session, failure); } else if (candidates.Count >= session.DirectoryExpectedCount || !(realtimeSinceStartup < session.DirectoryDeadline)) { if (candidates.Count == 0) { CompleteOrCancelDirectoryFallback(session, "The authorized portal records did not arrive before the directory timed out."); return; } session.DirectoryRequestId = string.Empty; session.DirectoryLocallyKnown = null; CompletePicker(candidates, truncated || candidates.Count < session.DirectoryExpectedCount); } } } private void CompleteOrCancelDirectoryFallback(PickerSession session, string failure) { List list = session?.DirectoryLocallyKnown; bool truncated = session?.DirectoryLocallyTruncated ?? false; if (session != null) { session.DirectoryRequestId = string.Empty; session.DirectoryLocallyKnown = null; } if (list != null && list.Count != 0) { CompletePicker(list, truncated); } else { CancelMapPicker(failure, closeMap: true); } } private void TickMapDirectorySync(string context, float realtime) { ZNet instance = ZNet.instance; if (string.IsNullOrEmpty(context) || (Object)(object)instance == (Object)null || instance.IsServer()) { return; } TickDirectorySyncTransport(); ZNetPeer serverPeer = instance.GetServerPeer(); if (_directoryRegisteredRpc == null || serverPeer == null || !serverPeer.IsReady()) { return; } if (!string.Equals(_mapDirectoryContext, context, StringComparison.Ordinal)) { _mapDirectoryContext = context; _mapDirectoryRequestId = string.Empty; _nextMapDirectoryRequest = 0f; } if (!(realtime < _nextMapDirectoryRequest)) { _nextMapDirectoryRequest = realtime + 15f; _mapDirectoryRequestId = Guid.NewGuid().ToString("N"); ZPackage val = WriteMapDirectoryRequest(_mapDirectoryRequestId); if (val.Size() <= 2048) { _directoryRegisteredRpc.InvokeRoutedRPC(serverPeer.m_uid, "RunicPortals.MapDirectory.Request.v1", new object[1] { val }); } } } private void ReceiveMapDirectoryRequest(long sender, ZPackage package) { //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_0138: Unknown result type (might be due to invalid IL or missing references) ZNet instance = ZNet.instance; if (!_ready || (Object)(object)instance == (Object)null || !instance.IsServer() || ZRoutedRpc.instance == null) { return; } if (!TryReadMapDirectoryRequest(package, out var requestId)) { SentinelSecurityBridge.Report(sender, "portal-map-envelope-invalid", "portal-map-envelope", 3, "The server rejected a malformed bounded portal-map request."); return; } if (!TryResolvePeerPlayer(sender, out var playerId, out var _)) { SentinelSecurityBridge.Report(sender, "portal-map-identity-unbound", requestId, 2, "The portal-map request had no exact current transport-owned player."); return; } bool accepted = false; bool truncated = false; string reason = "directory-index-unavailable"; int num = 0; _index.MarkDirty(); if (_index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f)) { List list = ValheimContracts.PortalObjects(); if (list != null && list.Count <= 4096) { accepted = true; reason = "ok"; string travelerStableId = PortalPermissionAdapter.Identity(playerId); for (int i = 0; i < list.Count; i++) { ZDO val = list[i]; if (val == null || !val.IsValid() || ((ZDOID)(ref val.m_uid)).IsNone()) { continue; } bool flag = PortalZdoCodec.GetMode(val) == 0; if (!flag && PortalZdoCodec.TryRead(val, out var endpoint, out var _)) { flag = endpoint.OnlineState == PortalOnlineState.Online && ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(val.GetPosition(), playerId)) && _permissions.Allows(endpoint, travelerStableId, PortalAccessAction.ViewDiscover); } if (flag) { if (num >= 512) { truncated = true; break; } ZDOMan.instance.ForceSendZDO(sender, val.m_uid); num++; } } } } ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RunicPortals.MapDirectory.Response.v1", new object[1] { WriteMapDirectoryResponse(requestId, accepted, num, truncated, reason) }); } private void ReceiveMapDirectoryResponse(long sender, ZPackage package) { ZNet instance = ZNet.instance; if (!_ready || (Object)(object)instance == (Object)null || instance.IsServer() || !TryReadMapDirectoryResponse(package, out var requestId, out var accepted, out var _, out var truncated, out var reason)) { return; } ZNetPeer serverPeer = instance.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady() && serverPeer.m_uid == sender && string.Equals(requestId, _mapDirectoryRequestId, StringComparison.Ordinal)) { _mapDirectoryRequestId = string.Empty; if (!accepted) { Diagnostics.Trace("Portal map directory request rejected: " + reason + "."); } _nextMapOverlayRefresh = 0f; _mapOverlay?.SetRefreshState(loading: false, truncated, !accepted); } } private static ZPackage WriteMapDirectoryRequest(string requestId) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(requestId ?? string.Empty); val.Write(1347570993); return val; } private static bool TryReadMapDirectoryRequest(ZPackage package, out string requestId) { requestId = string.Empty; try { if (package == null || package.Size() < 1 || package.Size() > 2048 || package.ReadInt() != 1) { return false; } requestId = package.ReadString(); return CanonicalDirectoryRequestId(requestId) && package.ReadInt() == 1347570993 && package.GetPos() == package.Size(); } catch { return false; } } private static ZPackage WriteMapDirectoryResponse(string requestId, bool accepted, int count, bool truncated, string reason) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(requestId ?? string.Empty); val.Write(accepted); val.Write(Math.Max(0, Math.Min(512, count))); val.Write(truncated); val.Write(BoundedDirectoryReason(reason)); val.Write(1347570993); return val; } private static bool TryReadMapDirectoryResponse(ZPackage package, out string requestId, out bool accepted, out int count, out bool truncated, out string reason) { requestId = (reason = string.Empty); accepted = (truncated = false); count = 0; try { if (package == null || package.Size() < 1 || package.Size() > 2048 || package.ReadInt() != 1) { return false; } requestId = package.ReadString(); accepted = package.ReadBool(); count = package.ReadInt(); truncated = package.ReadBool(); reason = package.ReadString(); return CanonicalDirectoryRequestId(requestId) && count >= 0 && count <= 512 && reason.Length <= 64 && package.ReadInt() == 1347570993 && package.GetPos() == package.Size(); } catch { return false; } } private static bool TryResolvePeerPlayer(long sender, out long playerId, out Vector3 position) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: 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_0089: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c6: Unknown result type (might be due to invalid IL or missing references) playerId = 0L; position = Vector3.zero; ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(sender) : null); if ((Object)(object)instance == (Object)null || !instance.IsServer() || val == null || val.m_uid != sender || !val.IsReady() || ((ZDOID)(ref val.m_characterID)).IsNone() || ZDOMan.instance == null) { return false; } ZDO zDO = ZDOMan.instance.GetZDO(val.m_characterID); if (zDO == null || !zDO.IsValid() || zDO.GetOwner() != sender || ZDOMan.instance.GetZDO(val.m_characterID) != zDO) { return false; } ZNetScene instance2 = ZNetScene.instance; GameObject val2 = ((instance2 != null) ? instance2.GetPrefab(zDO.GetPrefab()) : null); playerId = zDO.GetLong(ZDOVars.s_playerID, 0L); position = zDO.GetPosition(); if ((Object)(object)val2 != (Object)null && (Object)(object)val2.GetComponent() != (Object)null && playerId > 0 && !float.IsNaN(position.x) && !float.IsInfinity(position.x) && !float.IsNaN(position.y) && !float.IsInfinity(position.y) && !float.IsNaN(position.z)) { return !float.IsInfinity(position.z); } return false; } private static ZPackage WriteDirectoryRequest(string requestId, ZDOID sourceZdoId, string sourcePortalId, long sourceRevision, string networkId) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_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) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(2); val.Write(requestId ?? string.Empty); val.Write(sourceZdoId); val.Write(sourcePortalId ?? string.Empty); val.Write(sourceRevision); val.Write(networkId ?? string.Empty); val.Write(1347568689); return val; } private static bool TryReadDirectoryRequest(ZPackage package, out string requestId, out string sourcePortalId, out ZDOID sourceZdoId, out long sourceRevision, out string networkId) { //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_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) requestId = (sourcePortalId = (networkId = string.Empty)); sourceZdoId = ZDOID.None; sourceRevision = -1L; try { if (package == null || package.Size() < 1 || package.Size() > 2048 || package.ReadInt() != 2) { return false; } requestId = package.ReadString(); sourceZdoId = package.ReadZDOID(); sourcePortalId = package.ReadString(); sourceRevision = package.ReadLong(); networkId = package.ReadString(); return CanonicalDirectoryRequestId(requestId) && !((ZDOID)(ref sourceZdoId)).IsNone() && sourcePortalId.Length > 0 && sourcePortalId.Length <= 96 && sourceRevision >= 0 && networkId.Length > 0 && networkId.Length <= 64 && package.ReadInt() == 1347568689 && package.GetPos() == package.Size(); } catch { return false; } } private static ZPackage WriteDirectoryResponse(string requestId, bool accepted, int count, string reason) { return WriteDirectoryResponse(2, requestId, accepted, count, reason); } private static ZPackage WriteDirectoryResponse(int wireSchema, string requestId, bool accepted, int count, string reason) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(wireSchema); val.Write(requestId ?? string.Empty); val.Write(accepted); val.Write(Math.Max(0, Math.Min(512, count))); val.Write(BoundedDirectoryReason(reason)); val.Write(1347568689); return val; } private static bool TryReadLegacyDirectoryRequest(ZPackage package, out string requestId) { requestId = string.Empty; try { if (package == null || package.Size() < 1 || package.Size() > 2048 || package.ReadInt() != 1) { return false; } requestId = package.ReadString(); string text = package.ReadString(); long num = package.ReadLong(); string text2 = package.ReadString(); return CanonicalDirectoryRequestId(requestId) && text.Length > 0 && text.Length <= 96 && num >= 0 && text2.Length > 0 && text2.Length <= 64 && package.ReadInt() == 1347568689 && package.GetPos() == package.Size(); } catch { requestId = string.Empty; return false; } } private static bool TryReadDirectoryResponse(ZPackage package, out string requestId, out bool accepted, out int count, out string reason) { requestId = (reason = string.Empty); accepted = false; count = 0; try { if (package == null || package.Size() < 1 || package.Size() > 2048 || package.ReadInt() != 2) { return false; } requestId = package.ReadString(); accepted = package.ReadBool(); count = package.ReadInt(); reason = package.ReadString(); return CanonicalDirectoryRequestId(requestId) && count >= 0 && count <= 512 && reason.Length <= 64 && package.ReadInt() == 1347568689 && package.GetPos() == package.Size(); } catch { return false; } } private static bool CanonicalDirectoryRequestId(string value) { if (value != null && value.Length == 32 && Guid.TryParseExact(value, "N", out var result)) { return string.Equals(result.ToString("N"), value, StringComparison.Ordinal); } return false; } private static string BoundedDirectoryReason(string value) { string text = (value ?? string.Empty).Replace("\r", string.Empty).Replace("\n", string.Empty).Trim(); if (text.Length > 64) { return text.Substring(0, 64); } return text; } private void InitializeHoverPanel() { if (!Application.isBatchMode) { _hoverPanel = new PortalHoverPanel(this); } } private void ShutdownHoverPanel() { _hoverPanel?.Dispose(); _hoverPanel = null; } internal void NoteHoveredPortal(TeleportWorld portal) { _hoverPanel?.Observe(portal); } internal void DrawHoverPanel() { _hoverPanel?.Draw(); } internal void DisableHoverPanel(Exception exception) { _hoverPanel?.Disable(exception); } private void RefreshHoverPanelConfiguration() { _hoverPanel?.ResetStyles(); } internal bool TryGetHoverPanelState(TeleportWorld portal, out PortalHoverPanelState state) { //IL_0077: 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_0120: Unknown result type (might be due to invalid IL or missing references) state = default(PortalHoverPanelState); if (!FeatureEnabled || (Object)(object)portal == (Object)null) { return false; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val == null || !val.IsValid() || ((ZDOID)(ref val.m_uid)).IsNone()) { return false; } bool editorOpen = _edit != null && _edit.InstanceId == ((Object)portal).GetInstanceID(); bool vanillaConnected = val.GetConnectionZDOID((ConnectionType)1) != ZDOID.None; if (PortalZdoCodec.GetMode(val) != 1) { state = new PortalHoverPanelState(isNetwork: false, detailsVisible: true, vanillaConnected, editorOpen, string.Empty, string.Empty, string.Empty, acceptsArrival: true, permitsDeparture: true, string.Empty); return true; } if (!PortalZdoCodec.TryRead(val, out var endpoint, out var _)) { return false; } Player localPlayer = Player.m_localPlayer; bool flag = false; string text = (((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() == 0L) ? string.Empty : PortalPermissionAdapter.Identity(localPlayer.GetPlayerID())); if ((Object)(object)localPlayer != (Object)null && localPlayer.GetPlayerID() != 0L) { flag = string.Equals(endpoint.OwnerStableId, text, StringComparison.Ordinal); if (!flag) { flag = ValheimContracts.WardAllows(ValheimContracts.ResolveWard(val.GetPosition(), localPlayer.GetPlayerID())) && _permissions.Allows(endpoint, text, PortalAccessAction.ViewDiscover); } } if (!flag) { state = new PortalHoverPanelState(isNetwork: true, detailsVisible: false, vanillaConnected, editorOpen, string.Empty, string.Empty, "Restricted", acceptsArrival: false, permitsDeparture: false, string.Empty); return true; } string selectedDestination = SelectedDestinationLabel(endpoint, ((object)Unsafe.As(ref val.m_uid)/*cast due to .constrained prefix*/).ToString()); state = new PortalHoverPanelState(isNetwork: true, detailsVisible: true, vanillaConnected, editorOpen, endpoint.DisplayName, endpoint.NetworkId, PolicyLabel(endpoint.NetworkKind), endpoint.AcceptsArrival, endpoint.PermitsDeparture, selectedDestination); return true; } private string SelectedDestinationLabel(PortalEndpoint source, string portalId) { if (source == null || string.IsNullOrEmpty(portalId) || (Object)(object)Player.m_localPlayer == (Object)null) { return string.Empty; } string text = PortalPermissionAdapter.Identity(Player.m_localPlayer.GetPlayerID()); if (!_selections.TryGet(text, portalId, out var selection)) { return string.Empty; } if (!_graph.TryGetEndpoint(selection.DestinationPortalId, out var endpoint)) { return string.Empty; } return endpoint.DisplayName + (selection.IsReturn ? " (Return)" : (HasVisibleDuplicate(source, text, endpoint.DisplayName) ? (" [" + endpoint.PortalId + "]") : string.Empty)); } private static string PolicyLabel(PortalNetworkKind kind) { return kind switch { PortalNetworkKind.Public => "Public", PortalNetworkKind.Personal => "Private", PortalNetworkKind.Group => "Group", _ => "Restricted", }; } private void InitializeMapOverlay() { _mapOverlay?.Shutdown(); _mapOverlay = new PortalMapOverlayRuntime(); _mapOverlayContext = string.Empty; _nextMapOverlayRefresh = 0f; } private void ShutdownMapOverlay() { _mapOverlay?.Shutdown(); _mapOverlay = null; _mapOverlayContext = string.Empty; _nextMapOverlayRefresh = 0f; } private void TickMapOverlay(float realtime, float interval) { PortalMapOverlayRuntime mapOverlay = _mapOverlay; if (mapOverlay == null) { return; } if (!TryGetMapOverlayContext(out var context)) { if (_mapOverlayContext.Length != 0) { _mapOverlayContext = string.Empty; mapOverlay.OnIdentityOrWorldChanged(string.Empty); } mapOverlay.Tick(Minimap.instance, MapPickerActive); return; } if (!string.Equals(_mapOverlayContext, context, StringComparison.Ordinal)) { _mapOverlayContext = context; mapOverlay.OnIdentityOrWorldChanged(context); _nextMapOverlayRefresh = 0f; } mapOverlay.Tick(Minimap.instance, MapPickerActive); if (!MapPickerActive && mapOverlay.IsMapOpen) { TickMapDirectorySync(context, realtime); } if (MapPickerActive || !mapOverlay.IsMapOpen || realtime < _nextMapOverlayRefresh) { return; } _nextMapOverlayRefresh = realtime + Math.Max(0.5f, interval); mapOverlay.SetRefreshState(loading: true, truncated: false); List candidates; bool flag = TryBuildLocalMapSnapshot(out candidates); if (!TryGetMapOverlayContext(out var context2) || !string.Equals(context, context2, StringComparison.Ordinal)) { mapOverlay.OnIdentityOrWorldChanged(context2 ?? string.Empty); return; } string contextToken = context; IReadOnlyList candidates2; if (!flag) { IReadOnlyList emptyMapCandidates = EmptyMapCandidates; candidates2 = emptyMapCandidates; } else { IReadOnlyList emptyMapCandidates = candidates; candidates2 = emptyMapCandidates; } mapOverlay.ReplaceAuthorizedSnapshot(contextToken, candidates2); mapOverlay.SetRefreshState(loading: false, truncated: false, !flag); } private bool TryGetMapOverlayContext(out string context) { context = string.Empty; Player localPlayer = Player.m_localPlayer; ZNet instance = ZNet.instance; if ((Object)(object)localPlayer == (Object)null || (Object)(object)instance == (Object)null || localPlayer.GetPlayerID() <= 0) { return false; } long worldUID = instance.GetWorldUID(); if (worldUID == 0L) { return false; } ulong num = (ulong)worldUID; context = num.ToString("x16", CultureInfo.InvariantCulture) + ":" + localPlayer.GetPlayerID().ToString(CultureInfo.InvariantCulture); return true; } private void NoteMapNetworkUsed(string networkName) { if (_mapOverlay != null && TryGetMapOverlayContext(out var context)) { _mapOverlay.NoteNetworkUsed(context, networkName); } } private bool TryBuildLocalMapSnapshot(out List candidates) { //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00e1: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: 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_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0183: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) candidates = new List(); Player localPlayer = Player.m_localPlayer; List list = ValheimContracts.PortalObjects(); if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() <= 0 || list == null || list.Count > 4096) { return false; } string travelerStableId = PortalPermissionAdapter.Identity(localPlayer.GetPlayerID()); HashSet hashSet = new HashSet(StringComparer.Ordinal); for (int i = 0; i < list.Count; i++) { ZDO val = list[i]; if (val == null || !val.IsValid() || ((ZDOID)(ref val.m_uid)).IsNone()) { continue; } string text = ((object)Unsafe.As(ref val.m_uid)/*cast due to .constrained prefix*/).ToString(); if (!hashSet.Add(text)) { return false; } switch (PortalZdoCodec.GetMode(val)) { case 0: { Vector3 position2 = val.GetPosition(); candidates.Add(new PortalMapCandidate(text, "Standard Pair", MapDisplayName("Vanilla", VanillaPortalTag(val), string.Empty), position2.x, position2.y, position2.z, isNetworkPortal: false, acceptsArrival: true, permitsDeparture: true, viewAuthorized: true, departureAuthorized: true)); break; } case 1: { if (PortalZdoCodec.TryRead(val, out var endpoint, out var _) && endpoint.OnlineState == PortalOnlineState.Online && ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(val.GetPosition(), localPlayer.GetPlayerID())) && _permissions.Allows(endpoint, travelerStableId, PortalAccessAction.ViewDiscover)) { Vector3 position = val.GetPosition(); candidates.Add(new PortalMapCandidate(text, endpoint.NetworkId, MapDisplayName(MapCategory(endpoint), endpoint.DisplayName, endpoint.NetworkId), position.x, position.y, position.z, isNetworkPortal: true, endpoint.AcceptsArrival, endpoint.PermitsDeparture, viewAuthorized: true, endpoint.PermitsDeparture)); if (candidates.Count > 2048) { return false; } } break; } } } return true; } private static string VanillaPortalTag(ZDO zdo) { string text = ((zdo != null) ? zdo.GetString(ZDOVars.s_tag, string.Empty) : null) ?? string.Empty; if (!string.IsNullOrWhiteSpace(text)) { return text.Trim(); } return "Portal"; } private static string MapCategory(PortalEndpoint endpoint) { return endpoint?.NetworkKind switch { PortalNetworkKind.Public => "Public", PortalNetworkKind.Personal => "Private", PortalNetworkKind.Group => "Group", _ => "Runic", }; } private static string MapDisplayName(string category, string name, string network) { string text = "[" + SanitizeMapText(category, "Portal") + "] " + SanitizeMapText(name, "Portal"); string text2 = SanitizeMapText(network, string.Empty); if (text2.Length != 0) { text = text + " · " + text2; } if (text.Length <= 64) { return text; } int num = 64; if (char.IsHighSurrogate(text[num - 1])) { num--; } return text.Substring(0, num); } private static string SanitizeMapText(string value, string fallback) { if (string.IsNullOrWhiteSpace(value)) { return fallback; } StringBuilder stringBuilder = new StringBuilder(Math.Min(value.Length, 64)); for (int i = 0; i < value.Length; i++) { if (stringBuilder.Length >= 64) { break; } char c = value[i]; if (char.IsControl(c)) { stringBuilder.Append(' '); } else if (!char.IsSurrogate(c)) { stringBuilder.Append(c); } else if (char.IsHighSurrogate(c) && i + 1 < value.Length && char.IsLowSurrogate(value[i + 1]) && stringBuilder.Length + 2 <= 64) { stringBuilder.Append(c); stringBuilder.Append(value[++i]); } } string text = stringBuilder.ToString().Trim(); if (text.Length != 0) { return text; } return fallback; } internal bool BlocksPlayerMovement(Player player) { if (_mapPicker != null && (Object)(object)player != (Object)null) { return (Object)(object)_mapPicker.Player == (Object)(object)player; } return false; } private bool TryOpenMapPicker(TeleportWorld portal, Player player) { //IL_0045: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)portal == (Object)null || (Object)(object)player == (Object)null || (Object)(object)player != (Object)(object)Player.m_localPlayer) { return true; } if (_mapPicker != null) { return true; } if (!ValheimContracts.HasLocalPlayerAuthority) { Message(player, "Portal picker requires native ownership of the local player."); return true; } if (!ValheimContracts.WardAllows(ValheimContracts.ResolveWard(((Component)portal).transform.position, player.GetPlayerID()))) { Message(player, "Portal picker denied: ward access was denied at this portal."); RejectRoute(portal, RouteStopCode.AuthorityUnavailable); return true; } TravelPolicyState travelPolicyState = CurrentTravelPolicy(portal, player); if (travelPolicyState != TravelPolicyState.Allowed) { Message(player, RouteMessage(TravelStop(travelPolicyState))); RejectRoute(portal, TravelStop(travelPolicyState)); return true; } if (!TryReadVisibleEndpoint(portal, out var endpoint) || !endpoint.PermitsDeparture) { Message(player, "This portal is not configured for departures."); RejectRoute(portal, RouteStopCode.DepartureDenied); return true; } if (!BeginPicker(portal, player, endpoint)) { return true; } if (!TryBuildPickerCandidates(endpoint, player, out var candidates, out var truncated, out var failure)) { CancelMapPicker(failure, closeMap: true); return true; } if (!BeginDirectorySync(_mapPicker, endpoint, candidates, truncated)) { CompletePicker(candidates, truncated); } return true; } private bool BeginPicker(TeleportWorld portal, Player player, PortalEndpoint source) { //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_0089: 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_009c: 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) Minimap instance = Minimap.instance; Rigidbody component = ((Component)player).GetComponent(); if ((Object)(object)instance == (Object)null || (Object)(object)component == (Object)null) { Message(player, "Portal map picker is unavailable because the map or player body is not ready."); return false; } PickerSession pickerSession = new PickerSession { Source = portal, SourceInstanceId = ((Object)portal).GetInstanceID(), SourcePortalId = source.PortalId, SourceRevision = source.Revision, NetworkId = source.NetworkId, Player = player, PlayerId = player.GetPlayerID(), Body = component, OriginalConstraints = component.constraints, Map = instance, OriginalMapMode = instance.m_mode }; CaptureMapViewState(pickerSession); _mapPicker = pickerSession; NoteMapNetworkUsed(source.NetworkId); try { EnforcePickerFreeze(pickerSession); instance.ShowPointOnMap(((Component)portal).transform.position); if (!ExactLargeMapOpen(pickerSession)) { throw new InvalidOperationException("The large map could not open."); } _mapOverlay?.Tick(instance, suspend: true); return true; } catch (Exception ex) { CancelMapPicker("Portal map picker could not open: " + ex.Message, closeMap: true); return false; } } private bool TryBuildPickerCandidates(PortalEndpoint source, Player player, out List candidates, out bool truncated, out string failure) { //IL_0125: Unknown result type (might be due to invalid IL or missing references) //IL_0279: Unknown result type (might be due to invalid IL or missing references) //IL_027e: Unknown result type (might be due to invalid IL or missing references) candidates = new List(); truncated = false; failure = string.Empty; _index.MarkDirty(); if (!_index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f) || !_graph.TryGetEndpoint(source.PortalId, out var endpoint) || !SameRouteState(source, endpoint)) { failure = "Portal destinations are unavailable because the network changed."; return false; } string travelerStableId = PortalPermissionAdapter.Identity(player.GetPlayerID()); PortalDirectoryResult portalDirectoryResult = _graph.Query(new PortalDirectoryQuery(travelerStableId, source.PortalId, source.NetworkId, string.Empty, 128)); if (portalDirectoryResult.StopCode != RouteStopCode.Ready) { failure = RouteMessage(portalDirectoryResult.StopCode); return false; } foreach (PortalDirectoryEntry entry in portalDirectoryResult.Entries) { if (_graph.TryGetEndpoint(entry.PortalId, out var endpoint2) && _index.TryGetZdo(entry.PortalId, out var zdo) && PortalZdoCodec.TryRead(zdo, out var endpoint3, out var _) && SameRouteState(endpoint2, endpoint3) && ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(zdo.GetPosition(), player.GetPlayerID())) && _permissions.Allows(endpoint3, travelerStableId, PortalAccessAction.ViewDiscover) && _permissions.Allows(endpoint3, travelerStableId, PortalAccessAction.Arrive)) { RoutePlan routePlan = _graph.Plan(new RoutePlanRequest(travelerStableId, source.PortalId, endpoint2.PortalId, TravelPolicyState.Allowed, oneWayAcknowledged: true, source.Revision, endpoint2.Revision)); if (routePlan.IsReady) { string text = (entry.DuplicateName ? (" [" + entry.Disambiguator + "]") : string.Empty); string text2 = PolicyLabel(endpoint2.NetworkKind); string text3 = (endpoint2.PermitsDeparture ? "Both" : "Arrival only"); candidates.Add(new PickerCandidate { PortalId = endpoint2.PortalId, DisplayName = endpoint2.DisplayName, Label = endpoint2.DisplayName + text + " - " + text2 + " - " + text3 + (routePlan.IsOneWay ? " - ONE WAY" : string.Empty), Revision = endpoint2.Revision, Position = zdo.GetPosition(), OneWay = routePlan.IsOneWay }); } } } truncated = portalDirectoryResult.Truncated; return true; } private void CompletePicker(List candidates, bool truncated) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) PickerSession mapPicker = _mapPicker; if (mapPicker == null) { return; } mapPicker.Candidates.AddRange(candidates ?? new List()); if (mapPicker.Candidates.Count == 0) { CancelMapPicker("No authorized online arrival portals are available in network '" + mapPicker.NetworkId + "'.", closeMap: true); return; } CaptureAndEnablePickerIcon(mapPicker); foreach (PickerCandidate candidate in mapPicker.Candidates) { candidate.Pin = mapPicker.Map.AddPin(candidate.Position, (PinType)3, candidate.Label, false, false, 0L, default(PlatformUserID)); if (candidate.Pin == null) { CancelMapPicker("Portal destination markers could not be created.", closeMap: true); return; } PortalMapMarkerSprite.Apply(candidate.Pin); candidate.Pin.m_doubleSize = true; candidate.Pin.m_animate = true; } ShowAllPickerPins(mapPicker); Message(mapPicker.Player, mapPicker.Candidates.Count + " destination" + ((mapPicker.Candidates.Count == 1) ? string.Empty : "s") + " available. Click a marker to travel." + (truncated ? " The directory limit was reached." : string.Empty)); } internal bool TryHandleMapPickerClick(Vector3 screenPoint) { //IL_0014: 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_001c: 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_0090: 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_00a1: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: 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_00db: Unknown result type (might be due to invalid IL or missing references) PickerSession mapPicker = _mapPicker; if (mapPicker == null) { return false; } if (ExactLargeMapOpen(mapPicker)) { Rect val = PickerOverlayBounds(); if (!((Rect)(ref val)).Contains(new Vector2(screenPoint.x, (float)Screen.height - screenPoint.y))) { PickerCandidate pickerCandidate = null; float num = float.MaxValue; foreach (PickerCandidate candidate in mapPicker.Candidates) { RectTransform val2 = candidate.Pin?.m_uiElement; if (!((Object)(object)val2 == (Object)null) && ((Component)val2).gameObject.activeInHierarchy) { Vector2 val3 = RectTransformUtility.WorldToScreenPoint((Camera)null, ((Transform)val2).position); val = val2.rect; float width = ((Rect)(ref val)).width; val = val2.rect; float num2 = Mathf.Max(22f, Mathf.Max(width, ((Rect)(ref val)).height) * 0.75f); float num3 = Vector2.Distance(val3, new Vector2(screenPoint.x, screenPoint.y)); if (!(num3 > num2) && !(num3 >= num)) { pickerCandidate = candidate; num = num3; } } } if (pickerCandidate != null) { mapPicker.Pending = pickerCandidate; } return true; } } return true; } internal bool TryHandleMapPickerClick() { //IL_0001: Unknown result type (might be due to invalid IL or missing references) return TryHandleMapPickerClick(ZInput.mousePosition); } private void TickMapPicker() { PickerSession mapPicker = _mapPicker; if (mapPicker == null) { return; } if ((Object)(object)mapPicker.Player == (Object)null || (Object)(object)mapPicker.Player != (Object)(object)Player.m_localPlayer || mapPicker.Player.GetPlayerID() != mapPicker.PlayerId || ((Character)mapPicker.Player).IsDead() || ((Character)mapPicker.Player).IsTeleporting() || !ValheimContracts.HasLocalPlayerAuthority || (Object)(object)mapPicker.Source == (Object)null || ((Object)mapPicker.Source).GetInstanceID() != mapPicker.SourceInstanceId || (Object)(object)mapPicker.Map == (Object)null || (Object)(object)mapPicker.Map != (Object)(object)Minimap.instance || !ExactLargeMapOpen(mapPicker)) { CancelMapPicker(string.Empty, closeMap: true); return; } EnforcePickerFreeze(mapPicker); if (!string.IsNullOrEmpty(mapPicker.DirectoryRequestId)) { TickDirectoryPicker(mapPicker); } else if (mapPicker.Pending != null) { PickerCandidate pending = mapPicker.Pending; string travelerStableId = PortalPermissionAdapter.Identity(mapPicker.PlayerId); _selections.Set(new PortalSelection(travelerStableId, mapPicker.SourcePortalId, pending.PortalId, mapPicker.SourceRevision, pending.Revision, isReturn: false, DateTime.UtcNow.Ticks, pending.DisplayName)); _hoverCache.Remove(mapPicker.SourceInstanceId); _oneWay.Clear(); _diagnostics.Record(PortalDiagnosticCode.RouteSelected, RouteStopCode.Ready, mapPicker.SourcePortalId, 0L); TeleportWorld source = mapPicker.Source; Player player = mapPicker.Player; EndMapPicker(closeMap: true); TryCommitSelectedTeleport(source, player, oneWayAcknowledged: true, out var _); } } private static void EnforcePickerFreeze(PickerSession session) { //IL_0028: 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) if (!((Object)(object)session?.Body == (Object)null)) { session.Body.constraints = (RigidbodyConstraints)126; session.Body.linearVelocity = Vector3.zero; session.Body.angularVelocity = Vector3.zero; session.Body.Sleep(); } } private static bool ExactLargeMapOpen(PickerSession session) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Invalid comparison between Unknown and I4 if ((Object)(object)session?.Map != (Object)null && (Object)(object)session.Map == (Object)(object)Minimap.instance && (int)session.Map.m_mode == 2 && (Object)(object)session.Map.m_largeRoot != (Object)null) { return session.Map.m_largeRoot.activeInHierarchy; } return false; } private void CancelMapPicker(string message, bool closeMap) { Player player = _mapPicker?.Player; EndMapPicker(closeMap); if (!string.IsNullOrEmpty(message)) { Message(player, message); } } private void EndMapPicker(bool closeMap) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0082: 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_0048: Unknown result type (might be due to invalid IL or missing references) PickerSession mapPicker = _mapPicker; _mapPicker = null; if (mapPicker == null) { return; } RemovePickerPins(mapPicker); RestorePickerIconFilter(mapPicker); if (closeMap && (Object)(object)mapPicker.Map != (Object)null && (Object)(object)mapPicker.Map == (Object)(object)Minimap.instance) { try { mapPicker.Map.SetMapMode(mapPicker.OriginalMapMode); } catch (Exception) { } } RestoreMapViewState(mapPicker); if (!((Object)(object)mapPicker.Body != (Object)null)) { return; } try { mapPicker.Body.constraints = mapPicker.OriginalConstraints; mapPicker.Body.linearVelocity = Vector3.zero; mapPicker.Body.angularVelocity = Vector3.zero; mapPicker.Body.WakeUp(); } catch (Exception) { } } private static void CaptureMapViewState(PickerSession session) { //IL_006a: 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_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)session?.Map == (Object)null || LargeZoomField == null || MapOffsetField == null) { return; } try { if (LargeZoomField.GetValue(session.Map) is float originalLargeZoom && MapOffsetField.GetValue(session.Map) is Vector3 originalMapOffset) { session.OriginalLargeZoom = originalLargeZoom; session.OriginalMapOffset = originalMapOffset; session.MapViewCaptured = true; } } catch (Exception) { } } private static void RestoreMapViewState(PickerSession session) { //IL_005f: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)session?.Map == (Object)null || !session.MapViewCaptured || LargeZoomField == null || MapOffsetField == null) { return; } try { LargeZoomField.SetValue(session.Map, session.OriginalLargeZoom); MapOffsetField.SetValue(session.Map, session.OriginalMapOffset); } catch (Exception) { } } private static void CaptureAndEnablePickerIcon(PickerSession session) { bool[] array = VisibleIconTypesField?.GetValue(session.Map) as bool[]; int num = 3; if (array == null || num < 0 || num >= array.Length) { throw new InvalidOperationException("The installed map icon filter is unavailable."); } session.IconFilterCaptured = true; session.IconFilterWasVisible = array[num]; if (!session.IconFilterWasVisible) { TogglePickerIcon(session.Map); } } private static void RestorePickerIconFilter(PickerSession session) { if ((Object)(object)session?.Map == (Object)null || !session.IconFilterCaptured) { return; } try { bool[] array = VisibleIconTypesField?.GetValue(session.Map) as bool[]; int num = 3; if (array != null && num >= 0 && num < array.Length && array[num] != session.IconFilterWasVisible) { TogglePickerIcon(session.Map); } } catch (Exception) { } } private static void TogglePickerIcon(Minimap map) { if ((Object)(object)map == (Object)null || ToggleIconFilterMethod == null) { throw new MissingMethodException(typeof(Minimap).FullName, "ToggleIconFilter"); } ToggleIconFilterMethod.Invoke(map, new object[1] { (object)(PinType)3 }); } private static void RemovePickerPins(PickerSession session) { if (session == null) { return; } foreach (PickerCandidate candidate in session.Candidates) { if (candidate.Pin != null && !((Object)(object)session.Map == (Object)null)) { try { session.Map.RemovePin(candidate.Pin); } catch (Exception) { } candidate.Pin = null; } } } private static void ShowAllPickerPins(PickerSession session) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: 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_0012: 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_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: 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_0030: 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_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Unknown result type (might be due to invalid IL or missing references) Vector3 val = ((Component)session.Source).transform.position; Vector3 val2 = val; foreach (PickerCandidate candidate in session.Candidates) { val = Vector3.Min(val, candidate.Position); val2 = Vector3.Max(val2, candidate.Position); } try { if (LargeZoomField != null && MaximumZoomField != null) { LargeZoomField.SetValue(session.Map, MaximumZoomField.GetValue(session.Map)); } } catch (Exception) { } session.Map.ShowPointOnMap((val + val2) * 0.5f); } internal void DrawMapPickerOverlay() { //IL_00c7: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: 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_0156: 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_0033: Expected O, but got Unknown //IL_003e: 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_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown //IL_007d: 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_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Expected O, but got Unknown //IL_00bd: Unknown result type (might be due to invalid IL or missing references) PickerSession mapPicker = _mapPicker; if (mapPicker != null && ExactLargeMapOpen(mapPicker)) { if (_pickerBoxStyle == null) { _pickerBoxStyle = new GUIStyle(GUI.skin.box); _pickerTitleStyle = new GUIStyle(GUI.skin.label) { fontSize = 18, fontStyle = (FontStyle)1, alignment = (TextAnchor)4 }; _pickerTitleStyle.normal.textColor = new Color(0.96f, 0.82f, 0.4f, 1f); _pickerBodyStyle = new GUIStyle(GUI.skin.label) { fontSize = 14, alignment = (TextAnchor)4, wordWrap = true }; _pickerBodyStyle.normal.textColor = Color.white; } Rect val = PickerOverlayBounds(); GUI.Box(val, GUIContent.none, _pickerBoxStyle); GUI.Label(new Rect(((Rect)(ref val)).x + 10f, ((Rect)(ref val)).y + 6f, ((Rect)(ref val)).width - 20f, 26f), "Runic network: " + mapPicker.NetworkId, _pickerTitleStyle); GUI.Label(new Rect(((Rect)(ref val)).x + 10f, ((Rect)(ref val)).y + 34f, ((Rect)(ref val)).width - 20f, 34f), "Click a portal marker to travel. Arrival-only markers are one-way. Press Esc to cancel.", _pickerBodyStyle); } } private static Rect PickerOverlayBounds() { //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_0059: Unknown result type (might be due to invalid IL or missing references) Rect safeArea = Screen.safeArea; float num = Mathf.Min(720f, Mathf.Max(0f, ((Rect)(ref safeArea)).width - 24f)); return new Rect(((Rect)(ref safeArea)).x + (((Rect)(ref safeArea)).width - num) * 0.5f, (float)Screen.height - ((Rect)(ref safeArea)).yMax + 12f, num, 76f); } internal void FailMapPickerUi(Exception exception) { Diagnostics.Error(exception, "Portal map picker UI failed closed."); CancelMapPicker("Portal map picker closed after a display error.", closeMap: true); } private void ShutdownMapPicker() { EndMapPicker(closeMap: true); _pickerBoxStyle = (_pickerTitleStyle = (_pickerBodyStyle = null)); } private static RouteStopCode TravelStop(TravelPolicyState policy) { return policy switch { TravelPolicyState.RestrictedItems => RouteStopCode.RestrictedItems, TravelPolicyState.PortalsDisabled => RouteStopCode.PortalsDisabled, TravelPolicyState.BossTravelBlocked => RouteStopCode.BossTravelBlocked, _ => RouteStopCode.PolicyUnknown, }; } internal PortalRuntime(PortalGroupRuntime groups, PortalAuthorityGate authority, CorrelatedDiagnosticBuffer diagnostics, PortalOverwriteConfirmationGate confirmations = null) { _groups = groups ?? throw new ArgumentNullException("groups"); _permissions = new PortalPermissionAdapter(groups); _authority = authority ?? throw new ArgumentNullException("authority"); _diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics"); _graph = new PortalGraphService(_permissions, CurrentMaximumEndpoints()); _index = new PortalIndex(_graph, _diagnostics); _returns = new ReturnRouteStore(256); _selections = new PortalSelectionStore(256); _confirmations = confirmations ?? new PortalOverwriteConfirmationGate(); } internal void Initialize() { _ready = true; _disabledReason = string.Empty; _nextReturnPrune = 0f; _index.MarkDirty(); InitializeMapOverlay(); InitializeHoverPanel(); _diagnostics.Record(PortalDiagnosticCode.RuntimeReady, RouteStopCode.Ready, "", 0L); } internal void Shutdown() { CancelCurrentEdit(); ShutdownMapPicker(); ShutdownDirectorySyncTransport(); ShutdownMapOverlay(); PortalMapMarkerSprite.Shutdown(); _ready = false; _disabledReason = "runtime-shutdown"; _oneWay.Clear(); _arrivalSuppression.Clear(); _selections.Clear(); _instancePortalIds.Clear(); _hoverCache.Clear(); _index.Clear(); ShutdownHoverPanel(); _diagnostics.Record(PortalDiagnosticCode.RuntimeDisabled, RouteStopCode.FeatureDisabled, "", 0L); } internal void OnConfigurationChanged() { if (!FeatureEnabled) { CleanupDisabledFeatureState(); return; } _graph.SetMaximumEndpoints(CurrentMaximumEndpoints()); _index.MarkDirty(); _hoverCache.Clear(); RefreshHoverPanelConfiguration(); if (_mapOverlay == null) { InitializeMapOverlay(); } } internal void EnsureDisabledStateAfterConfigurationFault() { if (!FeatureEnabled) { CleanupDisabledFeatureState(); } } private void CleanupDisabledFeatureState() { CancelCurrentEdit(); CancelMapPicker(string.Empty, closeMap: true); ShutdownMapOverlay(); _oneWay.Clear(); _arrivalSuppression.Clear(); _selections.Clear(); } internal void Tick() { if (!_ready) { return; } if (!FeatureEnabled) { CleanupDisabledFeatureState(); return; } TickDirectorySyncTransport(); TickMapPicker(); float interval = PortalConfig.IndexRefreshSeconds?.Value ?? 2f; float realtimeSinceStartup = Time.realtimeSinceStartup; if (_index.Tick(realtimeSinceStartup, interval)) { _hoverCache.Clear(); } TickMapOverlay(realtimeSinceStartup, interval); if (!(realtimeSinceStartup < _nextReturnPrune)) { _nextReturnPrune = realtimeSinceStartup + 5f; _returns.Prune(DateTime.UtcNow.Ticks); } } internal void Observe(TeleportWorld portal) { if (!((Object)(object)portal == (Object)null)) { ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val != null && !((ZDOID)(ref val.m_uid)).IsNone()) { RememberPortal(((Object)portal).GetInstanceID(), ((object)Unsafe.As(ref val.m_uid)/*cast due to .constrained prefix*/).ToString()); _index.MarkDirty(); } } } internal bool IsNetworkPortal(TeleportWorld portal) { if (!FeatureEnabled || (Object)(object)portal == (Object)null) { return false; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val != null && val.IsValid() && !((ZDOID)(ref val.m_uid)).IsNone()) { return PortalZdoCodec.GetMode(val) == 1; } return false; } internal bool HasSelectedTarget(TeleportWorld portal) { if (!IsNetworkPortal(portal) || (Object)(object)Player.m_localPlayer == (Object)null) { return false; } string travelerStableId = PortalPermissionAdapter.Identity(Player.m_localPlayer.GetPlayerID()); if (!TryGetPortalId(portal, out var portalId) || !_selections.TryGet(travelerStableId, portalId, out var selection)) { return false; } if (_graph.TryGetEndpoint(selection.DestinationPortalId, out var endpoint)) { return endpoint.OnlineState == PortalOnlineState.Online; } return false; } internal bool TryGetHoverText(TeleportWorld portal, out string text) { text = null; if (!TryGetHoverPanelState(portal, out var state) || !state.IsNetwork) { return false; } int instanceID = ((Object)portal).GetInstanceID(); if (!state.DetailsVisible) { _hoverCache.Remove(instanceID); text = "Runic Portal: Restricted Network Portal\nDetails hidden by current permissions"; return true; } if (_hoverCache.TryGetValue(instanceID, out text)) { return true; } string text2 = ((state.SelectedDestination.Length == 0) ? "none" : state.SelectedDestination); text = "Runic Portal: " + state.DisplayName + " [" + state.NetworkId + "]\nSelected: " + text2; _hoverCache[instanceID] = text; return true; } private static bool TryReadVisibleEndpoint(TeleportWorld portal, out PortalEndpoint endpoint) { endpoint = null; if ((Object)(object)portal == (Object)null) { return false; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); string failure; if (val != null && val.IsValid() && !((ZDOID)(ref val.m_uid)).IsNone()) { return PortalZdoCodec.TryRead(val, out endpoint, out failure); } return false; } internal bool TryHandleInteract(TeleportWorld portal, Humanoid human, bool hold, bool alt, out bool result) { result = false; if (!FeatureEnabled || (Object)(object)portal == (Object)null || hold) { return false; } Player val = (Player)(object)((human is Player) ? human : null); bool flag = IsNetworkPortal(portal); if (!flag && !alt) { CancelEditFor(portal); return false; } if ((Object)(object)val == (Object)null) { result = false; return true; } if (flag && alt) { result = TryCycle(portal, val); return true; } result = TryBeginEdit(portal, val); return true; } internal bool TryConsumeSetText(TeleportWorld portal, string text) { //IL_0169: 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_02a4: Unknown result type (might be due to invalid IL or missing references) //IL_02a9: Unknown result type (might be due to invalid IL or missing references) if (_edit == null || (Object)(object)portal == (Object)null || _edit.InstanceId != ((Object)portal).GetInstanceID()) { return false; } EditSession edit = _edit; _edit = null; string targetId = PortalEditEvidence.OpaqueTarget(((object)Unsafe.As(ref edit.PortalId)/*cast due to .constrained prefix*/).ToString()); Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() != edit.ActorId) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: actor identity changed."); RejectEdit(portal, AuthorityStopCode.SenderIdentityUnbound); return true; } PortalEditCommand bound = PortalEditCommand.Parse(text); if (bound.Kind == PortalEditKind.Invalid) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: " + bound.Error); _diagnostics.Record(PortalDiagnosticCode.EditRejected, RouteStopCode.StaleSelection, ((object)Unsafe.As(ref edit.PortalId)/*cast due to .constrained prefix*/).ToString(), 0L); return true; } if (!TryBindAndAuthorizeGroupEdit(localPlayer, bound, out bound, out var failure)) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: " + failure + "."); RejectEdit(portal, AuthorityStopCode.OwnerPermissionDenied); return true; } if (!TryRevalidateEdit(edit, portal, localPlayer, out var current, out var zdo, out var creator, out var stop, out var reason)) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: " + reason + "."); RejectEdit(portal, stop); return true; } if (bound.Kind == PortalEditKind.PublicNetwork && zdo.GetConnectionZDOID((ConnectionType)1) != ZDOID.None) { _confirmations.Cancel(targetId); Message(localPlayer, "Unlink this Standard Pair with a unique vanilla tag before opting into a network."); RejectEdit(portal, AuthorityStopCode.CurrentStateChanged); return true; } if (current.Matches(bound, creator)) { _confirmations.Cancel(targetId); Message(localPlayer, "Portal metadata already matches; no world state was changed."); return true; } string text2 = current.Fingerprint(bound, creator); PortalConfirmationAdmission portalConfirmationAdmission = _confirmations.Request(current.RequiresConfirmation(bound, creator), targetId, text2); switch (portalConfirmationAdmission) { case PortalConfirmationAdmission.Pending: Message(localPlayer, "Repeat the identical portal overwrite to confirm it."); return true; case PortalConfirmationAdmission.Denied: case PortalConfirmationAdmission.Unavailable: _confirmations.Cancel(targetId); Message(localPlayer, (portalConfirmationAdmission == PortalConfirmationAdmission.Unavailable) ? "Runic portal edit denied: configured Safety confirmation is unavailable or incompatible." : "Runic portal edit denied by the current Safety confirmation policy."); RejectEdit(portal, AuthorityStopCode.CurrentStateChanged); return true; default: { if (!TryRevalidateEdit(edit, portal, localPlayer, out var current2, out zdo, out creator, out stop, out reason) || !current2.Matches(current) || !string.Equals(current2.Fingerprint(bound, creator), text2, StringComparison.Ordinal) || (portalConfirmationAdmission != PortalConfirmationAdmission.NotRequired && !_confirmations.RequesterIsActive())) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: authority or metadata changed after confirmation."); RejectEdit(portal, stop); return true; } if (bound.Kind == PortalEditKind.PublicNetwork && zdo.GetConnectionZDOID((ConnectionType)1) != ZDOID.None) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: the Standard Pair connection changed."); RejectEdit(portal, AuthorityStopCode.CurrentStateChanged); return true; } if (!TryAuthorizeBoundGroupEdit(localPlayer, bound, out failure)) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit cancelled: " + failure + "."); RejectEdit(portal, AuthorityStopCode.OwnerPermissionDenied); return true; } if (!PortalZdoCodec.TryWrite(zdo, bound, creator, out var _, out var failure2)) { _confirmations.Cancel(targetId); Message(localPlayer, "Runic portal edit failed closed: " + failure2 + "."); RejectEdit(portal, AuthorityStopCode.CurrentStateChanged); return true; } _confirmations.Cancel(targetId); _index.MarkDirty(); _index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f); _hoverCache.Remove(((Object)portal).GetInstanceID()); _selections.Clear(); _oneWay.Clear(); _diagnostics.Record(PortalDiagnosticCode.EditAccepted, RouteStopCode.Ready, ((object)Unsafe.As(ref zdo.m_uid)/*cast due to .constrained prefix*/).ToString(), 0L); if (bound.Kind == PortalEditKind.PublicNetwork) { NoteMapNetworkUsed(bound.NetworkId); } Message(localPlayer, (bound.Kind == PortalEditKind.PublicNetwork) ? (PolicyLabel(bound.NetworkKind) + " network portal saved. Walk into it, then click an authorized destination on the map.") : "Portal restored to Standard Pair mode; set its vanilla tag normally."); return true; } } } private bool TryBindAndAuthorizeGroupEdit(Player actor, PortalEditCommand command, out PortalEditCommand bound, out string failure) { bound = command; failure = string.Empty; if (command == null || command.NetworkKind != PortalNetworkKind.Group) { return command != null; } long num = (((Object)(object)actor == (Object)null) ? 0 : actor.GetPlayerID()); if (num <= 0) { failure = "your stable player identity is unavailable"; return false; } if (command.RequiresActiveGroup) { if (!_groups.TryGetActive(num, out var groupId, out var _)) { failure = "no active Group is selected; open chat and use /group list, then /group use "; return false; } bound = command.BindGroup(groupId); if (bound.Kind == PortalEditKind.Invalid) { failure = bound.Error; return false; } } return TryAuthorizeBoundGroupEdit(actor, bound, out failure); } private bool TryAuthorizeBoundGroupEdit(Player actor, PortalEditCommand command, out string failure) { failure = string.Empty; if (command == null || command.NetworkKind != PortalNetworkKind.Group) { return true; } long num = (((Object)(object)actor == (Object)null) ? 0 : actor.GetPlayerID()); if (num <= 0) { failure = "your stable player identity is unavailable"; return false; } if (!_groups.TryGetActive(num, out var groupId, out var _) || !string.Equals(groupId, command.GroupId, StringComparison.Ordinal)) { failure = "the portal Group is not your current active Group"; return false; } if (_groups.TryIsMember(command.GroupId, num, out var isMember) && isMember) { return true; } failure = "the selected Group is unavailable or you are no longer a member"; return false; } private void ConsumeEdit(long token, string text) { EditSession edit = _edit; if (edit != null && edit.Token == token) { TryConsumeSetText(edit.Portal, text); } } internal void OnTextInputHidden(TextInput input) { if ((Object)(object)input != (Object)null) { CancelCurrentEdit(); } } internal bool TryHandleTeleport(TeleportWorld portal, Player player, out bool handled) { handled = false; if (!FeatureEnabled || (Object)(object)portal == (Object)null || (Object)(object)player == (Object)null) { return false; } if (!IsNetworkPortal(portal)) { if (!StandardPairTargetsNetwork(portal)) { return false; } handled = true; Message(player, "This Standard Pair is linked to a Runic network portal. Give it a unique vanilla tag and pair it with another Standard portal."); RejectRoute(portal, RouteStopCode.DestinationUnavailable); return true; } handled = true; if (TryGetPortalId(portal, out var portalId) && _arrivalSuppression.Blocks(player.GetPlayerID(), portalId, DateTime.UtcNow.Ticks)) { return true; } return TryOpenMapPicker(portal, player); } private bool TryCommitSelectedTeleport(TeleportWorld portal, Player player, bool oneWayAcknowledged, out bool handled) { //IL_0222: Unknown result type (might be due to invalid IL or missing references) //IL_0236: Unknown result type (might be due to invalid IL or missing references) //IL_0354: Unknown result type (might be due to invalid IL or missing references) //IL_035f: Unknown result type (might be due to invalid IL or missing references) //IL_042a: Unknown result type (might be due to invalid IL or missing references) //IL_0431: Unknown result type (might be due to invalid IL or missing references) //IL_04a9: Unknown result type (might be due to invalid IL or missing references) //IL_04ab: Unknown result type (might be due to invalid IL or missing references) //IL_03a8: Unknown result type (might be due to invalid IL or missing references) //IL_03bd: Unknown result type (might be due to invalid IL or missing references) handled = true; if (!ValheimContracts.HasLocalPlayerAuthority || (Object)(object)player != (Object)(object)Player.m_localPlayer) { Message(player, "Portal travel requires native ownership of the local player."); RejectRoute(portal, RouteStopCode.AuthorityUnavailable); return true; } _index.MarkDirty(); if (!_index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f) || !TryGetPortalId(portal, out var portalId) || !_graph.TryGetEndpoint(portalId, out var endpoint)) { Message(player, "Portal route cancelled: the server graph is unavailable."); RejectRoute(portal, RouteStopCode.SourceUnavailable); return true; } string text = PortalPermissionAdapter.Identity(player.GetPlayerID()); if (!_selections.TryGet(text, portalId, out var selection)) { Message(player, "No destination selected. Walk into the portal and click a destination marker on the map."); RejectRoute(portal, RouteStopCode.StaleSelection); return true; } TravelPolicyState travelPolicyState = CurrentTravelPolicy(portal, player); RoutePlanRequest request = new RoutePlanRequest(text, portalId, selection.DestinationPortalId, travelPolicyState, oneWayAcknowledged: false, selection.SourceRevision, selection.DestinationRevision); RoutePlan routePlan = _graph.Plan(request); if (routePlan.StopCode == RouteStopCode.OneWayWarningRequired) { if (!oneWayAcknowledged) { long ticks = DateTime.UtcNow.Ticks; long ticks2 = TimeSpan.FromSeconds(PortalConfig.OneWayAcknowledgementSeconds?.Value ?? 10).Ticks; if (!_oneWay.ConsumeOrArm(text, portalId, selection.DestinationPortalId, ticks, ticks2)) { Message(player, "This destination is one-way. Walk into the portal and click its map marker to acknowledge and travel."); RejectRoute(portal, RouteStopCode.OneWayWarningRequired); return true; } } request = new RoutePlanRequest(text, portalId, selection.DestinationPortalId, travelPolicyState, oneWayAcknowledged: true, selection.SourceRevision, selection.DestinationRevision); routePlan = _graph.Plan(request); } else { _oneWay.Clear(); } if (!routePlan.IsReady) { Message(player, RouteMessage(routePlan.StopCode)); RejectRoute(portal, routePlan.StopCode); return true; } if (!_index.TryGetZdo(portalId, out var zdo) || !_index.TryGetZdo(selection.DestinationPortalId, out var zdo2) || !_graph.TryGetEndpoint(selection.DestinationPortalId, out var endpoint2)) { Message(player, "Portal route cancelled: an endpoint changed after selection."); RejectRoute(portal, RouteStopCode.DestinationUnavailable); return true; } WardContext ward = ValheimContracts.ResolveWard(zdo.GetPosition(), player.GetPlayerID()); WardContext ward2 = ValheimContracts.ResolveWard(zdo2.GetPosition(), player.GetPlayerID()); PortalEndpoint endpoint3 = null; PortalEndpoint endpoint4 = null; string failure; bool flag = PortalZdoCodec.TryRead(zdo, out endpoint3, out failure) && PortalZdoCodec.TryRead(zdo2, out endpoint4, out failure) && SameRouteState(endpoint, endpoint3) && SameRouteState(endpoint2, endpoint4); PortalRoutePermissionEvidence portalRoutePermissionEvidence = PortalRoutePermissionEvidence.Evaluate(_permissions, endpoint3 ?? endpoint, endpoint4 ?? endpoint2, text, ValheimContracts.WardAllows(ward), ValheimContracts.DestinationWardAllows(ward2)); ZNetView component = ((Component)player).GetComponent(); ZNetView component2 = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component2 != (Object)null && component2.IsValid()) ? component2.GetZDO() : null); PortalAuthorityEvidence evidence = new PortalAuthorityEvidence(PortalMutationKind.CommitTravel, FeatureEnabled, ValheimContracts.IsServer, isDedicated: false, dedicatedTransportAvailable: false, (Object)(object)player == (Object)(object)Player.m_localPlayer && player.GetPlayerID() != 0, (Object)(object)player == (Object)(object)Player.m_localPlayer, zdo.IsValid(), zdo2.IsValid(), sourceObjectOwned: false, (Object)(object)component != (Object)null && component.IsValid() && component.IsOwner(), portalRoutePermissionEvidence.PolicyAllowed, portalRoutePermissionEvidence.SourceWardAllowed, portalRoutePermissionEvidence.DestinationWardAllowed, InRange(((Component)player).transform.position, ((Component)portal).transform.position, CurrentEditRange()), flag && val == zdo && PortalZdoCodec.GetRevision(zdo) == routePlan.SourceRevision && PortalZdoCodec.GetRevision(zdo2) == routePlan.DestinationRevision && ZDOMan.instance != null && ZDOMan.instance.GetZDO(zdo.m_uid) == zdo && ZDOMan.instance.GetZDO(zdo2.m_uid) == zdo2, travelPolicyState == TravelPolicyState.Allowed); PortalAuthorityDecision portalAuthorityDecision = _authority.Evaluate(evidence); if (!portalAuthorityDecision.IsAllowed) { Message(player, "Portal route denied: " + AuthorityLabel(portalAuthorityDecision.StopCode) + "."); _diagnostics.Record(PortalDiagnosticCode.AuthorityRejected, RouteStopCode.AuthorityUnavailable, portalId, 0L); return true; } if (!PortalTravelTransformPolicy.TryResolve(zdo2.GetPosition(), zdo2.GetRotation(), portal.m_exitDistance, out var arrival, out var normalizedRotation)) { Message(player, "Portal route cancelled: the destination transform is invalid."); _diagnostics.Record(PortalDiagnosticCode.RouteRejected, RouteStopCode.TeleportRejected, selection.DestinationPortalId, 0L); return true; } _arrivalSuppression.Arm(player.GetPlayerID(), endpoint2.PortalId, DateTime.UtcNow.Ticks, TimeSpan.FromSeconds(10.0).Ticks); if (!((Character)player).TeleportTo(arrival, normalizedRotation, true)) { _arrivalSuppression.Clear(); Message(player, "Portal route cancelled: Valheim rejected the teleport state."); RejectRoute(portal, RouteStopCode.TeleportRejected); return true; } Game instance = Game.instance; if (instance != null) { instance.IncrementPlayerStat((PlayerStatType)15, 1f); } long expiresUtcTicks = checked(DateTime.UtcNow.Ticks + TimeSpan.FromMinutes(PortalConfig.ReturnRouteMinutes?.Value ?? 15).Ticks); _returns.Record(new ReturnRoute(text, portalId, endpoint2.PortalId, endpoint.Revision, endpoint2.Revision, expiresUtcTicks), DateTime.UtcNow.Ticks); _diagnostics.Record(PortalDiagnosticCode.RouteCommitted, RouteStopCode.Ready, portalId, 0L); return true; } public PortalDirectoryResult Query(PortalDirectoryQuery query) { if (!FeatureEnabled) { return new PortalDirectoryResult(Array.Empty(), truncated: false, RouteStopCode.FeatureDisabled); } return _graph.Query(query); } public PortalNameResolution ResolveName(PortalDirectoryQuery scope, string displayName) { if (!FeatureEnabled) { return new PortalNameResolution(RouteStopCode.FeatureDisabled, Array.Empty()); } return _graph.ResolveName(scope, displayName); } public RoutePlan Plan(RoutePlanRequest request) { if (!FeatureEnabled) { return new RoutePlan(RouteStopCode.FeatureDisabled, request?.SourcePortalId, request?.DestinationPortalId, oneWay: false, -1L, -1L); } return _graph.Plan(request); } private bool TryCycle(TeleportWorld portal, Player actor) { //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_0179: Unknown result type (might be due to invalid IL or missing references) //IL_02d4: Unknown result type (might be due to invalid IL or missing references) if (!ValheimContracts.HasLocalPlayerAuthority || (Object)(object)actor != (Object)(object)Player.m_localPlayer) { Message(actor, "Portal selection requires native ownership of the local player."); RejectRoute(portal, RouteStopCode.AuthorityUnavailable); return true; } if (!ValheimContracts.WardAllows(ValheimContracts.ResolveWard(((Component)portal).transform.position, actor.GetPlayerID()))) { Message(actor, "Portal selection denied: ward access was denied at this portal."); RejectRoute(portal, RouteStopCode.AuthorityUnavailable); return true; } _index.MarkDirty(); if (!_index.Rebuild(Time.realtimeSinceStartup, PortalConfig.IndexRefreshSeconds?.Value ?? 2f) || !TryGetPortalId(portal, out var portalId) || !_graph.TryGetEndpoint(portalId, out var endpoint)) { Message(actor, "Portal directory unavailable."); return true; } string travelerStableId = PortalPermissionAdapter.Identity(actor.GetPlayerID()); PortalDirectoryQuery query = new PortalDirectoryQuery(travelerStableId, portalId, endpoint.NetworkId, string.Empty, PortalConfig.DirectoryPageSize?.Value ?? 32); PortalDirectoryResult portalDirectoryResult = _graph.Query(query); if (portalDirectoryResult.StopCode != RouteStopCode.Ready) { Message(actor, RouteMessage(portalDirectoryResult.StopCode)); return true; } List list = new List(portalDirectoryResult.Entries.Count + 1); if (_returns.TryGet(travelerStableId, portalId, DateTime.UtcNow.Ticks, out var route)) { if (!_graph.TryGetEndpoint(route.OriginPortalId, out var endpoint2) || !_index.TryGetZdo(endpoint2.PortalId, out var zdo) || !ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(zdo.GetPosition(), actor.GetPlayerID())) || route.OriginRevision != endpoint2.Revision || route.ArrivalRevision != endpoint.Revision) { _returns.Remove(travelerStableId); } else if (_graph.Plan(new RoutePlanRequest(travelerStableId, portalId, endpoint2.PortalId, TravelPolicyState.Allowed, oneWayAcknowledged: true, endpoint.Revision, endpoint2.Revision)).IsReady) { list.Add(new CycleCandidate { PortalId = endpoint2.PortalId, Label = "Return: " + endpoint2.DisplayName, DisplayName = endpoint2.DisplayName, SourceRevision = endpoint.Revision, DestinationRevision = endpoint2.Revision, IsReturn = true }); } else { _returns.Remove(travelerStableId); } } for (int i = 0; i < portalDirectoryResult.Entries.Count; i++) { PortalDirectoryEntry portalDirectoryEntry = portalDirectoryResult.Entries[i]; if ((list.Count <= 0 || !string.Equals(list[0].PortalId, portalDirectoryEntry.PortalId, StringComparison.Ordinal)) && _graph.TryGetEndpoint(portalDirectoryEntry.PortalId, out var endpoint3) && _index.TryGetZdo(endpoint3.PortalId, out var zdo2) && ValheimContracts.DestinationWardAllows(ValheimContracts.ResolveWard(zdo2.GetPosition(), actor.GetPlayerID()))) { list.Add(new CycleCandidate { PortalId = portalDirectoryEntry.PortalId, Label = portalDirectoryEntry.DisplayName + (portalDirectoryEntry.DuplicateName ? (" [" + portalDirectoryEntry.Disambiguator + "]") : string.Empty), DisplayName = portalDirectoryEntry.DisplayName, SourceRevision = endpoint.Revision, DestinationRevision = endpoint3.Revision, IsReturn = false }); } } if (list.Count == 0) { Message(actor, "No authorized online destinations are available in this network."); return true; } int index = 0; if (_selections.TryGet(travelerStableId, portalId, out var selection)) { for (int j = 0; j < list.Count; j++) { if (string.Equals(list[j].PortalId, selection.DestinationPortalId, StringComparison.Ordinal) && list[j].IsReturn == selection.IsReturn) { index = (j + 1) % list.Count; break; } } } CycleCandidate cycleCandidate = list[index]; _selections.Set(new PortalSelection(travelerStableId, portalId, cycleCandidate.PortalId, cycleCandidate.SourceRevision, cycleCandidate.DestinationRevision, cycleCandidate.IsReturn, DateTime.UtcNow.Ticks, cycleCandidate.DisplayName)); _hoverCache.Remove(((Object)portal).GetInstanceID()); _oneWay.Clear(); _diagnostics.Record(PortalDiagnosticCode.RouteSelected, RouteStopCode.Ready, portalId, 0L); Message(actor, "Destination selected: " + cycleCandidate.Label + "."); return true; } private bool TryBeginEdit(TeleportWorld portal, Player actor) { //IL_0079: 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_0127: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) if (!TryConfigureEvidence(portal, actor, out var evidence, out var zdo, out var _, out var reason)) { Message(actor, "Runic portal editor unavailable: " + reason + "."); return true; } PortalAuthorityDecision portalAuthorityDecision = _authority.Evaluate(evidence); if (!portalAuthorityDecision.IsAllowed) { Message(actor, "Runic portal editor denied: " + AuthorityLabel(portalAuthorityDecision.StopCode) + "."); RejectEdit(portal, portalAuthorityDecision.StopCode); return true; } if (PortalZdoCodec.GetMode(zdo) != 1 && zdo.GetConnectionZDOID((ConnectionType)1) != ZDOID.None) { Message(actor, "Unlink this Standard Pair with a unique vanilla tag before opting into a network."); return true; } if ((Object)(object)TextInput.instance == (Object)null) { Message(actor, "Runic portal editor unavailable: text input is not ready."); return true; } long num = ++_editSequence; if (num == 0L) { num = ++_editSequence; } if (!PortalEditEvidence.TryCapture(zdo, out var evidence2)) { Message(actor, "Runic portal editor unavailable: current metadata evidence is invalid."); return true; } CancelCurrentEdit(); _edit = new EditSession { Portal = portal, InstanceId = ((Object)portal).GetInstanceID(), ActorId = actor.GetPlayerID(), PortalId = zdo.m_uid, Schema = PortalZdoCodec.GetSchema(zdo), Mode = PortalZdoCodec.GetMode(zdo), Revision = PortalZdoCodec.GetRevision(zdo), Evidence = evidence2, Token = num }; TextInput.instance.RequestText((TextReceiver)(object)new PortalEditReceiver(this, num), "network|public/private/group|... (or standard)", 256); return true; } private bool TryRevalidateEdit(EditSession session, TeleportWorld portal, Player actor, out PortalEditEvidence current, out ZDO zdo, out long creator, out AuthorityStopCode stop, out string reason) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) current = default(PortalEditEvidence); zdo = null; creator = 0L; stop = AuthorityStopCode.CurrentStateChanged; reason = "current portal evidence is unavailable"; if (session == null || (Object)(object)portal == (Object)null || (Object)(object)actor == (Object)null) { return false; } if (actor.GetPlayerID() != session.ActorId) { stop = AuthorityStopCode.SenderIdentityUnbound; reason = "actor identity changed"; return false; } if (!TryConfigureEvidence(portal, actor, out var evidence, out zdo, out creator, out reason)) { return false; } PortalAuthorityDecision portalAuthorityDecision = _authority.Evaluate(evidence); if (!portalAuthorityDecision.IsAllowed) { stop = portalAuthorityDecision.StopCode; reason = AuthorityLabel(portalAuthorityDecision.StopCode); return false; } if (zdo.m_uid != session.PortalId) { reason = "portal identity changed"; return false; } if (!PortalEditEvidence.TryCapture(zdo, out current)) { reason = "portal metadata is invalid or unsupported"; return false; } if (current.Schema != session.Schema || current.Mode != session.Mode || current.Revision != session.Revision || !current.Matches(session.Evidence)) { reason = "portal metadata changed while the editor was open"; return false; } reason = string.Empty; return true; } private bool TryConfigureEvidence(TeleportWorld portal, Player actor, out PortalAuthorityEvidence evidence, out ZDO zdo, out long creator, out string reason) { //IL_01b4: Unknown result type (might be due to invalid IL or missing references) //IL_022c: Unknown result type (might be due to invalid IL or missing references) //IL_0237: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Unknown result type (might be due to invalid IL or missing references) evidence = null; zdo = null; creator = 0L; reason = "authority context is incomplete"; if ((Object)(object)portal == (Object)null || (Object)(object)actor == (Object)null) { return false; } ZNetView component = ((Component)portal).GetComponent(); Piece val = ((Component)portal).GetComponent() ?? ((Component)portal).GetComponentInParent(); zdo = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); creator = (((Object)(object)val == (Object)null) ? 0 : val.GetCreator()); if (zdo == null || ((ZDOID)(ref zdo.m_uid)).IsNone() || creator == 0L) { reason = "portal owner or stable object identity is missing"; return false; } int schema = PortalZdoCodec.GetSchema(zdo); int mode = PortalZdoCodec.GetMode(zdo); bool num = mode == 0 || mode == 1; bool flag = (schema == 0 && mode == 0) || schema == 2; if (!num || !flag) { reason = "portal metadata mode or schema is unsupported"; return false; } string travelerStableId = PortalPermissionAdapter.Identity(actor.GetPlayerID()); PortalEndpoint endpoint2; if (mode == 1) { if (!PortalZdoCodec.TryRead(zdo, out var endpoint, out var _)) { reason = "portal metadata is invalid or uses an unsupported schema"; return false; } endpoint2 = new PortalEndpoint(endpoint.PortalId, endpoint.Mode, endpoint.DisplayName, endpoint.NetworkId, endpoint.NetworkKind, PortalPermissionAdapter.Identity(creator), endpoint.OwnerDisplayName, endpoint.OnlineState, endpoint.AcceptsArrival, endpoint.PermitsDeparture, endpoint.Access, endpoint.Revision, endpoint.KnownBiome); } else { endpoint2 = new PortalEndpoint(((object)Unsafe.As(ref zdo.m_uid)/*cast due to .constrained prefix*/).ToString(), PortalMode.StandardPair, string.Empty, string.Empty, PortalNetworkKind.Custom, PortalPermissionAdapter.Identity(creator), string.Empty, PortalOnlineState.Online, acceptsArrival: true, permitsDeparture: true, PortalAccessProfile.PublicNetwork, Math.Max(0, PortalZdoCodec.GetRevision(zdo))); } WardContext ward = ValheimContracts.ResolveWard(((Component)portal).transform.position, actor.GetPlayerID()); bool ownerPermissionAllowed = _permissions.Allows(endpoint2, travelerStableId, PortalAccessAction.Edit); evidence = new PortalAuthorityEvidence(PortalMutationKind.ConfigureMetadata, FeatureEnabled, ValheimContracts.IsServer, isDedicated: false, dedicatedTransportAvailable: false, (Object)(object)actor == (Object)(object)Player.m_localPlayer && actor.GetPlayerID() != 0, (Object)(object)actor == (Object)(object)Player.m_localPlayer, zdo.IsValid(), destinationExists: true, component.IsOwner(), playerObjectOwned: false, ownerPermissionAllowed, ValheimContracts.WardAllows(ward), destinationWardAllowed: true, InRange(((Component)actor).transform.position, ((Component)portal).transform.position, CurrentEditRange()), flag && ZDOMan.instance != null && ZDOMan.instance.GetZDO(zdo.m_uid) == zdo, vanillaTravelPolicyAllowed: true); reason = string.Empty; return true; } private bool HasVisibleDuplicate(PortalEndpoint source, string traveler, string name) { PortalDirectoryQuery scope = new PortalDirectoryQuery(traveler, source.PortalId, source.NetworkId, string.Empty, 128); RouteStopCode stopCode = _graph.ResolveName(scope, name).StopCode; if (stopCode != RouteStopCode.DuplicateName) { return stopCode == RouteStopCode.GraphLimitExceeded; } return true; } private bool TryGetPortalId(TeleportWorld portal, out string portalId) { portalId = null; if ((Object)(object)portal == (Object)null) { return false; } int instanceID = ((Object)portal).GetInstanceID(); if (_instancePortalIds.TryGetValue(instanceID, out portalId)) { return true; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val == null || ((ZDOID)(ref val.m_uid)).IsNone()) { return false; } portalId = ((object)Unsafe.As(ref val.m_uid)/*cast due to .constrained prefix*/).ToString(); RememberPortal(instanceID, portalId); return true; } private void RememberPortal(int instanceId, string portalId) { if (!_instancePortalIds.ContainsKey(instanceId) && _instancePortalIds.Count >= 4096) { _instancePortalIds.Clear(); _hoverCache.Clear(); } _instancePortalIds[instanceId] = portalId; _hoverCache.Remove(instanceId); } private void CancelEditFor(TeleportWorld portal) { if (_edit != null && (Object)(object)portal != (Object)null && _edit.InstanceId == ((Object)portal).GetInstanceID()) { CancelCurrentEdit(); } } private void CancelCurrentEdit() { EditSession edit = _edit; _edit = null; if (edit != null) { _confirmations.Cancel(PortalEditEvidence.OpaqueTarget(((object)Unsafe.As(ref edit.PortalId)/*cast due to .constrained prefix*/).ToString())); } } private void RejectEdit(TeleportWorld portal, AuthorityStopCode stop) { string portalId2; string portalId = (TryGetPortalId(portal, out portalId2) ? portalId2 : string.Empty); _diagnostics.Record(PortalDiagnosticCode.EditRejected, RouteStopCode.AuthorityUnavailable, portalId, 0L); Diagnostics.Trace("Portal edit rejected: " + stop.ToString() + "."); } private void RejectRoute(TeleportWorld portal, RouteStopCode stop) { string portalId2; string portalId = (TryGetPortalId(portal, out portalId2) ? portalId2 : string.Empty); _diagnostics.Record(PortalDiagnosticCode.RouteRejected, stop, portalId, 0L); Diagnostics.Trace("Portal route rejected: " + stop.ToString() + "."); } private static TravelPolicyState CurrentTravelPolicy(TeleportWorld portal, Player player) { if ((Object)(object)ZoneSystem.instance == (Object)null) { return TravelPolicyState.Unknown; } if (ZoneSystem.instance.GetGlobalKey((GlobalKeys)27)) { return TravelPolicyState.PortalsDisabled; } if (ZoneSystem.instance.GetGlobalKey((GlobalKeys)28)) { RandEventSystem instance = RandEventSystem.instance; float num = default(float); if (((instance != null) ? instance.GetBossEvent() : null) != null || (ZoneSystem.instance.GetGlobalKey((GlobalKeys)38, ref num) && num > 0f)) { return TravelPolicyState.BossTravelBlocked; } } if (!portal.m_allowAllItems && !((Humanoid)player).IsTeleportable()) { return TravelPolicyState.RestrictedItems; } return TravelPolicyState.Allowed; } private static bool InRange(Vector3 actor, Vector3 portal, float range) { //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_0002: 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) Vector3 val = actor - portal; return ((Vector3)(ref val)).sqrMagnitude <= range * range; } private static bool SameRouteState(PortalEndpoint expected, PortalEndpoint current) { if (expected != null && current != null && string.Equals(expected.PortalId, current.PortalId, StringComparison.Ordinal) && string.Equals(expected.DisplayName, current.DisplayName, StringComparison.Ordinal) && string.Equals(expected.NetworkId, current.NetworkId, StringComparison.Ordinal) && string.Equals(expected.OwnerStableId, current.OwnerStableId, StringComparison.Ordinal) && expected.Mode == current.Mode && expected.AcceptsArrival == current.AcceptsArrival && expected.PermitsDeparture == current.PermitsDeparture) { return expected.Revision == current.Revision; } return false; } private static float CurrentEditRange() { return PortalConfig.EditRangeMeters?.Value ?? 5f; } private static int CurrentMaximumEndpoints() { return PortalConfig.MaximumEndpoints?.Value ?? 1024; } private static void Message(Player player, string text) { if ((Object)(object)player != (Object)null && !string.IsNullOrEmpty(text)) { ((Character)player).Message((MessageType)2, text, 0, (Sprite)null); } } private static string RouteMessage(RouteStopCode stop) { return stop switch { RouteStopCode.RestrictedItems => "You cannot teleport with this inventory under the current world rules.", RouteStopCode.PortalsDisabled => "Portals are disabled by the current world rules.", RouteStopCode.BossTravelBlocked => "Portal travel is blocked by the active boss rule.", RouteStopCode.DepartureDenied => "You are not permitted to depart through this portal.", RouteStopCode.ArrivalDenied => "You are not permitted to arrive at that portal.", RouteStopCode.DestinationUnavailable => "The selected destination is no longer available.", RouteStopCode.SourceUnavailable => "This portal is no longer available.", RouteStopCode.StaleSelection => "The selected route changed; select a destination again.", _ => "Portal route cancelled: " + stop.ToString() + ".", }; } private static string AuthorityLabel(AuthorityStopCode stop) { switch (stop) { case AuthorityStopCode.ServerAuthorityMissing: return "server authority is missing"; case AuthorityStopCode.DedicatedTransportUnavailable: return "authenticated dedicated transport is unavailable"; case AuthorityStopCode.SenderIdentityUnbound: return "the sender identity is not bound"; case AuthorityStopCode.ActorNotLocalAuthority: return "the actor is not locally authoritative"; case AuthorityStopCode.SourceObjectNotOwned: return "the portal object is not owned by this authority"; case AuthorityStopCode.PlayerObjectNotOwned: return "the player object is not owned by this authority"; case AuthorityStopCode.OwnerPermissionDenied: return "portal permission was denied"; case AuthorityStopCode.SourceWardDenied: case AuthorityStopCode.DestinationWardDenied: return "ward access was denied"; case AuthorityStopCode.ActorOutOfRange: return "the actor is out of range"; case AuthorityStopCode.CurrentStateChanged: return "the portal changed before commit"; case AuthorityStopCode.VanillaTravelPolicyDenied: return "Valheim travel policy denied the route"; default: return stop.ToString(); } } internal bool ResolvePortalVisualState(TeleportWorld portal, bool vanillaResult, bool requireResolvedTarget) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) if (!FeatureEnabled || (Object)(object)portal == (Object)null) { return vanillaResult; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val == null || ((ZDOID)(ref val.m_uid)).IsNone()) { return false; } switch (PortalZdoCodec.GetMode(val)) { case 1: { if (PortalZdoCodec.TryRead(val, out var endpoint, out var _) && endpoint.Mode == PortalMode.Network) { return endpoint.OnlineState == PortalOnlineState.Online; } return false; } default: return false; case 0: { ZDOID connectionZDOID = val.GetConnectionZDOID((ConnectionType)1); if (((ZDOID)(ref connectionZDOID)).IsNone()) { return vanillaResult; } ZDOMan instance = ZDOMan.instance; ZDO val2 = ((instance != null) ? instance.GetZDO(connectionZDOID) : null); if (val2 == null) { if (!requireResolvedTarget) { return vanillaResult; } return false; } return PortalZdoCodec.GetMode(val2) == 0 && vanillaResult; } } } internal bool TryFilterVanillaPortalCandidates(ZDO source, List candidates, out List filtered) { filtered = null; if (!FeatureEnabled || source == null || candidates == null) { return false; } filtered = new List(candidates.Count); if (PortalZdoCodec.GetMode(source) != 0) { return true; } for (int i = 0; i < candidates.Count; i++) { ZDO val = candidates[i]; if (val != null && PortalZdoCodec.GetMode(val) == 0) { filtered.Add(val); } } return true; } private bool StandardPairTargetsNetwork(TeleportWorld portal) { //IL_0044: 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_0061: Unknown result type (might be due to invalid IL or missing references) if (!FeatureEnabled || (Object)(object)portal == (Object)null) { return false; } ZNetView component = ((Component)portal).GetComponent(); ZDO val = (((Object)(object)component != (Object)null && component.IsValid()) ? component.GetZDO() : null); if (val == null || PortalZdoCodec.GetMode(val) != 0) { return false; } ZDOID connectionZDOID = val.GetConnectionZDOID((ConnectionType)1); if (((ZDOID)(ref connectionZDOID)).IsNone()) { return false; } ZDOMan instance = ZDOMan.instance; ZDO val2 = ((instance != null) ? instance.GetZDO(connectionZDOID) : null); if (val2 != null) { return PortalZdoCodec.GetMode(val2) != 0; } return false; } } internal sealed class PortalGroupRuntime : IDisposable, IPortalGroupMembershipResolver { private sealed class Pending { internal string Id; internal byte[] Payload; internal Action Output; internal bool Silent; internal long Deadline; internal long NextAttempt; internal int Attempts; } private sealed class Replay { internal string Key; internal byte[] Request; internal ZPackage Response; internal long Expires; } private sealed class ConnectedPlayer { internal StableIdentity Identity; internal string Name; } [CompilerGenerated] private static class <>O { public static Func <0>__CurrentWorldScope; public static ConsoleEvent <1>__OnGroupCommand; } private const string RequestRpc = "RunicPortals.Groups.Request.v1"; private const string ResponseRpc = "RunicPortals.Groups.Response.v1"; private const int WireSchema = 1; private const int TerminalMarker = 1196576817; private const int MaximumPending = 32; private const int MaximumReplayEntries = 128; private const int MaximumEnvelopeBytes = 32768; private const float RefreshSeconds = 5f; private static readonly long RequestLifetimeTicks = TimeSpan.FromSeconds(6.0).Ticks; private static readonly long RetryTicks = TimeSpan.FromSeconds(2.0).Ticks; private static readonly long ReplayLifetimeTicks = TimeSpan.FromSeconds(30.0).Ticks; private static readonly long SnapshotLifetimeTicks = TimeSpan.FromSeconds(15.0).Ticks; private static readonly object CommandGate = new object(); private static PortalGroupRuntime _commandRuntime; private static ConsoleCommand _groupCommand; private readonly ManualLogSource _log; private readonly CompatibleGroupWorldStore _store; private readonly GroupActiveSelectionService _activeGroups; private readonly GroupCommandProcessor _processor; private readonly Dictionary _pending = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _replays = new Dictionary(StringComparer.Ordinal); private readonly Queue _replayOrder = new Queue(); private readonly HashSet _clientMemberships = new HashSet(StringComparer.Ordinal); private ZRoutedRpc _registeredRpc; private ActiveGroupSelection _clientActive = ActiveGroupSelection.Stale; private long _clientSnapshotExpires; private float _nextRefresh; private bool _refreshPending; private bool _disposed; internal PortalGroupRuntime(ManualLogSource log) { _log = log; string text = Path.Combine(Paths.ConfigPath, "RunicPermissions", "groups"); _store = new CompatibleGroupWorldStore(text); _activeGroups = new GroupActiveSelectionService(_store, new FileGroupActiveSelectionStore(Path.Combine(text, "active")), CurrentWorldScope); _processor = new GroupCommandProcessor(_store, CurrentWorldScope); } internal void Initialize() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0042: Expected O, but got Unknown lock (CommandGate) { _commandRuntime = this; if (_groupCommand == null) { object obj = <>O.<1>__OnGroupCommand; if (obj == null) { ConsoleEvent val = OnGroupCommand; <>O.<1>__OnGroupCommand = val; obj = (object)val; } _groupCommand = new ConsoleCommand("group", "Runic Group management. Type /group help.", (ConsoleEvent)obj, false, false, false, false, false, (ConsoleOptionsFetcher)null, false, false, false); } } } internal void Tick() { if (_disposed) { return; } ZRoutedRpc instance = ZRoutedRpc.instance; ZNet instance2 = ZNet.instance; if (instance == null || (Object)(object)instance2 == (Object)null) { return; } Register(instance); long ticks = DateTime.UtcNow.Ticks; Expire(ticks); if (instance2.IsServer()) { _refreshPending = false; return; } Pending[] array = _pending.Values.ToArray(); foreach (Pending pending in array) { if (ticks >= pending.Deadline) { _pending.Remove(pending.Id); if (!pending.Silent) { pending.Output?.Invoke("Runic Group: request timed out."); } if (pending.Silent) { _refreshPending = false; } } else if (pending.Attempts < 2 && ticks >= pending.NextAttempt) { SendPending(instance, instance2, pending, ticks); } } if (!_refreshPending && !(Time.realtimeSinceStartup < _nextRefresh) && TryLocalIdentity(out var _)) { _nextRefresh = Time.realtimeSinceStartup + 5f; _refreshPending = true; Submit(new GroupFriendlyRequest(GroupFriendlyOperation.Active), null, silent: true); } } public void Dispose() { if (_disposed) { return; } _disposed = true; lock (CommandGate) { if (_commandRuntime == this) { _commandRuntime = null; } } _pending.Clear(); _replays.Clear(); _replayOrder.Clear(); _clientMemberships.Clear(); _clientActive = ActiveGroupSelection.Stale; _registeredRpc = null; } internal bool TryIsMember(string groupId, long playerId, out bool isMember) { isMember = false; if (_disposed || !GroupIdentity.IsCanonicalId(groupId) || playerId <= 0) { return false; } StableIdentity identity = PlayerIdentity(playerId); if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { if (!TryReadCatalog(out var catalog, out var _)) { return false; } if (!Guid.TryParseExact(groupId, "N", out var result) || !catalog.TryGetGroup(result, out var group)) { return true; } isMember = group.TryGetMember(identity, out var _); return true; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() != playerId || DateTime.UtcNow.Ticks >= _clientSnapshotExpires) { return false; } isMember = _clientMemberships.Contains(groupId); return true; } bool IPortalGroupMembershipResolver.TryIsMember(string groupId, long playerId, out bool isMember) { return TryIsMember(groupId, playerId, out isMember); } internal bool TryGetActive(long playerId, out string groupId, out string displayName) { groupId = string.Empty; displayName = string.Empty; if (_disposed || playerId <= 0) { return false; } ActiveGroupSelection activeGroupSelection; if ((Object)(object)ZNet.instance != (Object)null && ZNet.instance.IsServer()) { activeGroupSelection = _activeGroups.Resolve(PlayerIdentity(playerId)); } else { Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || localPlayer.GetPlayerID() != playerId || DateTime.UtcNow.Ticks >= _clientSnapshotExpires) { return false; } activeGroupSelection = _clientActive; } if (activeGroupSelection == null || !activeGroupSelection.IsAvailable) { return false; } groupId = activeGroupSelection.GroupId; displayName = activeGroupSelection.DisplayName; return true; } private void Register(ZRoutedRpc routed) { if (_registeredRpc != routed) { routed.Register("RunicPortals.Groups.Request.v1", (Action)ReceiveRequest); routed.Register("RunicPortals.Groups.Response.v1", (Action)ReceiveResponse); _registeredRpc = routed; _pending.Clear(); _refreshPending = false; _nextRefresh = 0f; ClearClientSnapshot(); } } private void ReceiveRequest(long sender, ZPackage package) { if (_disposed || (Object)(object)ZNet.instance == (Object)null || !ZNet.instance.IsServer() || ZRoutedRpc.instance == null) { return; } if (!TryReadRequest(package, out var id, out var payload)) { SentinelSecurityBridge.Report(sender, "portal-group-envelope-invalid", "portal-group-envelope", 3, "The server rejected a malformed bounded Group request envelope."); return; } if (!TryResolvePeerIdentity(sender, out var identity)) { SentinelSecurityBridge.Report(sender, "portal-group-identity-unbound", id, 2, "The Group request had no exact current transport identity."); return; } long ticks = DateTime.UtcNow.Ticks; Expire(ticks); string text = sender.ToString(CultureInfo.InvariantCulture) + ":" + id; GroupFriendlyRequest request; string failure; if (_replays.TryGetValue(text, out var value)) { if (Exact(value.Request, payload)) { ZRoutedRpc.instance.InvokeRoutedRPC(sender, "RunicPortals.Groups.Response.v1", new object[1] { Clone(value.Response) }); } else { SentinelSecurityBridge.Report(sender, "portal-group-replay-conflict", id, 4, "One request identity was reused with different bytes."); SendResponse(sender, id, accepted: false, "request-id-conflict", null, identity, ticks); } } else if (!GroupFriendlyProtocol.TryDecodeRequest(payload, out request, out failure)) { SentinelSecurityBridge.Report(sender, "portal-group-payload-invalid", id, 3, "The server rejected malformed Group command bytes."); SendResponse(sender, id, accepted: false, failure, null, identity, ticks); } else { GroupFriendlyResponse response = (request.IsQuery ? EvaluateQuery(identity, request) : EvaluateMutation(identity, request)); SendResponse(sender, id, accepted: true, "ok", response, identity, ticks, payload, text); } } private void ReceiveResponse(long sender, ZPackage package) { if (_disposed || (Object)(object)ZNet.instance == (Object)null || ZNet.instance.IsServer()) { return; } ZNetPeer serverPeer = ZNet.instance.GetServerPeer(); if (serverPeer == null || !serverPeer.IsReady() || serverPeer.m_uid != sender || !TryReadResponse(package, out var id, out var accepted, out var reason, out var payload, out var memberships) || !_pending.TryGetValue(id, out var value)) { return; } _pending.Remove(id); if (value.Silent) { _refreshPending = false; } if (!accepted || !GroupFriendlyProtocol.TryDecodeResponse(payload, out var response, out var _)) { if (!value.Silent) { value.Output?.Invoke("Runic Group: request failed (" + reason + ")."); } return; } _clientMemberships.Clear(); for (int i = 0; i < memberships.Length; i++) { _clientMemberships.Add(memberships[i]); } _clientActive = response.Active; _clientSnapshotExpires = DateTime.UtcNow.Ticks + SnapshotLifetimeTicks; if (!value.Silent && response.Text.Length != 0) { value.Output?.Invoke(response.Text); } } private void SendResponse(long peerId, string requestId, bool accepted, string reason, GroupFriendlyResponse response, StableIdentity actor, long now, byte[] requestPayload = null, string replayKey = null) { string[] memberships = MembershipIds(actor); byte[] payload = ((response == null) ? Array.Empty() : GroupFriendlyProtocol.EncodeResponse(response)); ZPackage val = WriteResponse(requestId, accepted, reason, payload, memberships); if (val.Size() > 32768) { return; } if (replayKey != null && requestPayload != null) { while (_replays.Count >= 128 && _replayOrder.Count != 0) { _replays.Remove(_replayOrder.Dequeue()); } _replays[replayKey] = new Replay { Key = replayKey, Request = (byte[])requestPayload.Clone(), Response = Clone(val), Expires = now + ReplayLifetimeTicks }; _replayOrder.Enqueue(replayKey); } ZRoutedRpc instance = ZRoutedRpc.instance; if (instance != null) { instance.InvokeRoutedRPC(peerId, "RunicPortals.Groups.Response.v1", new object[1] { val }); } } private void Submit(GroupFriendlyRequest request, Action output, bool silent) { if (_disposed || request == null) { return; } ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null) { if (!silent) { output?.Invoke("Runic Group: network is unavailable."); } return; } if (instance.IsServer()) { if (!TryLocalIdentity(out var identity)) { if (!silent) { output?.Invoke("Runic Group: local player identity is unavailable."); } return; } GroupFriendlyResponse groupFriendlyResponse = (request.IsQuery ? EvaluateQuery(identity, request) : EvaluateMutation(identity, request)); if (!silent && groupFriendlyResponse.Text.Length != 0) { output?.Invoke(groupFriendlyResponse.Text); } return; } if (_pending.Count >= 32) { if (!silent) { output?.Invoke("Runic Group: too many requests are pending."); } return; } string text = Guid.NewGuid().ToString("N"); long ticks = DateTime.UtcNow.Ticks; Pending pending = new Pending { Id = text, Payload = GroupFriendlyProtocol.EncodeRequest(request), Output = output, Silent = silent, Deadline = ticks + RequestLifetimeTicks, NextAttempt = ticks }; _pending.Add(text, pending); if (_registeredRpc != null) { SendPending(_registeredRpc, instance, pending, ticks); } } private static void SendPending(ZRoutedRpc routed, ZNet network, Pending pending, long now) { ZNetPeer serverPeer = network.GetServerPeer(); if (serverPeer != null && serverPeer.IsReady()) { ZPackage val = WriteRequest(pending.Id, pending.Payload); if (val.Size() <= 32768) { routed.InvokeRoutedRPC(serverPeer.m_uid, "RunicPortals.Groups.Request.v1", new object[1] { val }); pending.Attempts++; pending.NextAttempt = now + RetryTicks; } } } private GroupFriendlyResponse EvaluateQuery(StableIdentity actor, GroupFriendlyRequest request) { ActiveGroupSelection activeGroupSelection = _activeGroups.Resolve(actor); if (!TryReadCatalog(out var catalog, out var failure)) { return Friendly("Group information is unavailable (" + failure + ").", activeGroupSelection); } switch (request.Operation) { case GroupFriendlyOperation.List: { IReadOnlyList memberships = catalog.GetMemberships(actor); if (memberships.Count == 0) { return Friendly("You are not in a Group. Use /group create .", activeGroupSelection); } List list2 = new List { "Your Groups:" }; foreach (GroupMembership item in memberships) { list2.Add("- " + item.DisplayName + " (" + item.Role.ToString().ToLowerInvariant() + ")" + ((activeGroupSelection.IsAvailable && activeGroupSelection.GroupId == item.GroupIdText) ? " [active]" : string.Empty)); } return Friendly(string.Join("\n", list2), activeGroupSelection); } case GroupFriendlyOperation.Active: return Friendly(ActiveLabel(activeGroupSelection), activeGroupSelection); case GroupFriendlyOperation.WhoAmI: return Friendly("Your exact Group identity is " + actor.CanonicalKey + ".", activeGroupSelection); case GroupFriendlyOperation.Members: { if (!TryActiveRecord(actor, catalog, activeGroupSelection, out var group, out failure)) { return Friendly(failure, activeGroupSelection); } List values = ConnectedPlayers(); List list = new List { "Members of " + group.DisplayName + ":" }; foreach (GroupMember member in group.Members) { list.Add("- " + ConnectedLabel(values, member.Identity) + " (" + member.Role.ToString().ToLowerInvariant() + ")"); } return Friendly(string.Join("\n", list), activeGroupSelection); } default: return Friendly("That Group query is unsupported.", activeGroupSelection); } } private GroupFriendlyResponse EvaluateMutation(StableIdentity actor, GroupFriendlyRequest request) { ActiveGroupSelection selection = _activeGroups.Resolve(actor); if (!TryReadCatalog(out var catalog, out var failure)) { return Friendly("Group command unavailable (" + failure + ").", selection); } if (request.Operation == GroupFriendlyOperation.Select) { if (!TryResolveMemberGroup(catalog, actor, request.Primary, out var group, out failure)) { return Friendly(failure, selection); } if (!_activeGroups.TrySetAuthoritative(actor, group.Id, out selection, out var reasonCode)) { return Friendly("Could not select that Group (" + reasonCode + ").", selection); } return Friendly("Active Group: " + group.DisplayName + ".", selection); } GroupRecord group2 = null; GroupCommand groupCommand; if (request.Operation == GroupFriendlyOperation.Create) { groupCommand = new GroupCommand(GroupCommandKind.Create, request.ProposedGroupId, request.Primary, null, GroupRole.Member, 0L); } else if (request.Operation == GroupFriendlyOperation.Accept) { if (!TryResolveInvitation(catalog, actor, request.Primary, out group2, out failure)) { return Friendly(failure, selection); } groupCommand = new GroupCommand(GroupCommandKind.Accept, group2.Id, "", null, GroupRole.Member, 0L); } else { if (!TryActiveRecord(actor, catalog, selection, out group2, out failure)) { return Friendly(failure, selection); } groupCommand = BuildActiveCommand(request, group2, out failure); if (groupCommand == null) { return Friendly(failure, selection); } } GroupCommandExecutionResult groupCommandExecutionResult = _processor.Execute(actor, groupCommand); if (!groupCommandExecutionResult.Success) { return Friendly("Group command denied: " + ReasonLabel(groupCommandExecutionResult.ReasonCode) + ".", selection); } bool num = request.Operation == GroupFriendlyOperation.Create || request.Operation == GroupFriendlyOperation.Accept; bool flag = request.Operation == GroupFriendlyOperation.Leave || request.Operation == GroupFriendlyOperation.Delete; string reasonCode2; if (num) { _activeGroups.TrySetAuthoritative(actor, groupCommand.GroupId, out selection, out reasonCode2); } else if (flag) { _activeGroups.TrySetAuthoritative(actor, Guid.Empty, out selection, out reasonCode2); } else { selection = _activeGroups.Resolve(actor); } return Friendly(SuccessLabel(request.Operation, groupCommandExecutionResult.Group ?? group2), selection); } private GroupCommand BuildActiveCommand(GroupFriendlyRequest request, GroupRecord group, out string failure) { failure = string.Empty; switch (request.Operation) { case GroupFriendlyOperation.Invite: { if (!TryResolveConnectedPlayer(request.Primary, out var identity2, out failure)) { return null; } return new GroupCommand(GroupCommandKind.Invite, group.Id, "", identity2, GroupRole.Member, DateTime.UtcNow.AddHours(request.Number).Ticks); } case GroupFriendlyOperation.Leave: return new GroupCommand(GroupCommandKind.Leave, group.Id, "", null, GroupRole.Member, 0L); case GroupFriendlyOperation.Rename: return new GroupCommand(GroupCommandKind.Rename, group.Id, request.Primary, null, GroupRole.Member, 0L); case GroupFriendlyOperation.CancelInvitation: case GroupFriendlyOperation.Remove: case GroupFriendlyOperation.SetRole: case GroupFriendlyOperation.TransferOwnership: { if (!TryResolveConnectedPlayer(request.Primary, out var identity, out failure)) { return null; } if (request.Operation == GroupFriendlyOperation.CancelInvitation) { return new GroupCommand(GroupCommandKind.CancelInvitation, group.Id, "", identity, GroupRole.Member, 0L); } if (request.Operation == GroupFriendlyOperation.Remove) { return new GroupCommand(GroupCommandKind.Remove, group.Id, "", identity, GroupRole.Member, 0L); } if (request.Operation == GroupFriendlyOperation.TransferOwnership) { return new GroupCommand(GroupCommandKind.TransferOwnership, group.Id, "", identity, GroupRole.Member, 0L); } return new GroupCommand(GroupCommandKind.SetRole, group.Id, "", identity, (!(request.Secondary == "officer")) ? GroupRole.Member : GroupRole.Officer, 0L); } case GroupFriendlyOperation.Delete: return new GroupCommand(GroupCommandKind.Delete, group.Id, "", null, GroupRole.Member, 0L); default: failure = "That Group command is unsupported."; return null; } } private bool TryReadCatalog(out GroupCatalog catalog, out string failure) { catalog = null; failure = "group-world-unavailable"; string text = CurrentWorldScope(); if (text.Length == 0) { return false; } GroupWorldReadResult groupWorldReadResult = _store.Read(text); if (groupWorldReadResult == null || groupWorldReadResult.State == GroupWorldReadState.Unavailable) { failure = "group-store-unavailable"; return false; } if (groupWorldReadResult.State == GroupWorldReadState.Corrupt || groupWorldReadResult.State == GroupWorldReadState.EvidenceConflict) { failure = "group-store-evidence-conflict"; return false; } catalog = ((groupWorldReadResult.State == GroupWorldReadState.Missing) ? GroupCatalog.Empty : groupWorldReadResult.Catalog); failure = ((catalog == null) ? "group-store-invalid" : string.Empty); return catalog != null; } private string[] MembershipIds(StableIdentity actor) { if (actor == null || !TryReadCatalog(out var catalog, out var _)) { return Array.Empty(); } return (from value in catalog.GetMemberships(actor).Take(64) select value.GroupIdText).ToArray(); } private static bool TryResolveMemberGroup(GroupCatalog catalog, StableIdentity actor, string selector, out GroupRecord group, out string failure) { group = null; failure = "No Group with that name is available to you."; GroupMember member; Guid groupId; GroupRecord[] array = catalog.Groups.Where((GroupRecord value) => value.TryGetMember(actor, out member) && ((!GroupIdentity.TryParseCanonicalId(selector, out groupId)) ? string.Equals(value.DisplayName, selector, StringComparison.OrdinalIgnoreCase) : (value.Id == groupId))).Take(2).ToArray(); if (array.Length == 1) { group = array[0]; failure = string.Empty; return true; } if (array.Length > 1) { failure = "That Group name is ambiguous."; } return false; } private static bool TryResolveInvitation(GroupCatalog catalog, StableIdentity actor, string selector, out GroupRecord group, out string failure) { group = null; failure = "No current invitation from that Group was found."; GroupInvitation invitation; Guid groupId; GroupRecord[] array = catalog.Groups.Where((GroupRecord value) => value.TryGetInvitation(actor, out invitation) && !invitation.IsExpired(DateTime.UtcNow.Ticks) && ((!GroupIdentity.TryParseCanonicalId(selector, out groupId)) ? string.Equals(value.DisplayName, selector, StringComparison.OrdinalIgnoreCase) : (value.Id == groupId))).Take(2).ToArray(); if (array.Length != 1) { return false; } group = array[0]; failure = string.Empty; return true; } private static bool TryActiveRecord(StableIdentity actor, GroupCatalog catalog, ActiveGroupSelection active, out GroupRecord group, out string failure) { group = null; failure = "No active Group. Use /group use first."; GroupMember member; if (active != null && active.IsAvailable && Guid.TryParseExact(active.GroupId, "N", out var result) && catalog.TryGetGroup(result, out group)) { return group.TryGetMember(actor, out member); } return false; } private bool TryResolveConnectedPlayer(string selector, out StableIdentity identity, out string failure) { identity = null; failure = "No connected player has that exact name."; if (TryCanonicalPlayerIdentity(selector, out identity)) { failure = string.Empty; return true; } ConnectedPlayer[] array = (from value in (from value in ConnectedPlayers() where string.Equals(value.Name, selector, StringComparison.OrdinalIgnoreCase) select value).GroupBy((ConnectedPlayer value) => value.Identity.CanonicalKey, StringComparer.Ordinal) select value.First()).Take(2).ToArray(); if (array.Length == 1) { identity = array[0].Identity; failure = string.Empty; return true; } if (array.Length > 1) { failure = "More than one connected player has that name."; } return false; } private static List ConnectedPlayers() { List list = new List(); HashSet seen = new HashSet(StringComparer.Ordinal); ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return list; } Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer != (Object)null && ((Character)localPlayer).IsOwner() && localPlayer.GetPlayerID() > 0) { AddConnected(list, seen, PlayerIdentity(localPlayer.GetPlayerID()), localPlayer.GetPlayerName()); } foreach (ZNetPeer item in instance.GetPeers().Take(64)) { if (item != null && item.IsReady() && TryResolvePeerIdentity(item.m_uid, out var identity)) { AddConnected(list, seen, identity, item.m_playerName); } } return list; } private static void AddConnected(ICollection values, ISet seen, StableIdentity identity, string name) { string text = SafeName(name); if (identity != null && text.Length != 0 && seen.Add(identity.CanonicalKey)) { values.Add(new ConnectedPlayer { Identity = identity, Name = text }); } } private static string ConnectedLabel(IEnumerable values, StableIdentity identity) { return values.FirstOrDefault((ConnectedPlayer value) => value.Identity.Equals(identity))?.Name ?? ("Player " + identity.SubjectId); } private static bool TryResolvePeerIdentity(long sender, out StableIdentity identity) { //IL_0058: 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) identity = null; ZNet instance = ZNet.instance; ZNetPeer val = ((instance != null) ? instance.GetPeer(sender) : null); if ((Object)(object)instance == (Object)null || !instance.IsServer() || val == null || val.m_uid != sender || !val.IsReady() || ((ZDOID)(ref val.m_characterID)).IsNone() || ZDOMan.instance == null) { return false; } ZDO zDO = ZDOMan.instance.GetZDO(val.m_characterID); if (zDO == null || !zDO.IsValid() || zDO.GetOwner() != sender || ZDOMan.instance.GetZDO(val.m_characterID) != zDO) { return false; } ZNetScene instance2 = ZNetScene.instance; GameObject val2 = ((instance2 != null) ? instance2.GetPrefab(zDO.GetPrefab()) : null); long num = zDO.GetLong(ZDOVars.s_playerID, 0L); if ((Object)(object)val2 == (Object)null || (Object)(object)val2.GetComponent() == (Object)null || num <= 0) { return false; } identity = PlayerIdentity(num); return true; } private static bool TryLocalIdentity(out StableIdentity identity) { identity = null; Player localPlayer = Player.m_localPlayer; if ((Object)(object)localPlayer == (Object)null || !((Character)localPlayer).IsOwner() || localPlayer.GetPlayerID() <= 0) { return false; } identity = PlayerIdentity(localPlayer.GetPlayerID()); return true; } private static StableIdentity PlayerIdentity(long playerId) { return new StableIdentity("valheim.player", playerId.ToString(CultureInfo.InvariantCulture)); } private static bool TryCanonicalPlayerIdentity(string value, out StableIdentity identity) { identity = null; if (value == null || !value.StartsWith("valheim.player:", StringComparison.Ordinal)) { return false; } string text = value.Substring("valheim.player:".Length); if (long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out var result) && result > 0 && text == result.ToString(CultureInfo.InvariantCulture)) { return StableIdentity.TryCreate("valheim.player", text, out identity); } return false; } private static string CurrentWorldScope() { try { ZNet instance = ZNet.instance; if ((Object)(object)instance == (Object)null || !instance.IsServer()) { return string.Empty; } long worldUID = instance.GetWorldUID(); string result; if (worldUID != 0L) { ulong num = (ulong)worldUID; result = "valheim." + num.ToString("x16", CultureInfo.InvariantCulture); } else { result = string.Empty; } return result; } catch { return string.Empty; } } private void Expire(long now) { while (_replayOrder.Count != 0) { string key = _replayOrder.Peek(); if (_replays.TryGetValue(key, out var value) && value.Expires > now) { break; } _replayOrder.Dequeue(); _replays.Remove(key); } if (_clientSnapshotExpires != 0L && now >= _clientSnapshotExpires) { ClearClientSnapshot(); } } private void ClearClientSnapshot() { _clientMemberships.Clear(); _clientActive = ActiveGroupSelection.Stale; _clientSnapshotExpires = 0L; } private static ZPackage WriteRequest(string id, byte[] payload) { //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_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id); val.Write(payload ?? Array.Empty()); val.Write(1196576817); return val; } private static bool TryReadRequest(ZPackage package, out string id, out byte[] payload) { id = string.Empty; payload = null; try { if (package == null || package.Size() < 1 || package.Size() > 32768 || package.ReadInt() != 1) { return false; } id = package.ReadString(); payload = package.ReadByteArray(); return CanonicalRequestId(id) && payload != null && payload.Length <= 1024 && package.ReadInt() == 1196576817 && package.GetPos() == package.Size(); } catch { return false; } } private static ZPackage WriteResponse(string id, bool accepted, string reason, byte[] payload, IReadOnlyList memberships) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Expected O, but got Unknown ZPackage val = new ZPackage(); val.Write(1); val.Write(id ?? string.Empty); val.Write(accepted); val.Write(BoundedReason(reason)); val.Write(payload ?? Array.Empty()); val.Write(memberships?.Count ?? 0); if (memberships != null) { for (int i = 0; i < memberships.Count; i++) { val.Write(memberships[i]); } } val.Write(1196576817); return val; } private static bool TryReadResponse(ZPackage package, out string id, out bool accepted, out string reason, out byte[] payload, out string[] memberships) { id = string.Empty; accepted = false; reason = string.Empty; payload = null; memberships = Array.Empty(); try { if (package == null || package.Size() < 1 || package.Size() > 32768 || package.ReadInt() != 1) { return false; } id = package.ReadString(); accepted = package.ReadBool(); reason = package.ReadString(); payload = package.ReadByteArray(); int num = package.ReadInt(); if (!CanonicalRequestId(id) || !CanonicalReason(reason) || payload == null || payload.Length > 65536 || num < 0 || num > 64) { return false; } memberships = new string[num]; for (int i = 0; i < num; i++) { memberships[i] = package.ReadString(); if (!GroupIdentity.IsCanonicalId(memberships[i])) { return false; } } return package.ReadInt() == 1196576817 && package.GetPos() == package.Size(); } catch { memberships = Array.Empty(); return false; } } private static ZPackage Clone(ZPackage source) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Expected O, but got Unknown return new ZPackage(source.GetArray()); } private static bool Exact(byte[] left, byte[] right) { if (left == null || right == null || left.Length != right.Length) { return false; } int num = 0; for (int i = 0; i < left.Length; i++) { num |= left[i] ^ right[i]; } return num == 0; } private static bool CanonicalRequestId(string value) { if (value != null && value.Length == 32 && Guid.TryParseExact(value, "N", out var result) && result != Guid.Empty) { return value == result.ToString("N"); } return false; } private static string BoundedReason(string value) { string text = (string.IsNullOrWhiteSpace(value) ? "unavailable" : value.Trim()); if (text.Length > 96) { text = text.Substring(0, 96); } StringBuilder stringBuilder = new StringBuilder(text.Length); string text2 = text; foreach (char c in text2) { bool flag = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.'; stringBuilder.Append(flag ? c : '-'); } return stringBuilder.ToString(); } private static bool CanonicalReason(string value) { if (value != null && value.Length > 0 && value.Length <= 96) { return value == BoundedReason(value); } return false; } private static GroupFriendlyResponse Friendly(string text, ActiveGroupSelection active) { return new GroupFriendlyResponse(text, active); } private static string ActiveLabel(ActiveGroupSelection active) { if (active == null || !active.IsAvailable) { return "Active Group: none. Use /group use ."; } return "Active Group: " + active.DisplayName + "."; } private static string SuccessLabel(GroupFriendlyOperation operation, GroupRecord group) { string text = group?.DisplayName ?? "the Group"; return operation switch { GroupFriendlyOperation.Create => "Group created: " + text + ". It is now active.", GroupFriendlyOperation.Invite => "Invitation sent for " + text + ".", GroupFriendlyOperation.Accept => "Joined " + text + ". It is now active.", GroupFriendlyOperation.Leave => "You left " + text + ".", GroupFriendlyOperation.Rename => "Group renamed to " + text + ".", GroupFriendlyOperation.CancelInvitation => "Invitation cancelled.", GroupFriendlyOperation.Remove => "Player removed from " + text + ".", GroupFriendlyOperation.SetRole => "Player role updated in " + text + ".", GroupFriendlyOperation.TransferOwnership => "Ownership of " + text + " transferred.", GroupFriendlyOperation.Delete => "Group deleted: " + text + ".", _ => "Group command completed.", }; } private static string ReasonLabel(string reason) { if (!string.IsNullOrEmpty(reason)) { return reason.Replace('-', ' '); } return "unavailable"; } private static string SafeName(string value) { string text = (value ?? string.Empty).Trim(); if (text.Length == 0 || !text.IsNormalized(NormalizationForm.FormC) || Encoding.UTF8.GetByteCount(text) > 128) { return string.Empty; } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { return string.Empty; } } return text; } private static void OnGroupCommand(ConsoleEventArgs args) { PortalGroupRuntime commandRuntime; lock (CommandGate) { commandRuntime = _commandRuntime; } GroupFriendlyRequest request; string message; if (commandRuntime == null || commandRuntime._disposed) { if (args != null) { Terminal context = args.Context; if (context != null) { context.AddString("Runic Group: service unavailable."); } } } else if (!TryParseCommand(args?.Args, out request, out message)) { if (args != null) { Terminal context2 = args.Context; if (context2 != null) { context2.AddString(message); } } } else { commandRuntime.Submit(request, (Action)args.Context.AddString, silent: false); } } private static bool TryParseCommand(string[] args, out GroupFriendlyRequest request, out string message) { request = null; args = args ?? Array.Empty(); string text = ((args.Length > 1) ? (args[1] ?? string.Empty).Trim().ToLowerInvariant() : "help"); try { switch (text) { case "help": message = "Group commands: create, list, use, active, members, whoami, invite, accept, leave, rename, cancel, remove, role, transfer, delete."; return false; case "list": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.List); break; case "active": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.Active); break; case "members": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.Members); break; case "whoami": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.WhoAmI); break; case "create": request = new GroupFriendlyRequest(GroupFriendlyOperation.Create, Join(args, 2), "", 0, Guid.NewGuid()); break; case "select": case "use": request = new GroupFriendlyRequest(GroupFriendlyOperation.Select, Join(args, 2)); break; case "invite": request = new GroupFriendlyRequest(GroupFriendlyOperation.Invite, Join(args, 2), "", 168); break; case "accept": request = new GroupFriendlyRequest(GroupFriendlyOperation.Accept, Join(args, 2)); break; case "leave": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.Leave); break; case "rename": request = new GroupFriendlyRequest(GroupFriendlyOperation.Rename, Join(args, 2)); break; case "cancel": request = new GroupFriendlyRequest(GroupFriendlyOperation.CancelInvitation, Join(args, 2)); break; case "remove": request = new GroupFriendlyRequest(GroupFriendlyOperation.Remove, Join(args, 2)); break; case "role": if (args.Length < 4) { throw new ArgumentException(); } request = new GroupFriendlyRequest(GroupFriendlyOperation.SetRole, Join(args, 2, args.Length - 1), (args[^1] ?? string.Empty).Trim().ToLowerInvariant()); break; case "transfer": request = new GroupFriendlyRequest(GroupFriendlyOperation.TransferOwnership, Join(args, 2)); break; case "delete": RequireCount(args, 2); request = new GroupFriendlyRequest(GroupFriendlyOperation.Delete); break; default: message = "Unknown Group command. Type /group help."; return false; } message = string.Empty; return true; } catch (Exception ex) when (ex is ArgumentException || ex is OverflowException) { request = null; message = "That Group command is incomplete or invalid. Type /group help."; return false; } } private static void RequireCount(string[] args, int count) { if (args.Length != count) { throw new ArgumentException(); } } private static string Join(string[] args, int start, int end = -1) { if (end < 0) { end = args.Length; } if (start >= end) { throw new ArgumentException(); } string text = string.Join(" ", args, start, end - start).Trim(); if (text.Length == 0) { throw new ArgumentException(); } return text; } } internal sealed class PortalHoverPanel : IDisposable { private const int MaximumBindingCharacters = 64; private readonly PortalRuntime _runtime; private TeleportWorld _observedPortal; private int _observedFrame = -10; private bool _disabled; private int _styledFontSize; private GUIStyle _boxStyle; private GUIStyle _titleStyle; private GUIStyle _statusStyle; private GUIStyle _warningStyle; private GUIStyle _headingStyle; private GUIStyle _bodyStyle; internal PortalHoverPanel(PortalRuntime runtime) { _runtime = runtime ?? throw new ArgumentNullException("runtime"); } internal void Observe(TeleportWorld portal) { if (!_disabled && !((Object)(object)portal == (Object)null)) { _observedPortal = portal; _observedFrame = Time.frameCount; } } internal void ResetStyles() { _styledFontSize = 0; } internal void Disable(Exception exception) { if (!_disabled) { _disabled = true; _observedPortal = null; Diagnostics.Warning("Runic Portals disabled only its setup panel after a presentation fault; portal routing remains active. " + ((exception == null) ? "Unknown failure." : (exception.GetType().Name + ": " + exception.Message))); } } internal void Draw() { //IL_00cd: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_01e9: Unknown result type (might be due to invalid IL or missing references) if (_disabled || Application.isBatchMode || Event.current == null) { return; } ConfigEntry showSetupPanel = PortalConfig.ShowSetupPanel; if ((showSetupPanel != null && !showSetupPanel.Value) || !_runtime.FeatureEnabled) { return; } TeleportWorld val = _runtime.ActiveEditPortalForPanel; if ((Object)(object)val == (Object)null) { int num = Time.frameCount - _observedFrame; if ((Object)(object)_observedPortal == (Object)null || num < 0 || num > 1) { return; } val = _observedPortal; } if ((Object)(object)val == (Object)null || (Object)(object)Player.m_localPlayer == (Object)null || (Object)(object)Hud.instance == (Object)null || Hud.instance.m_userHidden || !_runtime.TryGetHoverPanelState(val, out var state)) { return; } ResolveBindings(out var use, out var alternateUse); PortalSetupGuideContent content = PortalSetupGuide.Compose(state, use, alternateUse); Rect safeArea = Screen.safeArea; float num2 = (float)Screen.height - ((Rect)(ref safeArea)).yMax; float num3 = 12f; float num4 = ((Rect)(ref safeArea)).width - num3 * 2f; float num5 = ((Rect)(ref safeArea)).height - num3 * 2f; if (!Finite(num4) || !Finite(num5) || num4 < 340f || num5 < 300f) { return; } float num6 = FiniteClamp(PortalConfig.PanelScale?.Value ?? 1f, 0.75f, 1.5f, 1f); float num7 = Mathf.Min(700f * num6, num4); int num8 = Mathf.Clamp(Mathf.RoundToInt(14f * num6), 10, 22); float num9; while (true) { EnsureStyles(num8); num9 = Measure(content, num7, num8); if (num9 <= num5 || num8 <= 10) { break; } num8--; } if (!(num9 > num5)) { float num10 = ((Rect)(ref safeArea)).xMin + num3; float num11 = num2 + Mathf.Max(num3, (((Rect)(ref safeArea)).height - num9) * 0.5f); GUI.Box(new Rect(num10, num11, num7, num9), GUIContent.none, _boxStyle); float num12 = Mathf.Max(10f, (float)num8 * 0.8f); float num13 = Mathf.Max(4f, (float)num8 * 0.32f); float width = num7 - num12 * 2f; float y = num11 + num12; DrawLabel(ref y, num10 + num12, width, "Runic Portals - setup guide", _titleStyle, num13); DrawLabel(ref y, num10 + num12, width, content.Status, _statusStyle, num13); if (content.Warning.Length != 0) { DrawLabel(ref y, num10 + num12, width, content.Warning, _warningStyle, num13); } DrawLabel(ref y, num10 + num12, width, "Controls", _headingStyle, num13 * 0.5f); DrawLabel(ref y, num10 + num12, width, content.Controls, _bodyStyle, num13); DrawLabel(ref y, num10 + num12, width, "Setup commands and access", _headingStyle, num13 * 0.5f); DrawLabel(ref y, num10 + num12, width, content.Instructions, _bodyStyle, 0f); } } public void Dispose() { _observedPortal = null; _boxStyle = (_titleStyle = (_statusStyle = (_warningStyle = (_headingStyle = (_bodyStyle = null))))); } internal static string FriendlyControllerPath(string path, string fallback) { string text = (path ?? string.Empty).Trim().Replace('\\', '/').ToLowerInvariant(); if (text.EndsWith("/lefttrigger", StringComparison.Ordinal)) { return "Left Trigger"; } if (text.EndsWith("/righttrigger", StringComparison.Ordinal)) { return "Right Trigger"; } if (text.EndsWith("/leftshoulder", StringComparison.Ordinal)) { return "Left Bumper"; } if (text.EndsWith("/rightshoulder", StringComparison.Ordinal)) { return "Right Bumper"; } if (text.EndsWith("/buttonsouth", StringComparison.Ordinal)) { return "A / Cross"; } if (text.EndsWith("/buttoneast", StringComparison.Ordinal)) { return "B / Circle"; } if (text.EndsWith("/buttonwest", StringComparison.Ordinal)) { return "X / Square"; } if (text.EndsWith("/buttonnorth", StringComparison.Ordinal)) { return "Y / Triangle"; } if (text.EndsWith("/dpad/up", StringComparison.Ordinal)) { return "D-pad Up"; } if (text.EndsWith("/dpad/down", StringComparison.Ordinal)) { return "D-pad Down"; } if (text.EndsWith("/dpad/left", StringComparison.Ordinal)) { return "D-pad Left"; } if (text.EndsWith("/dpad/right", StringComparison.Ordinal)) { return "D-pad Right"; } if (text.EndsWith("/leftstickpress", StringComparison.Ordinal)) { return "Left Stick Click"; } if (text.EndsWith("/rightstickpress", StringComparison.Ordinal)) { return "Right Stick Click"; } if (text.EndsWith("/start", StringComparison.Ordinal)) { return "Menu"; } if (text.EndsWith("/select", StringComparison.Ordinal)) { return "View / Share"; } return BoundedLabel(fallback, "Controller action"); } private float Measure(PortalSetupGuideContent content, float width, int fontSize) { float num = Mathf.Max(10f, (float)fontSize * 0.8f); float num2 = Mathf.Max(4f, (float)fontSize * 0.32f); float width2 = width - num * 2f; float num3 = num * 2f; num3 += LabelHeight("Runic Portals - setup guide", _titleStyle, width2) + num2; num3 += LabelHeight(content.Status, _statusStyle, width2) + num2; if (content.Warning.Length != 0) { num3 += LabelHeight(content.Warning, _warningStyle, width2) + num2; } num3 += LabelHeight("Controls", _headingStyle, width2) + num2 * 0.5f; num3 += LabelHeight(content.Controls, _bodyStyle, width2) + num2; num3 += LabelHeight("Setup commands and access", _headingStyle, width2) + num2 * 0.5f; num3 += LabelHeight(content.Instructions, _bodyStyle, width2); return Mathf.Ceil(num3); } private static float LabelHeight(string text, GUIStyle style, float width) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Expected O, but got Unknown return Mathf.Ceil(style.CalcHeight(new GUIContent(text ?? string.Empty), width)); } private static void DrawLabel(ref float y, float x, float width, string text, GUIStyle style, float gap) { //IL_000f: Unknown result type (might be due to invalid IL or missing references) float num = LabelHeight(text, style, width); GUI.Label(new Rect(x, y, width, num), text, style); y += num + gap; } private void EnsureStyles(int fontSize) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Expected O, but got Unknown //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) if (_boxStyle == null || _styledFontSize != fontSize) { _styledFontSize = fontSize; _boxStyle = new GUIStyle(GUI.skin.box); _titleStyle = Label(fontSize + 3, (FontStyle)1, new Color(0.96f, 0.82f, 0.4f, 1f)); _statusStyle = Label(fontSize, (FontStyle)1, new Color(0.75f, 0.9f, 1f, 1f)); _warningStyle = Label(fontSize, (FontStyle)1, new Color(1f, 0.68f, 0.3f, 1f)); _headingStyle = Label(fontSize, (FontStyle)1, new Color(0.88f, 0.88f, 0.88f, 1f)); _bodyStyle = Label(Math.Max(10, fontSize - 1), (FontStyle)0, new Color(0.9f, 0.92f, 0.94f, 1f)); } } private static GUIStyle Label(int fontSize, FontStyle fontStyle, Color color) { //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_0016: 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_001d: 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_002b: 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_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown GUIStyle val = new GUIStyle(GUI.skin.label) { fontSize = fontSize, fontStyle = fontStyle, alignment = (TextAnchor)0, wordWrap = true, richText = false }; val.normal.textColor = color; return val; } private static void ResolveBindings(out string use, out string alternateUse) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) bool flag = false; try { flag = ZInput.IsGamepadActive(); } catch (Exception) { } if (!flag) { use = KeyboardBinding("Use", "Use"); string text = KeyboardBinding("AltPlace", "Alternate Place"); alternateUse = text + " + " + use; } else { use = ControllerBinding("JoyUse", "Use"); string text2 = ControllerBinding(((int)ZInput.InputLayout == 0) ? "JoyAltPlace" : "JoyAltKeys", "Controller Alt"); alternateUse = text2 + " + " + use; } } private static string KeyboardBinding(string action, string fallback) { try { ZInput instance = ZInput.instance; string text = ((instance != null) ? instance.GetBoundKeyString(action, true) : null); if (!string.IsNullOrWhiteSpace(text) && text.IndexOf(" 64) { text = text.Substring(0, 64); } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { return fallback; } } return text; } private static float FiniteClamp(float value, float minimum, float maximum, float fallback) { if (!Finite(value)) { return fallback; } return Mathf.Clamp(value, minimum, maximum); } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal sealed class PortalIndex { private readonly PortalGraphService _graph; private readonly CorrelatedDiagnosticBuffer _diagnostics; private readonly Dictionary _byId = new Dictionary(StringComparer.Ordinal); private readonly List _sorted = new List(); private readonly List _endpoints = new List(); private float _nextRefresh; private bool _dirty = true; internal int Count => _graph.Count; internal PortalIndex(PortalGraphService graph, CorrelatedDiagnosticBuffer diagnostics) { _graph = graph ?? throw new ArgumentNullException("graph"); _diagnostics = diagnostics ?? throw new ArgumentNullException("diagnostics"); } internal void MarkDirty() { _dirty = true; } internal bool Tick(float realtime, float interval) { if (!ValheimContracts.IsServer || (!_dirty && realtime < _nextRefresh)) { return false; } Rebuild(realtime, interval); return true; } internal bool Rebuild(float realtime, float interval) { _nextRefresh = realtime + Math.Max(0.5f, interval); _dirty = false; List list = ValheimContracts.PortalObjects(); if (list == null) { _graph.RejectAuthoritativeSnapshot(RouteStopCode.AuthorityUnavailable); _diagnostics.Record(PortalDiagnosticCode.SnapshotRejected, RouteStopCode.AuthorityUnavailable, "", 0L); return false; } if (list.Count > 4096) { _graph.RejectAuthoritativeSnapshot(RouteStopCode.GraphLimitExceeded); _diagnostics.Record(PortalDiagnosticCode.SnapshotRejected, RouteStopCode.GraphLimitExceeded, "", 0L); return false; } _sorted.Clear(); for (int i = 0; i < list.Count; i++) { ZDO val = list[i]; if (val != null && val.IsValid() && !((ZDOID)(ref val.m_uid)).IsNone()) { _sorted.Add(val); } } _sorted.Sort(CompareZdo); _endpoints.Clear(); Dictionary dictionary = new Dictionary(StringComparer.Ordinal); for (int j = 0; j < _sorted.Count; j++) { ZDO val2 = _sorted[j]; if (!PortalZdoCodec.TryRead(val2, out var endpoint, out var failure)) { switch (failure) { case "schema-unsupported": case "mode-unsupported": case "record-invalid": _diagnostics.Record(PortalDiagnosticCode.SnapshotRejected, RouteStopCode.StaleSelection, ((object)Unsafe.As(ref val2.m_uid)/*cast due to .constrained prefix*/).ToString(), 0L); break; } continue; } if (dictionary.ContainsKey(endpoint.PortalId)) { _graph.RejectAuthoritativeSnapshot(RouteStopCode.StaleSelection); _diagnostics.Record(PortalDiagnosticCode.SnapshotRejected, RouteStopCode.StaleSelection, endpoint.PortalId, 0L); return false; } _endpoints.Add(endpoint); dictionary.Add(endpoint.PortalId, val2); } if (!_graph.TryReplaceAuthoritativeSnapshot(_endpoints, out var failure2)) { Diagnostics.Warning("Portal graph snapshot rejected: " + failure2); _diagnostics.Record(PortalDiagnosticCode.SnapshotRejected, _graph.SnapshotStop, "", 0L); return false; } _byId.Clear(); foreach (KeyValuePair item in dictionary) { _byId.Add(item.Key, item.Value); } _diagnostics.Record(PortalDiagnosticCode.SnapshotAccepted, RouteStopCode.Ready, "", 0L); return true; } internal bool TryGetZdo(string portalId, out ZDO zdo) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) zdo = null; if (!_byId.TryGetValue(portalId ?? string.Empty, out var value) || value == null || !value.IsValid() || ((ZDOID)(ref value.m_uid)).IsNone()) { return false; } if (ZDOMan.instance == null || ZDOMan.instance.GetZDO(value.m_uid) != value) { return false; } zdo = value; return true; } internal void Clear() { _byId.Clear(); _sorted.Clear(); _endpoints.Clear(); _graph.TryReplaceAuthoritativeSnapshot(Array.Empty(), out var _); _dirty = true; _nextRefresh = 0f; } private static int CompareZdo(ZDO left, ZDO right) { if (left == right) { return 0; } if (left == null) { return -1; } if (right == null) { return 1; } int num = ((ZDOID)(ref left.m_uid)).UserID.CompareTo(((ZDOID)(ref right.m_uid)).UserID); if (num == 0) { return ((ZDOID)(ref left.m_uid)).ID.CompareTo(((ZDOID)(ref right.m_uid)).ID); } return num; } } internal static class PortalMapMarkerSprite { internal const string ResourceName = "RunicPortals.Assets.RunicPortalMapPin.png"; internal const int ExpectedWidth = 128; internal const int ExpectedHeight = 128; internal const int MaximumPngBytes = 262144; private static Texture2D _texture; private static Sprite _sprite; private static bool _loadAttempted; private static bool _failureLogged; internal static bool Apply(PinData pin) { if (pin == null) { return false; } try { Sprite orCreate = GetOrCreate(); if (!Object.op_Implicit((Object)(object)orCreate)) { return false; } pin.m_icon = orCreate; if (Object.op_Implicit((Object)(object)pin.m_iconElement)) { pin.m_iconElement.sprite = orCreate; } return true; } catch (Exception exception) { LogFallback(exception); return false; } } internal static void Shutdown() { Sprite sprite = _sprite; Texture2D texture = _texture; _sprite = null; _texture = null; _loadAttempted = false; _failureLogged = false; if (Object.op_Implicit((Object)(object)sprite)) { Object.Destroy((Object)(object)sprite); } if (Object.op_Implicit((Object)(object)texture)) { Object.Destroy((Object)(object)texture); } } private static Sprite GetOrCreate() { //IL_0030: 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_0040: 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_004f: Unknown result type (might be due to invalid IL or missing references) //IL_0057: Expected O, but got Unknown //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)_sprite)) { return _sprite; } if (_loadAttempted) { return null; } _loadAttempted = true; Texture2D val = null; Sprite val2 = null; try { byte[] array = ReadEmbeddedPng(); val = new Texture2D(2, 2, (TextureFormat)4, false, false) { name = "Runic Portal Map Pin Texture", hideFlags = (HideFlags)61, filterMode = (FilterMode)1, wrapMode = (TextureWrapMode)1 }; if (!ImageConversion.LoadImage(val, array, true)) { throw new InvalidDataException("Unity rejected the embedded portal marker PNG."); } if (((Texture)val).width != 128 || ((Texture)val).height != 128) { throw new InvalidDataException("Embedded portal marker dimensions are not 128x128."); } val2 = Sprite.Create(val, new Rect(0f, 0f, (float)((Texture)val).width, (float)((Texture)val).height), new Vector2(0.5f, 0.5f), 100f, 0u, (SpriteMeshType)0); if (!Object.op_Implicit((Object)(object)val2)) { throw new InvalidOperationException("Unity did not create the portal marker sprite."); } ((Object)val2).name = "Runic Portal Map Pin"; ((Object)val2).hideFlags = (HideFlags)61; _texture = val; _sprite = val2; return val2; } catch (Exception exception) { if (Object.op_Implicit((Object)(object)val2)) { Object.Destroy((Object)(object)val2); } if (Object.op_Implicit((Object)(object)val)) { Object.Destroy((Object)(object)val); } LogFallback(exception); return null; } } private static byte[] ReadEmbeddedPng() { using Stream stream = typeof(PortalMapMarkerSprite).Assembly.GetManifestResourceStream("RunicPortals.Assets.RunicPortalMapPin.png"); if (stream == null) { throw new MissingManifestResourceException("RunicPortals.Assets.RunicPortalMapPin.png"); } long length = stream.Length; if (length < 32 || length > 262144) { throw new InvalidDataException("Embedded portal marker PNG size is invalid."); } byte[] array = new byte[(int)length]; int num; for (int i = 0; i < array.Length; i += num) { num = stream.Read(array, i, array.Length - i); if (num <= 0) { throw new EndOfStreamException("Embedded portal marker PNG ended early."); } } return array; } private static void LogFallback(Exception exception) { if (!_failureLogged) { _failureLogged = true; Diagnostics.Warning("The custom portal map marker could not be loaded; Runic Portals will use Valheim's fallback marker for this session. " + exception.GetType().Name + ": " + exception.Message); } } } internal sealed class PortalMapOverlayRuntime { private const PinType PortalPinType = (PinType)3; private const float OverlayWidth = 430f; private const float OverlayHeight = 44f; private static readonly FieldInfo VisibleIconTypesField = typeof(Minimap).GetField("m_visibleIconTypes", BindingFlags.Instance | BindingFlags.NonPublic); private static readonly MethodInfo ToggleIconFilterMethod = typeof(Minimap).GetMethod("ToggleIconFilter", BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[1] { typeof(PinType) }, null); private readonly PortalMapOverlayModel _model = new PortalMapOverlayModel(); private readonly List _pins = new List(); private Minimap _map; private PortalMapOverlayView _view; private bool _mapOpen; private bool _suspended; private bool _faulted; private bool _filterStateCaptured; private bool _portalPinTypeWasVisible; private bool _loading; private bool _truncated; private bool _unavailable; private bool _directoryVisible; private int _renderedRevision = int.MinValue; internal string SelectedNetworkName => _model.SelectedNetwork; internal int VisiblePinCount => _pins.Count; internal bool IsMapOpen => _mapOpen; internal bool ReplaceAuthorizedSnapshot(string contextToken, IReadOnlyList candidates) { if (_faulted) { return false; } try { if (_model.SetContext(contextToken)) { ClearPins(); } bool result = _model.TryReplace(candidates); InvalidatePins(); RefreshPinsIfVisible(); return result; } catch (Exception exception) { DisableOverlay(exception, "snapshot"); return false; } } internal void NoteNetworkUsed(string contextToken, string networkName) { if (_faulted) { return; } try { if (_model.SetContext(contextToken)) { ClearPins(); } if (_model.NoteNetworkUsed(networkName)) { InvalidatePins(); RefreshPinsIfVisible(); } } catch (Exception exception) { DisableOverlay(exception, "recent-network"); } } internal void SetRefreshState(bool loading, bool truncated, bool unavailable = false) { _loading = loading; _truncated = truncated; _unavailable = unavailable; } internal void OnMapOpened(Minimap map) { if (!_faulted && IsLargeMap(map)) { if ((Object)(object)_map != (Object)(object)map) { ClearPins(); DestroyView(); _map = map; } _mapOpen = true; _suspended = false; _directoryVisible = false; EnsureView(map); SetViewVisible(visible: true); InvalidatePins(); RefreshPinsIfVisible(); } } internal void Tick(Minimap map, bool suspend) { if (_faulted) { return; } try { if (!IsLargeMap(map)) { OnMapClosed(); return; } if (!_mapOpen || (Object)(object)_map != (Object)(object)map) { OnMapOpened(map); } if (suspend) { if (!_suspended) { ClearPins(); } _suspended = true; SetViewVisible(visible: false); return; } if (_suspended) { _suspended = false; InvalidatePins(); SetViewVisible(visible: true); } if (CanReadCycleInput() && ZInput.GetKeyDown((KeyCode)112, false) && NoKeyboardModifiersHeld()) { _directoryVisible = ToggleDirectoryVisibility(_directoryVisible); if (!_directoryVisible) { ClearPins(); } InvalidatePins(); } RefreshPinsIfVisible(); } catch (Exception exception) { DisableOverlay(exception, "tick"); } } internal bool TryCycleNetwork(int direction) { if (_faulted || !_mapOpen || _suspended || !_model.Cycle(direction)) { return false; } InvalidatePins(); RefreshPinsIfVisible(); return true; } internal void OnMapClosed() { ClearPins(); _mapOpen = false; _suspended = false; _directoryVisible = false; SetViewVisible(visible: false); _map = null; } internal void OnIdentityOrWorldChanged(string contextToken) { if (_faulted) { return; } try { if (_model.SetContext(contextToken)) { _directoryVisible = false; ClearPins(); InvalidatePins(); RefreshPinsIfVisible(); } } catch (Exception exception) { DisableOverlay(exception, "context"); } } internal void Shutdown() { ClearPins(); _model.SetContext(string.Empty); _mapOpen = false; _suspended = false; _directoryVisible = false; _map = null; _loading = false; _truncated = false; _unavailable = false; DestroyView(); } internal void DrawOverlay() { //IL_00e9: Unknown result type (might be due to invalid IL or missing references) if (!_faulted && _mapOpen && !_suspended && IsLargeMap(_map)) { string text = (_unavailable ? "Runic portal directory unavailable - retrying" : ((!_directoryVisible) ? "Portal directory: [P] show authorized portals" : ((_model.EligibleCount == 0) ? "Portal directory: no authorized portals [P] hide" : ("Portal directory: " + _pins.Count + " authorized portals [P] hide")))); if (_loading) { text += " Refreshing…"; } else if (_truncated) { text += " Result bound reached"; } float num = Math.Min(430f, Math.Max(220f, (float)Screen.width - 32f)); GUI.Box(new Rect(Math.Max(16f, (float)Screen.width - num - 20f), 20f, num, 44f), text); } } internal static bool ToggleDirectoryVisibility(bool current) { return !current; } private void RefreshPinsIfVisible() { //IL_007c: 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) if (!_mapOpen || _suspended || !_directoryVisible || !IsLargeMap(_map) || _renderedRevision == _model.Revision) { return; } ClearPins(); PortalMapCandidate[] array = _model.SelectedCandidates(); if (array.Length != 0) { CaptureFilterState(); } for (int i = 0; i < array.Length; i++) { PortalMapCandidate portalMapCandidate = array[i]; PinData val = _map.AddPin(new Vector3(portalMapCandidate.X, portalMapCandidate.Y, portalMapCandidate.Z), (PinType)3, EscapeRichText(portalMapCandidate.DisplayName), false, false, 0L, PlatformUserID.None); if (val == null) { throw new InvalidOperationException("Minimap.AddPin returned no pin data."); } PortalMapMarkerSprite.Apply(val); _pins.Add(val); } _renderedRevision = _model.Revision; } private void ClearPins() { Minimap map = _map; if (Object.op_Implicit((Object)(object)map)) { for (int num = _pins.Count - 1; num >= 0; num--) { PinData val = _pins[num]; if (val != null) { try { map.RemovePin(val); } catch (Exception ex) { Diagnostics.Trace("Temporary portal map pin cleanup yielded: " + ex.GetType().Name + ": " + ex.Message); } } } } _pins.Clear(); RestoreFilterState(map); _renderedRevision = int.MinValue; } private void CaptureFilterState() { _filterStateCaptured = false; if (!Object.op_Implicit((Object)(object)_map) || VisibleIconTypesField == null || ToggleIconFilterMethod == null) { throw new MissingMemberException("Installed Minimap icon-filter contract is unavailable."); } bool[] array = VisibleIconTypesField.GetValue(_map) as bool[]; int num = 3; if (array == null || num < 0 || num >= array.Length) { throw new InvalidOperationException("Installed Minimap icon filters are invalid."); } _portalPinTypeWasVisible = array[num]; _filterStateCaptured = true; if (ShouldToggleFilterOnCapture(_portalPinTypeWasVisible)) { ToggleIconFilterMethod.Invoke(_map, new object[1] { (object)(PinType)3 }); } } private void RestoreFilterState(Minimap map) { if (!_filterStateCaptured) { return; } _filterStateCaptured = false; if (Object.op_Implicit((Object)(object)map) && !_portalPinTypeWasVisible && !(VisibleIconTypesField == null) && !(ToggleIconFilterMethod == null)) { bool[] array = VisibleIconTypesField.GetValue(map) as bool[]; int num = 3; if (array != null && num >= 0 && num < array.Length && ShouldToggleFilterOnCleanup(_portalPinTypeWasVisible, array[num])) { ToggleIconFilterMethod.Invoke(map, new object[1] { (object)(PinType)3 }); } } } internal static bool ShouldToggleFilterOnCapture(bool initiallyVisible) { return !initiallyVisible; } internal static bool ShouldToggleFilterOnCleanup(bool initiallyVisible, bool currentlyVisible) { return !initiallyVisible && currentlyVisible; } private void InvalidatePins() { _renderedRevision = int.MinValue; } private void EnsureView(Minimap map) { if (!Object.op_Implicit((Object)(object)_view) || !((Object)(object)((Component)_view).gameObject == (Object)(object)((Component)map).gameObject)) { DestroyView(); _view = ((Component)map).gameObject.AddComponent(); ((Object)_view).hideFlags = (HideFlags)54; _view.Bind(this); } } private void DestroyView() { if (Object.op_Implicit((Object)(object)_view)) { _view.Bind(null); Object.Destroy((Object)(object)_view); _view = null; } } private void SetViewVisible(bool visible) { if (Object.op_Implicit((Object)(object)_view)) { ((Behaviour)_view).enabled = visible; } } private void DisableOverlay(Exception exception, string operation) { Diagnostics.Error(exception, "Normal-map portal overlay failed closed during " + operation + "; vanilla map behavior remains available."); ClearPins(); _faulted = true; _mapOpen = false; _suspended = false; SetViewVisible(visible: false); } private static bool IsLargeMap(Minimap map) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Invalid comparison between Unknown and I4 if (Object.op_Implicit((Object)(object)map) && (int)map.m_mode == 2 && Object.op_Implicit((Object)(object)map.m_largeRoot)) { return map.m_largeRoot.activeInHierarchy; } return false; } private static bool CanReadCycleInput() { if (ZInput.VirtualKeyboardOpen || TextInput.IsVisible() || Console.IsVisible() || Menu.IsVisible() || InventoryGui.IsVisible()) { return false; } if (!((Object)(object)Chat.instance == (Object)null)) { return !Chat.instance.HasFocus(); } return true; } private static bool NoKeyboardModifiersHeld() { if (!ZInput.GetKey((KeyCode)308, false) && !ZInput.GetKey((KeyCode)307, false) && !ZInput.GetKey((KeyCode)306, false) && !ZInput.GetKey((KeyCode)305, false) && !ZInput.GetKey((KeyCode)304, false)) { return !ZInput.GetKey((KeyCode)303, false); } return false; } private static string EscapeRichText(string value) { return (value ?? string.Empty).Replace('<', '‹').Replace('>', '›'); } } internal sealed class PortalMapOverlayView : MonoBehaviour { private PortalMapOverlayRuntime _owner; internal void Bind(PortalMapOverlayRuntime owner) { _owner = owner; } private void OnGUI() { _owner?.DrawOverlay(); } } internal static class PortalZdoCodec { private sealed class PortalZdoSnapshot { internal int Schema; internal int Mode; internal string Network; internal string Name; internal long Owner; internal string OwnerIdentity; internal int NetworkKind; internal string Group; internal int Direction; internal int Revision; internal static PortalZdoSnapshot Capture(ZDO zdo) { return new PortalZdoSnapshot { Schema = zdo.GetInt("runic.portals.schema", 0), Mode = zdo.GetInt("runic.portals.mode", 0), Network = zdo.GetString("runic.portals.network", string.Empty), Name = zdo.GetString("runic.portals.name", string.Empty), Owner = zdo.GetLong("runic.portals.owner", 0L), OwnerIdentity = zdo.GetString("runic.portals.ownerIdentity", string.Empty), NetworkKind = zdo.GetInt("runic.portals.networkKind", 0), Group = zdo.GetString("runic.portals.group", string.Empty), Direction = zdo.GetInt("runic.portals.direction", 0), Revision = zdo.GetInt("runic.portals.revision", 0) }; } internal void Restore(ZDO zdo) { zdo.Set("runic.portals.mode", 0); zdo.Set("runic.portals.schema", Schema); zdo.Set("runic.portals.network", Network ?? string.Empty); zdo.Set("runic.portals.name", Name ?? string.Empty); zdo.Set("runic.portals.owner", Owner); zdo.Set("runic.portals.ownerIdentity", OwnerIdentity ?? string.Empty); zdo.Set("runic.portals.networkKind", NetworkKind); zdo.Set("runic.portals.group", Group ?? string.Empty); zdo.Set("runic.portals.direction", Direction); zdo.Set("runic.portals.revision", Revision); zdo.Set("runic.portals.mode", Mode); } } internal const int SchemaVersion = 2; internal const int LegacySchemaVersion = 1; internal const string SchemaKey = "runic.portals.schema"; internal const string ModeKey = "runic.portals.mode"; internal const string NetworkKey = "runic.portals.network"; internal const string NameKey = "runic.portals.name"; internal const string OwnerKey = "runic.portals.owner"; internal const string OwnerIdentityKey = "runic.portals.ownerIdentity"; internal const string NetworkKindKey = "runic.portals.networkKind"; internal const string GroupKey = "runic.portals.group"; internal const string DirectionKey = "runic.portals.direction"; internal const string RevisionKey = "runic.portals.revision"; internal const string AuthoritativeRecordKey = "runic.portals.record"; internal const string PublicationPulseKey = "runic.portals.publicationPulse"; private const uint RecordMagic = 827478098u; private const ushort RecordSchema = 1; private const int MaximumRecordBytes = 2048; private static readonly UTF8Encoding StrictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); internal const int ArrivalFlag = 1; internal const int DepartureFlag = 2; internal static bool TryRead(ZDO zdo, out PortalEndpoint endpoint, out string failure) { endpoint = null; failure = string.Empty; if (zdo == null || !zdo.IsValid() || ((ZDOID)(ref zdo.m_uid)).IsNone()) { failure = "zdo-missing"; return false; } if (!PortalEditEvidence.TryCapture(zdo, out var evidence)) { int num = zdo.GetInt("runic.portals.schema", 0); int num2 = zdo.GetInt("runic.portals.mode", 0); PortalNetworkKind portalNetworkKind = (PortalNetworkKind)zdo.GetInt("runic.portals.networkKind", 5); string value = zdo.GetString("runic.portals.group", string.Empty); failure = ((num == 2 && num2 == 1 && portalNetworkKind == PortalNetworkKind.Group && !GroupIdentity.IsCanonicalId(value)) ? "record-group-invalid" : "record-invalid"); return false; } switch (evidence.Mode) { case 0: return false; default: failure = "mode-unsupported"; return false; case 1: { int schema = evidence.Schema; if (schema != 1 && schema != 2) { failure = "schema-unsupported"; return false; } int revision = evidence.Revision; long owner = evidence.Owner; int direction = evidence.Direction; if (revision < 0 || owner == 0L) { failure = "record-invalid"; return false; } try { PortalNetworkKind networkKind = evidence.NetworkKind; string ownerIdentity = evidence.OwnerIdentity; string groupId = evidence.GroupId; PortalAccessProfile access = PortalAccessProfile.PublicNetwork; if (schema == 2) { if ((networkKind != PortalNetworkKind.Public && networkKind != PortalNetworkKind.Personal && networkKind != PortalNetworkKind.Group) || !PortalPermissionAdapter.TryParseIdentity(ownerIdentity, out var _)) { failure = "record-policy-invalid"; return false; } if (networkKind == PortalNetworkKind.Group) { if (!GroupIdentity.IsCanonicalId(groupId)) { failure = "record-group-invalid"; return false; } access = PortalAccessProfile.ForGroup(groupId); } else { if (groupId.Length != 0) { failure = "record-group-invalid"; return false; } access = ((networkKind == PortalNetworkKind.Personal) ? PortalAccessProfile.PrivateNetwork : PortalAccessProfile.PublicNetwork); } } endpoint = new PortalEndpoint(((object)Unsafe.As(ref zdo.m_uid)/*cast due to .constrained prefix*/).ToString(), PortalMode.Network, evidence.Name, evidence.Network, networkKind, ownerIdentity, string.Empty, PortalOnlineState.Online, (direction & 1) != 0, (direction & 2) != 0, access, revision, string.Empty); return true; } catch (ArgumentException) { failure = "record-invalid"; return false; } } } } internal static bool TryWrite(ZDO zdo, PortalEditCommand command, long owner, out int committedRevision, out string failure) { return TryWrite(zdo, command, owner, PortalPermissionAdapter.Identity(owner), out committedRevision, out failure); } internal static bool TryWrite(ZDO zdo, PortalEditCommand command, long owner, string ownerStableIdentity, out int committedRevision, out string failure) { committedRevision = -1; failure = string.Empty; if (zdo == null || !zdo.IsValid() || ((ZDOID)(ref zdo.m_uid)).IsNone() || command == null || owner == 0L || !PortalPermissionAdapter.TryParseIdentity(ownerStableIdentity, out var _)) { failure = "write-context-invalid"; return false; } if (command.Kind == PortalEditKind.PublicNetwork && ((command.NetworkKind != PortalNetworkKind.Public && command.NetworkKind != PortalNetworkKind.Personal && command.NetworkKind != PortalNetworkKind.Group) || command.NetworkKind == PortalNetworkKind.Group != GroupIdentity.IsCanonicalId(command.GroupId))) { failure = "command-policy-invalid"; return false; } if (!PortalEditEvidence.TryCapture(zdo, out var evidence)) { failure = "record-invalid"; return false; } int schema = evidence.Schema; int mode = evidence.Mode; if (mode != 0 && mode != 1) { failure = "mode-unsupported"; return false; } if (schema != 0 && schema != 1 && schema != 2) { failure = "schema-unsupported"; return false; } if (evidence.Revision == int.MaxValue) { failure = "revision-exhausted"; return false; } if (!PortalEditEvidence.TryProject(evidence, command, owner, ownerStableIdentity, out var after)) { failure = "command-invalid"; return false; } return TryPublishAuthoritativeRecord(zdo, evidence, after, string.Empty, 0L, out committedRevision, out failure); } internal static int GetSchema(ZDO zdo) { if (!PortalEditEvidence.TryCapture(zdo, out var evidence)) { return -1; } return evidence.Schema; } internal static int GetMode(ZDO zdo) { if (!PortalEditEvidence.TryCapture(zdo, out var evidence)) { return -1; } return evidence.Mode; } internal static int GetRevision(ZDO zdo) { if (!PortalEditEvidence.TryCapture(zdo, out var evidence)) { return -1; } return evidence.Revision; } internal static bool TryPublishAuthoritativeRecord(ZDO zdo, PortalEditEvidence expectedBefore, PortalEditEvidence after, string worldEpoch, long commitSequence, out int committedRevision, out string failure) { committedRevision = -1; failure = string.Empty; if (zdo == null || !zdo.IsValid() || ((ZDOID)(ref zdo.m_uid)).IsNone() || (!expectedBefore.IsCanonicalRecord() && expectedBefore.Schema != 0 && expectedBefore.Schema != 1) || !after.IsCanonicalRecord() || commitSequence < 0 || commitSequence == 0 != string.IsNullOrEmpty(worldEpoch) || (commitSequence > 0 && (!Guid.TryParseExact(worldEpoch, "N", out var result) || result == Guid.Empty || !string.Equals(result.ToString("N"), worldEpoch, StringComparison.Ordinal)))) { failure = "record-publication-context-invalid"; return false; } if (!PortalEditEvidence.TryCapture(zdo, out var evidence) || !evidence.Matches(expectedBefore)) { failure = "record-publication-before-changed"; return false; } string text; try { text = EncodeRecord(after, worldEpoch, commitSequence); } catch { failure = "record-publication-encode"; return false; } try { zdo.Set("runic.portals.record", text); if (!string.Equals(zdo.GetString("runic.portals.record", string.Empty), text, StringComparison.Ordinal) || !TryReadAuthoritativeRecord(zdo, out var evidence2, out var worldEpoch2, out var commitSequence2, out var recordPresent, out var _) || !recordPresent || !evidence2.Matches(after) || commitSequence2 != commitSequence || !string.Equals(worldEpoch2, worldEpoch, StringComparison.Ordinal)) { failure = "record-publication-readback"; return false; } TryProjectLegacy(zdo, after); committedRevision = after.Revision; return true; } catch { failure = "record-publication-fault"; return false; } } internal static bool TryAdvancePublicationPulse(ZDO zdo, out string failure) { failure = "publication-pulse-context-invalid"; if (zdo == null || !zdo.IsValid() || ((ZDOID)(ref zdo.m_uid)).IsNone()) { return false; } long num = zdo.GetLong("runic.portals.publicationPulse", 0L); if (num < 0 || num == long.MaxValue) { failure = "publication-pulse-exhausted"; return false; } try { long num2 = num + 1; zdo.Set("runic.portals.publicationPulse", num2); if (zdo.GetLong("runic.portals.publicationPulse", 0L) != num2) { failure = "publication-pulse-readback"; return false; } failure = string.Empty; return true; } catch { failure = "publication-pulse-fault"; return false; } } internal static bool TryReadAuthoritativeRecord(ZDO zdo, out PortalEditEvidence evidence, out string worldEpoch, out long commitSequence, out bool recordPresent, out string failure) { evidence = default(PortalEditEvidence); worldEpoch = string.Empty; commitSequence = 0L; recordPresent = false; failure = string.Empty; if (zdo == null || !zdo.IsValid()) { failure = "record-zdo-invalid"; return false; } string text = zdo.GetString("runic.portals.record", string.Empty); if (text.Length == 0) { return true; } recordPresent = true; try { byte[] array = Convert.FromBase64String(text); if (array.Length == 0 || array.Length > 2048) { throw new InvalidDataException(); } using (MemoryStream memoryStream = new MemoryStream(array, writable: false)) { using BinaryReader binaryReader = new BinaryReader(memoryStream, StrictUtf8, leaveOpen: true); if (binaryReader.ReadUInt32() != 827478098 || binaryReader.ReadUInt16() != 1 || binaryReader.ReadUInt16() != 0) { throw new InvalidDataException(); } evidence = ReadEvidence(binaryReader); worldEpoch = ReadText(binaryReader, 32); commitSequence = binaryReader.ReadInt64(); if (memoryStream.Position != memoryStream.Length || !evidence.IsCanonicalRecord() || commitSequence < 0 || commitSequence == 0 != (worldEpoch.Length == 0) || (commitSequence > 0 && (!Guid.TryParseExact(worldEpoch, "N", out var result) || result == Guid.Empty || !string.Equals(result.ToString("N"), worldEpoch, StringComparison.Ordinal))) || !string.Equals(EncodeRecord(evidence, worldEpoch, commitSequence), text, StringComparison.Ordinal)) { throw new InvalidDataException(); } } return true; } catch { evidence = default(PortalEditEvidence); worldEpoch = string.Empty; commitSequence = 0L; failure = "record-corrupt"; return false; } } private static string EncodeRecord(PortalEditEvidence evidence, string worldEpoch, long commitSequence) { if (!evidence.IsCanonicalRecord()) { throw new ArgumentException("evidence"); } using MemoryStream memoryStream = new MemoryStream(512); using BinaryWriter binaryWriter = new BinaryWriter(memoryStream, StrictUtf8, leaveOpen: true); binaryWriter.Write(827478098u); binaryWriter.Write((ushort)1); binaryWriter.Write((ushort)0); WriteEvidence(binaryWriter, evidence); WriteText(binaryWriter, worldEpoch ?? string.Empty, 32); binaryWriter.Write(commitSequence); binaryWriter.Flush(); if (memoryStream.Length > 2048) { throw new InvalidDataException(); } return Convert.ToBase64String(memoryStream.ToArray()); } private static void WriteEvidence(BinaryWriter writer, PortalEditEvidence value) { writer.Write(value.Schema); writer.Write(value.Mode); writer.Write(value.Revision); WriteText(writer, value.Network, 64); WriteText(writer, value.Name, 64); writer.Write(value.Owner); WriteText(writer, value.OwnerIdentity, 96); writer.Write((byte)value.NetworkKind); WriteText(writer, value.GroupId, 64); writer.Write(value.Direction); } private static PortalEditEvidence ReadEvidence(BinaryReader reader) { return new PortalEditEvidence(reader.ReadInt32(), reader.ReadInt32(), reader.ReadInt32(), ReadText(reader, 64), ReadText(reader, 64), reader.ReadInt64(), ReadText(reader, 96), (PortalNetworkKind)reader.ReadByte(), ReadText(reader, 64), reader.ReadInt32()); } private static void WriteText(BinaryWriter writer, string value, int maximum) { string s = OptionalText(value, maximum); byte[] bytes = StrictUtf8.GetBytes(s); writer.Write((ushort)bytes.Length); writer.Write(bytes); } private static string ReadText(BinaryReader reader, int maximum) { int num = reader.ReadUInt16(); if (num > 2048) { throw new InvalidDataException(); } byte[] array = reader.ReadBytes(num); if (array.Length != num) { throw new EndOfStreamException(); } string text = StrictUtf8.GetString(array); if (!string.Equals(text, OptionalText(text, maximum), StringComparison.Ordinal)) { throw new InvalidDataException(); } return text; } private static string OptionalText(string value, int maximum) { return PortalText.NormalizeOptional(value, maximum, "value"); } private static void TryProjectLegacy(ZDO zdo, PortalEditEvidence value) { try { zdo.Set("runic.portals.mode", 0); zdo.Set("runic.portals.schema", value.Schema); zdo.Set("runic.portals.revision", value.Revision); zdo.Set("runic.portals.network", value.Network); zdo.Set("runic.portals.name", value.Name); zdo.Set("runic.portals.owner", value.Owner); zdo.Set("runic.portals.ownerIdentity", value.OwnerIdentity); zdo.Set("runic.portals.networkKind", (int)value.NetworkKind); zdo.Set("runic.portals.group", value.GroupId); zdo.Set("runic.portals.direction", value.Direction); zdo.Set("runic.portals.mode", value.Mode); } catch { } } } internal static class SentinelSecurityBridge { private static readonly object Gate = new object(); private static MethodInfo _report; internal static void Report(long peerId, string rule, string correlationId, int confidence, string detail) { if (peerId == 0L) { return; } try { MethodInfo report; lock (Gate) { if (_report == null) { _report = Type.GetType("RunicSentinel.Api.SentinelIntegrationApi, RunicSentinel", throwOnError: false)?.GetMethod("ReportRejectedServerRequest", BindingFlags.Static | BindingFlags.Public, null, new Type[7] { typeof(string), typeof(long), typeof(string), typeof(string), typeof(string), typeof(int), typeof(string) }, null); } report = _report; } report?.Invoke(null, new object[7] { "runic.portals", peerId, "peer:" + peerId, rule, string.IsNullOrEmpty(correlationId) ? "portal-request" : correlationId, confidence, detail }); } catch { } } } internal enum WardState { None, Allows, Denies, OverlappingHostile, Ambiguous } internal sealed class WardContext { internal WardState State { get; } internal static WardContext NoWard { get; } = new WardContext(WardState.None); internal WardContext(WardState state) { State = state; } } internal static class ValheimContracts { internal const string AuditedGameVersion = "0.221.12"; internal const int MaximumPortalObjectsScanned = 4096; private static FieldInfo _portalObjectsField; private static FieldInfo _privateAreasField; private static MethodInfo _privateAreaEnabled; private static MethodInfo _privateAreaInside; private static MethodInfo _privateAreaPermitted; private static MethodInfo _zonePokeLocal; internal static bool IsServer { get { if ((Object)(object)ZNet.instance != (Object)null) { return ZNet.instance.IsServer(); } return false; } } internal static bool HasLocalPlayerAuthority { get { Player localPlayer = Player.m_localPlayer; ZNetView val = (((Object)(object)localPlayer == (Object)null) ? null : ((Component)localPlayer).GetComponent()); if ((Object)(object)localPlayer != (Object)null && (Object)(object)val != (Object)null && val.IsValid()) { return val.IsOwner(); } return false; } } internal static bool Initialize(out string problem) { problem = string.Empty; try { string text = ReadGameVersion(); if (!string.Equals(text, "0.221.12", StringComparison.Ordinal)) { throw new NotSupportedException("Installed Valheim " + text + " is not audited; expected 0.221.12."); } RequireMethod(typeof(TeleportWorld), "Awake", Type.EmptyTypes, isPublic: false, typeof(void)); RequireMethod(typeof(TeleportWorld), "Interact", new Type[3] { typeof(Humanoid), typeof(bool), typeof(bool) }, isPublic: true, typeof(bool)); RequireMethod(typeof(TeleportWorld), "GetHoverText", Type.EmptyTypes, isPublic: true, typeof(string)); RequireMethod(typeof(TeleportWorld), "SetText", new Type[1] { typeof(string) }, isPublic: true, typeof(void)); RequireMethod(typeof(TeleportWorld), "Teleport", new Type[1] { typeof(Player) }, isPublic: true, typeof(void)); RequireMethod(typeof(TeleportWorld), "HaveTarget", Type.EmptyTypes, isPublic: false, typeof(bool)); RequireMethod(typeof(TeleportWorld), "TargetFound", Type.EmptyTypes, isPublic: false, typeof(bool)); RequireMethod(typeof(Player), "IsTeleportable", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(Player), "TeleportTo", new Type[3] { typeof(Vector3), typeof(Quaternion), typeof(bool) }, isPublic: true, typeof(bool)); RequireMethod(typeof(Player), "GetPlayerID", Type.EmptyTypes, isPublic: true, typeof(long)); RequireMethod(typeof(Game), "FindRandomUnconnectedPortal", new Type[3] { typeof(List), typeof(ZDO), typeof(string) }, isPublic: false, typeof(ZDO)); RequireMethod(typeof(Piece), "GetCreator", Type.EmptyTypes, isPublic: true, typeof(long)); RequireMethod(typeof(ZNetView), "IsValid", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(ZNetView), "GetZDO", Type.EmptyTypes, isPublic: true, typeof(ZDO)); RequireMethod(typeof(ZNetView), "IsOwner", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(ZDO), "IsValid", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(ZDO), "GetPosition", Type.EmptyTypes, isPublic: true, typeof(Vector3)); RequireMethod(typeof(ZDO), "GetRotation", Type.EmptyTypes, isPublic: true, typeof(Quaternion)); RequireMethod(typeof(ZDO), "GetConnectionZDOID", new Type[1] { typeof(ConnectionType) }, isPublic: true, typeof(ZDOID)); RequireMethod(typeof(ZNet), "IsServer", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(ZNet), "GetWorldUID", Type.EmptyTypes, isPublic: true, typeof(long)); RequireMethod(typeof(ZNet), "GetPeer", new Type[1] { typeof(long) }, isPublic: true, typeof(ZNetPeer)); RequireMethod(typeof(ZDOMan), "GetZDO", new Type[1] { typeof(ZDOID) }, isPublic: true, typeof(ZDO)); RequireMethod(typeof(ZDOMan), "ForceSendZDO", new Type[2] { typeof(long), typeof(ZDOID) }, isPublic: true, typeof(void)); RequireMethod(typeof(ZNetScene), "GetPrefab", new Type[1] { typeof(int) }, isPublic: true, typeof(GameObject)); RequireMethod(typeof(TextInput), "RequestText", new Type[3] { typeof(TextReceiver), typeof(string), typeof(int) }, isPublic: true, typeof(void)); RequireMethod(typeof(TextInput), "Hide", Type.EmptyTypes, isPublic: true, typeof(void)); RequireStaticMethod(typeof(ZInput), "IsGamepadActive", Type.EmptyTypes, isPublic: true, typeof(bool)); RequireMethod(typeof(ZInput), "GetBoundKeyString", new Type[2] { typeof(string), typeof(bool) }, isPublic: true, typeof(string)); RequireMethod(typeof(ZInput), "GetButtonDef", new Type[1] { typeof(string) }, isPublic: true, typeof(ButtonDef)); RequireMethod(typeof(ButtonDef), "GetActionPath", new Type[1] { typeof(bool) }, isPublic: true, typeof(string)); RequireMethod(typeof(Localization), "Localize", new Type[1] { typeof(string) }, isPublic: true, typeof(string)); RequireMethod(typeof(Game), "IncrementPlayerStat", new Type[2] { typeof(PlayerStatType), typeof(float) }, isPublic: true, typeof(void)); RequireMethod(typeof(ZoneSystem), "GetGlobalKey", new Type[1] { typeof(GlobalKeys) }, isPublic: true, typeof(bool)); RequireMethod(typeof(ZoneSystem), "GetGlobalKey", new Type[2] { typeof(GlobalKeys), typeof(float).MakeByRefType() }, isPublic: true, typeof(bool)); RequireMethod(typeof(ZoneSystem), "IsZoneLoaded", new Type[1] { typeof(Vector3) }, isPublic: true, typeof(bool)); _zonePokeLocal = RequireMethod(typeof(ZoneSystem), "PokeLocalZone", new Type[1] { typeof(Vector2i) }, isPublic: false, typeof(bool)); RequireMethod(typeof(RandEventSystem), "GetBossEvent", Type.EmptyTypes, isPublic: true, typeof(string)); RequireField(typeof(TeleportWorld), "m_allowAllItems", typeof(bool), isPublic: true); RequireField(typeof(TeleportWorld), "m_exitDistance", typeof(float), isPublic: true); RequireField(typeof(ZDO), "m_uid", typeof(ZDOID), isPublic: true); RequireField(typeof(ZNetPeer), "m_uid", typeof(long), isPublic: true); RequireField(typeof(ZNetPeer), "m_characterID", typeof(ZDOID), isPublic: true); _portalObjectsField = typeof(ZDOMan).GetField("m_portalObjects", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeof(ZDOMan).FullName, "m_portalObjects"); _privateAreasField = typeof(PrivateArea).GetField("m_allAreas", BindingFlags.Static | BindingFlags.NonPublic) ?? throw new MissingFieldException(typeof(PrivateArea).FullName, "m_allAreas"); _privateAreaEnabled = RequireMethod(typeof(PrivateArea), "IsEnabled", Type.EmptyTypes, isPublic: false, typeof(bool)); _privateAreaInside = RequireMethod(typeof(PrivateArea), "IsInside", new Type[2] { typeof(Vector3), typeof(float) }, isPublic: false, typeof(bool)); _privateAreaPermitted = RequireMethod(typeof(PrivateArea), "IsPermitted", new Type[1] { typeof(long) }, isPublic: false, typeof(bool)); return true; } catch (Exception ex) { problem = ex.GetType().Name + ": " + ex.Message; return false; } } internal static List PortalObjects() { if (ZDOMan.instance != null) { return _portalObjectsField?.GetValue(ZDOMan.instance) as List; } return null; } internal static string ReadGameVersion() { return typeof(Player).Assembly.GetType("Version", throwOnError: true).GetProperty("CurrentVersion", BindingFlags.Static | BindingFlags.Public)?.GetValue(null, null)?.ToString() ?? string.Empty; } internal static WardContext ResolveWard(Vector3 position, long playerId) { //IL_0017: 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) if (playerId <= 0 || (Object)(object)ZoneSystem.instance == (Object)null || !ZoneSystem.instance.IsZoneLoaded(position)) { return new WardContext(WardState.Ambiguous); } if (!(_privateAreasField?.GetValue(null) is List list)) { return new WardContext(WardState.Ambiguous); } bool flag = false; bool flag2 = false; bool flag3 = false; for (int i = 0; i < list.Count; i++) { PrivateArea val = list[i]; if (!((Object)(object)val == (Object)null) && (bool)_privateAreaEnabled.Invoke(val, null) && (bool)_privateAreaInside.Invoke(val, new object[2] { position, 0f })) { flag = true; Piece component = ((Component)val).GetComponent(); long num = (((Object)(object)component == (Object)null) ? 0 : component.GetCreator()); if (num == 0L) { return new WardContext(WardState.Ambiguous); } bool flag4 = num == playerId || (bool)_privateAreaPermitted.Invoke(val, new object[1] { playerId }); flag2 = flag2 || flag4; flag3 = flag3 || !flag4; } } if (!flag) { return WardContext.NoWard; } if (flag3) { return new WardContext(flag2 ? WardState.OverlappingHostile : WardState.Denies); } return new WardContext(WardState.Allows); } internal static bool WardAllows(WardContext ward) { if (ward != null) { if (ward.State != WardState.None) { return ward.State == WardState.Allows; } return true; } return false; } internal static bool SourceWardAllows(Vector3 position, long playerId, out bool evidencePending) { //IL_0000: 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) WardContext wardContext = ResolveWard(position, playerId); evidencePending = wardContext != null && wardContext.State == WardState.Ambiguous; if (!evidencePending) { return WardAllows(wardContext); } RequestSourceZone(position); return false; } private static void RequestSourceZone(Vector3 position) { //IL_0017: 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) ZoneSystem instance = ZoneSystem.instance; if ((Object)(object)instance == (Object)null || !IsServer || instance.IsZoneLoaded(position)) { return; } try { _zonePokeLocal?.Invoke(instance, new object[1] { ZoneSystem.GetZone(position) }); } catch { } } internal static bool DestinationWardAllows(WardContext ward) { if (ward != null && ward.State != WardState.Denies) { return ward.State != WardState.OverlappingHostile; } return false; } private static MethodInfo RequireMethod(Type type, string name, Type[] parameters, bool isPublic, Type returnType) { BindingFlags bindingAttr = (BindingFlags)(4 | (isPublic ? 16 : 32)); MethodInfo method = type.GetMethod(name, bindingAttr, null, parameters, null); if (method == null || method.ReturnType != returnType) { throw new MissingMethodException(type.FullName, name); } return method; } private static MethodInfo RequireStaticMethod(Type type, string name, Type[] parameters, bool isPublic, Type returnType) { BindingFlags bindingAttr = (BindingFlags)(8 | (isPublic ? 16 : 32)); MethodInfo method = type.GetMethod(name, bindingAttr, null, parameters, null); if (method == null || method.ReturnType != returnType) { throw new MissingMethodException(type.FullName, name); } return method; } private static FieldInfo RequireField(Type type, string name, Type fieldType, bool isPublic) { BindingFlags bindingAttr = (BindingFlags)(4 | (isPublic ? 16 : 32)); FieldInfo field = type.GetField(name, bindingAttr); if (field == null || field.FieldType != fieldType) { throw new MissingFieldException(type.FullName, name); } return field; } } } namespace RunicPortals.Core { internal sealed class CorrelatedDiagnosticBuffer : IPortalDiagnosticService { private readonly object _sync = new object(); private readonly PortalDiagnosticEvent[] _events; private int _start; private int _count; private long _sequence; private long _dropped; public long DroppedCount { get { lock (_sync) { return _dropped; } } } internal CorrelatedDiagnosticBuffer(int capacity) { if (capacity < 1 || capacity > 256) { throw new ArgumentOutOfRangeException("capacity"); } _events = new PortalDiagnosticEvent[capacity]; } internal string Record(PortalDiagnosticCode code, RouteStopCode stopCode = RouteStopCode.Ready, string portalId = "", long utcTicks = 0L) { string text = "rp-" + Interlocked.Increment(ref _sequence).ToString("x16"); PortalDiagnosticEvent portalDiagnosticEvent = new PortalDiagnosticEvent(text, (utcTicks == 0L) ? DateTime.UtcNow.Ticks : utcTicks, code, stopCode, BoundPortalId(portalId)); lock (_sync) { if (_count < _events.Length) { _events[(_start + _count) % _events.Length] = portalDiagnosticEvent; _count++; } else { _events[_start] = portalDiagnosticEvent; _start = (_start + 1) % _events.Length; _dropped++; } } return text; } public IReadOnlyList Snapshot() { lock (_sync) { PortalDiagnosticEvent[] array = new PortalDiagnosticEvent[_count]; for (int i = 0; i < _count; i++) { array[i] = _events[(_start + i) % _events.Length]; } return Array.AsReadOnly(array); } } private static string BoundPortalId(string value) { string text = value ?? string.Empty; if (text.Length > 96) { text = text.Substring(0, 96); } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { return string.Empty; } } return text; } } internal sealed class PortalArrivalSuppression { private long _playerId; private string _destinationPortalId = string.Empty; private long _expiresUtcTicks; internal void Arm(long playerId, string destinationPortalId, long nowUtcTicks, long durationTicks) { if (playerId == 0L) { throw new ArgumentOutOfRangeException("playerId"); } _destinationPortalId = PortalText.Require(destinationPortalId, 96, "destinationPortalId"); if (durationTicks <= 0) { throw new ArgumentOutOfRangeException("durationTicks"); } _playerId = playerId; _expiresUtcTicks = checked(nowUtcTicks + durationTicks); } internal bool Blocks(long playerId, string portalId, long nowUtcTicks) { if (_playerId == 0L || nowUtcTicks > _expiresUtcTicks) { Clear(); return false; } if (playerId == _playerId) { return string.Equals(portalId, _destinationPortalId, StringComparison.Ordinal); } return false; } internal void Clear() { _playerId = 0L; _destinationPortalId = string.Empty; _expiresUtcTicks = 0L; } } internal sealed class PortalAuthorityGate : IPortalAuthorityGate { public PortalAuthorityDecision Evaluate(PortalAuthorityEvidence evidence) { if (evidence == null) { throw new ArgumentNullException("evidence"); } if (!evidence.FeatureEnabled) { return Stop(AuthorityStopCode.FeatureDisabled); } if (!evidence.SenderIdentityBound) { return Stop(AuthorityStopCode.SenderIdentityUnbound); } if (!evidence.ActorIsLocalAuthority) { return Stop(AuthorityStopCode.ActorNotLocalAuthority); } if (!evidence.SourceExists) { return Stop(AuthorityStopCode.SourceMissing); } if (evidence.Mutation == PortalMutationKind.CommitTravel && !evidence.DestinationExists) { return Stop(AuthorityStopCode.DestinationMissing); } if (evidence.Mutation == PortalMutationKind.ConfigureMetadata && !evidence.SourceObjectOwned) { return Stop(AuthorityStopCode.SourceObjectNotOwned); } if (evidence.Mutation == PortalMutationKind.CommitTravel && !evidence.PlayerObjectOwned) { return Stop(AuthorityStopCode.PlayerObjectNotOwned); } if (!evidence.OwnerPermissionAllowed) { return Stop(AuthorityStopCode.OwnerPermissionDenied); } if (!evidence.SourceWardAllowed) { return Stop(AuthorityStopCode.SourceWardDenied); } if (evidence.Mutation == PortalMutationKind.CommitTravel && !evidence.DestinationWardAllowed) { return Stop(AuthorityStopCode.DestinationWardDenied); } if (!evidence.ActorInRange) { return Stop(AuthorityStopCode.ActorOutOfRange); } if (!evidence.CurrentStateMatches) { return Stop(AuthorityStopCode.CurrentStateChanged); } if (evidence.Mutation == PortalMutationKind.CommitTravel && !evidence.VanillaTravelPolicyAllowed) { return Stop(AuthorityStopCode.VanillaTravelPolicyDenied); } if (evidence.Mutation != PortalMutationKind.ConfigureMetadata && evidence.Mutation != PortalMutationKind.CommitTravel) { return Stop(AuthorityStopCode.UnsupportedMutation); } return Stop(AuthorityStopCode.Allowed); } private static PortalAuthorityDecision Stop(AuthorityStopCode code) { return new PortalAuthorityDecision(code); } } internal readonly struct PortalEditEvidence { internal int Schema { get; } internal int Mode { get; } internal int Revision { get; } internal string Network { get; } internal string Name { get; } internal long Owner { get; } internal string OwnerIdentity { get; } internal PortalNetworkKind NetworkKind { get; } internal string GroupId { get; } internal int Direction { get; } internal string SemanticRevision => "portal.metadata.v2." + Revision.ToString(CultureInfo.InvariantCulture); internal PortalEditEvidence(int schema, int mode, int revision, string network, string name, long owner, int direction) : this(schema, mode, revision, network, name, owner, (mode == 1) ? PortalPermissionAdapter.Identity(owner) : string.Empty, (mode == 1) ? PortalNetworkKind.Public : PortalNetworkKind.Custom, string.Empty, direction) { } internal PortalEditEvidence(int schema, int mode, int revision, string network, string name, long owner, string ownerIdentity, PortalNetworkKind networkKind, string groupId, int direction) { Schema = schema; Mode = mode; Revision = revision; Network = network ?? string.Empty; Name = name ?? string.Empty; Owner = owner; OwnerIdentity = ownerIdentity ?? string.Empty; NetworkKind = networkKind; GroupId = groupId ?? string.Empty; Direction = direction; } internal bool IsCanonicalRecord() { bool flag = Mode == 0; bool flag2 = Mode == 1; if ((!flag && !flag2) || Revision < 0 || Schema != 2) { return false; } if (flag) { if (Network.Length == 0 && Name.Length == 0 && Owner == 0L && OwnerIdentity.Length == 0 && NetworkKind == PortalNetworkKind.Custom && GroupId.Length == 0) { return Direction == 0; } return false; } if (BoundedText(Network, 64) && BoundedText(Name, 64) && Owner != 0L && Direction >= 1 && Direction <= 3 && PortalPermissionAdapter.TryParseIdentity(OwnerIdentity, out var _) && (NetworkKind == PortalNetworkKind.Public || NetworkKind == PortalNetworkKind.Personal || NetworkKind == PortalNetworkKind.Group)) { if (NetworkKind != PortalNetworkKind.Group) { return GroupId.Length == 0; } return GroupIdentity.IsCanonicalId(GroupId); } return false; } internal bool Matches(PortalEditEvidence other) { if (Schema == other.Schema && Mode == other.Mode && Revision == other.Revision && Owner == other.Owner && Direction == other.Direction && NetworkKind == other.NetworkKind && string.Equals(Network, other.Network, StringComparison.Ordinal) && string.Equals(Name, other.Name, StringComparison.Ordinal) && string.Equals(OwnerIdentity, other.OwnerIdentity, StringComparison.Ordinal)) { return string.Equals(GroupId, other.GroupId, StringComparison.Ordinal); } return false; } internal string StateFingerprint() { StringBuilder stringBuilder = new StringBuilder(256); Append(stringBuilder, "runic.portals.metadata.v2"); Append(stringBuilder, Schema); Append(stringBuilder, Mode); Append(stringBuilder, Revision); Append(stringBuilder, Network); Append(stringBuilder, Name); Append(stringBuilder, Owner); Append(stringBuilder, OwnerIdentity); Append(stringBuilder, (int)NetworkKind); Append(stringBuilder, GroupId); Append(stringBuilder, Direction); return Hash(stringBuilder.ToString()); } internal static bool TryProject(PortalEditEvidence before, PortalEditCommand command, long creator, string ownerStableIdentity, out PortalEditEvidence after) { after = default(PortalEditEvidence); if (command == null || command.Kind == PortalEditKind.Invalid || creator == 0L || before.Revision == int.MaxValue || !PortalPermissionAdapter.TryParseIdentity(ownerStableIdentity, out var _)) { return false; } int revision = Math.Max(0, before.Revision) + 1; if (command.Kind == PortalEditKind.StandardPair) { after = new PortalEditEvidence(2, 0, revision, string.Empty, string.Empty, 0L, string.Empty, PortalNetworkKind.Custom, string.Empty, 0); return true; } int direction = (command.AcceptsArrival ? 1 : 0) | (command.PermitsDeparture ? 2 : 0); after = new PortalEditEvidence(2, 1, revision, command.NetworkId, command.DisplayName, creator, ownerStableIdentity, command.NetworkKind, command.GroupId, direction); return after.Matches(command, creator, ownerStableIdentity); } internal bool Matches(PortalEditCommand command, long creator) { return Matches(command, creator, PortalPermissionAdapter.Identity(creator)); } internal bool Matches(PortalEditCommand command, long creator, string ownerStableIdentity) { if (command == null || command.Kind == PortalEditKind.Invalid) { return false; } if (command.Kind == PortalEditKind.StandardPair) { return Mode == 0; } int num = (command.AcceptsArrival ? 1 : 0) | (command.PermitsDeparture ? 2 : 0); if (Mode == 1 && Owner == creator && Direction == num && NetworkKind == command.NetworkKind && string.Equals(Network, command.NetworkId, StringComparison.Ordinal) && string.Equals(Name, command.DisplayName, StringComparison.Ordinal) && string.Equals(OwnerIdentity, ownerStableIdentity, StringComparison.Ordinal)) { return string.Equals(GroupId, command.GroupId, StringComparison.Ordinal); } return false; } internal bool RequiresConfirmation(PortalEditCommand command, long creator) { if (Mode == 1) { return !Matches(command, creator); } return false; } internal string Fingerprint(PortalEditCommand command, long creator) { return Fingerprint(command, creator, PortalPermissionAdapter.Identity(creator)); } internal string Fingerprint(PortalEditCommand command, long creator, string ownerStableIdentity) { StringBuilder stringBuilder = new StringBuilder(384); Append(stringBuilder, Schema); Append(stringBuilder, Mode); Append(stringBuilder, Revision); Append(stringBuilder, Network); Append(stringBuilder, Name); Append(stringBuilder, Owner); Append(stringBuilder, OwnerIdentity); Append(stringBuilder, (int)NetworkKind); Append(stringBuilder, GroupId); Append(stringBuilder, Direction); Append(stringBuilder, (int)(command?.Kind ?? PortalEditKind.Invalid)); Append(stringBuilder, command?.NetworkId ?? string.Empty); Append(stringBuilder, command?.DisplayName ?? string.Empty); Append(stringBuilder, (int)(command?.NetworkKind ?? PortalNetworkKind.Custom)); Append(stringBuilder, command?.GroupId ?? string.Empty); Append(stringBuilder, (command != null && command.AcceptsArrival) ? 1 : 0); Append(stringBuilder, (command != null && command.PermitsDeparture) ? 1 : 0); Append(stringBuilder, creator); Append(stringBuilder, ownerStableIdentity ?? string.Empty); return "portal-v2:" + Hash(stringBuilder.ToString()); } internal string LegacyFingerprint(PortalEditCommand command, long creator) { StringBuilder stringBuilder = new StringBuilder(384); Append(stringBuilder, Schema); Append(stringBuilder, Mode); Append(stringBuilder, Revision); Append(stringBuilder, Network); Append(stringBuilder, Name); Append(stringBuilder, Owner); Append(stringBuilder, Direction); Append(stringBuilder, (int)(command?.Kind ?? PortalEditKind.Invalid)); Append(stringBuilder, command?.NetworkId ?? string.Empty); Append(stringBuilder, command?.DisplayName ?? string.Empty); Append(stringBuilder, (command != null && command.AcceptsArrival) ? 1 : 0); Append(stringBuilder, (command != null && command.PermitsDeparture) ? 1 : 0); Append(stringBuilder, creator); return "portal-v1:" + Hash(stringBuilder.ToString()); } internal static string OpaqueTarget(string portalId) { string text = portalId ?? string.Empty; if (text.Length == 0 || text.Length > 96) { throw new ArgumentOutOfRangeException("portalId"); } return "zdo-sha256:" + Hash(text); } internal static bool TryCapture(ZDO zdo, out PortalEditEvidence evidence) { evidence = default(PortalEditEvidence); if (zdo == null || !zdo.IsValid() || ((ZDOID)(ref zdo.m_uid)).IsNone()) { return false; } if (PortalZdoCodec.TryReadAuthoritativeRecord(zdo, out var evidence2, out var _, out var _, out var recordPresent, out var _)) { if (recordPresent) { evidence = evidence2; return evidence.IsCanonicalRecord(); } } else if (recordPresent) { return false; } int num = zdo.GetInt("runic.portals.schema", 0); int num2 = zdo.GetInt("runic.portals.mode", 0); int num3 = zdo.GetInt("runic.portals.revision", 0); bool flag = num2 == 0; bool flag2 = num2 == 1; if ((!flag && !flag2) || num3 < 0) { return false; } if (flag && num != 0 && num != 1 && num != 2) { return false; } if (flag2 && num != 1 && num != 2) { return false; } string text = string.Empty; string text2 = string.Empty; long num4 = 0L; string text3 = string.Empty; PortalNetworkKind portalNetworkKind = PortalNetworkKind.Custom; string text4 = string.Empty; int num5 = 0; if (flag2) { text = zdo.GetString("runic.portals.network", string.Empty); text2 = zdo.GetString("runic.portals.name", string.Empty); num4 = zdo.GetLong("runic.portals.owner", 0L); text3 = ((num == 1) ? PortalPermissionAdapter.Identity(num4) : zdo.GetString("runic.portals.ownerIdentity", string.Empty)); portalNetworkKind = ((num == 1) ? PortalNetworkKind.Public : ((PortalNetworkKind)zdo.GetInt("runic.portals.networkKind", 5))); text4 = ((num == 1) ? string.Empty : zdo.GetString("runic.portals.group", string.Empty)); num5 = zdo.GetInt("runic.portals.direction", 0); if (!BoundedText(text, 64) || !BoundedText(text2, 64) || num4 == 0L || num5 < 1 || num5 > 3 || !PortalPermissionAdapter.TryParseIdentity(text3, out var _) || (portalNetworkKind != PortalNetworkKind.Public && portalNetworkKind != PortalNetworkKind.Personal && portalNetworkKind != PortalNetworkKind.Group) || ((portalNetworkKind == PortalNetworkKind.Group) ? (!GroupIdentity.IsCanonicalId(text4)) : ((byte)text4.Length != 0))) { return false; } } evidence = new PortalEditEvidence(num, num2, num3, text, text2, num4, text3, portalNetworkKind, text4, num5); return true; } private static bool BoundedText(string value, int maximum) { if (string.IsNullOrEmpty(value) || value.Length > maximum) { return false; } for (int i = 0; i < value.Length; i++) { char c = value[i]; if (char.IsControl(c)) { return false; } if (char.IsSurrogate(c)) { if (!char.IsHighSurrogate(c) || i + 1 >= value.Length || !char.IsLowSurrogate(value[i + 1])) { return false; } i++; } } return true; } private static void Append(StringBuilder builder, int value) { Append(builder, value.ToString(CultureInfo.InvariantCulture)); } private static void Append(StringBuilder builder, long value) { Append(builder, value.ToString(CultureInfo.InvariantCulture)); } private static void Append(StringBuilder builder, string value) { string text = value ?? string.Empty; builder.Append(text.Length.ToString(CultureInfo.InvariantCulture)).Append(':').Append(text) .Append(';'); } private static string Hash(string value) { byte[] bytes = Encoding.UTF8.GetBytes(value); byte[] array; using (SHA256 sHA = SHA256.Create()) { array = sHA.ComputeHash(bytes); } StringBuilder stringBuilder = new StringBuilder(64); for (int i = 0; i < array.Length; i++) { stringBuilder.Append(array[i].ToString("x2", CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } } internal enum PortalEditKind { Invalid, StandardPair, PublicNetwork } internal sealed class PortalEditCommand { internal PortalEditKind Kind { get; } internal string NetworkId { get; } internal string DisplayName { get; } internal PortalNetworkKind NetworkKind { get; } internal string GroupId { get; } internal bool RequiresActiveGroup { get { if (NetworkKind == PortalNetworkKind.Group) { return GroupId.Length == 0; } return false; } } internal bool AcceptsArrival { get; } internal bool PermitsDeparture { get; } internal string Error { get; } private PortalEditCommand(PortalEditKind kind, string networkId, string displayName, PortalNetworkKind networkKind, string groupId, bool acceptsArrival, bool permitsDeparture, string error) { Kind = kind; NetworkId = networkId; DisplayName = displayName; NetworkKind = networkKind; GroupId = groupId; AcceptsArrival = acceptsArrival; PermitsDeparture = permitsDeparture; Error = error; } internal PortalEditCommand BindGroup(string groupId) { if (!RequiresActiveGroup) { return this; } if (!GroupIdentity.IsCanonicalId(groupId)) { return Invalid("Select a Runic Group in chat before configuring a Group portal."); } return new PortalEditCommand(Kind, NetworkId, DisplayName, NetworkKind, groupId, AcceptsArrival, PermitsDeparture, string.Empty); } internal static PortalEditCommand Parse(string text) { string text2 = text ?? string.Empty; if (text2.Length > 256) { return Invalid(Usage()); } string text3 = text2.Trim(); if (string.Equals(text3, "standard", StringComparison.OrdinalIgnoreCase)) { return new PortalEditCommand(PortalEditKind.StandardPair, string.Empty, string.Empty, PortalNetworkKind.Custom, string.Empty, acceptsArrival: true, permitsDeparture: true, string.Empty); } if (text3.Length == 0 || text3.Length > 256) { return Invalid(Usage()); } for (int i = 0; i < text3.Length; i++) { if (char.IsControl(text3[i])) { return Invalid("Control characters are not allowed."); } } string[] array = text3.Split('|'); if (array.Length < 4 || !string.Equals(array[0].Trim(), "network", StringComparison.OrdinalIgnoreCase)) { return Invalid(Usage()); } try { PortalNetworkKind networkKind = PortalNetworkKind.Public; string text4 = string.Empty; int num = 1; int num2 = 2; int num3 = 3; if (array.Length == 5) { string a = array[1].Trim(); if (string.Equals(a, "public", StringComparison.OrdinalIgnoreCase)) { networkKind = PortalNetworkKind.Public; } else if (string.Equals(a, "private", StringComparison.OrdinalIgnoreCase)) { networkKind = PortalNetworkKind.Personal; } else { if (!string.Equals(a, "group", StringComparison.OrdinalIgnoreCase)) { return Invalid(Usage()); } networkKind = PortalNetworkKind.Group; } num = 2; num2 = 3; num3 = 4; } else if (array.Length == 6 && string.Equals(array[1].Trim(), "group", StringComparison.OrdinalIgnoreCase)) { networkKind = PortalNetworkKind.Group; text4 = array[2].Trim(); if (!GroupIdentity.IsCanonicalId(text4)) { return Invalid("Group portals require the exact lowercase 32-character Group UUID."); } num = 3; num2 = 4; num3 = 5; } else if (array.Length != 4) { return Invalid(Usage()); } string networkId = PortalText.Require(array[num], 64, "text"); string displayName = PortalText.Require(array[num2], 64, "text"); string a2 = array[num3].Trim(); bool acceptsArrival; bool permitsDeparture; if (string.Equals(a2, "both", StringComparison.OrdinalIgnoreCase)) { acceptsArrival = true; permitsDeparture = true; } else if (string.Equals(a2, "arrive", StringComparison.OrdinalIgnoreCase)) { acceptsArrival = true; permitsDeparture = false; } else { if (!string.Equals(a2, "depart", StringComparison.OrdinalIgnoreCase)) { return Invalid("Direction must be both, arrive, or depart."); } acceptsArrival = false; permitsDeparture = true; } return new PortalEditCommand(PortalEditKind.PublicNetwork, networkId, displayName, networkKind, text4, acceptsArrival, permitsDeparture, string.Empty); } catch (ArgumentException ex) { return Invalid(ex.Message); } } private static PortalEditCommand Invalid(string error) { return new PortalEditCommand(PortalEditKind.Invalid, string.Empty, string.Empty, PortalNetworkKind.Custom, string.Empty, acceptsArrival: false, permitsDeparture: false, error ?? "Invalid portal command."); } private static string Usage() { return "Use network|NETWORK|NAME|DIRECTION, network|public|NETWORK|NAME|DIRECTION, network|private|NETWORK|NAME|DIRECTION, network|group|NETWORK|NAME|DIRECTION, or standard. Select the Group in chat first."; } } internal interface IPortalAccessEvaluator { bool Allows(PortalEndpoint endpoint, string travelerStableId, PortalAccessAction action); } internal sealed class PortalGraph { private static readonly PortalEndpoint[] EmptyEndpoints = Array.Empty(); private readonly Dictionary _byId; private readonly Dictionary _byNetwork; internal PortalEndpoint[] Endpoints { get; } internal int Count => Endpoints.Length; internal PortalGraph(IEnumerable endpoints, int maximumEndpoints) { if (maximumEndpoints < 1 || maximumEndpoints > 2048) { throw new ArgumentOutOfRangeException("maximumEndpoints"); } if (endpoints == null) { throw new ArgumentNullException("endpoints"); } List list = new List(); _byId = new Dictionary(StringComparer.Ordinal); foreach (PortalEndpoint endpoint in endpoints) { if (endpoint == null) { throw new ArgumentException("Graph endpoints cannot contain null.", "endpoints"); } if (_byId.ContainsKey(endpoint.PortalId)) { throw new ArgumentException("Duplicate stable portal ID: " + endpoint.PortalId, "endpoints"); } if (list.Count >= maximumEndpoints) { throw new PortalGraphLimitException(maximumEndpoints); } _byId.Add(endpoint.PortalId, endpoint); list.Add(endpoint); } list.Sort(CompareEndpoint); Endpoints = list.ToArray(); _byNetwork = BuildNetworks(Endpoints); } internal bool TryGet(string portalId, out PortalEndpoint endpoint) { endpoint = null; if (!string.IsNullOrEmpty(portalId)) { return _byId.TryGetValue(portalId, out endpoint); } return false; } internal PortalDirectoryResult Query(PortalDirectoryQuery query, IPortalAccessEvaluator access) { if (query == null) { throw new ArgumentNullException("query"); } if (access == null) { throw new ArgumentNullException("access"); } if (!ValidateSource(query, access, out var stop, out var _)) { return new PortalDirectoryResult(Array.Empty(), truncated: false, stop); } PortalEndpoint[] array = Network(query.NetworkId); List list = new List(Math.Min(array.Length, query.MaximumResults + 1)); Dictionary dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase); bool truncated = false; foreach (PortalEndpoint portalEndpoint in array) { if (EligibleDirectoryEndpoint(portalEndpoint, query, access)) { if (dictionary.TryGetValue(portalEndpoint.DisplayName, out var value)) { dictionary[portalEndpoint.DisplayName] = value + 1; } else { dictionary.Add(portalEndpoint.DisplayName, 1); } if (list.Count < query.MaximumResults) { list.Add(portalEndpoint); } else { truncated = true; } } } PortalDirectoryEntry[] array2 = new PortalDirectoryEntry[list.Count]; for (int j = 0; j < list.Count; j++) { PortalEndpoint portalEndpoint2 = list[j]; array2[j] = new PortalDirectoryEntry(portalEndpoint2, dictionary[portalEndpoint2.DisplayName] > 1); } return new PortalDirectoryResult(array2, truncated, RouteStopCode.Ready); } internal PortalNameResolution ResolveName(PortalDirectoryQuery scope, string displayName, IPortalAccessEvaluator access) { if (scope == null) { throw new ArgumentNullException("scope"); } if (access == null) { throw new ArgumentNullException("access"); } string b = PortalText.Require(displayName, 64, "displayName"); if (!ValidateSource(scope, access, out var stop, out var _)) { return new PortalNameResolution(stop, Array.Empty()); } PortalEndpoint[] array = Network(scope.NetworkId); List list = new List(); foreach (PortalEndpoint portalEndpoint in array) { if (string.Equals(portalEndpoint.DisplayName, b, StringComparison.OrdinalIgnoreCase) && EligibleDirectoryEndpoint(portalEndpoint, scope, access)) { list.Add(portalEndpoint); if (list.Count > 128) { return new PortalNameResolution(RouteStopCode.GraphLimitExceeded, Array.Empty()); } } } if (list.Count == 0) { return new PortalNameResolution(RouteStopCode.NotFoundOrUnauthorized, Array.Empty()); } bool flag = list.Count > 1; PortalDirectoryEntry[] array2 = new PortalDirectoryEntry[list.Count]; for (int j = 0; j < list.Count; j++) { array2[j] = new PortalDirectoryEntry(list[j], flag); } return new PortalNameResolution(flag ? RouteStopCode.DuplicateName : RouteStopCode.Ready, array2); } internal RoutePlan Plan(RoutePlanRequest request, IPortalAccessEvaluator access) { if (request == null) { throw new ArgumentNullException("request"); } if (access == null) { throw new ArgumentNullException("access"); } if (!_byId.TryGetValue(request.SourcePortalId, out var value) || !_byId.TryGetValue(request.DestinationPortalId, out var value2) || !CanDiscover(value, request.TravelerStableId, access) || !CanDiscover(value2, request.TravelerStableId, access)) { return Stop(RouteStopCode.NotFoundOrUnauthorized, request, null, null, oneWay: false); } if (value.Mode != PortalMode.Network || value.OnlineState != PortalOnlineState.Online) { return Stop(RouteStopCode.SourceUnavailable, request, value, value2, oneWay: false); } if (value2.Mode != PortalMode.Network || value2.OnlineState != PortalOnlineState.Online) { return Stop(RouteStopCode.DestinationUnavailable, request, value, value2, oneWay: false); } if (!string.Equals(value.NetworkId, value2.NetworkId, StringComparison.Ordinal)) { return Stop(RouteStopCode.NetworkMismatch, request, value, value2, oneWay: false); } if (string.Equals(value.PortalId, value2.PortalId, StringComparison.Ordinal)) { return Stop(RouteStopCode.SameEndpoint, request, value, value2, oneWay: false); } if ((request.SourceRevision >= 0 && request.SourceRevision != value.Revision) || (request.DestinationRevision >= 0 && request.DestinationRevision != value2.Revision)) { return Stop(RouteStopCode.StaleSelection, request, value, value2, oneWay: false); } if (!value.PermitsDeparture || !access.Allows(value, request.TravelerStableId, PortalAccessAction.Depart)) { return Stop(RouteStopCode.DepartureDenied, request, value, value2, oneWay: false); } if (!value2.AcceptsArrival || !access.Allows(value2, request.TravelerStableId, PortalAccessAction.Arrive)) { return Stop(RouteStopCode.ArrivalDenied, request, value, value2, oneWay: false); } RouteStopCode routeStopCode = TravelPolicyStop(request.TravelPolicy); if (routeStopCode != RouteStopCode.Ready) { return Stop(routeStopCode, request, value, value2, oneWay: false); } bool flag = !value2.PermitsDeparture || !value.AcceptsArrival || !access.Allows(value2, request.TravelerStableId, PortalAccessAction.Depart) || !access.Allows(value, request.TravelerStableId, PortalAccessAction.Arrive); if (flag && !request.OneWayAcknowledged) { return Stop(RouteStopCode.OneWayWarningRequired, request, value, value2, oneWay: true); } return Stop(RouteStopCode.Ready, request, value, value2, flag); } private bool ValidateSource(PortalDirectoryQuery query, IPortalAccessEvaluator access, out RouteStopCode stop, out PortalEndpoint source) { stop = RouteStopCode.Ready; source = null; if (query.SourcePortalId.Length == 0) { return true; } if (!_byId.TryGetValue(query.SourcePortalId, out source) || source.Mode != PortalMode.Network || !string.Equals(source.NetworkId, query.NetworkId, StringComparison.Ordinal) || !CanDiscover(source, query.TravelerStableId, access)) { stop = RouteStopCode.NotFoundOrUnauthorized; return false; } if (source.OnlineState != PortalOnlineState.Online || !source.PermitsDeparture || !access.Allows(source, query.TravelerStableId, PortalAccessAction.Depart)) { stop = RouteStopCode.SourceUnavailable; return false; } return true; } private static bool EligibleDirectoryEndpoint(PortalEndpoint endpoint, PortalDirectoryQuery query, IPortalAccessEvaluator access) { if (endpoint.Mode != PortalMode.Network || string.Equals(endpoint.PortalId, query.SourcePortalId, StringComparison.Ordinal) || !endpoint.AcceptsArrival || endpoint.OnlineState == PortalOnlineState.Disabled || endpoint.OnlineState == PortalOnlineState.Destroyed || endpoint.OnlineState == PortalOnlineState.Stale || (!query.IncludeOffline && endpoint.OnlineState != PortalOnlineState.Online) || !CanDiscover(endpoint, query.TravelerStableId, access) || !access.Allows(endpoint, query.TravelerStableId, PortalAccessAction.Arrive)) { return false; } if (query.Search.Length == 0) { return true; } if (endpoint.DisplayName.IndexOf(query.Search, StringComparison.OrdinalIgnoreCase) < 0 && endpoint.OwnerDisplayName.IndexOf(query.Search, StringComparison.OrdinalIgnoreCase) < 0) { return endpoint.KnownBiome.IndexOf(query.Search, StringComparison.OrdinalIgnoreCase) >= 0; } return true; } private static bool CanDiscover(PortalEndpoint endpoint, string travelerStableId, IPortalAccessEvaluator access) { return access.Allows(endpoint, travelerStableId, PortalAccessAction.ViewDiscover); } private PortalEndpoint[] Network(string networkId) { if (!_byNetwork.TryGetValue(networkId, out var value)) { return EmptyEndpoints; } return value; } private static Dictionary BuildNetworks(PortalEndpoint[] endpoints) { Dictionary> dictionary = new Dictionary>(StringComparer.Ordinal); foreach (PortalEndpoint portalEndpoint in endpoints) { if (!dictionary.TryGetValue(portalEndpoint.NetworkId, out var value)) { value = new List(); dictionary.Add(portalEndpoint.NetworkId, value); } value.Add(portalEndpoint); } Dictionary dictionary2 = new Dictionary(StringComparer.Ordinal); foreach (KeyValuePair> item in dictionary) { dictionary2.Add(item.Key, item.Value.ToArray()); } return dictionary2; } private static int CompareEndpoint(PortalEndpoint left, PortalEndpoint right) { int num = string.Compare(left.NetworkId, right.NetworkId, StringComparison.Ordinal); if (num != 0) { return num; } int num2 = string.Compare(left.DisplayName, right.DisplayName, StringComparison.OrdinalIgnoreCase); if (num2 != 0) { return num2; } int num3 = string.Compare(left.OwnerStableId, right.OwnerStableId, StringComparison.Ordinal); if (num3 == 0) { return string.Compare(left.PortalId, right.PortalId, StringComparison.Ordinal); } return num3; } private static RoutePlan Stop(RouteStopCode stop, RoutePlanRequest request, PortalEndpoint source, PortalEndpoint destination, bool oneWay) { return new RoutePlan(stop, request.SourcePortalId, request.DestinationPortalId, oneWay, source?.Revision ?? (-1), destination?.Revision ?? (-1)); } private static RouteStopCode TravelPolicyStop(TravelPolicyState policy) { return policy switch { TravelPolicyState.Allowed => RouteStopCode.Ready, TravelPolicyState.RestrictedItems => RouteStopCode.RestrictedItems, TravelPolicyState.PortalsDisabled => RouteStopCode.PortalsDisabled, TravelPolicyState.BossTravelBlocked => RouteStopCode.BossTravelBlocked, _ => RouteStopCode.PolicyUnknown, }; } } internal sealed class PortalGraphLimitException : InvalidOperationException { internal int Maximum { get; } internal PortalGraphLimitException(int maximum) : base("Portal graph exceeded the configured endpoint limit of " + maximum + ".") { Maximum = maximum; } } internal sealed class PortalGraphService : IPortalDirectoryService, IPortalRoutePlanner { private readonly object _sync = new object(); private readonly IPortalAccessEvaluator _access; private PortalGraph _graph; private int _maximumEndpoints; private RouteStopCode _snapshotStop; internal int Count { get { lock (_sync) { return _graph.Count; } } } internal RouteStopCode SnapshotStop { get { lock (_sync) { return _snapshotStop; } } } internal PortalGraphService(IPortalAccessEvaluator access, int maximumEndpoints) { _access = access ?? throw new ArgumentNullException("access"); SetMaximumEndpoints(maximumEndpoints); _graph = new PortalGraph(Array.Empty(), _maximumEndpoints); } internal void SetMaximumEndpoints(int maximumEndpoints) { if (maximumEndpoints < 1 || maximumEndpoints > 2048) { throw new ArgumentOutOfRangeException("maximumEndpoints"); } lock (_sync) { _maximumEndpoints = maximumEndpoints; } } internal bool TryReplaceAuthoritativeSnapshot(IEnumerable endpoints, out string failure) { failure = string.Empty; try { int maximumEndpoints; lock (_sync) { maximumEndpoints = _maximumEndpoints; } PortalGraph graph = new PortalGraph(endpoints, maximumEndpoints); lock (_sync) { _graph = graph; _snapshotStop = RouteStopCode.Ready; } return true; } catch (PortalGraphLimitException ex) { lock (_sync) { _snapshotStop = RouteStopCode.GraphLimitExceeded; } failure = ex.Message; return false; } catch (ArgumentException ex2) { lock (_sync) { _snapshotStop = RouteStopCode.StaleSelection; } failure = ex2.Message; return false; } } internal void RejectAuthoritativeSnapshot(RouteStopCode stop) { if (stop == RouteStopCode.Ready) { throw new ArgumentOutOfRangeException("stop"); } lock (_sync) { _snapshotStop = stop; } } public PortalDirectoryResult Query(PortalDirectoryQuery query) { lock (_sync) { if (_snapshotStop != RouteStopCode.Ready) { return new PortalDirectoryResult(Array.Empty(), truncated: false, _snapshotStop); } return _graph.Query(query, _access); } } public PortalNameResolution ResolveName(PortalDirectoryQuery scope, string displayName) { lock (_sync) { if (_snapshotStop != RouteStopCode.Ready) { return new PortalNameResolution(_snapshotStop, Array.Empty()); } return _graph.ResolveName(scope, displayName, _access); } } public RoutePlan Plan(RoutePlanRequest request) { lock (_sync) { if (_snapshotStop != RouteStopCode.Ready) { return new RoutePlan(_snapshotStop, request?.SourcePortalId, request?.DestinationPortalId, oneWay: false, -1L, -1L); } return _graph.Plan(request, _access); } } internal bool TryGetEndpoint(string portalId, out PortalEndpoint endpoint) { lock (_sync) { if (_snapshotStop != RouteStopCode.Ready) { endpoint = null; return false; } return _graph.TryGet(portalId, out endpoint); } } internal PortalEndpoint[] SnapshotEndpoints() { lock (_sync) { if (_snapshotStop != RouteStopCode.Ready) { return Array.Empty(); } return (PortalEndpoint[])_graph.Endpoints.Clone(); } } } internal readonly struct PortalMapCandidate { internal string PortalId { get; } internal string NetworkName { get; } internal string DisplayName { get; } internal float X { get; } internal float Y { get; } internal float Z { get; } internal bool IsNetworkPortal { get; } internal bool AcceptsArrival { get; } internal bool PermitsDeparture { get; } internal bool ViewAuthorized { get; } internal bool DepartureAuthorized { get; } internal bool IsFinite { get { if (Finite(X) && Finite(Y)) { return Finite(Z); } return false; } } internal bool IsAuthorizedDirectoryEntry => ViewAuthorized; internal PortalMapCandidate(string portalId, string networkName, string displayName, float x, float y, float z, bool isNetworkPortal, bool acceptsArrival, bool permitsDeparture, bool viewAuthorized, bool departureAuthorized) { PortalId = PortalText.Require(portalId, 96, "portalId"); NetworkName = PortalText.Require(networkName, 64, "networkName"); DisplayName = PortalText.Require(displayName, 64, "displayName"); X = x; Y = y; Z = z; IsNetworkPortal = isNetworkPortal; AcceptsArrival = acceptsArrival; PermitsDeparture = permitsDeparture; ViewAuthorized = viewAuthorized; DepartureAuthorized = departureAuthorized; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal sealed class PortalMapOverlayModel { internal const int MaximumContextLength = 256; private static readonly PortalMapCandidate[] EmptyCandidates = Array.Empty(); private readonly List _eligible = new List(); private string _contextToken = string.Empty; private int _revision; internal string ContextToken => _contextToken; internal string SelectedNetwork => string.Empty; internal int NetworkCount => 0; internal int EligibleCount => _eligible.Count; internal int Revision => _revision; internal bool SetContext(string contextToken) { string text = PortalText.NormalizeOptional(contextToken, 256, "contextToken"); if (string.Equals(_contextToken, text, StringComparison.Ordinal)) { return false; } _contextToken = text; _eligible.Clear(); AdvanceRevision(); return true; } internal bool TryReplace(IReadOnlyList candidates) { if (_contextToken.Length == 0 || candidates == null || candidates.Count > 2048) { RejectSnapshot(); return false; } HashSet hashSet = new HashSet(StringComparer.Ordinal); List list = new List(candidates.Count); for (int i = 0; i < candidates.Count; i++) { PortalMapCandidate portalMapCandidate = candidates[i]; if (!CandidateIsCanonical(portalMapCandidate) || !hashSet.Add(portalMapCandidate.PortalId)) { RejectSnapshot(); return false; } if (portalMapCandidate.IsAuthorizedDirectoryEntry) { list.Add(portalMapCandidate); } } list.Sort(CompareCandidate); _eligible.Clear(); _eligible.AddRange(list); AdvanceRevision(); return true; } internal bool NoteNetworkUsed(string networkName) { try { PortalText.Require(networkName, 64, "networkName"); } catch (ArgumentException) { return false; } return false; } internal bool Cycle(int direction) { return false; } internal PortalMapCandidate[] SelectedCandidates() { if (_eligible.Count != 0) { return _eligible.ToArray(); } return EmptyCandidates; } private void RejectSnapshot() { _eligible.Clear(); AdvanceRevision(); } private static bool CandidateIsCanonical(PortalMapCandidate candidate) { if (!candidate.IsFinite || string.IsNullOrEmpty(candidate.PortalId) || string.IsNullOrEmpty(candidate.NetworkName) || string.IsNullOrEmpty(candidate.DisplayName)) { return false; } try { return string.Equals(candidate.PortalId, PortalText.Require(candidate.PortalId, 96, "PortalId"), StringComparison.Ordinal) && string.Equals(candidate.NetworkName, PortalText.Require(candidate.NetworkName, 64, "NetworkName"), StringComparison.Ordinal) && string.Equals(candidate.DisplayName, PortalText.Require(candidate.DisplayName, 64, "DisplayName"), StringComparison.Ordinal); } catch (ArgumentException) { return false; } } private static int CompareCandidate(PortalMapCandidate left, PortalMapCandidate right) { int num = string.Compare(left.NetworkName, right.NetworkName, StringComparison.Ordinal); if (num != 0) { return num; } int num2 = string.Compare(left.DisplayName, right.DisplayName, StringComparison.OrdinalIgnoreCase); if (num2 == 0) { return string.Compare(left.PortalId, right.PortalId, StringComparison.Ordinal); } return num2; } private void AdvanceRevision() { _revision++; } } internal enum PortalConfirmationAdmission { NotRequired, Approved, Pending, Denied, Unavailable } internal sealed class PortalOverwriteConfirmationGate { private sealed class PendingConfirmation { internal string Fingerprint; internal long ExpiresUtcTicks; } private readonly Dictionary _pending = new Dictionary(StringComparer.Ordinal); internal PortalConfirmationAdmission Request(bool overwriteRequired, string targetId, string stateFingerprint) { if (!overwriteRequired) { return PortalConfirmationAdmission.NotRequired; } if (string.IsNullOrEmpty(targetId) || string.IsNullOrEmpty(stateFingerprint)) { return PortalConfirmationAdmission.Denied; } long ticks = DateTime.UtcNow.Ticks; if (_pending.TryGetValue(targetId, out var value) && value.ExpiresUtcTicks >= ticks && string.Equals(value.Fingerprint, stateFingerprint, StringComparison.Ordinal)) { _pending.Remove(targetId); return PortalConfirmationAdmission.Approved; } if (_pending.Count >= 64) { Prune(ticks); } if (_pending.Count >= 64) { _pending.Clear(); } _pending[targetId] = new PendingConfirmation { Fingerprint = stateFingerprint, ExpiresUtcTicks = ticks + TimeSpan.FromSeconds(4.0).Ticks }; return PortalConfirmationAdmission.Pending; } internal void Cancel(string targetId) { if (!string.IsNullOrEmpty(targetId)) { _pending.Remove(targetId); } } internal bool RequesterIsActive() { return true; } private void Prune(long now) { List list = new List(); foreach (KeyValuePair item in _pending) { if (item.Value == null || item.Value.ExpiresUtcTicks < now) { list.Add(item.Key); } } for (int i = 0; i < list.Count; i++) { _pending.Remove(list[i]); } } } internal interface IPortalGroupMembershipResolver { bool TryIsMember(string groupId, long playerId, out bool isMember); } internal sealed class PortalPermissionAdapter : IPortalAccessEvaluator { private readonly IPortalGroupMembershipResolver _groups; internal PortalPermissionAdapter(IPortalGroupMembershipResolver groups) { _groups = groups; } public bool Allows(PortalEndpoint endpoint, string travelerStableId, PortalAccessAction action) { if (endpoint == null || string.IsNullOrEmpty(travelerStableId)) { return false; } PortalAccessPolicy policy = endpoint.Access.GetPolicy(action); switch (policy.Kind) { case PortalPolicyKind.Everyone: case PortalPolicyKind.Ward: return true; case PortalPolicyKind.Approved: case PortalPolicyKind.WardWithExceptions: return policy.IsExplicitlyApproved(travelerStableId); case PortalPolicyKind.Owner: return string.Equals(endpoint.OwnerStableId, travelerStableId, StringComparison.Ordinal); case PortalPolicyKind.Group: { long playerId; bool isMember = default(bool); return TryPlayerId(travelerStableId, out playerId) && _groups != null && _groups.TryIsMember(policy.GroupId, playerId, out isMember) && isMember; } default: return false; } } internal static string Identity(long playerId) { return "valheim.player:" + playerId.ToString(CultureInfo.InvariantCulture); } internal static bool TryPlayerId(string canonical, out long playerId) { playerId = 0L; if (canonical == null || !canonical.StartsWith("valheim.player:", StringComparison.Ordinal)) { return false; } string text = canonical.Substring("valheim.player:".Length); if (long.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out playerId) && playerId > 0) { return string.Equals(text, playerId.ToString(CultureInfo.InvariantCulture), StringComparison.Ordinal); } return false; } internal static bool TryParseIdentity(string canonical, out StableIdentity identity) { identity = null; if (!TryPlayerId(canonical, out var playerId)) { return false; } return StableIdentity.TryCreate("valheim.player", playerId.ToString(CultureInfo.InvariantCulture), out identity); } } internal sealed class PortalRoutePermissionEvidence { internal bool PolicyAllowed { get; } internal bool SourceWardAllowed { get; } internal bool DestinationWardAllowed { get; } private PortalRoutePermissionEvidence(bool policyAllowed, bool sourceWardAllowed, bool destinationWardAllowed) { PolicyAllowed = policyAllowed; SourceWardAllowed = sourceWardAllowed; DestinationWardAllowed = destinationWardAllowed; } internal static PortalRoutePermissionEvidence Evaluate(IPortalAccessEvaluator permissions, PortalEndpoint source, PortalEndpoint destination, string travelerStableId, bool sourceWardAllowed, bool destinationWardAllowed) { if (permissions == null) { throw new ArgumentNullException("permissions"); } return new PortalRoutePermissionEvidence(source != null && destination != null && permissions.Allows(source, travelerStableId, PortalAccessAction.Depart) && permissions.Allows(destination, travelerStableId, PortalAccessAction.Arrive), sourceWardAllowed, destinationWardAllowed); } } internal sealed class PortalSelection { internal string TravelerStableId { get; } internal string SourcePortalId { get; } internal string DestinationPortalId { get; } internal long SourceRevision { get; } internal long DestinationRevision { get; } internal bool IsReturn { get; } internal long SelectedUtcTicks { get; } internal string DestinationDisplayName { get; } internal PortalSelection(string travelerStableId, string sourcePortalId, string destinationPortalId, long sourceRevision, long destinationRevision, bool isReturn, long selectedUtcTicks, string destinationDisplayName = "") { TravelerStableId = travelerStableId; SourcePortalId = sourcePortalId; DestinationPortalId = destinationPortalId; SourceRevision = sourceRevision; DestinationRevision = destinationRevision; IsReturn = isReturn; SelectedUtcTicks = selectedUtcTicks; DestinationDisplayName = PortalText.NormalizeOptional(destinationDisplayName, 64, "destinationDisplayName"); } } internal sealed class PortalSelectionStore { private sealed class Slot { internal PortalSelection Selection; internal long Sequence; } private readonly Dictionary _values = new Dictionary(StringComparer.Ordinal); private readonly int _capacity; private long _sequence; internal int Count => _values.Count; internal PortalSelectionStore(int capacity) { if (capacity < 1 || capacity > 256) { throw new ArgumentOutOfRangeException("capacity"); } _capacity = capacity; } internal void Set(PortalSelection selection) { if (selection == null) { throw new ArgumentNullException("selection"); } if (_values.TryGetValue(selection.TravelerStableId, out var value)) { value.Selection = selection; value.Sequence = ++_sequence; return; } if (_values.Count >= _capacity) { EvictOldest(); } _values.Add(selection.TravelerStableId, new Slot { Selection = selection, Sequence = ++_sequence }); } internal bool TryGet(string travelerStableId, string sourcePortalId, out PortalSelection selection) { selection = null; if (!_values.TryGetValue(travelerStableId ?? string.Empty, out var value) || !string.Equals(value.Selection.SourcePortalId, sourcePortalId, StringComparison.Ordinal)) { return false; } value.Sequence = ++_sequence; selection = value.Selection; return true; } internal void Clear() { _values.Clear(); } private void EvictOldest() { string text = null; long num = long.MaxValue; foreach (KeyValuePair value in _values) { if (value.Value.Sequence < num) { num = value.Value.Sequence; text = value.Key; } } if (text != null) { _values.Remove(text); } } } internal sealed class OneWayAcknowledgementStore { private string _traveler; private string _source; private string _destination; private long _expiresUtcTicks; internal bool ConsumeOrArm(string traveler, string source, string destination, long nowUtcTicks, long durationTicks) { if (_expiresUtcTicks > nowUtcTicks && string.Equals(_traveler, traveler, StringComparison.Ordinal) && string.Equals(_source, source, StringComparison.Ordinal) && string.Equals(_destination, destination, StringComparison.Ordinal)) { Clear(); return true; } _traveler = traveler; _source = source; _destination = destination; _expiresUtcTicks = checked(nowUtcTicks + Math.Max(1L, durationTicks)); return false; } internal void Clear() { _traveler = null; _source = null; _destination = null; _expiresUtcTicks = 0L; } } internal readonly struct PortalHoverPanelState { internal bool IsNetwork { get; } internal bool DetailsVisible { get; } internal bool VanillaConnected { get; } internal bool EditorOpen { get; } internal string DisplayName { get; } internal string NetworkId { get; } internal string Policy { get; } internal bool AcceptsArrival { get; } internal bool PermitsDeparture { get; } internal string SelectedDestination { get; } internal PortalHoverPanelState(bool isNetwork, bool detailsVisible, bool vanillaConnected, bool editorOpen, string displayName, string networkId, string policy, bool acceptsArrival, bool permitsDeparture, string selectedDestination) { IsNetwork = isNetwork; DetailsVisible = detailsVisible; VanillaConnected = vanillaConnected; EditorOpen = editorOpen; DisplayName = Bounded(displayName, 64); NetworkId = Bounded(networkId, 64); Policy = Bounded(policy, 16); AcceptsArrival = acceptsArrival; PermitsDeparture = permitsDeparture; SelectedDestination = Bounded(selectedDestination, 96); } private static string Bounded(string value, int maximum) { string text = value ?? string.Empty; if (text.Length > maximum) { return text.Substring(0, maximum); } return text; } } internal readonly struct PortalSetupGuideContent { internal string Status { get; } internal string Warning { get; } internal string Controls { get; } internal string Instructions { get; } internal PortalSetupGuideContent(string status, string warning, string controls, string instructions) { Status = status ?? string.Empty; Warning = warning ?? string.Empty; Controls = controls ?? string.Empty; Instructions = instructions ?? string.Empty; } } internal static class PortalSetupGuide { internal const string PublicCommand = "network|public|NETWORK|NAME|both"; internal const string PrivateCommand = "network|private|NETWORK|NAME|both"; internal const string GroupCommand = "network|group|NETWORK|NAME|both"; internal static PortalSetupGuideContent Compose(PortalHoverPanelState state, string useBinding, string alternateUseBinding) { string text = Binding(useBinding, "Use"); string text2 = Binding(alternateUseBinding, "Alternate Place + Use"); string status = (state.IsNetwork ? NetworkStatus(state) : ("Current mode: Standard Pair (vanilla) | Link: " + (state.VanillaConnected ? "connected" : "unlinked"))); string warning = string.Empty; if (state.EditorOpen) { warning = "Runic editor open: enter one exact command from this guide and confirm it."; } else if (!state.IsNetwork && state.VanillaConnected) { warning = "Connected vanilla pair: first use " + text + " to give this portal a unique vanilla tag. Runic Portals refuses conversion while the vanilla pair is connected."; } string controls = ((!state.IsNetwork) ? (text + " - edit the vanilla tag\n" + text2 + " - open the Runic portal editor") : ((!state.DetailsVisible) ? "This portal's details and route controls are hidden from your current identity." : (state.PermitsDeparture ? (text + " - edit this Runic portal\nWalk into the portal - open its safe destination map\nClick an authorized arrival portal on the map to travel" + (state.AcceptsArrival ? string.Empty : "\nDeparture-only endpoint: a destination click confirms that the trip may be one-way.")) : (text + " - edit this Runic portal\nArrival-only endpoint: it appears in a portal picker, but walking into it does not start a trip.")))); string instructions = "PUBLIC - players may discover and use it, subject to wards and current route/world rules\nnetwork|public|NETWORK|NAME|both\n\nPRIVATE - only the portal owner's persisted identity may discover and use it, subject to wards and route/world rules\nnetwork|private|NETWORK|NAME|both\n\nGROUP - current members of one Runic group may discover and use it, subject to wards and route/world rules\nnetwork|group|NETWORK|NAME|both\n\nNETWORK is the map and route filter. Only portals with the exact same NETWORK spelling appear together. Public, your private, and your current Group endpoints may share a NETWORK; access is checked separately for every portal. Give every portal a distinct NAME.\n\nDirection replaces 'both': both = arrive and depart; arrive = destination only; depart = source only.\n\nGroups use Valheim chat, not F5: /group create , /group invite , /group accept , and /group use . The active Group is used automatically by the Group portal command; no UUID is entered.\n\nSetup: open the Runic editor on each unlinked Standard Pair portal and enter its command. Walk into a configured depart/both portal to open the destination map, then click an authorized arrive/both portal. On the normal large map, press P to show or hide the world-wide authorized directory: vanilla Standard Pair portals, public Runic portals, your private Runic portals, and Runic Group portals for groups you currently belong to. This directory includes arrive-only, depart-only, and bidirectional Runic portals and does not require standing near a portal. To restore vanilla mode, edit a configured portal with " + text + " and enter 'standard'. Changing an existing Network portal or restoring 'standard' requires repeating the identical command once to confirm."; return new PortalSetupGuideContent(status, warning, controls, instructions); } private static string NetworkStatus(PortalHoverPanelState state) { if (!state.DetailsVisible) { return "Current mode: Restricted Network Portal | Details hidden by current permissions"; } string text = ((state.AcceptsArrival && state.PermitsDeparture) ? "both" : (state.AcceptsArrival ? "arrive only" : "depart only")); string text2 = ((state.SelectedDestination.Length == 0) ? "none" : state.SelectedDestination); return "Current mode: " + ((state.Policy.Length == 0) ? "Network" : (state.Policy + " Network")) + " | Network: " + state.NetworkId + " | Name: " + state.DisplayName + " | Direction: " + text + "\nSelected destination: " + text2; } private static string Binding(string value, string fallback) { string text = (value ?? string.Empty).Trim(); if (text.Length == 0) { return fallback; } if (text.Length > 64) { text = text.Substring(0, 64); } for (int i = 0; i < text.Length; i++) { if (char.IsControl(text[i])) { return fallback; } } return text; } } internal static class PortalTravelTransformPolicy { internal const float MaximumExitOffsetMeters = 64f; private const float MinimumQuaternionMagnitudeSquared = 0.99f; private const float MaximumQuaternionMagnitudeSquared = 1.01f; internal static bool TryResolve(Vector3 destination, Quaternion destinationRotation, float exitOffset, out Vector3 arrival, out Quaternion normalizedRotation) { //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: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0029: 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_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Unknown result type (might be due to invalid IL or missing references) //IL_008a: 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_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00a5: 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_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_00ff: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010f: Unknown result type (might be due to invalid IL or missing references) //IL_0117: 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_0123: 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_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0133: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_015d: 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_0165: Unknown result type (might be due to invalid IL or missing references) //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Unknown result type (might be due to invalid IL or missing references) arrival = default(Vector3); normalizedRotation = default(Quaternion); if (!Finite(destination.x) || !Finite(destination.y) || !Finite(destination.z) || !Finite(destinationRotation.x) || !Finite(destinationRotation.y) || !Finite(destinationRotation.z) || !Finite(destinationRotation.w) || !Finite(exitOffset) || exitOffset < 0f || exitOffset > 64f) { return false; } float num = destinationRotation.x * destinationRotation.x + destinationRotation.y * destinationRotation.y + destinationRotation.z * destinationRotation.z + destinationRotation.w * destinationRotation.w; if (!Finite(num) || num < 0.99f || num > 1.01f) { return false; } float num2 = 1f / Mathf.Sqrt(num); if (!Finite(num2) || num2 <= 0f) { return false; } normalizedRotation = new Quaternion(destinationRotation.x * num2, destinationRotation.y * num2, destinationRotation.z * num2, destinationRotation.w * num2); Vector3 val = normalizedRotation * Vector3.forward; if (!Finite(val.x) || !Finite(val.y) || !Finite(val.z)) { return false; } arrival = destination + val * exitOffset + Vector3.up; if (Finite(arrival.x) && Finite(arrival.y)) { return Finite(arrival.z); } return false; } private static bool Finite(float value) { if (!float.IsNaN(value)) { return !float.IsInfinity(value); } return false; } } internal sealed class ReturnRoute { internal string TravelerStableId { get; } internal string OriginPortalId { get; } internal string ArrivalPortalId { get; } internal long OriginRevision { get; } internal long ArrivalRevision { get; } internal long ExpiresUtcTicks { get; } internal ReturnRoute(string travelerStableId, string originPortalId, string arrivalPortalId, long originRevision, long arrivalRevision, long expiresUtcTicks) { TravelerStableId = travelerStableId; OriginPortalId = originPortalId; ArrivalPortalId = arrivalPortalId; OriginRevision = originRevision; ArrivalRevision = arrivalRevision; ExpiresUtcTicks = expiresUtcTicks; } } internal sealed class ReturnRouteStore { private sealed class Slot { internal ReturnRoute Route; internal long Sequence; } private readonly Dictionary _routes = new Dictionary(StringComparer.Ordinal); private readonly List _expired = new List(); private readonly int _capacity; private long _sequence; internal int Count => _routes.Count; internal ReturnRouteStore(int capacity) { if (capacity < 1 || capacity > 256) { throw new ArgumentOutOfRangeException("capacity"); } _capacity = capacity; } internal void Record(ReturnRoute route, long nowUtcTicks) { if (route == null) { throw new ArgumentNullException("route"); } Prune(nowUtcTicks); if (_routes.TryGetValue(route.TravelerStableId, out var value)) { value.Route = route; value.Sequence = ++_sequence; return; } if (_routes.Count >= _capacity) { EvictOldest(); } _routes.Add(route.TravelerStableId, new Slot { Route = route, Sequence = ++_sequence }); } internal bool TryGet(string travelerStableId, string currentPortalId, long nowUtcTicks, out ReturnRoute route) { route = null; if (string.IsNullOrEmpty(travelerStableId) || string.IsNullOrEmpty(currentPortalId)) { return false; } if (!_routes.TryGetValue(travelerStableId, out var value)) { return false; } if (value.Route.ExpiresUtcTicks <= nowUtcTicks) { _routes.Remove(travelerStableId); return false; } if (!string.Equals(value.Route.ArrivalPortalId, currentPortalId, StringComparison.Ordinal)) { return false; } value.Sequence = ++_sequence; route = value.Route; return true; } internal bool Remove(string travelerStableId) { if (!string.IsNullOrEmpty(travelerStableId)) { return _routes.Remove(travelerStableId); } return false; } internal int Prune(long nowUtcTicks) { _expired.Clear(); foreach (KeyValuePair route in _routes) { if (route.Value.Route.ExpiresUtcTicks <= nowUtcTicks) { _expired.Add(route.Key); } } for (int i = 0; i < _expired.Count; i++) { _routes.Remove(_expired[i]); } return _expired.Count; } private void EvictOldest() { string text = null; long num = long.MaxValue; foreach (KeyValuePair route in _routes) { if (route.Value.Sequence < num) { num = route.Value.Sequence; text = route.Key; } } if (text != null) { _routes.Remove(text); } } } } namespace RunicPortals.Api { public static class GroupIntegrationApi { private static readonly object Gate = new object(); private static PortalGroupRuntime _runtime; public static bool TryIsMember(string groupId, long playerId, out bool isMember) { lock (Gate) { if (_runtime != null) { return _runtime.TryIsMember(groupId, playerId, out isMember); } isMember = false; return false; } } internal static void Attach(PortalGroupRuntime runtime) { lock (Gate) { _runtime = runtime; } } internal static void Detach(PortalGroupRuntime runtime) { lock (Gate) { if (_runtime == runtime) { _runtime = null; } } } } public enum PortalMutationKind { ConfigureMetadata = 1, CommitTravel } public enum AuthorityStopCode { Allowed, FeatureDisabled, ServerAuthorityMissing, DedicatedTransportUnavailable, SenderIdentityUnbound, ActorNotLocalAuthority, SourceMissing, DestinationMissing, SourceObjectNotOwned, PlayerObjectNotOwned, OwnerPermissionDenied, SourceWardDenied, DestinationWardDenied, ActorOutOfRange, CurrentStateChanged, VanillaTravelPolicyDenied, UnsupportedMutation } public sealed class PortalAuthorityEvidence { public PortalMutationKind Mutation { get; } public bool FeatureEnabled { get; } public bool IsServer { get; } public bool IsDedicated { get; } public bool DedicatedTransportAvailable { get; } public bool SenderIdentityBound { get; } public bool ActorIsLocalAuthority { get; } public bool SourceExists { get; } public bool DestinationExists { get; } public bool SourceObjectOwned { get; } public bool PlayerObjectOwned { get; } public bool OwnerPermissionAllowed { get; } public bool SourceWardAllowed { get; } public bool DestinationWardAllowed { get; } public bool ActorInRange { get; } public bool CurrentStateMatches { get; } public bool VanillaTravelPolicyAllowed { get; } public PortalAuthorityEvidence(PortalMutationKind mutation, bool featureEnabled, bool isServer, bool isDedicated, bool dedicatedTransportAvailable, bool senderIdentityBound, bool actorIsLocalAuthority, bool sourceExists, bool destinationExists, bool sourceObjectOwned, bool playerObjectOwned, bool ownerPermissionAllowed, bool sourceWardAllowed, bool destinationWardAllowed, bool actorInRange, bool currentStateMatches, bool vanillaTravelPolicyAllowed) { if (!Enum.IsDefined(typeof(PortalMutationKind), mutation)) { throw new ArgumentOutOfRangeException("mutation"); } Mutation = mutation; FeatureEnabled = featureEnabled; IsServer = isServer; IsDedicated = isDedicated; DedicatedTransportAvailable = dedicatedTransportAvailable; SenderIdentityBound = senderIdentityBound; ActorIsLocalAuthority = actorIsLocalAuthority; SourceExists = sourceExists; DestinationExists = destinationExists; SourceObjectOwned = sourceObjectOwned; PlayerObjectOwned = playerObjectOwned; OwnerPermissionAllowed = ownerPermissionAllowed; SourceWardAllowed = sourceWardAllowed; DestinationWardAllowed = destinationWardAllowed; ActorInRange = actorInRange; CurrentStateMatches = currentStateMatches; VanillaTravelPolicyAllowed = vanillaTravelPolicyAllowed; } } public sealed class PortalAuthorityDecision { public AuthorityStopCode StopCode { get; } public bool IsAllowed => StopCode == AuthorityStopCode.Allowed; internal PortalAuthorityDecision(AuthorityStopCode stopCode) { StopCode = stopCode; } } public interface IPortalAuthorityGate { PortalAuthorityDecision Evaluate(PortalAuthorityEvidence evidence); } public static class PortalContractLimits { public const int MaximumPortalIdLength = 96; public const int MaximumNameLength = 64; public const int MaximumNetworkIdLength = 64; public const int MaximumOwnerLabelLength = 64; public const int MaximumBiomeLabelLength = 48; public const int MaximumSearchLength = 64; public const int MaximumApprovedIdentities = 64; public const int MaximumGraphEndpoints = 2048; public const int MaximumDirectoryResults = 128; public const int MaximumDiagnostics = 256; public const int MaximumReturnRoutes = 256; public const int MaximumSelections = 256; } public enum PortalMode { StandardPair, Network } public enum PortalNetworkKind { Public = 1, Personal, Ward, Group, Custom } public enum PortalOnlineState { Online = 1, Offline, Disabled, Destroyed, Stale } public enum PortalPolicyKind { Everyone = 1, Approved, Owner, Ward, WardWithExceptions, Group, Hidden, Disabled } public enum PortalAccessAction { ViewDiscover = 1, Arrive, Depart, Edit, Invite, Publish } public enum TravelPolicyState { Allowed = 1, RestrictedItems, PortalsDisabled, BossTravelBlocked, Unknown } public enum RouteStopCode { Ready, FeatureDisabled, NotFoundOrUnauthorized, SourceUnavailable, DestinationUnavailable, NetworkMismatch, SameEndpoint, DepartureDenied, ArrivalDenied, RestrictedItems, PortalsDisabled, BossTravelBlocked, PolicyUnknown, OneWayWarningRequired, DuplicateName, GraphLimitExceeded, StaleSelection, AuthorityUnavailable, TeleportRejected } public sealed class PortalAccessPolicy { private readonly string[] _approved; private readonly ReadOnlyCollection _approvedView; public PortalPolicyKind Kind { get; } public IReadOnlyList ApprovedStableIds => _approvedView; public string GroupId { get; } public PortalAccessPolicy(PortalPolicyKind kind, IEnumerable approvedStableIds = null, string groupId = "") { if (!Enum.IsDefined(typeof(PortalPolicyKind), kind)) { throw new ArgumentOutOfRangeException("kind"); } Kind = kind; GroupId = PortalText.NormalizeOptional(groupId, 64, "groupId"); SortedSet sortedSet = new SortedSet(StringComparer.Ordinal); if (approvedStableIds != null) { foreach (string approvedStableId in approvedStableIds) { string item = PortalText.Require(approvedStableId, 96, "approvedStableIds"); sortedSet.Add(item); if (sortedSet.Count > 64) { throw new ArgumentOutOfRangeException("approvedStableIds"); } } } _approved = new string[sortedSet.Count]; sortedSet.CopyTo(_approved); _approvedView = Array.AsReadOnly(_approved); if (Kind == PortalPolicyKind.Group && GroupId.Length == 0) { throw new ArgumentException("Group policy requires a group identifier.", "groupId"); } } public bool IsExplicitlyApproved(string stableId) { if (string.IsNullOrEmpty(stableId)) { return false; } return Array.BinarySearch(_approved, stableId, (IComparer?)StringComparer.Ordinal) >= 0; } } public sealed class PortalAccessProfile { private static readonly PortalAccessPolicy DisabledPolicy = new PortalAccessPolicy(PortalPolicyKind.Disabled); private readonly Dictionary _policies; private readonly ReadOnlyDictionary _view; public IReadOnlyDictionary Policies => _view; public static PortalAccessProfile PublicNetwork { get; } = CreatePublicNetwork(); public static PortalAccessProfile PrivateNetwork { get; } = CreatePrivateNetwork(); public PortalAccessProfile(IEnumerable> policies) { _policies = new Dictionary(); if (policies == null) { throw new ArgumentNullException("policies"); } foreach (KeyValuePair policy in policies) { if (!Enum.IsDefined(typeof(PortalAccessAction), policy.Key) || policy.Value == null) { throw new ArgumentException("Access profiles cannot contain unknown actions or null policies.", "policies"); } if (_policies.ContainsKey(policy.Key)) { throw new ArgumentException("Access profiles cannot contain duplicate actions.", "policies"); } _policies.Add(policy.Key, policy.Value); } _view = new ReadOnlyDictionary(_policies); } public PortalAccessPolicy GetPolicy(PortalAccessAction action) { if (!_policies.TryGetValue(action, out var value)) { return DisabledPolicy; } return value; } public static PortalAccessProfile ForGroup(string groupId) { PortalAccessPolicy policy = new PortalAccessPolicy(PortalPolicyKind.Group, null, groupId); PortalAccessPolicy policy2 = new PortalAccessPolicy(PortalPolicyKind.Owner); return new PortalAccessProfile(new KeyValuePair[6] { Pair(PortalAccessAction.ViewDiscover, policy), Pair(PortalAccessAction.Arrive, policy), Pair(PortalAccessAction.Depart, policy), Pair(PortalAccessAction.Edit, policy2), Pair(PortalAccessAction.Invite, policy2), Pair(PortalAccessAction.Publish, policy2) }); } private static PortalAccessProfile CreatePublicNetwork() { PortalAccessPolicy policy = new PortalAccessPolicy(PortalPolicyKind.Everyone); PortalAccessPolicy policy2 = new PortalAccessPolicy(PortalPolicyKind.Owner); return new PortalAccessProfile(new KeyValuePair[6] { Pair(PortalAccessAction.ViewDiscover, policy), Pair(PortalAccessAction.Arrive, policy), Pair(PortalAccessAction.Depart, policy), Pair(PortalAccessAction.Edit, policy2), Pair(PortalAccessAction.Invite, policy2), Pair(PortalAccessAction.Publish, policy2) }); } private static PortalAccessProfile CreatePrivateNetwork() { PortalAccessPolicy policy = new PortalAccessPolicy(PortalPolicyKind.Owner); return new PortalAccessProfile(new KeyValuePair[6] { Pair(PortalAccessAction.ViewDiscover, policy), Pair(PortalAccessAction.Arrive, policy), Pair(PortalAccessAction.Depart, policy), Pair(PortalAccessAction.Edit, policy), Pair(PortalAccessAction.Invite, policy), Pair(PortalAccessAction.Publish, policy) }); } private static KeyValuePair Pair(PortalAccessAction action, PortalAccessPolicy policy) { return new KeyValuePair(action, policy); } } public sealed class PortalEndpoint { public string PortalId { get; } public PortalMode Mode { get; } public string DisplayName { get; } public string NetworkId { get; } public PortalNetworkKind NetworkKind { get; } public string OwnerStableId { get; } public string OwnerDisplayName { get; } public PortalOnlineState OnlineState { get; } public bool AcceptsArrival { get; } public bool PermitsDeparture { get; } public PortalAccessProfile Access { get; } public long Revision { get; } public string KnownBiome { get; } public PortalEndpoint(string portalId, PortalMode mode, string displayName, string networkId, PortalNetworkKind networkKind, string ownerStableId, string ownerDisplayName, PortalOnlineState onlineState, bool acceptsArrival, bool permitsDeparture, PortalAccessProfile access, long revision, string knownBiome = "") { PortalId = PortalText.Require(portalId, 96, "portalId"); if (!Enum.IsDefined(typeof(PortalMode), mode)) { throw new ArgumentOutOfRangeException("mode"); } if (!Enum.IsDefined(typeof(PortalNetworkKind), networkKind)) { throw new ArgumentOutOfRangeException("networkKind"); } if (!Enum.IsDefined(typeof(PortalOnlineState), onlineState)) { throw new ArgumentOutOfRangeException("onlineState"); } if (revision < 0) { throw new ArgumentOutOfRangeException("revision"); } Mode = mode; DisplayName = PortalText.NormalizeOptional(displayName, 64, "displayName"); NetworkId = PortalText.NormalizeOptional(networkId, 64, "networkId"); NetworkKind = networkKind; OwnerStableId = PortalText.NormalizeOptional(ownerStableId, 96, "ownerStableId"); OwnerDisplayName = PortalText.NormalizeOptional(ownerDisplayName, 64, "ownerDisplayName"); OnlineState = onlineState; AcceptsArrival = acceptsArrival; PermitsDeparture = permitsDeparture; Access = access ?? throw new ArgumentNullException("access"); Revision = revision; KnownBiome = PortalText.NormalizeOptional(knownBiome, 48, "knownBiome"); if (Mode == PortalMode.Network && (DisplayName.Length == 0 || NetworkId.Length == 0 || OwnerStableId.Length == 0)) { throw new ArgumentException("Network endpoints require a name, network, and stable owner."); } } } public sealed class PortalDirectoryQuery { public string TravelerStableId { get; } public string SourcePortalId { get; } public string NetworkId { get; } public string Search { get; } public int MaximumResults { get; } public bool IncludeOffline { get; } public PortalDirectoryQuery(string travelerStableId, string sourcePortalId, string networkId, string search = "", int maximumResults = 32, bool includeOffline = false) { TravelerStableId = PortalText.Require(travelerStableId, 96, "travelerStableId"); SourcePortalId = PortalText.NormalizeOptional(sourcePortalId, 96, "sourcePortalId"); NetworkId = PortalText.Require(networkId, 64, "networkId"); Search = PortalText.NormalizeOptional(search, 64, "search"); if (maximumResults < 1 || maximumResults > 128) { throw new ArgumentOutOfRangeException("maximumResults"); } MaximumResults = maximumResults; IncludeOffline = includeOffline; } } public sealed class PortalDirectoryEntry { public string PortalId { get; } public string DisplayName { get; } public string NetworkId { get; } public string OwnerDisplayName { get; } public PortalOnlineState OnlineState { get; } public string KnownBiome { get; } public bool AcceptsArrival { get; } public bool PermitsDeparture { get; } public bool DuplicateName { get; } public string Disambiguator { get; } internal PortalDirectoryEntry(PortalEndpoint endpoint, bool duplicateName) { PortalId = endpoint.PortalId; DisplayName = endpoint.DisplayName; NetworkId = endpoint.NetworkId; OwnerDisplayName = endpoint.OwnerDisplayName; OnlineState = endpoint.OnlineState; KnownBiome = endpoint.KnownBiome; AcceptsArrival = endpoint.AcceptsArrival; PermitsDeparture = endpoint.PermitsDeparture; DuplicateName = duplicateName; Disambiguator = (duplicateName ? endpoint.PortalId : string.Empty); } } public sealed class PortalDirectoryResult { public IReadOnlyList Entries { get; } public bool Truncated { get; } public RouteStopCode StopCode { get; } internal PortalDirectoryResult(IEnumerable entries, bool truncated, RouteStopCode stopCode) { Entries = Array.AsReadOnly(new List(entries).ToArray()); Truncated = truncated; StopCode = stopCode; } } public sealed class PortalNameResolution { public RouteStopCode StopCode { get; } public IReadOnlyList Candidates { get; } public bool IsUnique { get { if (StopCode == RouteStopCode.Ready) { return Candidates.Count == 1; } return false; } } internal PortalNameResolution(RouteStopCode stopCode, IEnumerable candidates) { StopCode = stopCode; Candidates = Array.AsReadOnly(new List(candidates).ToArray()); } } public sealed class RoutePlanRequest { public string TravelerStableId { get; } public string SourcePortalId { get; } public string DestinationPortalId { get; } public TravelPolicyState TravelPolicy { get; } public bool OneWayAcknowledged { get; } public long SourceRevision { get; } public long DestinationRevision { get; } public RoutePlanRequest(string travelerStableId, string sourcePortalId, string destinationPortalId, TravelPolicyState travelPolicy, bool oneWayAcknowledged, long sourceRevision = -1L, long destinationRevision = -1L) { TravelerStableId = PortalText.Require(travelerStableId, 96, "travelerStableId"); SourcePortalId = PortalText.Require(sourcePortalId, 96, "sourcePortalId"); DestinationPortalId = PortalText.Require(destinationPortalId, 96, "destinationPortalId"); if (!Enum.IsDefined(typeof(TravelPolicyState), travelPolicy)) { throw new ArgumentOutOfRangeException("travelPolicy"); } if (sourceRevision < -1 || destinationRevision < -1) { throw new ArgumentOutOfRangeException("sourceRevision"); } TravelPolicy = travelPolicy; OneWayAcknowledged = oneWayAcknowledged; SourceRevision = sourceRevision; DestinationRevision = destinationRevision; } } public sealed class RoutePlan { public RouteStopCode StopCode { get; } public string SourcePortalId { get; } public string DestinationPortalId { get; } public bool IsOneWay { get; } public long SourceRevision { get; } public long DestinationRevision { get; } public bool IsReady => StopCode == RouteStopCode.Ready; public bool IsAdvisoryOnly => true; internal RoutePlan(RouteStopCode stopCode, string sourcePortalId, string destinationPortalId, bool oneWay, long sourceRevision, long destinationRevision) { StopCode = stopCode; SourcePortalId = sourcePortalId ?? string.Empty; DestinationPortalId = destinationPortalId ?? string.Empty; IsOneWay = oneWay; SourceRevision = sourceRevision; DestinationRevision = destinationRevision; } } public interface IPortalDirectoryService { PortalDirectoryResult Query(PortalDirectoryQuery query); PortalNameResolution ResolveName(PortalDirectoryQuery scope, string displayName); } public interface IPortalRoutePlanner { RoutePlan Plan(RoutePlanRequest request); } public interface IPortalStatusService { bool FeatureEnabled { get; } bool RuntimeReady { get; } bool LocalHostMutationAvailable { get; } bool DedicatedMutationTransportAvailable { get; } int IndexedEndpointCount { get; } string DisabledReason { get; } } internal static class PortalText { internal static string Require(string value, int maximum, string parameter) { string text = NormalizeOptional(value, maximum, parameter); if (text.Length == 0) { throw new ArgumentException("A value is required.", parameter); } return text; } internal static string NormalizeOptional(string value, int maximum, string parameter) { string obj = value ?? string.Empty; if (obj.Length > maximum) { throw new ArgumentOutOfRangeException(parameter); } string text = obj.Trim(); if (text.Length > maximum) { throw new ArgumentOutOfRangeException(parameter); } for (int i = 0; i < text.Length; i++) { char c = text[i]; if (char.IsControl(c)) { throw new ArgumentException("Control characters are not permitted.", parameter); } if (char.IsSurrogate(c)) { if (!char.IsHighSurrogate(c) || i + 1 >= text.Length || !char.IsLowSurrogate(text[i + 1])) { throw new ArgumentException("Malformed Unicode is not permitted.", parameter); } i++; } } return text; } } public enum PortalDiagnosticCode { RuntimeReady = 1, RuntimeDisabled, SnapshotAccepted, SnapshotRejected, EditAccepted, EditRejected, RouteSelected, RouteRejected, RouteCommitted, ReturnExpired, AuthorityRejected } public sealed class PortalDiagnosticEvent { public string CorrelationId { get; } public long UtcTicks { get; } public PortalDiagnosticCode Code { get; } public RouteStopCode StopCode { get; } public string PortalId { get; } internal PortalDiagnosticEvent(string correlationId, long utcTicks, PortalDiagnosticCode code, RouteStopCode stopCode, string portalId) { CorrelationId = correlationId; UtcTicks = utcTicks; Code = code; StopCode = stopCode; PortalId = portalId ?? string.Empty; } } public interface IPortalDiagnosticService { long DroppedCount { get; } IReadOnlyList Snapshot(); } }