Scripting prototype?

This commit is contained in:
Sardelka
2022-06-04 18:09:42 +08:00
parent 1607b7025c
commit 9f2cf2eb03
14 changed files with 391 additions and 603 deletions

View File

@ -11,7 +11,7 @@ namespace RageCoop.Client.Scripting
{
internal class Engine : Core.Scripting.ScriptingEngine
{
public Engine() : base(typeof(ClientScript), Main.Logger) { }
public Engine() : base("RageCoop.Client.Scripting.ClientScript", Main.Logger) { }
}
}

View File

@ -12,9 +12,16 @@ namespace RageCoop.Core.Scripting
{
internal class ScriptingEngine
{
private Type BaseScriptType;
protected List<string> ToIgnore = new List<string>
{
"RageCoop.Client.dll",
"RageCoop.Core.dll",
"RageCoop.Server.dll",
"ScriptHookVDotNet3.dll"
};
private string BaseScriptType;
public Logging.Logger Logger { get; set; }
public ScriptingEngine(Type baseScriptType, Logging.Logger logger)
public ScriptingEngine(string baseScriptType, Logging.Logger logger)
{
BaseScriptType = baseScriptType;
Logger = logger;
@ -22,29 +29,29 @@ namespace RageCoop.Core.Scripting
/// <summary>
/// Loads scripts from the specified assembly file.
/// </summary>
/// <param name="filename">The path to the assembly file to load.</param>
/// <param name="path">The path to the assembly file to load.</param>
/// <returns><see langword="true" /> on success, <see langword="false" /> otherwise</returns>
private bool LoadScriptsFromAssembly(string filename)
protected bool LoadScriptsFromAssembly(string path)
{
if (!IsManagedAssembly(filename))
return false;
if (!IsManagedAssembly(path)) { return false; }
if (ToIgnore.Contains(Path.GetFileName(path))) { return false; }
Logger?.Debug($"Loading assembly {Path.GetFileName(filename)} ...");
Logger?.Debug($"Loading assembly {Path.GetFileName(path)} ...");
Assembly assembly;
try
{
assembly = Assembly.LoadFrom(filename);
assembly = Assembly.LoadFrom(path);
}
catch (Exception ex)
{
Logger?.Error( "Unable to load "+Path.GetFileName(filename));
Logger?.Error( "Unable to load "+Path.GetFileName(path));
Logger?.Error(ex);
return false;
}
return LoadScriptsFromAssembly(assembly, filename);
return LoadScriptsFromAssembly(assembly, path);
}
/// <summary>
/// Loads scripts from the specified assembly object.
@ -59,7 +66,7 @@ namespace RageCoop.Core.Scripting
try
{
// Find all script types in the assembly
foreach (var type in assembly.GetTypes().Where(x => IsSubclassOf(x,nameof(BaseScriptType))))
foreach (var type in assembly.GetTypes().Where(x => IsSubclassOf(x,BaseScriptType)))
{
ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
@ -90,7 +97,7 @@ namespace RageCoop.Core.Scripting
return false;
}
Logger?.Info($"Loaded {count.ToString()} script(s) in {Path.GetFileName(filename)}");
Logger?.Info($"Loaded {count} script(s) in {Path.GetFileName(filename)}");
return count != 0;
}
private bool IsManagedAssembly(string filename)

View File

@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RageCoop.Client", "Client\R
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Core", "Core\RageCoop.Core.csproj", "{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RageCoop.Resources.Base", "Resources\Base\RageCoop.Resources.Base.csproj", "{9DC11623-8A8B-4D17-B18B-91852922163D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Resources", "Resources", "{F49A2617-832B-44DA-9D42-29B5DD09A186}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -41,10 +45,21 @@ Global
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|Any CPU.Build.0 = Release|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|x64.ActiveCfg = Release|Any CPU
{CC2E8102-E568-4524-AA1F-F8E0F1CFE58A}.Release|x64.Build.0 = Release|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Debug|x64.ActiveCfg = Debug|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Debug|x64.Build.0 = Debug|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Release|Any CPU.Build.0 = Release|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Release|x64.ActiveCfg = Release|Any CPU
{9DC11623-8A8B-4D17-B18B-91852922163D}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9DC11623-8A8B-4D17-B18B-91852922163D} = {F49A2617-832B-44DA-9D42-29B5DD09A186}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6CC7EA75-E4FF-4534-8EB6-0AEECF2620B7}
EndGlobalSection

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Reference Include="RageCoop.Server">
<HintPath>..\..\Server\bin\Debug\net6.0\RageCoop.Server.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,17 @@
using RageCoop.Server.Scripting;
namespace RageCoop.Resources.Base
{
public class ServerBase :ServerScript
{
public ServerBase()
{
API.RegisterCommand("kick", (ctx) =>
{
if (ctx.Args.Length<1) { return; }
var reason = "eat poop";
if(ctx.Args.Length>=2) { reason=ctx.Args[1]; }
API.GetClientByUsername(ctx.Args[0]).Kick(reason);
});
}
}
}

View File

@ -9,21 +9,7 @@ namespace RageCoop.Server
public class Client
{
public long NetID = 0;
private float _currentLatency = 0f;
public NetConnection Connection { get; set; }
public float Latency
{
get => _currentLatency;
internal set
{
_currentLatency = value;
if ((value * 1000f) > Server.MainSettings.MaxLatency)
{
Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID)?.Disconnect($"Too high latency [{value * 1000f}/{(float)Server.MainSettings.MaxLatency}]");
}
}
}
internal NetConnection Connection { get; set; }
public PlayerData Player;
private readonly Dictionary<string, object> _customData = new();
private long _callbacksCount = 0;
@ -64,13 +50,13 @@ namespace RageCoop.Server
#endregion
#region FUNCTIONS
public void Kick(string reason)
public void Kick(string reason="You has been kicked!")
{
Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID)?.Disconnect(reason);
Connection?.Disconnect(reason);
}
public void Kick(string[] reason)
public void Kick(params string[] reasons)
{
Kick(string.Join(" ", reason));
Kick(string.Join(" ", reasons));
}
public void SendChatMessage(string message, string from = "Server")
@ -201,32 +187,6 @@ namespace RageCoop.Server
Server.MainNetServer.SendMessage(outgoingMessage, userConnection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default);
}
public void SendModPacket(string modName, byte customID, byte[] bytes)
{
try
{
NetConnection userConnection = Server.MainNetServer.Connections.Find(x => x.RemoteUniqueIdentifier == NetID);
if (userConnection == null)
{
return;
}
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
new Packets.Mod()
{
Name = modName,
CustomPacketID = customID,
Bytes = bytes
}.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, userConnection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Mod);
Server.MainNetServer.FlushSendQueue();
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
public void SendTriggerEvent(string eventName, params object[] args)
{
if (!FilesReceived)

View File

@ -105,7 +105,7 @@ namespace RageCoop.Server
{
lock (Server.Clients)
{
Client x = Util.GetClientByID(client.NetHandle);
Client x = Util.GetClientByNetID(client.NetHandle);
if (x != null)
{
x.FilesReceived = true;

192
Server/Scripting/API.cs Normal file
View File

@ -0,0 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Lidgren.Network;
using RageCoop.Core;
using System.Net;
namespace RageCoop.Server.Scripting
{
public static class API
{
public static class Events
{
#region DELEGATES
public delegate void EmptyEvent();
public delegate void PlayerConnect(int id,string name);
public delegate void PlayerDisconnect(int id, string name);
#endregion
public static event EmptyEvent OnStop;
public static event EventHandler<ChatEventArgs> OnChatMessage;
public static event EventHandler<HandshakeEventArgs> OnPlayerHandshake;
public static event PlayerConnect OnPlayerConnected;
public static event PlayerDisconnect OnPlayerDisconnected;
// public static event EventHandler OnPlayerUpdate;
#region INVOKE
internal static void InvokeOnStop() { OnStop?.Invoke(); }
internal static void InvokeOnChatMessage(Packets.ChatMessage p,NetConnection con)
{
OnChatMessage?.Invoke(null,new ChatEventArgs() {
Sender=Util.GetClientByNetID(con.RemoteUniqueIdentifier),
Message=p.Message
});
}
internal static void InvokePlayerConnected(Client client)
{ OnPlayerConnected?.Invoke(client.Player.PedID,client.Player.Username); }
internal static void InvokePlayerDisconnected(Client client)
{ OnPlayerDisconnected?.Invoke(client.Player.PedID, client.Player.Username); }
internal static void InvokePlayerHandshake(HandshakeEventArgs args)
{
OnPlayerHandshake?.Invoke(null, args);
}
#endregion
}
#region FUNCTIONS
/// <summary>
/// Send a native call (Function.Call) to all players.
/// Keys = int, float, bool, string and lvector3
/// </summary>
/// <param name="hash">The hash (Example: 0x25223CA6B4D20B7F = GET_CLOCK_HOURS)</param>
/// <param name="args">The arguments (Example: string = int, object = 5)</param>
public static void SendNativeCallToAll(GTA.Native.Hash hash, params object[] args)
{
try
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
if (args != null && args.Length == 0)
{
Program.Logger.Error($"[ServerScript->SendNativeCallToAll(ulong hash, params object[] args)]: args is not null!");
return;
}
Packets.NativeCall packet = new()
{
Hash = (ulong)hash,
Args = new List<object>(args) ?? new List<object>()
};
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, Server.MainNetServer.Connections, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Get a list of all Clients
/// </summary>
/// <returns>All clients as a dictionary indexed by NetID</returns>
public static Dictionary<long, Client> GetAllClients()
{
return Server.Clients;
}
/// <summary>
/// Get the client by its username
/// </summary>
/// <param name="username">The username to search for (non case-sensitive)</param>
/// <returns>The Client from this user or null</returns>
public static Client GetClientByUsername(string username)
{
return Server.Clients.Values.FirstOrDefault(x => x.Player.Username.ToLower() == username.ToLower());
}
/// <summary>
/// Send a chat message to all players
/// </summary>
/// <param name="message">The chat message</param>
/// <param name="username">The username which send this message (default = "Server")</param>
/// <param name="netHandleList">The list of connections (players) who received this chat message</param>
public static void SendChatMessageToAll(string message, string username = "Server", List<long> netHandleList = null)
{
try
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
List<NetConnection> connections = netHandleList == null
? Server.MainNetServer.Connections
: Server.MainNetServer.Connections.FindAll(c => netHandleList.Contains(c.RemoteUniqueIdentifier));
Server.SendChatMessage(username, message, connections);
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Send CleanUpWorld to all players to delete all objects created by the server
/// </summary>
public static void SendCleanUpWorldToAll(List<long> netHandleList = null)
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
List<NetConnection> connections = netHandleList == null
? Server.MainNetServer.Connections
: Server.MainNetServer.Connections.FindAll(c => netHandleList.Contains(c.RemoteUniqueIdentifier));
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
outgoingMessage.Write((byte)PacketTypes.CleanUpWorld);
Server.MainNetServer.SendMessage(outgoingMessage, connections, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default);
}
/// <summary>
/// Register a new command chat command (Example: "/test")
/// </summary>
/// <param name="name">The name of the command (Example: "test" for "/test")</param>
/// <param name="usage">How to use this message (argsLength required!)</param>
/// <param name="argsLength">The length of args (Example: "/message USERNAME MESSAGE" = 2) (usage required!)</param>
/// <param name="callback">Create a new function!</param>
public static void RegisterCommand(string name, string usage, short argsLength, Action<CommandContext> callback)
{
Server.RegisterCommand(name, usage, argsLength, callback);
}
/// <summary>
/// Register a new command chat command (Example: "/test")
/// </summary>
/// <param name="name">The name of the command (Example: "test" for "/test")</param>
/// <param name="callback">Create a new function!</param>
public static void RegisterCommand(string name, Action<CommandContext> callback)
{
Server.RegisterCommand(name, callback);
}
/// <summary>
/// Register a class of commands
/// </summary>
/// <typeparam name="T">The name of your class with functions</typeparam>
public static void RegisterCommands<T>()
{
Server.RegisterCommands<T>();
}
/// <summary>
/// Register a class of events
/// </summary>
/// <typeparam name="T">The name of your class with functions</typeparam>
public static void RegisterEvents<T>()
{
Server.RegisterEvents<T>();
}
#endregion
}
}

View File

@ -4,17 +4,30 @@ using System.Linq;
using System.Text;
using System.Threading.Tasks;
using RageCoop.Core.Scripting;
using System.IO;
namespace RageCoop.Server.Scripting
{
internal class Engine:ScriptingEngine
{
public Engine() : base(typeof(ServerScript), Program.Logger)
public Engine() : base("RageCoop.Server.Scripting.ServerScript", Program.Logger)
{
}
public static void Load()
public void LoadAll()
{
var path = Path.Combine("Resources", "Server");
Directory.CreateDirectory(path);
foreach (var resource in Directory.GetDirectories(path))
{
Logger.Info($"Loading resource: {Path.GetFileName(resource)}");
LoadResource(resource);
}
}
private void LoadResource(string path)
{
foreach(var assembly in Directory.GetFiles(path,"*.dll"))
{
LoadScriptsFromAssembly(assembly);
}
}
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net;
namespace RageCoop.Server.Scripting
{
public class ChatEventArgs : EventArgs
{
public Client Sender { get; set; }
public string Message { get; set; }
}
public class HandshakeEventArgs : EventArgs
{
public int ID { get; set; }
public string Username { get; set; }
public IPEndPoint EndPoint { get; set; }
public void Deny(string reason)
{
DenyReason=reason;
Cancel=true;
}
internal string DenyReason { get; set; }
internal bool Cancel { get; set; }=false;
}
}

View File

@ -8,401 +8,10 @@ using System.Threading.Tasks;
using RageCoop.Core;
using Lidgren.Network;
namespace RageCoop.Server
namespace RageCoop.Server.Scripting
{
public abstract class ServerScript
{
public API API { get; } = new();
}
public class Resource
{
public bool ReadyToStop = false;
private static Thread _mainThread;
private static Queue _actionQueue;
private static ServerScript _script;
public Resource(ServerScript script)
{
_actionQueue = Queue.Synchronized(new Queue());
_mainThread = new(ThreadLoop) { IsBackground = true };
_mainThread.Start();
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(() =>
{
_script = script;
_script.API.InvokeStart();
});
}
}
private void ThreadLoop()
{
while (!Program.ReadyToStop)
{
Queue localQueue;
lock (_actionQueue.SyncRoot)
{
localQueue = new(_actionQueue);
_actionQueue.Clear();
}
while (localQueue.Count > 0)
{
(localQueue.Dequeue() as Action)?.Invoke();
}
// 15 milliseconds to sleep to reduce CPU usage
Thread.Sleep(15);
}
_script.API.InvokeStop();
ReadyToStop = true;
}
public bool InvokeModPacketReceived(long from, long target, string modName, byte customID, byte[] bytes)
{
Task<bool> task = new(() => _script.API.InvokeModPacketReceived(from, target, modName, customID, bytes));
task.Start();
task.Wait(5000);
return task.Result;
}
public void InvokePlayerHandshake(Client client)
{
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(new Action(() => _script.API.InvokePlayerHandshake(client)));
}
}
public void InvokePlayerConnected(Client client)
{
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(new Action(() => _script.API.InvokePlayerConnected(client)));
}
}
public void InvokePlayerDisconnected(Client client)
{
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(new Action(() => _script.API.InvokePlayerDisconnected(client)));
}
}
public bool InvokeChatMessage(string username, string message)
{
Task<bool> task = new(() => _script.API.InvokeChatMessage(username, message));
task.Start();
task.Wait(5000);
return task.Result;
}
public void InvokePlayerUpdate(Client client)
{
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(new Action(() => _script.API.InvokePlayerUpdate(client)));
}
}
public void InvokeTick(long tick)
{
lock (_actionQueue.SyncRoot)
{
_actionQueue.Enqueue(new Action(() => _script.API.InvokeTick(tick)));
}
}
}
public class API
{
#region DELEGATES
public delegate void EmptyEvent();
public delegate void OnTickEvent(long tick);
public delegate void ChatEvent(string username, string message, CancelEventArgs cancel);
public delegate void PlayerEvent(Client client);
public delegate void ModEvent(long from, long target, string modName, byte customID, byte[] bytes, CancelEventArgs args);
#endregion
#region EVENTS
/// <summary>
/// Called every tick
/// </summary>
public event OnTickEvent OnTick;
/// <summary>
/// Called when the server has started
/// </summary>
public event EmptyEvent OnStart;
/// <summary>
/// Called when the server has stopped
/// </summary>
public event EmptyEvent OnStop;
/// <summary>
/// Called when the server receives a new chat message for players
/// </summary>
public event ChatEvent OnChatMessage;
/// <summary>
/// Called when the server receives a new incoming connection
/// </summary>
public event PlayerEvent OnPlayerHandshake;
/// <summary>
/// Called when a new player has successfully connected
/// </summary>
public event PlayerEvent OnPlayerConnected;
/// <summary>
/// Called when a new player has successfully disconnected
/// </summary>
public event PlayerEvent OnPlayerDisconnected;
/// <summary>
/// Called when a new player sends data like health
/// </summary>
public event PlayerEvent OnPlayerUpdate;
public event ModEvent OnModPacketReceived;
public void InvokeTick(long tick)
{
OnTick?.Invoke(tick);
}
public void InvokeStart()
{
OnStart?.Invoke();
}
public void InvokeStop()
{
OnStop?.Invoke();
}
public void InvokePlayerHandshake(Client client)
{
OnPlayerHandshake?.Invoke(client);
}
public void InvokePlayerConnected(Client client)
{
OnPlayerConnected?.Invoke(client);
}
public void InvokePlayerDisconnected(Client client)
{
OnPlayerDisconnected?.Invoke(client);
}
public void InvokePlayerUpdate(Client client)
{
OnPlayerUpdate?.Invoke(client);
}
public bool InvokeChatMessage(string username, string message)
{
CancelEventArgs args = new(false);
OnChatMessage?.Invoke(username, message, args);
return args.Cancel;
}
public bool InvokeModPacketReceived(long from, long target, string modName, byte customID, byte[] bytes)
{
CancelEventArgs args = new(false);
OnModPacketReceived?.Invoke(from, target, modName, customID, bytes, args);
return args.Cancel;
}
#endregion
#region FUNCTIONS
/// <summary>
/// Send a mod packet to all players
/// </summary>
/// <param name="modName">The name of the modification that will receive the data</param>
/// <param name="customID">The ID to check what this data is</param>
/// <param name="bytes">The serialized data</param>
/// <param name="playerList">The list of player ID (PedID) that will receive the data</param>
public static void SendModPacketToAll(string modName, byte customID, byte[] bytes, List<int> playerList = null)
{
try
{// A resource can be calling this function on disconnect of the last player in the server and we will
// get an empty connection list, make sure connections has at least one handle in it
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
new Packets.Mod()
{
Name = modName,
CustomPacketID = customID,
Bytes = bytes
}.Pack(outgoingMessage);
if (playerList==null)
{
Server.MainNetServer.SendMessage(outgoingMessage, Server.MainNetServer.Connections, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Mod);
}
else
{
foreach(var c in Server.Clients.Values)
{
if (playerList.Contains(c.Player.PedID))
{
Server.MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Mod);
}
}
}
Server.MainNetServer.FlushSendQueue();
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Send a native call (Function.Call) to all players.
/// Keys = int, float, bool, string and lvector3
/// </summary>
/// <param name="hash">The hash (Example: 0x25223CA6B4D20B7F = GET_CLOCK_HOURS)</param>
/// <param name="args">The arguments (Example: string = int, object = 5)</param>
public static void SendNativeCallToAll(ulong hash, params object[] args)
{
try
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
if (args != null && args.Length == 0)
{
Program.Logger.Error($"[ServerScript->SendNativeCallToAll(ulong hash, params object[] args)]: args is not null!");
return;
}
Packets.NativeCall packet = new()
{
Hash = hash,
Args = new List<object>(args) ?? new List<object>()
};
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, Server.MainNetServer.Connections, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Native);
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Get a list of all Clients
/// </summary>
/// <returns>All clients as a dictionary indexed by NetID</returns>
public static Dictionary<long,Client> GetAllClients()
{
return Server.Clients;
}
/// <summary>
/// Get the client by its username
/// </summary>
/// <param name="username">The username to search for</param>
/// <returns>The Client from this user or null</returns>
public static Client GetClientByUsername(string username)
{
return Server.Clients.Values.FirstOrDefault(x => x.Player.Username.ToLower() == username.ToLower());
}
/// <summary>
/// Send a chat message to all players
/// </summary>
/// <param name="message">The chat message</param>
/// <param name="username">The username which send this message (default = "Server")</param>
/// <param name="netHandleList">The list of connections (players) who received this chat message</param>
public static void SendChatMessageToAll(string message, string username = "Server", List<long> netHandleList = null)
{
try
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
List<NetConnection> connections = netHandleList == null
? Server.MainNetServer.Connections
: Server.MainNetServer.Connections.FindAll(c => netHandleList.Contains(c.RemoteUniqueIdentifier));
Server.SendChatMessage(username, message, connections);
}
catch (Exception e)
{
Program.Logger.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Send CleanUpWorld to all players to delete all objects created by the server
/// </summary>
public static void SendCleanUpWorldToAll(List<long> netHandleList = null)
{
if (Server.MainNetServer.ConnectionsCount == 0)
{
return;
}
List<NetConnection> connections = netHandleList == null
? Server.MainNetServer.Connections
: Server.MainNetServer.Connections.FindAll(c => netHandleList.Contains(c.RemoteUniqueIdentifier));
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
outgoingMessage.Write((byte)PacketTypes.CleanUpWorld);
Server.MainNetServer.SendMessage(outgoingMessage, connections, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Default);
}
/// <summary>
/// Register a new command chat command (Example: "/test")
/// </summary>
/// <param name="name">The name of the command (Example: "test" for "/test")</param>
/// <param name="usage">How to use this message (argsLength required!)</param>
/// <param name="argsLength">The length of args (Example: "/message USERNAME MESSAGE" = 2) (usage required!)</param>
/// <param name="callback">Create a new function!</param>
public static void RegisterCommand(string name, string usage, short argsLength, Action<CommandContext> callback)
{
Server.RegisterCommand(name, usage, argsLength, callback);
}
/// <summary>
/// Register a new command chat command (Example: "/test")
/// </summary>
/// <param name="name">The name of the command (Example: "test" for "/test")</param>
/// <param name="callback">Create a new function!</param>
public static void RegisterCommand(string name, Action<CommandContext> callback)
{
Server.RegisterCommand(name, callback);
}
/// <summary>
/// Register a class of commands
/// </summary>
/// <typeparam name="T">The name of your class with functions</typeparam>
public static void RegisterCommands<T>()
{
Server.RegisterCommands<T>();
}
/// <summary>
/// Register a class of events
/// </summary>
/// <typeparam name="T">The name of your class with functions</typeparam>
public static void RegisterEvents<T>()
{
Server.RegisterEvents<T>();
}
#endregion
}
[AttributeUsage(AttributeTargets.Method, Inherited = false)]

View File

@ -12,6 +12,7 @@ using Newtonsoft.Json;
using System.Threading.Tasks;
using Lidgren.Network;
using System.Timers;
using RageCoop.Server.Scripting;
namespace RageCoop.Server
{
@ -21,21 +22,20 @@ namespace RageCoop.Server
public string Address { get; set; }
}
public class Server
internal class Server
{
private static readonly string _compatibleVersion = "V0_4";
private static long _currentTick = 0;
public static readonly Settings MainSettings = Util.Read<Settings>("Settings.xml");
private readonly Blocklist _mainBlocklist = Util.Read<Blocklist>("Blocklist.xml");
public static NetServer MainNetServer;
public static Resource RunningResource = null;
public static readonly Dictionary<Command, Action<CommandContext>> Commands = new();
public static readonly Dictionary<TriggerEvent, Action<EventContext>> TriggerEvents = new();
public static readonly Dictionary<long,Client> Clients = new();
private static System.Timers.Timer SendLatencyTimer = new System.Timers.Timer(5000);
public static readonly Engine ScriptingEngine = new();
public Server()
{
Program.Logger.Info("================");
@ -169,40 +169,7 @@ namespace RageCoop.Server
}).Start();
#endregion
}
if (!string.IsNullOrEmpty(MainSettings.Resource))
{
try
{
string resourcepath = AppDomain.CurrentDomain.BaseDirectory + "resources" + Path.DirectorySeparatorChar + MainSettings.Resource + ".dll";
Program.Logger.Info($"Loading resource \"{MainSettings.Resource}.dll\"...");
Assembly asm = Assembly.LoadFrom(resourcepath);
Type[] types = asm.GetExportedTypes();
IEnumerable<Type> validTypes = types.Where(t => !t.IsInterface && !t.IsAbstract).Where(t => typeof(ServerScript).IsAssignableFrom(t));
Type[] enumerable = validTypes as Type[] ?? validTypes.ToArray();
if (!enumerable.Any())
{
Program.Logger.Error("ERROR: No classes that inherit from ServerScript have been found in the assembly. Starting freeroam.");
}
else
{
if (Activator.CreateInstance(enumerable.ToArray()[0]) is ServerScript script)
{
RunningResource = new(script);
}
else
{
Program.Logger.Warning("Could not create resource: it is null.");
}
}
}
catch (Exception e)
{
Program.Logger.Error(e.InnerException.Message);
}
}
ScriptingEngine.LoadAll();
Program.Logger.Info("Searching for client-side files...");
DownloadManager.CheckForDirectoryAndFiles();
@ -224,10 +191,6 @@ namespace RageCoop.Server
}), new SynchronizationContext());
while (!Program.ReadyToStop)
{
if (RunningResource != null)
{
RunningResource.InvokeTick(++_currentTick);
}
// Only new clients that did not receive files on connection will receive the current files in "clientside"
if (DownloadManager.AnyFileExists)
@ -249,23 +212,15 @@ namespace RageCoop.Server
// 3 milliseconds to sleep to reduce CPU usage
Thread.Sleep(3);
// 15 milliseconds to sleep to reduce CPU usage
Thread.Sleep(15);
}
Program.Logger.Warning("Server is shutting down!");
if (RunningResource != null)
{
// Waiting for resource...
while (!RunningResource.ReadyToStop)
{
// 16 milliseconds to sleep to reduce CPU usage
Thread.Sleep(1000 / 60);
}
}
if (MainNetServer.Connections.Count > 0)
{
API.Events.InvokeOnStop();
MainNetServer.Connections.ForEach(x => x.Disconnect("Server is shutting down!"));
// We have to wait some time for all Disconnect() messages to be sent successfully
// Sleep for 1 second
@ -437,6 +392,7 @@ namespace RageCoop.Server
Packets.ChatMessage packet = new();
packet.Unpack(data);
API.Events.InvokeOnChatMessage(packet,message.SenderConnection);
SendChatMessage(packet);
}
catch (Exception e)
@ -456,7 +412,7 @@ namespace RageCoop.Server
Packets.NativeResponse packet = new();
packet.Unpack(data);
Client client = Util.GetClientByID(message.SenderConnection.RemoteUniqueIdentifier);
Client client = Util.GetClientByNetID(message.SenderConnection.RemoteUniqueIdentifier);
if (client != null)
{
if (client.Callbacks.ContainsKey(packet.ID))
@ -484,7 +440,7 @@ namespace RageCoop.Server
Packets.FileTransferComplete packet = new();
packet.Unpack(data);
Client client = Util.GetClientByID(message.SenderConnection.RemoteUniqueIdentifier);
Client client = Util.GetClientByNetID(message.SenderConnection.RemoteUniqueIdentifier);
if (client != null && !client.FilesReceived)
{
DownloadManager.TryToRemoveClient(client.NetID, packet.ID);
@ -511,7 +467,7 @@ namespace RageCoop.Server
Client client = null;
lock (Clients)
{
client = Util.GetClientByID(senderNetHandle);
client = Util.GetClientByNetID(senderNetHandle);
}
if (client != null)
@ -571,10 +527,10 @@ namespace RageCoop.Server
break;
case NetIncomingMessageType.ConnectionLatencyUpdated:
{
Client client = Util.GetClientByID(message.SenderConnection.RemoteUniqueIdentifier);
Client client = Util.GetClientByNetID(message.SenderConnection.RemoteUniqueIdentifier);
if (client != null)
{
client.Latency = message.ReadFloat();
client.Player.Latency = message.ReadFloat();
}
}
break;
@ -607,7 +563,7 @@ namespace RageCoop.Server
{
PedID=c.Player.PedID,
Username=c.Player.Username,
Latency=c.Player.Latency=c.Latency,
Latency=c.Player.Latency,
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, x, NetDeliveryMethod.ReliableSequenced, (byte)ConnectionChannel.Default);
});
@ -625,38 +581,38 @@ namespace RageCoop.Server
#region -- PLAYER --
// Before we approve the connection, we must shake hands
private void GetHandshake(NetConnection local, Packets.Handshake packet)
private void GetHandshake(NetConnection connection, Packets.Handshake packet)
{
Program.Logger.Debug("New handshake from: [Name: " + packet.Username + " | Address: " + local.RemoteEndPoint.Address.ToString() + "]");
Program.Logger.Debug("New handshake from: [Name: " + packet.Username + " | Address: " + connection.RemoteEndPoint.Address.ToString() + "]");
if (!packet.ModVersion.StartsWith(_compatibleVersion))
{
local.Deny($"RAGECOOP version {_compatibleVersion.Replace('_', '.')}.x required!");
connection.Deny($"RAGECOOP version {_compatibleVersion.Replace('_', '.')}.x required!");
return;
}
if (string.IsNullOrWhiteSpace(packet.Username))
{
local.Deny("Username is empty or contains spaces!");
connection.Deny("Username is empty or contains spaces!");
return;
}
if (packet.Username.Any(p => !char.IsLetterOrDigit(p) && !(p == '_') && !(p=='-')))
{
local.Deny("Username contains special chars!");
connection.Deny("Username contains special chars!");
return;
}
if (_mainBlocklist.Username.Contains(packet.Username.ToLower()))
{
local.Deny("This Username has been blocked by this server!");
connection.Deny("This Username has been blocked by this server!");
return;
}
if (_mainBlocklist.IP.Contains(local.RemoteEndPoint.ToString().Split(":")[0]))
if (_mainBlocklist.IP.Contains(connection.RemoteEndPoint.ToString().Split(":")[0]))
{
local.Deny("This IP was blocked by this server!");
connection.Deny("This IP was blocked by this server!");
return;
}
if (Clients.Values.Any(x => x.Player.Username.ToLower() == packet.Username.ToLower()))
{
local.Deny("Username is already taken!");
connection.Deny("Username is already taken!");
return;
}
@ -667,11 +623,11 @@ namespace RageCoop.Server
// Add the player to Players
lock (Clients)
{
Clients.Add(local.RemoteUniqueIdentifier,
Clients.Add(connection.RemoteUniqueIdentifier,
tmpClient = new Client()
{
NetID = local.RemoteUniqueIdentifier,
Connection=local,
NetID = connection.RemoteUniqueIdentifier,
Connection=connection,
Player = new()
{
Username = packet.Username,
@ -680,6 +636,19 @@ namespace RageCoop.Server
}
);;
}
var args = new HandshakeEventArgs()
{
EndPoint=connection.RemoteEndPoint,
ID=packet.PedID,
Username=packet.Username
};
API.Events.InvokePlayerHandshake(args);
if (args.Cancel)
{
connection.Deny(args.DenyReason);
return;
}
Program.Logger.Info($"Handshake sucess, Player:{packet.Username} PedID:{packet.PedID}");
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
@ -690,20 +659,14 @@ namespace RageCoop.Server
Username = string.Empty,
ModVersion = string.Empty,
}.Pack(outgoingMessage);
// Accept the connection and send back a new handshake packet with the connection ID
local.Approve(outgoingMessage);
if (RunningResource != null)
{
RunningResource.InvokePlayerHandshake(tmpClient);
}
connection.Approve(outgoingMessage);
}
// The connection has been approved, now we need to send all other players to the new player and the new player to all players
private static void SendPlayerConnectPacket(NetConnection local)
{
Client localClient = Util.GetClientByID(local.RemoteUniqueIdentifier);
Client localClient = Util.GetClientByNetID(local.RemoteUniqueIdentifier);
if (localClient == null)
{
local.Disconnect("No data found!");
@ -718,7 +681,7 @@ namespace RageCoop.Server
{
long targetNetHandle = targetPlayer.RemoteUniqueIdentifier;
Client targetClient = Util.GetClientByID(targetNetHandle);
Client targetClient = Util.GetClientByNetID(targetNetHandle);
if (targetClient != null)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
@ -743,15 +706,9 @@ namespace RageCoop.Server
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, clients, NetDeliveryMethod.ReliableOrdered, 0);
}
if (RunningResource != null)
{
RunningResource.InvokePlayerConnected(localClient);
}
else
{
Program.Logger.Info($"Player {localClient.Player.Username} connected!");
}
API.Events.InvokePlayerConnected(localClient);
Program.Logger.Info($"Player {localClient.Player.Username} connected!");
if (!string.IsNullOrEmpty(MainSettings.WelcomeMessage))
{
@ -775,7 +732,7 @@ namespace RageCoop.Server
MainNetServer.SendMessage(outgoingMessage, clients, NetDeliveryMethod.ReliableOrdered, 0);
}
Client localClient = Util.GetClientByID( nethandle);
Client localClient = Util.GetClientByNetID( nethandle);
if (localClient == null)
{
return;
@ -783,14 +740,9 @@ namespace RageCoop.Server
Clients.Remove(localClient.NetID);
if (RunningResource != null)
{
RunningResource.InvokePlayerDisconnected(localClient);
}
else
{
Program.Logger.Info($"Player {localClient.Player.Username} disconnected! ID:{playerPedID}");
}
API.Events.InvokePlayerDisconnected(localClient);
Program.Logger.Info($"Player {localClient.Player.Username} disconnected! ID:{playerPedID}");
}
#region SyncEntities
@ -798,7 +750,7 @@ namespace RageCoop.Server
{
Client client = Util.GetClientByID(ClientID);
Client client = Util.GetClientByNetID(ClientID);
if (client == null)
{
return;
@ -812,14 +764,10 @@ namespace RageCoop.Server
if (c.NetID==client.NetID) { continue; }
MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.PedSync);
}
if (RunningResource != null && packet.ID==client.Player.PedID)
{
RunningResource.InvokePlayerUpdate(client);
}
}
private static void VehicleStateSync(Packets.VehicleStateSync packet, long ClientID)
{
Client client = Util.GetClientByID(ClientID);
Client client = Util.GetClientByNetID(ClientID);
if (client == null)
{
return;
@ -841,7 +789,7 @@ namespace RageCoop.Server
}
private static void PedSync(Packets.PedSync packet, long ClientID)
{
Client client = Util.GetClientByID(ClientID);
Client client = Util.GetClientByNetID(ClientID);
if (client == null)
{
return;
@ -870,15 +818,10 @@ namespace RageCoop.Server
}
MainNetServer.SendMessage(outgoingMessage,c.Connection, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.PedSync);
}
if (RunningResource != null && isPlayer)
{
RunningResource.InvokePlayerUpdate(client);
}
}
private static void VehicleSync(Packets.VehicleSync packet, long ClientID)
{
Client client = Util.GetClientByID(ClientID);
Client client = Util.GetClientByNetID(ClientID);
if (client == null)
{
return;
@ -907,7 +850,7 @@ namespace RageCoop.Server
}
private static void ProjectileSync(Packets.ProjectileSync packet, long ClientID)
{
Client client = Util.GetClientByID(ClientID);
Client client = Util.GetClientByNetID(ClientID);
if (client == null)
{
return;
@ -926,39 +869,23 @@ namespace RageCoop.Server
// Send a message to targets or all players
private static void SendChatMessage(Packets.ChatMessage packet, List<NetConnection> targets = null)
{
if (RunningResource != null)
if (packet.Message.StartsWith('/'))
{
if (packet.Message.StartsWith('/'))
string[] cmdArgs = packet.Message.Split(" ");
string cmdName = cmdArgs[0].Remove(0, 1);
if (Commands.Any(x => x.Key.Name == cmdName))
{
string[] cmdArgs = packet.Message.Split(" ");
string cmdName = cmdArgs[0].Remove(0, 1);
if (Commands.Any(x => x.Key.Name == cmdName))
string[] argsWithoutCmd = cmdArgs.Skip(1).ToArray();
CommandContext ctx = new()
{
string[] argsWithoutCmd = cmdArgs.Skip(1).ToArray();
Client = Clients.Values.Where(x => x.Player.Username == packet.Username).FirstOrDefault(),
Args = argsWithoutCmd
};
CommandContext ctx = new()
{
Client = Clients.Values.Where(x => x.Player.Username == packet.Username).FirstOrDefault(),
Args = argsWithoutCmd
};
KeyValuePair<Command, Action<CommandContext>> command = Commands.First(x => x.Key.Name == cmdName);
KeyValuePair<Command, Action<CommandContext>> command = Commands.First(x => x.Key.Name == cmdName);
if (command.Key.Usage != null && command.Key.ArgsLength != argsWithoutCmd.Length)
{
NetConnection userConnection = Util.GetConnectionByUsername(packet.Username);
if (userConnection == default)
{
return;
}
SendChatMessage("Server", command.Key.Usage, userConnection);
return;
}
command.Value.Invoke(ctx);
}
else
if (command.Key.Usage != null && command.Key.ArgsLength != argsWithoutCmd.Length)
{
NetConnection userConnection = Util.GetConnectionByUsername(packet.Username);
if (userConnection == default)
@ -966,20 +893,26 @@ namespace RageCoop.Server
return;
}
SendChatMessage("Server", "Command not found!", userConnection);
SendChatMessage("Server", command.Key.Usage, userConnection);
return;
}
return;
command.Value.Invoke(ctx);
}
if (RunningResource.InvokeChatMessage(packet.Username, packet.Message))
else
{
return;
NetConnection userConnection = Util.GetConnectionByUsername(packet.Username);
if (userConnection == default)
{
return;
}
SendChatMessage("Server", "Command not found!", userConnection);
}
return;
}
packet.Message = packet.Message.Replace("~", "");
SendChatMessage(packet.Username, packet.Message, targets);
Program.Logger.Info(packet.Username + ": " + packet.Message);

View File

@ -7,7 +7,6 @@
public int MaxLatency { get; set; } = 500;
public string Name { get; set; } = "RAGECOOP server";
public string WelcomeMessage { get; set; } = "Welcome on this server :)";
public string Resource { get; set; } = "";
public bool UPnP { get; set; } = true;
public bool AnnounceSelf { get; set; } = false;
public string MasterServer { get; set; } = "[AUTO]";

View File

@ -47,7 +47,7 @@ namespace RageCoop.Server
};
}
public static Client GetClientByID(long id)
public static Client GetClientByNetID(long id)
{
Client result = null;
Server.Clients.TryGetValue(id,out result);