namespace XFEEliteDangerous.NativeFormat.Replication; public enum EdServerSessionPeerState { None = 0, Requested = 1, AwaitingJoined = 2, Joined = 3 } /// An already existing, authorized local session; all membership and object sets are caller supplied. public sealed record EdServerLocalSessionDefinition(ulong SessionId, ulong LocalMachineRunId, ulong InitialObjectIdBase, IReadOnlyList MemberMachineRunIds, IReadOnlyList FirstObjectIds, IReadOnlyList SecondObjectIds); /// The caller pins a peer and its previously allocated ordinary object-ID block before negotiation. public readonly record struct EdServerSessionPeerDefinition(ulong MachineRunId, ulong InitialObjectIdBase); public abstract record EdServerSessionOutbound(ulong DestinationMachineRunId) { public abstract EdServerSessionLetter Letter { get; } } public sealed record EdServerSessionRequestOutbound(ulong DestinationMachineRunId, EdServerSessionRequest Message) : EdServerSessionOutbound(DestinationMachineRunId) { public override EdServerSessionLetter Letter => EdServerSessionLetter.Request; } public sealed record EdServerSessionMembersOutbound(ulong DestinationMachineRunId, EdServerSessionMembers Message) : EdServerSessionOutbound(DestinationMachineRunId) { public override EdServerSessionLetter Letter => EdServerSessionLetter.Members; } public sealed record EdServerSessionJoinedOutbound(ulong DestinationMachineRunId, EdServerSessionJoined Message) : EdServerSessionOutbound(DestinationMachineRunId) { public override EdServerSessionLetter Letter => EdServerSessionLetter.Joined; } public sealed record EdServerSessionNegotiationResult(EdServerSessionPeerState State, bool StateChanged, bool IsDuplicate, IReadOnlyList Outbound); /// /// Bounded SESRQ/MEM/JOINED negotiation for one pre-existing local session. Native /// state transitions follow 0x64BDC0, 0x65BA80, 0x65C2C0 and 0x65BF60. Explicit /// peer/base authorization and ordinary aligned blocks are private-server policy. /// This component does not allocate a session, connect discovered machines, assign /// object ownership, ACK reliable letters, or create a world. The caller must /// authenticate the sender and finish transport sequencing before invoking it. /// Outbound letters must be sent in order using the existing reliable transport. /// public sealed class EdServerSessionNegotiationMachine { public const int DefaultMaximumPeers = 64; public const int HardMaximumPeers = 1024; private readonly object _gate = new(); private readonly Dictionary _peers = []; private readonly HashSet _allowedMachines; private readonly EdServerSessionRequest _request; private readonly EdServerSessionMembers _members; private readonly EdServerSessionJoined _joined; private readonly int _maximumObjectReferences; public ulong SessionId { get; } public ulong LocalMachineRunId { get; } public bool WorldReady => false; public int PeerCount => _peers.Count; public EdServerSessionNegotiationMachine(EdServerLocalSessionDefinition existingLocalSession, IReadOnlyList authorizedPeers, int maximumPeers = DefaultMaximumPeers, int maximumObjectReferences = EdServerSessionMessageCodec.DefaultMaximumCollectionCount) { ArgumentNullException.ThrowIfNull(existingLocalSession); ArgumentNullException.ThrowIfNull(authorizedPeers); if (maximumPeers is < 1 or > HardMaximumPeers) throw new ArgumentOutOfRangeException(nameof(maximumPeers)); if (maximumObjectReferences is < 0 or > EdServerSessionMessageCodec.HardMaximumCollectionCount) throw new ArgumentOutOfRangeException(nameof(maximumObjectReferences)); if (authorizedPeers.Count > maximumPeers) throw new ArgumentOutOfRangeException(nameof(authorizedPeers)); ValidateId(existingLocalSession.SessionId); ValidateId(existingLocalSession.LocalMachineRunId); ValidateBase(existingLocalSession.InitialObjectIdBase); SessionId = existingLocalSession.SessionId; LocalMachineRunId = existingLocalSession.LocalMachineRunId; _maximumObjectReferences = maximumObjectReferences; _allowedMachines = [LocalMachineRunId]; var bases = new HashSet { existingLocalSession.InitialObjectIdBase }; foreach (var definition in authorizedPeers) { ValidateId(definition.MachineRunId); ValidateBase(definition.InitialObjectIdBase); if (!_allowedMachines.Add(definition.MachineRunId) || !bases.Add(definition.InitialObjectIdBase)) throw new ArgumentException("Local peer IDs and allocated blocks must be distinct.", nameof(authorizedPeers)); _peers.Add(definition.MachineRunId, new Peer(definition.InitialObjectIdBase)); } var members = CopyMembers(existingLocalSession.MemberMachineRunIds); if (!members.Contains(LocalMachineRunId)) throw new ArgumentException("The existing session must contain its local machine.", nameof(existingLocalSession)); var (first, second) = CopyObjects(existingLocalSession.FirstObjectIds, existingLocalSession.SecondObjectIds); _request = new(SessionId, existingLocalSession.InitialObjectIdBase); _members = new(SessionId, existingLocalSession.InitialObjectIdBase, members); _joined = new(SessionId, first, second); } public EdServerSessionPeerState GetPeerState(ulong machineRunId) { lock (_gate) return RequirePeer(machineRunId).State; } /// An independent candidate for transactional transport admission. All copied collections are immutable. public EdServerSessionNegotiationMachine Fork() { lock(_gate)return new(this); } private EdServerSessionNegotiationMachine(EdServerSessionNegotiationMachine source) { SessionId=source.SessionId;LocalMachineRunId=source.LocalMachineRunId; _allowedMachines=new(source._allowedMachines);_request=source._request;_members=source._members;_joined=source._joined; _maximumObjectReferences=source._maximumObjectReferences; foreach(var (id,peer) in source._peers)_peers.Add(id,new(peer.InitialObjectIdBase) {State=peer.State,ReceivedMembers=peer.ReceivedMembers,FirstObjects=peer.FirstObjects,SecondObjects=peer.SecondObjects}); } /// A repeated call while Requested retransmits the same request; it never resets a later state. public EdServerSessionNegotiationResult BeginJoin(ulong machineRunId) { lock (_gate) { var peer = RequirePeer(machineRunId); var previous = peer.State; if (previous >= EdServerSessionPeerState.AwaitingJoined) return Result(peer, false, true); peer.State = EdServerSessionPeerState.Requested; return Result(peer, previous != peer.State, previous == peer.State, new EdServerSessionRequestOutbound(machineRunId, _request)); } } public EdServerSessionNegotiationResult ReceiveRequest(ulong senderMachineRunId, EdServerSessionRequest message) { ArgumentNullException.ThrowIfNull(message); lock (_gate) { var peer = RequireMessage(senderMachineRunId, message.SessionId, message.InitialObjectIdBase); if (peer.State >= EdServerSessionPeerState.AwaitingJoined) return Result(peer, false, true); // Native already-joining branch, not a new join. peer.State = EdServerSessionPeerState.AwaitingJoined; return Result(peer, true, false, new EdServerSessionMembersOutbound(senderMachineRunId, _members), new EdServerSessionJoinedOutbound(senderMachineRunId, _joined)); } } public EdServerSessionNegotiationResult ReceiveMembers(ulong senderMachineRunId, EdServerSessionMembers message) { ArgumentNullException.ThrowIfNull(message); lock (_gate) { var peer = RequireMessage(senderMachineRunId, message.SessionId, message.InitialObjectIdBase); if (peer.State == EdServerSessionPeerState.None) throw new InvalidDataException("SESMEM requires a pending or established peer exchange."); // Validation and bounded snapshots precede every mutation. Listed peers // are references only; no remote connection or state is created here. var members = CopyMembers(message.MachineRunIds); var duplicate = peer.ReceivedMembers is not null && peer.ReceivedMembers.SequenceEqual(members); var changed = peer.State == EdServerSessionPeerState.Requested; peer.ReceivedMembers = members; if (changed) peer.State = EdServerSessionPeerState.AwaitingJoined; return Result(peer, changed, duplicate, new EdServerSessionJoinedOutbound(senderMachineRunId, _joined)); } } public EdServerSessionNegotiationResult ReceiveJoined(ulong senderMachineRunId, EdServerSessionJoined message) { ArgumentNullException.ThrowIfNull(message); lock (_gate) { var peer = RequireMessage(senderMachineRunId, message.SessionId); if (peer.State < EdServerSessionPeerState.AwaitingJoined) throw new InvalidDataException("SESJOINED requires a peer awaiting the joined message."); var (first, second) = CopyObjects(message.FirstObjectIds, message.SecondObjectIds); if (peer.State == EdServerSessionPeerState.Joined) { if (!peer.FirstObjects!.SequenceEqual(first) || !peer.SecondObjects!.SequenceEqual(second)) throw new InvalidDataException("A duplicate SESJOINED cannot replace the accepted object-reference sets."); return Result(peer, false, true); } peer.FirstObjects = first; peer.SecondObjects = second; peer.State = EdServerSessionPeerState.Joined; return Result(peer, true, false); } } private Peer RequireMessage(ulong sender, ulong session, ulong? initialBase = null) { var peer = RequirePeer(sender); if (session != SessionId || initialBase.HasValue && initialBase.Value != peer.InitialObjectIdBase) throw new InvalidDataException("The session or pinned peer object-ID base does not match this local definition."); return peer; } private Peer RequirePeer(ulong machineRunId) => _peers.TryGetValue(machineRunId, out var peer) ? peer : throw new InvalidDataException("The sender is not an explicitly authorized peer of this local session."); private IReadOnlyList CopyMembers(IReadOnlyList ids) { ArgumentNullException.ThrowIfNull(ids); if (ids.Count > _allowedMachines.Count) throw new InvalidDataException("The session machine collection exceeds the configured peer bound."); var result = new ulong[ids.Count]; var distinct = new HashSet(); for (var i = 0; i < result.Length; i++) { var id = ids[i]; if (!_allowedMachines.Contains(id) || !distinct.Add(id)) throw new InvalidDataException("The session machine collection contains an unauthorized or duplicate reference."); result[i] = id; } return Array.AsReadOnly(result); } private (IReadOnlyList, IReadOnlyList) CopyObjects(IReadOnlyList first, IReadOnlyList second) { ArgumentNullException.ThrowIfNull(first); ArgumentNullException.ThrowIfNull(second); if ((long)first.Count + second.Count > _maximumObjectReferences) throw new InvalidDataException("The combined object-reference count exceeds the configured bound."); return (CopyIds(first), CopyIds(second)); } private static IReadOnlyList CopyIds(IReadOnlyList source) { var result = new ulong[source.Count]; for (var i = 0; i < result.Length; i++) { ValidateId(source[i]); result[i] = source[i]; } return Array.AsReadOnly(result); } private static void ValidateId(ulong id) { if (id == 0 || id > EdServerSessionMessageCodec.MaximumWireId) throw new InvalidDataException("Session, machine and object references must be nonzero 48-bit IDs."); } private static void ValidateBase(ulong initialBase) { if (initialBase == 0 || initialBase % NativeObjectIdAllocator.BlockSize != 0 || initialBase >= NativeObjectIdAllocator.ReservedStart) throw new ArgumentOutOfRangeException(nameof(initialBase), "A previously allocated ordinary aligned object-ID block is required."); } private static EdServerSessionNegotiationResult Result(Peer peer, bool changed, bool duplicate, params EdServerSessionOutbound[] outbound) => new(peer.State, changed, duplicate, Array.AsReadOnly(outbound)); private sealed class Peer(ulong initialObjectIdBase) { public ulong InitialObjectIdBase { get; } = initialObjectIdBase; public EdServerSessionPeerState State { get; set; } public IReadOnlyList? ReceivedMembers { get; set; } public IReadOnlyList? FirstObjects { get; set; } public IReadOnlyList? SecondObjects { get; set; } } }