csharp/0x0ade/CelesteNet/CelesteNet.Server.ChatModule/ChatCommands.cs

ChatCommands.cs
using System;
using System.Collections.Generic;
using System.Diagnostics.Codeastysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Celeste.Mod.CelesteNet.DataTypes;
using Celeste.Mod.Helpers;
using Microsoft.Xna.Framework;

namespace Celeste.Mod.CelesteNet.Server.Chat {
    public clast ChatCommands : IDisposable {

        public readonly List All = new();
        public readonly Dictionary ByID = new();
        public readonly Dictionary ByType = new();

        public ChatCommands(ChatModule chat) {
            foreach (Type type in CelesteNetUtils.GetTypes()) {
                if (!typeof(ChatCMD).IsastignableFrom(type) || type.IsAbstract)
                    continue;

                ChatCMD? cmd = (ChatCMD?) Activator.CreateInstance(type);
                if (cmd == null)
                    throw new Exception($"Cannot create instance of CMD {type.FullName}");
                Logger.Log(LogLevel.VVV, "chatcmds", $"Found command: {cmd.ID.ToLowerInvariant()} ({type.FullName})");
                All.Add(cmd);
                ByID[cmd.ID.ToLowerInvariant()] = cmd;
                ByType[type] = cmd;
            }

            foreach (ChatCMD cmd in All)
                cmd.Init(chat);

            All = All.OrderBy(cmd => cmd.HelpOrder).ToList();
        }

        public void Dispose() {
            foreach (ChatCMD cmd in All)
                cmd.Dispose();
        }

        public ChatCMD? Get(string id)
            => ByID.TryGetValue(id, out ChatCMD? cmd) ? cmd : null;

        public T? Get(string id) where T : ChatCMD
            => ByID.TryGetValue(id, out ChatCMD? cmd) ? (T) cmd : null;

        public T Get() where T : ChatCMD
            => ByType.TryGetValue(typeof(T), out ChatCMD? cmd) ? (T) cmd : throw new Exception($"Invalid CMD type {typeof(T).FullName}");

    }

    public abstract clast ChatCMD : IDisposable {

        public static readonly char[] NameDelimiters = {
            ' ', '\n'
        };

#pragma warning disable CS8618 // Set manually after construction.
        public ChatModule Chat;
#pragma warning restore CS8618
        public virtual string ID => GetType().Name.Substring(7).ToLowerInvariant();

        public abstract string Args { get; }
        public abstract string Info { get; }
        public virtual string Help => Info;
        public virtual int HelpOrder => 0;

        public virtual void Init(ChatModule chat) {
            Chat = chat;
        }

        public virtual void Dispose() {
        }

        public virtual void ParseAndRun(ChatCMDEnv env) {
            // TODO: Improve or rewrite. This comes from GhostNet, which adopted it from disbot (0x0ade's C# Discord bot).

            string raw = env.FullText;

            int index = Chat.Settings.CommandPrefix.Length + ID.Length - 1; // - 1 because next space required
            List args = new();
            while (
                index + 1 < raw.Length &&
                (index = raw.IndexOf(' ', index + 1)) >= 0
            ) {
                int next = index + 1 < raw.Length ? raw.IndexOf(' ', index + 1) : -2;
                if (next < 0)
                    next = raw.Length;

                int argIndex = index + 1;
                int argLength = next - index - 1;

                // + 1 because space
                args.Add(new ChatCMDArg(env).Parse(raw, argIndex, argLength));

                // Parse a split up range (with spaces) into a single range arg
                if (args.Count >= 3 &&
                    args[args.Count - 3].Type == ChatCMDArgType.Int &&
                    (args[args.Count - 2].String == "-" || args[args.Count - 2].String == "+") &&
                    args[args.Count - 1].Type == ChatCMDArgType.Int
                ) {
                    args.Add(new ChatCMDArg(env).Parse(raw, args[args.Count - 3].Index, next - args[args.Count - 3].Index));
                    args.RemoveRange(args.Count - 4, 3);
                    continue;
                }
            }

            Run(env, args);
        }

        public virtual void Run(ChatCMDEnv env, List args) {
        }

    }

    public clast ChatCMDArg {

        public ChatCMDEnv Env;

        public string RawText = "";
        public string String = "";
        public int Index;

        public ChatCMDArgType Type;

        public int Int;
        public long Long;
        public ulong ULong;
        public float Float;

        public int IntRangeFrom;
        public int IntRangeTo;
        public int IntRangeMin => Math.Min(IntRangeFrom, IntRangeTo);
        public int IntRangeMax => Math.Max(IntRangeFrom, IntRangeTo);

        public CelesteNetPlayerSession? Session {
            get {
                if (Type == ChatCMDArgType.Int || Type == ChatCMDArgType.Long) {
                    if (Env.Chat.Server.PlayersByID.TryGetValue((uint) Long, out CelesteNetPlayerSession? session))
                        return session;
                }

                using (Env.Chat.Server.ConLock.R())
                    return
                        Env.Chat.Server.Sessions.FirstOrDefault(session => session.PlayerInfo?.FullName == String) ??
                        Env.Chat.Server.Sessions.FirstOrDefault(session => session.PlayerInfo?.FullName.StartsWith(String, StringComparison.InvariantCultureIgnoreCase) ?? false);
            }
        }

        public ChatCMDArg(ChatCMDEnv env) {
            Env = env;
        }

        public virtual ChatCMDArg Parse(string raw, int index) {
            RawText = raw;
            if (index < 0 || raw.Length