csharp/0x0ade/CelesteNet/CelesteNet.Server/Channels.cs

Channels.cs
using Celeste.Mod.CelesteNet.DataTypes;
using Mono.Options;
using MonoMod.Utils;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Celeste.Mod.CelesteNet.Server {
    public clast Channels : IDisposable {

        public const string NameDefault = "main";
        public const string NamePrivate = "!";
        public const string PrefixPrivate = "!";

        public readonly CelesteNetServer Server;

        public readonly Channel Default;
        public readonly List All = new();
        public readonly Dictionary ByID = new();
        public readonly Dictionary ByName = new();
        public uint NextID = (uint) (DateTime.UtcNow.Ticks / TimeSpan.TicksPerSecond);

        public Channels(CelesteNetServer server) {
            Server = server;

            Server.Data.RegisterHandlersIn(this);

            Default = new(this, NameDefault, 0);

            Server.OnSessionStart += OnSessionStart;
        }

        public void Start() {
            Logger.Log(LogLevel.INF, "channels", "Startup");
        }

        public void Dispose() {
            Logger.Log(LogLevel.INF, "channels", "Shutdown");
        }

        private void OnSessionStart(CelesteNetPlayerSession session) {
            if (Server.UserData.TryLoad(session.UID, out LastChannelUserInfo last) &&
                last.Name != NameDefault) {
                Move(session, last.Name);
            } else {
                Default.Add(session);
                BroadcastList();
            }
        }

        public void SendListTo(CelesteNetPlayerSession session) {
            Channel own = session.Channel;

            List channels;
            lock (All) {
                channels = new(All.Count);
                foreach (Channel c in All) {
                    if (c.IsPrivate && c != own)
                        continue;
                    uint[] players;
                    using (c.Lock.R()) {
                        players = new uint[c.Players.Count];
                        int i = 0;
                        foreach (CelesteNetPlayerSession p in c.Players)
                            players[i++] = p.ID;
                    }
                    channels.Add(new DataChannelList.Channel {
                        Name = c.Name,
                        ID = c.ID,
                        Players = players
                    });
                }
            }

            session.Con.Send(new DataChannelList {
                List = channels.ToArray()
            });
        }

        public Action? OnBroadcastList;

        public void BroadcastList() {
            using (ListSnapshot snapshot = All.ToSnapshot())
                foreach (Channel c in snapshot)
                    c.RemoveStale();

            OnBroadcastList?.Invoke(this);

            lock (All)
                using (Server.ConLock.R())
                    foreach (CelesteNetPlayerSession session in Server.Sessions)
                        SendListTo(session);
        }

        public Tuple Move(CelesteNetPlayerSession session, string name) {
            name = name.Sanitize();
            if (name.Length > Server.Settings.MaxChannelNameLength)
                name = name.Substring(0, Server.Settings.MaxChannelNameLength);
            if (name == NamePrivate)
                throw new Exception("Invalid private channel name.");

            lock (All) {
                Channel prev = session.Channel;

                Channel c;

                if (ByName.TryGetValue(name, out Channel? existing)) {
                    c = existing;
                    if (prev == c)
                        return Tuple.Create(c, c);

                } else {
                    c = new(this, name, NextID++);
                }

                prev.Remove(session);

                if (session.PlayerInfo != null)
                    c.Add(session);

                DataInternalBlob move = new(Server.Data, new DataChannelMove {
                    Player = session.PlayerInfo
                });
                session.Con.Send(move);
                using (prev.Lock.R())
                    foreach (CelesteNetPlayerSession other in prev.Players)
                        other.Con.Send(move);

                BroadcastList();

                session.ResendPlayerStates();

                if (!Server.UserData.GetKey(session.UID).IsNullOrEmpty()) {
                    Server.UserData.Save(session.UID, new LastChannelUserInfo {
                        Name = name
                    });
                }

                return Tuple.Create(prev, c);
            }
        }

    }

    public clast Channel : IDisposable {
        public readonly Channels Ctx;
        public readonly string Name;
        public readonly uint ID;
        public readonly RWLock Lock = new();
        public readonly HashSet Players = new();

        public readonly bool IsPrivate;
        public readonly string PublicName;

        public Channel(Channels ctx, string name, uint id) {
            Ctx = ctx;
            Name = name;
            ID = id;

            if (IsPrivate = name.StartsWith(Channels.PrefixPrivate)) {
                PublicName = Channels.NamePrivate;

            } else {
                PublicName = name;
            }

            lock (Ctx.All) {
                Ctx.All.Add(this);
                Ctx.ByName[Name] = this;
                Ctx.ByID[ID] = this;
            }
        }

        public void RemoveStale() {
            using (Lock.W()) {
                List stale = new();
                foreach (CelesteNetPlayerSession session in Players)
                    if (session.PlayerInfo == null)
                        stale.Add(session);
                foreach (CelesteNetPlayerSession session in stale)
                    Remove(session);
            }
        }

        public void Add(CelesteNetPlayerSession session) {
            using (Lock.W())
                if (!Players.Add(session))
                    return;

            session.Channel = this;
            session.OnEnd += RemoveByDC;

            if (session.PlayerInfo == null)
                Remove(session);
        }

        public void Remove(CelesteNetPlayerSession session) {
            using (Lock.W())
                if (!Players.Remove(session))
                    return;

            // Hopefully nobody will get stuck in channel limbo...
            session.OnEnd -= RemoveByDC;

            if (ID == 0)
                return;

            lock (Ctx.All) {
                if (Players.Count > 0)
                    return;

                Ctx.All.Remove(this);
                Ctx.ByName.Remove(Name);
                Ctx.ByID.Remove(ID);
            }
        }

        private void RemoveByDC(CelesteNetPlayerSession session, DataPlayerInfo? lastInfo) {
            Remove(session);
            Ctx.BroadcastList();
        }

        public void Dispose() {
            Lock.Dispose();
        }
    }

    public clast LastChannelUserInfo {
        public string Name = "main";
    }
}