using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using GTA.Native; using Lidgren.Network; using RageCoop.Core; using RageCoop.Core.Scripting; namespace RageCoop.Server.Scripting; /// /// public class ServerEvents { private readonly Server Server; #region INTERNAL internal Dictionary>> CustomEventHandlers = new(); #endregion internal ServerEvents(Server server) { Server = server; } /// /// Invoked when a chat message is received. /// public event EventHandler OnChatMessage; /// /// Will be invoked from main thread before registered handlers /// public event EventHandler OnCommandReceived; /// /// Will be invoked from main thread when a client is attempting to connect, use /// to deny the connection request. /// public event EventHandler OnPlayerHandshake; /// /// Will be invoked when a player is connected, but this player might not be ready yet(client resources not loaded), /// using is recommended. /// public event EventHandler OnPlayerConnected; /// /// Will be invoked after the client connected and all resources(if any) have been loaded. /// public event EventHandler OnPlayerReady; /// /// Invoked when a player disconnected, all method won't be effective in this scope. /// public event EventHandler OnPlayerDisconnected; /// /// Invoked everytime a player's main ped has been updated /// public event EventHandler OnPlayerUpdate; internal void ClearHandlers() { OnChatMessage = null; OnPlayerHandshake = null; OnPlayerConnected = null; OnPlayerReady = null; OnPlayerDisconnected = null; // OnCustomEventReceived=null; OnCommandReceived = null; OnPlayerUpdate = null; } #region INVOKE internal void InvokePlayerHandshake(HandshakeEventArgs args) { OnPlayerHandshake?.Invoke(this, args); } internal void InvokeOnCommandReceived(string cmdName, string[] cmdArgs, Client sender) { var args = new OnCommandEventArgs { Name = cmdName, Args = cmdArgs, Client = sender }; OnCommandReceived?.Invoke(this, args); if (args.Cancel) return; if (Server.Commands.Any(x => x.Key.Name == cmdName)) { var argsWithoutCmd = cmdArgs.Skip(1).ToArray(); CommandContext ctx = new() { Client = sender, Args = argsWithoutCmd }; var command = Server.Commands.First(x => x.Key.Name == cmdName); command.Value.Invoke(ctx); } else { Server.SendChatMessage("Server", "Command not found!", sender); } } internal void InvokeOnChatMessage(string msg, Client sender, string clamiedSender = null) { OnChatMessage?.Invoke(this, new ChatEventArgs { Client = sender, Message = msg, ClaimedSender = clamiedSender }); } internal void InvokePlayerConnected(Client client) { OnPlayerConnected?.Invoke(this, client); } internal void InvokePlayerReady(Client client) { OnPlayerReady?.Invoke(this, client); } internal void InvokePlayerDisconnected(Client client) { OnPlayerDisconnected?.Invoke(this, client); } internal void InvokeCustomEventReceived(Packets.CustomEvent p, Client sender) { var args = new CustomEventReceivedArgs { Hash = p.Hash, Args = p.Args, Client = sender }; if (CustomEventHandlers.TryGetValue(p.Hash, out var handlers)) handlers.ForEach(x => { x.Invoke(args); }); } internal void InvokePlayerUpdate(Client client) { OnPlayerUpdate?.Invoke(this, client); } #endregion } /// /// An class that can be used to interact with RageCoop server. /// public class API { /// /// Server side events /// public readonly ServerEvents Events; internal readonly Dictionary> RegisteredFiles = new(); internal readonly Server Server; internal API(Server server) { Server = server; Events = new ServerEvents(server); Server.RequestHandlers.Add(PacketType.FileTransferRequest, (data, client) => { var p = new Packets.FileTransferRequest(); p.Deserialize(data); var id = Server.NewFileID(); if (RegisteredFiles.TryGetValue(p.Name, out var s)) { Task.Run(() => { Server.SendFile(s(), p.Name, client, id); }); return new Packets.FileTransferResponse { ID = id, Response = FileResponse.Loaded }; } return new Packets.FileTransferResponse { ID = id, Response = FileResponse.LoadFailed }; }); } /// /// All synchronized entities on this server. /// public ServerEntities Entities => Server.Entities; #region FUNCTIONS /// /// Get a list of all Clients /// /// All clients as a dictionary indexed by their main character's id public Dictionary GetAllClients() { return new Dictionary(Server.ClientsByID); } /// /// Get the client by its username /// /// The username to search for (non case-sensitive) /// The Client from this user or null public Client GetClientByUsername(string username) { Server.ClientsByName.TryGetValue(username, out var c); return c; } /// /// Send a chat message to all players, use to send to an /// individual client. /// /// The clients to send message, leave it null to send to all clients /// The chat message /// The username which send this message (default = "Server") /// /// Weather to raise the event defined in /// /// /// /// When is unspecified and is null or unspecified, /// will be set to true /// public void SendChatMessage(string message, List targets = null, string username = "Server", bool? raiseEvent = null) { raiseEvent ??= targets == null; try { if (Server.MainNetServer.ConnectionsCount != 0) { targets ??= new List(Server.ClientsByNetHandle.Values); foreach (var client in targets) Server.SendChatMessage(username, message, client); } } catch (Exception e) { Server.Logger?.Error( $">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<"); } if (raiseEvent.Value) Events.InvokeOnChatMessage(message, null, username); } /// /// Register a file to be shared with clients /// /// name of this file /// path to this file public void RegisterSharedFile(string name, string path) { RegisteredFiles.Add(name, () => { return File.OpenRead(path); }); } /// /// Register a file to be shared with clients /// /// name of this file /// public void RegisterSharedFile(string name, ResourceFile file) { RegisteredFiles.Add(name, file.GetStream); } /// /// Register a new command chat command (Example: "/test") /// /// The name of the command (Example: "test" for "/test") /// How to use this message (argsLength required!) /// The length of args (Example: "/message USERNAME MESSAGE" = 2) (usage required!) /// A callback to invoke when the command received. public void RegisterCommand(string name, string usage, short argsLength, Action callback) { Server.RegisterCommand(name, usage, argsLength, callback); } /// /// Register a new command chat command (Example: "/test") /// /// The name of the command (Example: "test" for "/test") /// A callback to invoke when the command received. public void RegisterCommand(string name, Action callback) { Server.RegisterCommand(name, callback); } /// /// Register all commands in a static class /// /// Your static class with commands public void RegisterCommands() { Server.RegisterCommands(); } /// /// Register all commands inside an class instance /// /// The instance of type containing the commands public void RegisterCommands(object obj) { var commands = obj.GetType().GetMethods() .Where(method => method.GetCustomAttributes(typeof(Command), false).Any()); foreach (var method in commands) { var attribute = method.GetCustomAttribute(true); RegisterCommand(attribute.Name, attribute.Usage, attribute.ArgsLength, ctx => { method.Invoke(obj, new object[] { ctx }); }); } } /// /// Send native call specified clients. /// /// /// /// /// /// Clients to send, null for all clients public void SendNativeCall(List clients, Hash hash, params object[] args) { var argsList = new List(args); argsList.InsertRange(0, new object[] { (byte)TypeCode.Empty, (ulong)hash }); SendCustomEvent(CustomEventFlags.Queued, clients, CustomEvents.NativeCall, argsList.ToArray()); } /// /// Send an event and data to the specified clients. /// /// /// An unique identifier of the event/> to get it from a string /// /// The objects conataing your data, see for /// supported types. /// /// The target clients to send. Leave it null to send to all clients public void SendCustomEvent(CustomEventFlags flags, List targets, CustomEventHash eventHash, params object[] args) { var p = new Packets.CustomEvent(flags) { Args = args, Hash = eventHash }; if (targets == null) Server.SendToAll(p, ConnectionChannel.Event, NetDeliveryMethod.ReliableOrdered); else foreach (var c in targets) Server.Send(p, c, ConnectionChannel.Event, NetDeliveryMethod.ReliableOrdered); } public void SendCustomEvent(List targets, CustomEventHash eventHash, params object[] args) { SendCustomEvent(CustomEventFlags.None, targets, eventHash, args); } public void SendCustomEventQueued(List targets, CustomEventHash eventHash, params object[] args) { SendCustomEvent(CustomEventFlags.Queued, targets, eventHash, args); } /// /// Register an handler to the specifed event hash, one event can have multiple handlers. /// /// An unique identifier of the event> /// An handler to be invoked when the event is received from the server. public void RegisterCustomEventHandler(CustomEventHash hash, Action handler) { lock (Events.CustomEventHandlers) { if (!Events.CustomEventHandlers.TryGetValue(hash, out var handlers)) Events.CustomEventHandlers.Add(hash, handlers = new List>()); handlers.Add(handler); } } /// /// Find a script matching the specified type /// /// The full name of the script's type, e.g. RageCoop.Resources.Discord.Main /// Which resource to search for this script. Will search in all loaded resources if unspecified /// A object reprensenting the script, or if not found. /// /// Explicitly casting the return value to orginal type will case a exception to be thrown due to the dependency /// isolation mechanism in resource system. /// You shouldn't reference the target resource assemblies either, since it causes the referenced assembly to be loaded /// and started in your resource. /// public dynamic FindScript(string scriptFullName, string resourceName = null) { if (resourceName == null) { foreach (var res in LoadedResources.Values) if (res.Scripts.TryGetValue(scriptFullName, out var script)) return script; } else if (LoadedResources.TryGetValue(resourceName, out var res)) { if (res.Scripts.TryGetValue(scriptFullName, out var script)) return script; } return null; } #endregion #region PROPERTIES /// /// Get a that the server is currently using, you should use /// to display resource-specific information. /// public Logger Logger => Server.Logger; /// /// Gets or sets the client that is resposible for synchronizing time and weather /// public Client Host { get => Server._hostClient; set { if (Server._hostClient != value) { Server._hostClient?.SendCustomEvent(CustomEvents.IsHost, false); value.SendCustomEvent(CustomEvents.IsHost, true); Server._hostClient = value; } } } /// /// Get all currently loaded as a dictionary indexed by their names /// /// /// Accessing this property from script constructor is stronly discouraged since other scripts and resources might have /// yet been loaded. /// Accessing from is not recommended either. Although all script assemblies will /// have been loaded to memory and instantiated, invocation of other scripts are /// not guaranteed. /// public Dictionary LoadedResources { get { if (!Server.Resources.IsLoaded) { Logger?.Warning("Attempting to get resources before all scripts are loaded"); Logger.Trace(new StackTrace().ToString()); } return Server.Resources.LoadedResources; } } #endregion }