This commit is contained in:
sardelka9515
2022-10-23 19:02:39 +08:00
parent 6b34ab6e36
commit 2828b9b74f
114 changed files with 7374 additions and 7205 deletions

View File

@ -1,209 +1,226 @@
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using RageCoop.Server.Scripting;
using System;
using System;
using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using GTA.Native;
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using RageCoop.Server.Scripting;
namespace RageCoop.Server
namespace RageCoop.Server;
/// <summary>
/// Represent a player connected to this server.
/// </summary>
public class Client
{
/// <summary>
/// Represent a player connected to this server.
/// </summary>
public class Client
private readonly Stopwatch _latencyWatch = new();
internal readonly Dictionary<int, Action<object>> Callbacks = new();
private readonly Server Server;
private bool _autoRespawn = true;
private bool _displayNameTag = true;
internal long NetHandle = 0;
internal Client(Server server)
{
private readonly Server Server;
internal Client(Server server)
{
Server = server;
}
/// <summary>
/// Gets the total number of entities owned by this client
/// </summary>
public int EntitiesCount { get; internal set; }
/// <summary>
/// Th client's IP address and port.
/// </summary>
public IPEndPoint EndPoint => Connection?.RemoteEndPoint;
/// <summary>
/// Internal(LAN) address of this client, used for NAT hole-punching
/// </summary>
public IPEndPoint InternalEndPoint { get; internal set; }
internal long NetHandle = 0;
internal NetConnection Connection { get; set; }
/// <summary>
/// The <see cref="ServerPed"/> instance representing the client's main character.
/// </summary>
public ServerPed Player { get; internal set; }
/// <summary>
/// The client's latency in seconds.
/// </summary>
public float Latency => Connection.AverageRoundtripTime / 2;
internal readonly Dictionary<int, Action<object>> Callbacks = new();
internal byte[] PublicKey { get; set; }
/// <summary>
/// Indicates whether the client has succefully loaded all resources.
/// </summary>
public bool IsReady { get; internal set; } = false;
/// <summary>
/// The client's username.
/// </summary>
public string Username { get; internal set; } = "N/A";
private bool _autoRespawn = true;
/// <summary>
/// Gets or sets whether to enable automatic respawn for this client's main ped.
/// </summary>
public bool EnableAutoRespawn
{
get => _autoRespawn;
set
{
BaseScript.SetAutoRespawn(this, value);
_autoRespawn = value;
}
}
private bool _displayNameTag = true;
private readonly Stopwatch _latencyWatch = new Stopwatch();
/// <summary>
/// Gets or sets whether to enable automatic respawn for this client's main ped.
/// </summary>
public bool DisplayNameTag
{
get => _displayNameTag;
set
{
Server.BaseScript.SetNameTag(this, value);
_displayNameTag = value;
}
}
#region FUNCTIONS
/// <summary>
/// Kick this client
/// </summary>
/// <param name="reason"></param>
public void Kick(string reason = "You have been kicked!")
{
Connection?.Disconnect(reason);
}
/// <summary>
/// Kick this client
/// </summary>
/// <param name="reasons">Reasons to kick</param>
public void Kick(params string[] reasons)
{
Kick(string.Join(" ", reasons));
}
/// <summary>
/// Send a chat messsage to this client, not visible to others.
/// </summary>
/// <param name="message"></param>
/// <param name="from"></param>
public void SendChatMessage(string message, string from = "Server")
{
try
{
Server.SendChatMessage(from, message, this);
}
catch (Exception e)
{
Server.Logger?.Error($">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Send a native call to client and do a callback when the response received.
/// </summary>
/// <typeparam name="T">Type of the response</typeparam>
/// <param name="callBack"></param>
/// <param name="hash"></param>
/// <param name="args"></param>
public void SendNativeCall<T>(Action<object> callBack, GTA.Native.Hash hash, params object[] args)
{
var argsList = new List<object>(args);
argsList.InsertRange(0, new object[] { (byte)Type.GetTypeCode(typeof(T)), RequestNativeCallID<T>(callBack), (ulong)hash });
SendCustomEventQueued(CustomEvents.NativeCall, argsList.ToArray());
}
/// <summary>
/// Send a native call to client and ignore it's response.
/// </summary>
/// <param name="hash"></param>
/// <param name="args"></param>
public void SendNativeCall(GTA.Native.Hash hash, params object[] args)
{
var argsList = new List<object>(args);
argsList.InsertRange(0, new object[] { (byte)TypeCode.Empty, (ulong)hash });
// Server.Logger?.Debug(argsList.DumpWithType());
SendCustomEventQueued(CustomEvents.NativeCall, argsList.ToArray());
}
private int RequestNativeCallID<T>(Action<object> callback)
{
int ID = 0;
lock (Callbacks)
{
while ((ID == 0)
|| Callbacks.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
Callbacks.Add(ID, callback);
}
return ID;
}
/// <summary>
/// Trigger a CustomEvent for this client
/// </summary>
/// <param name="flags"></param>
/// <param name="hash">An unique identifier of the event</param>
/// <param name="args">Arguments</param>
public void SendCustomEvent(CustomEventFlags flags, CustomEventHash hash, params object[] args)
{
if (!IsReady)
{
Server.Logger?.Warning($"Player \"{Username}\" is not ready!");
}
try
{
NetOutgoingMessage outgoingMessage = Server.MainNetServer.CreateMessage();
new Packets.CustomEvent(flags)
{
Hash = hash,
Args = args
}.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, Connection, NetDeliveryMethod.ReliableOrdered, (byte)ConnectionChannel.Event);
}
catch (Exception ex)
{
Server.Logger?.Error(ex);
}
}
public void SendCustomEventQueued(CustomEventHash hash, params object[] args)
{
SendCustomEvent(CustomEventFlags.Queued, hash, args);
}
public void SendCustomEvent(CustomEventHash hash, params object[] args)
{
SendCustomEvent(CustomEventFlags.None, hash, args);
}
#endregion
Server = server;
}
}
/// <summary>
/// Gets the total number of entities owned by this client
/// </summary>
public int EntitiesCount { get; internal set; }
/// <summary>
/// Th client's IP address and port.
/// </summary>
public IPEndPoint EndPoint => Connection?.RemoteEndPoint;
/// <summary>
/// Internal(LAN) address of this client, used for NAT hole-punching
/// </summary>
public IPEndPoint InternalEndPoint { get; internal set; }
internal NetConnection Connection { get; set; }
/// <summary>
/// The <see cref="ServerPed" /> instance representing the client's main character.
/// </summary>
public ServerPed Player { get; internal set; }
/// <summary>
/// The client's latency in seconds.
/// </summary>
public float Latency => Connection.AverageRoundtripTime / 2;
internal byte[] PublicKey { get; set; }
/// <summary>
/// Indicates whether the client has succefully loaded all resources.
/// </summary>
public bool IsReady { get; internal set; } = false;
/// <summary>
/// The client's username.
/// </summary>
public string Username { get; internal set; } = "N/A";
/// <summary>
/// Gets or sets whether to enable automatic respawn for this client's main ped.
/// </summary>
public bool EnableAutoRespawn
{
get => _autoRespawn;
set
{
BaseScript.SetAutoRespawn(this, value);
_autoRespawn = value;
}
}
/// <summary>
/// Gets or sets whether to enable automatic respawn for this client's main ped.
/// </summary>
public bool DisplayNameTag
{
get => _displayNameTag;
set
{
Server.BaseScript.SetNameTag(this, value);
_displayNameTag = value;
}
}
#region FUNCTIONS
/// <summary>
/// Kick this client
/// </summary>
/// <param name="reason"></param>
public void Kick(string reason = "You have been kicked!")
{
Connection?.Disconnect(reason);
}
/// <summary>
/// Kick this client
/// </summary>
/// <param name="reasons">Reasons to kick</param>
public void Kick(params string[] reasons)
{
Kick(string.Join(" ", reasons));
}
/// <summary>
/// Send a chat messsage to this client, not visible to others.
/// </summary>
/// <param name="message"></param>
/// <param name="from"></param>
public void SendChatMessage(string message, string from = "Server")
{
try
{
Server.SendChatMessage(from, message, this);
}
catch (Exception e)
{
Server.Logger?.Error(
$">> {e.Message} <<>> {e.Source ?? string.Empty} <<>> {e.StackTrace ?? string.Empty} <<");
}
}
/// <summary>
/// Send a native call to client and do a callback when the response received.
/// </summary>
/// <typeparam name="T">Type of the response</typeparam>
/// <param name="callBack"></param>
/// <param name="hash"></param>
/// <param name="args"></param>
public void SendNativeCall<T>(Action<object> callBack, Hash hash, params object[] args)
{
var argsList = new List<object>(args);
argsList.InsertRange(0,
new object[] { (byte)Type.GetTypeCode(typeof(T)), RequestNativeCallID<T>(callBack), (ulong)hash });
SendCustomEventQueued(CustomEvents.NativeCall, argsList.ToArray());
}
/// <summary>
/// Send a native call to client and ignore it's response.
/// </summary>
/// <param name="hash"></param>
/// <param name="args"></param>
public void SendNativeCall(Hash hash, params object[] args)
{
var argsList = new List<object>(args);
argsList.InsertRange(0, new object[] { (byte)TypeCode.Empty, (ulong)hash });
// Server.Logger?.Debug(argsList.DumpWithType());
SendCustomEventQueued(CustomEvents.NativeCall, argsList.ToArray());
}
private int RequestNativeCallID<T>(Action<object> callback)
{
var ID = 0;
lock (Callbacks)
{
while (ID == 0
|| Callbacks.ContainsKey(ID))
{
var rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
Callbacks.Add(ID, callback);
}
return ID;
}
/// <summary>
/// Trigger a CustomEvent for this client
/// </summary>
/// <param name="flags"></param>
/// <param name="hash">An unique identifier of the event</param>
/// <param name="args">Arguments</param>
public void SendCustomEvent(CustomEventFlags flags, CustomEventHash hash, params object[] args)
{
if (!IsReady) Server.Logger?.Warning($"Player \"{Username}\" is not ready!");
try
{
var outgoingMessage = Server.MainNetServer.CreateMessage();
new Packets.CustomEvent(flags)
{
Hash = hash,
Args = args
}.Pack(outgoingMessage);
Server.MainNetServer.SendMessage(outgoingMessage, Connection, NetDeliveryMethod.ReliableOrdered,
(byte)ConnectionChannel.Event);
}
catch (Exception ex)
{
Server.Logger?.Error(ex);
}
}
public void SendCustomEventQueued(CustomEventHash hash, params object[] args)
{
SendCustomEvent(CustomEventFlags.Queued, hash, args);
}
public void SendCustomEvent(CustomEventHash hash, params object[] args)
{
SendCustomEvent(CustomEventFlags.None, hash, args);
}
#endregion
}

View File

@ -1,10 +1,9 @@
namespace RageCoop.Server
namespace RageCoop.Server;
internal class FileTransfer
{
internal class FileTransfer
{
public int ID { get; set; }
public float Progress { get; set; }
public string Name { get; set; }
public bool Cancel { get; set; } = false;
}
}
public int ID { get; set; }
public float Progress { get; set; }
public string Name { get; set; }
public bool Cancel { get; set; } = false;
}

View File

@ -1,3 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura ExcludeAssemblies="RageCoop.Core" />
<Costura ExcludeAssemblies="RageCoop.Core" />
</Weavers>

View File

@ -1,7 +1,5 @@
namespace RageCoop.Server
{
internal class HolePunch
{
namespace RageCoop.Server;
}
}
internal class HolePunch
{
}

View File

@ -1,9 +1,4 @@
using ICSharpCode.SharpZipLib.Zip;
using Lidgren.Network;
using Newtonsoft.Json;
using RageCoop.Core;
using RageCoop.Server.Scripting;
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
@ -12,182 +7,193 @@ using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using ICSharpCode.SharpZipLib.Zip;
using Lidgren.Network;
using Newtonsoft.Json;
using RageCoop.Core;
namespace RageCoop.Server
namespace RageCoop.Server;
public partial class Server
{
public partial class Server
{
private void SendPlayerUpdate()
{
foreach (var c in ClientsByNetHandle.Values.ToArray())
{
try
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerInfoUpdate()
{
PedID = c.Player.ID,
Username = c.Username,
Latency = c.Latency,
Position = c.Player.Position,
IsHost = c == _hostClient
}.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, NetDeliveryMethod.ReliableSequenced, (byte)ConnectionChannel.Default);
}
catch (Exception ex)
{
Logger?.Error(ex);
}
}
}
private IpInfo IpInfo = null;
private bool CanAnnounce = false;
private void Announce()
{
HttpResponseMessage response = null;
HttpClient httpClient = new();
if (IpInfo == null)
{
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 |
SecurityProtocolType.Tls12 |
SecurityProtocolType.Tls11 |
SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
private bool CanAnnounce;
private IpInfo IpInfo;
try
{
IpInfo = CoreUtils.GetIPInfo();
Logger?.Info($"Your public IP is {IpInfo.Address}, announcing to master server...");
}
catch (Exception ex)
{
Logger?.Error(ex.InnerException?.Message ?? ex.Message);
return;
}
}
catch (HttpRequestException ex)
{
Logger?.Error($"MasterServer: {ex.InnerException.Message}");
}
catch (Exception ex)
{
Logger?.Error($"MasterServer: {ex.Message}");
}
}
if (!CanAnnounce)
{
var existing = JsonConvert.DeserializeObject<List<ServerInfo>>(HttpHelper.DownloadString(Util.GetFinalRedirect(Settings.MasterServer))).Where(x => x.address == IpInfo.Address).FirstOrDefault();
if (existing != null)
{
Logger.Warning("Server info already present in master server, waiting for 10 seconds...");
return;
}
else
{
CanAnnounce = true;
}
}
private void SendPlayerUpdate()
{
foreach (var c in ClientsByNetHandle.Values.ToArray())
try
{
Security.GetPublicKey(out var pModulus, out var pExpoenet);
var serverInfo = new ServerInfo
var outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerInfoUpdate
{
address = IpInfo.Address,
port = Settings.Port.ToString(),
country = IpInfo.Country,
name = Settings.Name,
version = Version.ToString(),
players = MainNetServer.ConnectionsCount.ToString(),
maxPlayers = Settings.MaxPlayers.ToString(),
description = Settings.Description,
website = Settings.Website,
gameMode = Settings.GameMode,
language = Settings.Language,
useP2P = Settings.UseP2P,
useZT = Settings.UseZeroTier,
ztID = Settings.UseZeroTier ? Settings.ZeroTierNetworkID : "",
ztAddress = Settings.UseZeroTier ? ZeroTierHelper.Networks[Settings.ZeroTierNetworkID].Addresses.Where(x => !x.Contains(":")).First() : "0.0.0.0",
publicKeyModulus = Convert.ToBase64String(pModulus),
publicKeyExponent = Convert.ToBase64String(pExpoenet)
};
string msg = JsonConvert.SerializeObject(serverInfo);
PedID = c.Player.ID,
Username = c.Username,
Latency = c.Latency,
Position = c.Player.Position,
IsHost = c == _hostClient
}.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, NetDeliveryMethod.ReliableSequenced,
(byte)ConnectionChannel.Default);
}
catch (Exception ex)
{
Logger?.Error(ex);
}
}
var realUrl = Util.GetFinalRedirect(Settings.MasterServer);
response = httpClient.PostAsync(realUrl, new StringContent(msg, Encoding.UTF8, "application/json")).GetAwaiter().GetResult();
private void Announce()
{
HttpResponseMessage response = null;
HttpClient httpClient = new();
if (IpInfo == null)
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 |
SecurityProtocolType.Tls12 |
SecurityProtocolType.Tls11 |
SecurityProtocolType.Tls;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
try
{
IpInfo = CoreUtils.GetIPInfo();
Logger?.Info($"Your public IP is {IpInfo.Address}, announcing to master server...");
}
catch (Exception ex)
{
Logger?.Error(ex.InnerException?.Message ?? ex.Message);
return;
}
}
catch (HttpRequestException ex)
{
Logger?.Error($"MasterServer: {ex.InnerException.Message}");
}
catch (Exception ex)
{
Logger?.Error($"MasterServer: {ex.Message}");
}
if (!CanAnnounce)
{
var existing = JsonConvert
.DeserializeObject<List<ServerInfo>>(
HttpHelper.DownloadString(Util.GetFinalRedirect(Settings.MasterServer)))
.Where(x => x.address == IpInfo.Address).FirstOrDefault();
if (existing != null)
{
Logger.Warning("Server info already present in master server, waiting for 10 seconds...");
return;
}
if (response == null)
{
Logger?.Error("MasterServer: Something went wrong!");
}
else if (response.StatusCode != HttpStatusCode.OK)
{
if (response.StatusCode == HttpStatusCode.BadRequest)
{
string requestContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Logger?.Error($"MasterServer: [{(int)response.StatusCode}], {requestContent}");
}
else
{
Logger?.Error($"MasterServer: [{(int)response.StatusCode}]");
Logger?.Error($"MasterServer: [{response.Content.ReadAsStringAsync().GetAwaiter().GetResult()}]");
}
}
}
private void CheckUpdate()
{
try
{
var latest = CoreUtils.GetLatestVersion();
if (latest <= Version) { return; }
// wait ten minutes for the build to complete
API.SendChatMessage($"New server version found: {latest}, server will update in 10 minutes");
Thread.Sleep(10 * 60 * 1000);
API.SendChatMessage("downloading update...");
var downloadURL = $"https://github.com/RAGECOOP/RAGECOOP-V/releases/download/nightly/RageCoop.Server-{CoreUtils.GetInvariantRID()}.zip";
if (Directory.Exists("Update")) { Directory.Delete("Update", true); }
HttpHelper.DownloadFile(downloadURL, "Update.zip", null);
Logger?.Info("Installing update");
Directory.CreateDirectory("Update");
new FastZip().ExtractZip("Update.zip", "Update", FastZip.Overwrite.Always, null, null, null, true);
MainNetServer.Shutdown("Server updating");
Logger.Info("Server shutting down!");
Logger.Flush();
Process.Start(Path.Combine("Update", RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "RageCoop.Server.exe" : "RageCoop.Server"), "update \"" + AppDomain.CurrentDomain.BaseDirectory[0..^1] + "\"");
Environment.Exit(0);
}
catch (Exception ex)
{
Logger?.Error("Update", ex);
}
CanAnnounce = true;
}
private void KickAssholes()
try
{
foreach (var c in ClientsByNetHandle.Values.ToArray())
Security.GetPublicKey(out var pModulus, out var pExpoenet);
var serverInfo = new ServerInfo
{
if (c.EntitiesCount > Settings.SpamLimit && Settings.KickSpamming)
{
c.Kick("Bye bye asshole: spamming");
API.SendChatMessage($"Asshole {c.Username} was kicked: Spamming");
}
else if (Settings.KickGodMode && c.Player.IsInvincible)
{
c.Kick("Bye bye asshole: godmode");
API.SendChatMessage($"Asshole {c.Username} was kicked: GodMode");
}
address = IpInfo.Address,
port = Settings.Port.ToString(),
country = IpInfo.Country,
name = Settings.Name,
version = Version.ToString(),
players = MainNetServer.ConnectionsCount.ToString(),
maxPlayers = Settings.MaxPlayers.ToString(),
description = Settings.Description,
website = Settings.Website,
gameMode = Settings.GameMode,
language = Settings.Language,
useP2P = Settings.UseP2P,
useZT = Settings.UseZeroTier,
ztID = Settings.UseZeroTier ? Settings.ZeroTierNetworkID : "",
ztAddress = Settings.UseZeroTier
? ZeroTierHelper.Networks[Settings.ZeroTierNetworkID].Addresses.Where(x => !x.Contains(":")).First()
: "0.0.0.0",
publicKeyModulus = Convert.ToBase64String(pModulus),
publicKeyExponent = Convert.ToBase64String(pExpoenet)
};
var msg = JsonConvert.SerializeObject(serverInfo);
var realUrl = Util.GetFinalRedirect(Settings.MasterServer);
response = httpClient.PostAsync(realUrl, new StringContent(msg, Encoding.UTF8, "application/json"))
.GetAwaiter().GetResult();
}
catch (Exception ex)
{
Logger?.Error($"MasterServer: {ex.Message}");
return;
}
if (response == null)
{
Logger?.Error("MasterServer: Something went wrong!");
}
else if (response.StatusCode != HttpStatusCode.OK)
{
if (response.StatusCode == HttpStatusCode.BadRequest)
{
var requestContent = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
Logger?.Error($"MasterServer: [{(int)response.StatusCode}], {requestContent}");
}
else
{
Logger?.Error($"MasterServer: [{(int)response.StatusCode}]");
Logger?.Error($"MasterServer: [{response.Content.ReadAsStringAsync().GetAwaiter().GetResult()}]");
}
}
}
}
private void CheckUpdate()
{
try
{
var latest = CoreUtils.GetLatestVersion();
if (latest <= Version) return;
// wait ten minutes for the build to complete
API.SendChatMessage($"New server version found: {latest}, server will update in 10 minutes");
Thread.Sleep(10 * 60 * 1000);
API.SendChatMessage("downloading update...");
var downloadURL =
$"https://github.com/RAGECOOP/RAGECOOP-V/releases/download/nightly/RageCoop.Server-{CoreUtils.GetInvariantRID()}.zip";
if (Directory.Exists("Update")) Directory.Delete("Update", true);
HttpHelper.DownloadFile(downloadURL, "Update.zip");
Logger?.Info("Installing update");
Directory.CreateDirectory("Update");
new FastZip().ExtractZip("Update.zip", "Update", FastZip.Overwrite.Always, null, null, null, true);
MainNetServer.Shutdown("Server updating");
Logger.Info("Server shutting down!");
Logger.Flush();
Process.Start(
Path.Combine("Update",
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "RageCoop.Server.exe" : "RageCoop.Server"),
"update \"" + AppDomain.CurrentDomain.BaseDirectory[..^1] + "\"");
Environment.Exit(0);
}
catch (Exception ex)
{
Logger?.Error("Update", ex);
}
}
private void KickAssholes()
{
foreach (var c in ClientsByNetHandle.Values.ToArray())
if (c.EntitiesCount > Settings.SpamLimit && Settings.KickSpamming)
{
c.Kick("Bye bye asshole: spamming");
API.SendChatMessage($"Asshole {c.Username} was kicked: Spamming");
}
else if (Settings.KickGodMode && c.Player.IsInvincible)
{
c.Kick("Bye bye asshole: godmode");
API.SendChatMessage($"Asshole {c.Username} was kicked: GodMode");
}
}
}

View File

@ -1,197 +1,187 @@
using Lidgren.Network;
using System;
using System.Linq;
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using RageCoop.Server.Scripting;
using System;
using System.Linq;
using System.Text;
namespace RageCoop.Server
namespace RageCoop.Server;
public partial class Server
{
public partial class Server
private void DisconnectAndLog(NetConnection senderConnection, PacketType type, Exception e)
{
private void DisconnectAndLog(NetConnection senderConnection, PacketType type, Exception e)
{
Logger?.Error($"Error receiving a packet of type {type}");
Logger?.Error(e.Message);
Logger?.Error(e.StackTrace);
senderConnection.Disconnect(e.Message);
}
private void GetHandshake(NetConnection connection, Packets.Handshake packet)
{
Logger?.Debug("New handshake from: [Name: " + packet.Username + " | Address: " + connection.RemoteEndPoint.Address.ToString() + "]");
if (!packet.ModVersion.StartsWith(Version.ToString(3)))
{
connection.Deny($"RAGECOOP version {Version.ToString(3)} required!");
return;
}
if (string.IsNullOrWhiteSpace(packet.Username))
{
connection.Deny("Username is empty or contains spaces!");
return;
}
if (packet.Username.Any(p => !_allowedCharacterSet.Contains(p)))
{
connection.Deny("Username contains special chars!");
return;
}
if (ClientsByNetHandle.Values.Any(x => x.Username.ToLower() == packet.Username.ToLower()))
{
connection.Deny("Username is already taken!");
return;
}
try
{
Security.AddConnection(connection.RemoteEndPoint, packet.AesKeyCrypted, packet.AesIVCrypted);
var args = new HandshakeEventArgs()
{
EndPoint = connection.RemoteEndPoint,
ID = packet.PedID,
Username = packet.Username,
PasswordHash = Security.Decrypt(packet.PasswordEncrypted, connection.RemoteEndPoint).GetString().GetSHA256Hash().ToHexString(),
};
API.Events.InvokePlayerHandshake(args);
if (args.Cancel)
{
connection.Deny(args.DenyReason);
return;
}
}
catch (Exception ex)
{
Logger?.Error($"Cannot process handshake packet from {connection.RemoteEndPoint}");
Logger?.Error(ex);
connection.Deny("Malformed handshak packet!");
return;
}
var handshakeSuccess = MainNetServer.CreateMessage();
var currentClients = ClientsByID.Values.ToArray();
var players = new Packets.PlayerData[currentClients.Length];
for (int i = 0; i < players.Length; i++)
{
players[i] = new Packets.PlayerData()
{
ID = currentClients[i].Player.ID,
Username = currentClients[i].Username,
};
}
new Packets.HandshakeSuccess()
{
Players = players
}.Pack(handshakeSuccess);
connection.Approve(handshakeSuccess);
Client tmpClient;
// Add the player to Players
lock (ClientsByNetHandle)
{
var player = new ServerPed(this)
{
ID = packet.PedID,
};
Entities.Add(player);
ClientsByNetHandle.Add(connection.RemoteUniqueIdentifier,
tmpClient = new Client(this)
{
NetHandle = connection.RemoteUniqueIdentifier,
Connection = connection,
Username = packet.Username,
Player = player,
InternalEndPoint = packet.InternalEndPoint,
}
);
player.Owner = tmpClient;
ClientsByName.Add(packet.Username.ToLower(), tmpClient);
ClientsByID.Add(player.ID, tmpClient);
if (ClientsByNetHandle.Count == 1)
{
_hostClient = tmpClient;
}
}
Logger?.Debug($"Handshake sucess, Player:{packet.Username} PedID:{packet.PedID}");
}
// 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 void PlayerConnected(Client newClient)
{
if (newClient == _hostClient)
{
API.SendCustomEvent(new() { newClient }, CustomEvents.IsHost, true);
}
// Send new client to all players
var cons = MainNetServer.Connections.Exclude(newClient.Connection);
if (cons.Count != 0)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerConnect()
{
PedID = newClient.Player.ID,
Username = newClient.Username
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, cons, NetDeliveryMethod.ReliableOrdered, 0);
}
// Send all props to this player
BaseScript.SendServerPropsTo(new(Entities.ServerProps.Values), new() { newClient });
// Send all blips to this player
BaseScript.SendServerBlipsTo(new(Entities.Blips.Values), new() { newClient });
// Create P2P connection
if (Settings.UseP2P)
{
ClientsByNetHandle.Values.ForEach(target =>
{
if (target == newClient) { return; }
HolePunch(target, newClient);
});
}
Logger?.Info($"Player {newClient.Username} connected!");
if (!string.IsNullOrEmpty(Settings.WelcomeMessage))
{
SendChatMessage("Server", Settings.WelcomeMessage, newClient);
}
}
// Send all players a message that someone has left the server
private void PlayerDisconnected(Client localClient)
{
var cons = MainNetServer.Connections.Exclude(localClient.Connection);
if (cons.Count != 0)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerDisconnect()
{
PedID = localClient.Player.ID,
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, cons, NetDeliveryMethod.ReliableOrdered, 0);
}
Entities.CleanUp(localClient);
QueueJob(() => API.Events.InvokePlayerDisconnected(localClient));
Logger?.Info($"Player {localClient.Username} disconnected! ID:{localClient.Player.ID}");
if (ClientsByNetHandle.ContainsKey(localClient.NetHandle)) { ClientsByNetHandle.Remove(localClient.NetHandle); }
if (ClientsByName.ContainsKey(localClient.Username.ToLower())) { ClientsByName.Remove(localClient.Username.ToLower()); }
if (ClientsByID.ContainsKey(localClient.Player.ID)) { ClientsByID.Remove(localClient.Player.ID); }
if (localClient == _hostClient)
{
_hostClient = ClientsByNetHandle.Values.FirstOrDefault();
_hostClient?.SendCustomEvent(CustomEvents.IsHost, true);
}
Security.RemoveConnection(localClient.Connection.RemoteEndPoint);
}
Logger?.Error($"Error receiving a packet of type {type}");
Logger?.Error(e.Message);
Logger?.Error(e.StackTrace);
senderConnection.Disconnect(e.Message);
}
}
private void GetHandshake(NetConnection connection, Packets.Handshake packet)
{
Logger?.Debug("New handshake from: [Name: " + packet.Username + " | Address: " +
connection.RemoteEndPoint.Address + "]");
if (!packet.ModVersion.StartsWith(Version.ToString(3)))
{
connection.Deny($"RAGECOOP version {Version.ToString(3)} required!");
return;
}
if (string.IsNullOrWhiteSpace(packet.Username))
{
connection.Deny("Username is empty or contains spaces!");
return;
}
if (packet.Username.Any(p => !_allowedCharacterSet.Contains(p)))
{
connection.Deny("Username contains special chars!");
return;
}
if (ClientsByNetHandle.Values.Any(x => x.Username.ToLower() == packet.Username.ToLower()))
{
connection.Deny("Username is already taken!");
return;
}
try
{
Security.AddConnection(connection.RemoteEndPoint, packet.AesKeyCrypted, packet.AesIVCrypted);
var args = new HandshakeEventArgs
{
EndPoint = connection.RemoteEndPoint,
ID = packet.PedID,
Username = packet.Username,
PasswordHash = Security.Decrypt(packet.PasswordEncrypted, connection.RemoteEndPoint).GetString()
.GetSHA256Hash().ToHexString()
};
API.Events.InvokePlayerHandshake(args);
if (args.Cancel)
{
connection.Deny(args.DenyReason);
return;
}
}
catch (Exception ex)
{
Logger?.Error($"Cannot process handshake packet from {connection.RemoteEndPoint}");
Logger?.Error(ex);
connection.Deny("Malformed handshak packet!");
return;
}
var handshakeSuccess = MainNetServer.CreateMessage();
var currentClients = ClientsByID.Values.ToArray();
var players = new Packets.PlayerData[currentClients.Length];
for (var i = 0; i < players.Length; i++)
players[i] = new Packets.PlayerData
{
ID = currentClients[i].Player.ID,
Username = currentClients[i].Username
};
new Packets.HandshakeSuccess
{
Players = players
}.Pack(handshakeSuccess);
connection.Approve(handshakeSuccess);
Client tmpClient;
// Add the player to Players
lock (ClientsByNetHandle)
{
var player = new ServerPed(this)
{
ID = packet.PedID
};
Entities.Add(player);
ClientsByNetHandle.Add(connection.RemoteUniqueIdentifier,
tmpClient = new Client(this)
{
NetHandle = connection.RemoteUniqueIdentifier,
Connection = connection,
Username = packet.Username,
Player = player,
InternalEndPoint = packet.InternalEndPoint
}
);
player.Owner = tmpClient;
ClientsByName.Add(packet.Username.ToLower(), tmpClient);
ClientsByID.Add(player.ID, tmpClient);
if (ClientsByNetHandle.Count == 1) _hostClient = tmpClient;
}
Logger?.Debug($"Handshake sucess, Player:{packet.Username} PedID:{packet.PedID}");
}
// 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 void PlayerConnected(Client newClient)
{
if (newClient == _hostClient) API.SendCustomEvent(new List<Client> { newClient }, CustomEvents.IsHost, true);
// Send new client to all players
var cons = MainNetServer.Connections.Exclude(newClient.Connection);
if (cons.Count != 0)
{
var outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerConnect
{
PedID = newClient.Player.ID,
Username = newClient.Username
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, cons, NetDeliveryMethod.ReliableOrdered, 0);
}
// Send all props to this player
BaseScript.SendServerPropsTo(new List<ServerProp>(Entities.ServerProps.Values), new List<Client> { newClient });
// Send all blips to this player
BaseScript.SendServerBlipsTo(new List<ServerBlip>(Entities.Blips.Values), new List<Client> { newClient });
// Create P2P connection
if (Settings.UseP2P)
ClientsByNetHandle.Values.ForEach(target =>
{
if (target == newClient) return;
HolePunch(target, newClient);
});
Logger?.Info($"Player {newClient.Username} connected!");
if (!string.IsNullOrEmpty(Settings.WelcomeMessage))
SendChatMessage("Server", Settings.WelcomeMessage, newClient);
}
// Send all players a message that someone has left the server
private void PlayerDisconnected(Client localClient)
{
var cons = MainNetServer.Connections.Exclude(localClient.Connection);
if (cons.Count != 0)
{
var outgoingMessage = MainNetServer.CreateMessage();
new Packets.PlayerDisconnect
{
PedID = localClient.Player.ID
}.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, cons, NetDeliveryMethod.ReliableOrdered, 0);
}
Entities.CleanUp(localClient);
QueueJob(() => API.Events.InvokePlayerDisconnected(localClient));
Logger?.Info($"Player {localClient.Username} disconnected! ID:{localClient.Player.ID}");
if (ClientsByNetHandle.ContainsKey(localClient.NetHandle)) ClientsByNetHandle.Remove(localClient.NetHandle);
if (ClientsByName.ContainsKey(localClient.Username.ToLower()))
ClientsByName.Remove(localClient.Username.ToLower());
if (ClientsByID.ContainsKey(localClient.Player.ID)) ClientsByID.Remove(localClient.Player.ID);
if (localClient == _hostClient)
{
_hostClient = ClientsByNetHandle.Values.FirstOrDefault();
_hostClient?.SendCustomEvent(CustomEvents.IsHost, true);
}
Security.RemoveConnection(localClient.Connection.RemoteEndPoint);
}
}

View File

@ -1,78 +1,74 @@
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Server.Scripting;
namespace RageCoop.Server
namespace RageCoop.Server;
public partial class Server
{
public partial class Server
private void PedSync(Packets.PedSync packet, Client client)
{
private void PedSync(Packets.PedSync packet, Client client)
{
QueueJob(() => Entities.Update(packet, client));
QueueJob(() => Entities.Update(packet, client));
bool isPlayer = packet.ID == client.Player.ID;
var isPlayer = packet.ID == client.Player.ID;
if (isPlayer) QueueJob(() => API.Events.InvokePlayerUpdate(client));
if (Settings.UseP2P) return;
foreach (var c in ClientsByNetHandle.Values)
{
// Don't send data back
if (c.NetHandle == client.NetHandle) continue;
// Check streaming distance
if (isPlayer)
{
QueueJob(() => API.Events.InvokePlayerUpdate(client));
if (Settings.PlayerStreamingDistance != -1 &&
packet.Position.DistanceTo(c.Player.Position) > Settings.PlayerStreamingDistance) continue;
}
if (Settings.UseP2P) { return; }
foreach (var c in ClientsByNetHandle.Values)
else if (Settings.NpcStreamingDistance != -1 &&
packet.Position.DistanceTo(c.Player.Position) > Settings.NpcStreamingDistance)
{
// Don't send data back
if (c.NetHandle == client.NetHandle) { continue; }
// Check streaming distance
if (isPlayer)
{
if ((Settings.PlayerStreamingDistance != -1) && (packet.Position.DistanceTo(c.Player.Position) > Settings.PlayerStreamingDistance))
{
continue;
}
}
else if ((Settings.NpcStreamingDistance != -1) && (packet.Position.DistanceTo(c.Player.Position) > Settings.NpcStreamingDistance))
{
continue;
}
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.PedSync);
continue;
}
}
private void VehicleSync(Packets.VehicleSync packet, Client client)
{
QueueJob(() => Entities.Update(packet, client));
bool isPlayer = packet.ID == client.Player?.LastVehicle?.ID;
if (Settings.UseP2P) { return; }
foreach (var c in ClientsByNetHandle.Values)
{
if (c.NetHandle == client.NetHandle) { continue; }
if (isPlayer)
{
// Player's vehicle
if ((Settings.PlayerStreamingDistance != -1) && (packet.Position.DistanceTo(c.Player.Position) > Settings.PlayerStreamingDistance))
{
continue;
}
}
else if ((Settings.NpcStreamingDistance != -1) && (packet.Position.DistanceTo(c.Player.Position) > Settings.NpcStreamingDistance))
{
continue;
}
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.VehicleSync);
}
var outgoingMessage = MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.UnreliableSequenced,
(byte)ConnectionChannel.PedSync);
}
private void ProjectileSync(Packets.ProjectileSync packet, Client client)
{
if (Settings.UseP2P) { return; }
Forward(packet, client, ConnectionChannel.ProjectileSync);
}
}
}
private void VehicleSync(Packets.VehicleSync packet, Client client)
{
QueueJob(() => Entities.Update(packet, client));
var isPlayer = packet.ID == client.Player?.LastVehicle?.ID;
if (Settings.UseP2P) return;
foreach (var c in ClientsByNetHandle.Values)
{
if (c.NetHandle == client.NetHandle) continue;
if (isPlayer)
{
// Player's vehicle
if (Settings.PlayerStreamingDistance != -1 &&
packet.Position.DistanceTo(c.Player.Position) > Settings.PlayerStreamingDistance) continue;
}
else if (Settings.NpcStreamingDistance != -1 &&
packet.Position.DistanceTo(c.Player.Position) > Settings.NpcStreamingDistance)
{
continue;
}
var outgoingMessage = MainNetServer.CreateMessage();
packet.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, c.Connection, NetDeliveryMethod.UnreliableSequenced,
(byte)ConnectionChannel.VehicleSync);
}
}
private void ProjectileSync(Packets.ProjectileSync packet, Client client)
{
if (Settings.UseP2P) return;
Forward(packet, client, ConnectionChannel.ProjectileSync);
}
}

View File

@ -1,29 +1,28 @@
using Lidgren.Network;
using RageCoop.Core;
namespace RageCoop.Server
{
public partial class Server
{
private void HolePunch(Client host, Client client)
{
// Send to host
Send(new Packets.HolePunchInit
{
Connect = false,
TargetID = client.Player.ID,
TargetInternal = client.InternalEndPoint.ToString(),
TargetExternal = client.EndPoint.ToString()
}, host, ConnectionChannel.Default, NetDeliveryMethod.ReliableOrdered);
namespace RageCoop.Server;
// Send to client
Send(new Packets.HolePunchInit
{
Connect = true,
TargetID = host.Player.ID,
TargetInternal = host.InternalEndPoint.ToString(),
TargetExternal = host.EndPoint.ToString()
}, client, ConnectionChannel.Default, NetDeliveryMethod.ReliableOrdered);
}
public partial class Server
{
private void HolePunch(Client host, Client client)
{
// Send to host
Send(new Packets.HolePunchInit
{
Connect = false,
TargetID = client.Player.ID,
TargetInternal = client.InternalEndPoint.ToString(),
TargetExternal = client.EndPoint.ToString()
}, host, ConnectionChannel.Default, NetDeliveryMethod.ReliableOrdered);
// Send to client
Send(new Packets.HolePunchInit
{
Connect = true,
TargetID = host.Player.ID,
TargetInternal = host.InternalEndPoint.ToString(),
TargetExternal = host.EndPoint.ToString()
}, client, ConnectionChannel.Default, NetDeliveryMethod.ReliableOrdered);
}
}
}

View File

@ -1,247 +1,240 @@
using Lidgren.Network;
using System;
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Server.Scripting;
using System;
namespace RageCoop.Server
namespace RageCoop.Server;
public partial class Server
{
public partial class Server
private void Listen()
{
private void Listen()
{
NetIncomingMessage msg = null;
while (!_stopping)
{
try
{
msg = MainNetServer.WaitMessage(200);
ProcessMessage(msg);
}
catch (Exception ex)
{
Logger?.Error("Error processing message");
Logger?.Error(ex);
if (msg != null)
{
DisconnectAndLog(msg.SenderConnection, PacketType.Unknown, ex);
}
}
}
Logger?.Info("Server is shutting down!");
MainNetServer.Shutdown("Server is shutting down!");
BaseScript.OnStop();
Resources.UnloadAll();
}
private void ProcessMessage(NetIncomingMessage message)
{
Client sender;
if (message == null) { return; }
switch (message.MessageType)
{
case NetIncomingMessageType.ConnectionApproval:
{
Logger?.Info($"New incoming connection from: [{message.SenderConnection.RemoteEndPoint}]");
if (message.ReadByte() != (byte)PacketType.Handshake)
{
Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: Wrong packet!");
message.SenderConnection.Deny("Wrong packet!");
}
else
{
try
{
GetHandshake(message.SenderConnection, message.GetPacket<Packets.Handshake>());
}
catch (Exception e)
{
Logger?.Info($"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: {e.Message}");
Logger?.Error(e);
message.SenderConnection.Deny(e.Message);
}
}
break;
}
case NetIncomingMessageType.StatusChanged:
{
// Get sender client
if (!ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender))
{
break;
}
NetConnectionStatus status = (NetConnectionStatus)message.ReadByte();
if (status == NetConnectionStatus.Disconnected)
{
PlayerDisconnected(sender);
}
else if (status == NetConnectionStatus.Connected)
{
PlayerConnected(sender);
QueueJob(() => API.Events.InvokePlayerConnected(sender));
Resources.SendTo(sender);
}
break;
}
case NetIncomingMessageType.Data:
{
// Get sender client
if (ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender))
{
// Get packet type
var type = (PacketType)message.ReadByte();
switch (type)
{
case PacketType.Response:
{
int id = message.ReadInt32();
if (PendingResponses.TryGetValue(id, out var callback))
{
callback((PacketType)message.ReadByte(), message);
PendingResponses.Remove(id);
}
break;
}
case PacketType.Request:
{
int id = message.ReadInt32();
var reqType = (PacketType)message.ReadByte();
if (RequestHandlers.TryGetValue(reqType, out var handler))
{
var response = MainNetServer.CreateMessage();
response.Write((byte)PacketType.Response);
response.Write(id);
handler(message, sender).Pack(response);
MainNetServer.SendMessage(response, message.SenderConnection, NetDeliveryMethod.ReliableOrdered);
}
else
{
Logger.Warning("Did not find a request handler of type: " + reqType);
}
break;
}
default:
{
if (type.IsSyncEvent())
{
// Sync Events
if (Settings.UseP2P) { break; }
try
{
var toSend = MainNetServer.Connections.Exclude(message.SenderConnection);
if (toSend.Count != 0)
{
var outgoingMessage = MainNetServer.CreateMessage();
outgoingMessage.Write((byte)type);
outgoingMessage.Write(message.ReadBytes(message.LengthBytes - 1));
MainNetServer.SendMessage(outgoingMessage, toSend, NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.SyncEvents);
}
}
catch (Exception e)
{
DisconnectAndLog(message.SenderConnection, type, e);
}
}
else
{
HandlePacket(type, message, sender);
}
break;
}
}
}
break;
}
case NetIncomingMessageType.ErrorMessage:
Logger?.Error(message.ReadString());
break;
case NetIncomingMessageType.WarningMessage:
Logger?.Warning(message.ReadString());
break;
case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.VerboseDebugMessage:
Logger?.Debug(message.ReadString());
break;
case NetIncomingMessageType.UnconnectedData:
{
if (message.ReadByte() == (byte)PacketType.PublicKeyRequest)
{
var msg = MainNetServer.CreateMessage();
var p = new Packets.PublicKeyResponse();
Security.GetPublicKey(out p.Modulus, out p.Exponent);
p.Pack(msg);
Logger?.Debug($"Sending public key to {message.SenderEndPoint}, length:{msg.LengthBytes}");
MainNetServer.SendUnconnectedMessage(msg, message.SenderEndPoint);
}
}
break;
default:
Logger?.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType, message.LengthBytes, message.DeliveryMethod, message.SequenceChannel));
break;
}
MainNetServer.Recycle(message);
}
private void HandlePacket(PacketType type, NetIncomingMessage msg, Client sender)
{
NetIncomingMessage msg = null;
while (!_stopping)
try
{
switch (type)
msg = MainNetServer.WaitMessage(200);
ProcessMessage(msg);
}
catch (Exception ex)
{
Logger?.Error("Error processing message");
Logger?.Error(ex);
if (msg != null) DisconnectAndLog(msg.SenderConnection, PacketType.Unknown, ex);
}
Logger?.Info("Server is shutting down!");
MainNetServer.Shutdown("Server is shutting down!");
BaseScript.OnStop();
Resources.UnloadAll();
}
private void ProcessMessage(NetIncomingMessage message)
{
Client sender;
if (message == null) return;
switch (message.MessageType)
{
case NetIncomingMessageType.ConnectionApproval:
{
Logger?.Info($"New incoming connection from: [{message.SenderConnection.RemoteEndPoint}]");
if (message.ReadByte() != (byte)PacketType.Handshake)
{
case PacketType.PedSync:
PedSync(msg.GetPacket<Packets.PedSync>(), sender);
break;
Logger?.Info(
$"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: Wrong packet!");
message.SenderConnection.Deny("Wrong packet!");
}
else
{
try
{
GetHandshake(message.SenderConnection, message.GetPacket<Packets.Handshake>());
}
catch (Exception e)
{
Logger?.Info(
$"IP [{message.SenderConnection.RemoteEndPoint.Address}] was blocked, reason: {e.Message}");
Logger?.Error(e);
message.SenderConnection.Deny(e.Message);
}
}
case PacketType.VehicleSync:
VehicleSync(msg.GetPacket<Packets.VehicleSync>(), sender);
break;
break;
}
case NetIncomingMessageType.StatusChanged:
{
// Get sender client
if (!ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender)) break;
var status = (NetConnectionStatus)message.ReadByte();
case PacketType.ProjectileSync:
ProjectileSync(msg.GetPacket<Packets.ProjectileSync>(), sender);
break;
if (status == NetConnectionStatus.Disconnected)
{
PlayerDisconnected(sender);
}
else if (status == NetConnectionStatus.Connected)
{
PlayerConnected(sender);
QueueJob(() => API.Events.InvokePlayerConnected(sender));
Resources.SendTo(sender);
}
case PacketType.ChatMessage:
break;
}
case NetIncomingMessageType.Data:
{
// Get sender client
if (ClientsByNetHandle.TryGetValue(message.SenderConnection.RemoteUniqueIdentifier, out sender))
{
// Get packet type
var type = (PacketType)message.ReadByte();
switch (type)
{
case PacketType.Response:
{
Packets.ChatMessage packet = new((b) =>
var id = message.ReadInt32();
if (PendingResponses.TryGetValue(id, out var callback))
{
return Security.Decrypt(b, sender.EndPoint);
});
packet.Deserialize(msg);
ChatMessageReceived(packet.Username, packet.Message, sender);
}
break;
case PacketType.Voice:
{
if (Settings.UseVoice && !Settings.UseP2P)
{
Forward(msg.GetPacket<Packets.Voice>(), sender, ConnectionChannel.Voice);
callback((PacketType)message.ReadByte(), message);
PendingResponses.Remove(id);
}
}
break;
case PacketType.CustomEvent:
break;
}
case PacketType.Request:
{
Packets.CustomEvent packet = new Packets.CustomEvent();
packet.Deserialize(msg);
QueueJob(() => API.Events.InvokeCustomEventReceived(packet, sender));
}
break;
default:
Logger?.Error("Unhandled Data / Packet type");
break;
var id = message.ReadInt32();
var reqType = (PacketType)message.ReadByte();
if (RequestHandlers.TryGetValue(reqType, out var handler))
{
var response = MainNetServer.CreateMessage();
response.Write((byte)PacketType.Response);
response.Write(id);
handler(message, sender).Pack(response);
MainNetServer.SendMessage(response, message.SenderConnection,
NetDeliveryMethod.ReliableOrdered);
}
else
{
Logger.Warning("Did not find a request handler of type: " + reqType);
}
break;
}
default:
{
if (type.IsSyncEvent())
{
// Sync Events
if (Settings.UseP2P) break;
try
{
var toSend = MainNetServer.Connections.Exclude(message.SenderConnection);
if (toSend.Count != 0)
{
var outgoingMessage = MainNetServer.CreateMessage();
outgoingMessage.Write((byte)type);
outgoingMessage.Write(message.ReadBytes(message.LengthBytes - 1));
MainNetServer.SendMessage(outgoingMessage, toSend,
NetDeliveryMethod.UnreliableSequenced, (byte)ConnectionChannel.SyncEvents);
}
}
catch (Exception e)
{
DisconnectAndLog(message.SenderConnection, type, e);
}
}
else
{
HandlePacket(type, message, sender);
}
break;
}
}
}
break;
}
case NetIncomingMessageType.ErrorMessage:
Logger?.Error(message.ReadString());
break;
case NetIncomingMessageType.WarningMessage:
Logger?.Warning(message.ReadString());
break;
case NetIncomingMessageType.DebugMessage:
case NetIncomingMessageType.VerboseDebugMessage:
Logger?.Debug(message.ReadString());
break;
case NetIncomingMessageType.UnconnectedData:
{
if (message.ReadByte() == (byte)PacketType.PublicKeyRequest)
{
var msg = MainNetServer.CreateMessage();
var p = new Packets.PublicKeyResponse();
Security.GetPublicKey(out p.Modulus, out p.Exponent);
p.Pack(msg);
Logger?.Debug($"Sending public key to {message.SenderEndPoint}, length:{msg.LengthBytes}");
MainNetServer.SendUnconnectedMessage(msg, message.SenderEndPoint);
}
}
catch (Exception e)
break;
default:
Logger?.Error(string.Format("Unhandled type: {0} {1} bytes {2} | {3}", message.MessageType,
message.LengthBytes, message.DeliveryMethod, message.SequenceChannel));
break;
}
MainNetServer.Recycle(message);
}
private void HandlePacket(PacketType type, NetIncomingMessage msg, Client sender)
{
try
{
switch (type)
{
DisconnectAndLog(sender.Connection, type, e);
case PacketType.PedSync:
PedSync(msg.GetPacket<Packets.PedSync>(), sender);
break;
case PacketType.VehicleSync:
VehicleSync(msg.GetPacket<Packets.VehicleSync>(), sender);
break;
case PacketType.ProjectileSync:
ProjectileSync(msg.GetPacket<Packets.ProjectileSync>(), sender);
break;
case PacketType.ChatMessage:
{
Packets.ChatMessage packet = new(b => { return Security.Decrypt(b, sender.EndPoint); });
packet.Deserialize(msg);
ChatMessageReceived(packet.Username, packet.Message, sender);
}
break;
case PacketType.Voice:
{
if (Settings.UseVoice && !Settings.UseP2P)
Forward(msg.GetPacket<Packets.Voice>(), sender, ConnectionChannel.Voice);
}
break;
case PacketType.CustomEvent:
{
var packet = new Packets.CustomEvent();
packet.Deserialize(msg);
QueueJob(() => API.Events.InvokeCustomEventReceived(packet, sender));
}
break;
default:
Logger?.Error("Unhandled Data / Packet type");
break;
}
}
catch (Exception e)
{
DisconnectAndLog(sender.Connection, type, e);
}
}
}
}

View File

@ -1,412 +1,409 @@
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Server.Scripting;
using System;
using System;
using System.IO;
using System.Linq;
using System.Net.NetworkInformation;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Server.Scripting;
using Timer = System.Timers.Timer;
namespace RageCoop.Server
namespace RageCoop.Server;
/// <summary>
/// The instantiable RageCoop server class
/// </summary>
public partial class Server
{
/// <summary>
/// Get the current server version
/// </summary>
public static readonly Version Version = typeof(Server).Assembly.GetName().Version;
private readonly HashSet<char> _allowedCharacterSet;
private readonly Timer _announceTimer = new();
private readonly Timer _antiAssholesTimer = new();
private readonly Thread _listenerThread;
private readonly Timer _playerUpdateTimer = new();
private readonly Timer _updateTimer = new();
private readonly Worker _worker;
internal readonly BaseScript BaseScript;
internal readonly Dictionary<int, Client> ClientsByID = new();
internal readonly Dictionary<string, Client> ClientsByName = new();
internal readonly Dictionary<long, Client> ClientsByNetHandle = new();
internal readonly Dictionary<Command, Action<CommandContext>> Commands = new();
private readonly Dictionary<int, FileTransfer> InProgressFileTransfers = new();
private readonly Dictionary<int, Action<PacketType, NetIncomingMessage>> PendingResponses = new();
internal readonly Settings Settings;
internal Client _hostClient;
private bool _stopping;
internal ServerEntities Entities;
internal Logger Logger;
internal NetServer MainNetServer;
internal Dictionary<PacketType, Func<NetIncomingMessage, Client, Packet>> RequestHandlers = new();
internal Resources Resources;
internal Security Security;
/// <summary>
/// The instantiable RageCoop server class
/// Instantiate a server.
/// </summary>
public partial class Server
/// <param name="settings"></param>
/// <param name="logger"></param>
/// <exception cref="ArgumentNullException"></exception>
public Server(Settings settings, Logger logger = null)
{
/// <summary>
/// The API for controlling server and hooking events.
/// </summary>
public API API { get; private set; }
internal readonly BaseScript BaseScript;
internal readonly Settings Settings;
internal NetServer MainNetServer;
internal ServerEntities Entities;
Settings = settings;
if (settings == null) throw new ArgumentNullException("Server settings cannot be null!");
Logger = logger;
if (Logger != null) Logger.LogLevel = Settings.LogLevel;
API = new API(this);
Resources = new Resources(this);
Security = new Security(Logger);
Entities = new ServerEntities(this);
BaseScript = new BaseScript(this);
_allowedCharacterSet = new HashSet<char>(Settings.AllowedUsernameChars.ToCharArray());
internal readonly Dictionary<Command, Action<CommandContext>> Commands = new();
internal readonly Dictionary<long, Client> ClientsByNetHandle = new();
internal readonly Dictionary<string, Client> ClientsByName = new();
internal readonly Dictionary<int, Client> ClientsByID = new();
internal Client _hostClient;
private readonly Dictionary<int, FileTransfer> InProgressFileTransfers = new();
internal Resources Resources;
internal Logger Logger;
internal Security Security;
private bool _stopping = false;
private readonly Thread _listenerThread;
private readonly Timer _announceTimer = new();
private readonly Timer _playerUpdateTimer = new();
private readonly Timer _antiAssholesTimer = new();
private readonly Timer _updateTimer = new();
private readonly Worker _worker;
private readonly HashSet<char> _allowedCharacterSet;
private readonly Dictionary<int, Action<PacketType, NetIncomingMessage>> PendingResponses = new();
internal Dictionary<PacketType, Func<NetIncomingMessage, Client, Packet>> RequestHandlers = new();
/// <summary>
/// Get the current server version
/// </summary>
public static readonly Version Version = typeof(Server).Assembly.GetName().Version;
/// <summary>
/// Instantiate a server.
/// </summary>
/// <param name="settings"></param>
/// <param name="logger"></param>
/// <exception cref="ArgumentNullException"></exception>
public Server(Settings settings, Logger logger = null)
_worker = new Worker("ServerWorker", Logger);
_listenerThread = new Thread(() => Listen());
_announceTimer.Interval = 1;
_announceTimer.Elapsed += (s, e) =>
{
Settings = settings;
if (settings == null) { throw new ArgumentNullException("Server settings cannot be null!"); }
Logger = logger;
if (Logger != null) { Logger.LogLevel = Settings.LogLevel; }
API = new API(this);
Resources = new Resources(this);
Security = new Security(Logger);
Entities = new ServerEntities(this);
BaseScript = new BaseScript(this);
_allowedCharacterSet = new HashSet<char>(Settings.AllowedUsernameChars.ToCharArray());
_announceTimer.Interval = 10000;
_announceTimer.Stop();
Announce();
_announceTimer.Start();
};
_playerUpdateTimer.Interval = 1000;
_playerUpdateTimer.Elapsed += (s, e) => SendPlayerUpdate();
_worker = new Worker("ServerWorker", Logger);
_listenerThread = new Thread(() => Listen());
_announceTimer.Interval = 1;
_announceTimer.Elapsed += (s, e) =>
{
_announceTimer.Interval = 10000;
_announceTimer.Stop();
Announce();
_announceTimer.Start();
};
_playerUpdateTimer.Interval = 1000;
_playerUpdateTimer.Elapsed += (s, e) => SendPlayerUpdate();
_antiAssholesTimer.Interval = 5000;
_antiAssholesTimer.Elapsed += (s, e) => KickAssholes();
_antiAssholesTimer.Interval = 5000;
_antiAssholesTimer.Elapsed += (s, e) => KickAssholes();
_updateTimer.Interval = 1;
_updateTimer.Elapsed += (s, e) =>
{
_updateTimer.Interval = 1000 * 60 * 10; // 10 minutes
_updateTimer.Stop();
CheckUpdate();
_updateTimer.Start();
};
}
/// <summary>
/// The API for controlling server and hooking events.
/// </summary>
public API API { get; }
_updateTimer.Interval = 1;
_updateTimer.Elapsed += (s, e) =>
{
_updateTimer.Interval = 1000 * 60 * 10; // 10 minutes
_updateTimer.Stop();
CheckUpdate();
_updateTimer.Start();
};
/// <summary>
/// Spawn threads and start the server
/// </summary>
public void Start()
{
Logger?.Info("================");
Logger?.Info($"Listening port: {Settings.Port}");
Logger?.Info($"Server version: {Version}");
Logger?.Info($"Compatible client version: {Version.ToString(3)}");
Logger?.Info($"Runtime: {CoreUtils.GetInvariantRID()} => {RuntimeInformation.RuntimeIdentifier}");
Logger?.Info("================");
Logger?.Info("Listening addresses:");
foreach (var netInterface in NetworkInterface.GetAllNetworkInterfaces())
{
Logger?.Info($"[{netInterface.Description}]:");
var ipProps = netInterface.GetIPProperties();
foreach (var addr in ipProps.UnicastAddresses) Logger.Info(string.Join(", ", addr.Address));
Logger.Info("");
}
/// <summary>
/// Spawn threads and start the server
/// </summary>
public void Start()
if (Settings.UseZeroTier)
{
Logger?.Info("================");
Logger?.Info($"Listening port: {Settings.Port}");
Logger?.Info($"Server version: {Version}");
Logger?.Info($"Compatible client version: {Version.ToString(3)}");
Logger?.Info($"Runtime: {CoreUtils.GetInvariantRID()} => {System.Runtime.InteropServices.RuntimeInformation.RuntimeIdentifier}");
Logger?.Info("================");
Logger?.Info($"Listening addresses:");
foreach (NetworkInterface netInterface in NetworkInterface.GetAllNetworkInterfaces())
{
Logger?.Info($"[{netInterface.Description}]:");
IPInterfaceProperties ipProps = netInterface.GetIPProperties();
foreach (UnicastIPAddressInformation addr in ipProps.UnicastAddresses)
{
Logger.Info(string.Join(", ", addr.Address));
}
Logger.Info("");
}
if (Settings.UseZeroTier)
{
Logger?.Info($"Joining ZeroTier network: " + Settings.ZeroTierNetworkID);
if (ZeroTierHelper.Join(Settings.ZeroTierNetworkID) == null)
{
throw new Exception("Failed to obtain ZeroTier network IP");
}
}
else if (Settings.UseP2P)
{
Logger?.Warning("ZeroTier is not enabled, P2P connection may not work as expected.");
}
// 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP
NetPeerConfiguration config = new("623c92c287cc392406e7aaaac1c0f3b0")
{
Port = Settings.Port,
MaximumConnections = Settings.MaxPlayers,
EnableUPnP = false,
AutoFlushSendQueue = true,
PingInterval = 5
};
config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
MainNetServer = new NetServer(config);
MainNetServer.Start();
BaseScript.API = API;
BaseScript.OnStart();
Resources.LoadAll();
_listenerThread.Start();
Logger?.Info("Listening for clients");
_playerUpdateTimer.Enabled = true;
if (Settings.AnnounceSelf)
{
_announceTimer.Enabled = true;
}
if (Settings.AutoUpdate)
{
_updateTimer.Enabled = true;
}
_antiAssholesTimer.Enabled = true;
Logger?.Info("Joining ZeroTier network: " + Settings.ZeroTierNetworkID);
if (ZeroTierHelper.Join(Settings.ZeroTierNetworkID) == null)
throw new Exception("Failed to obtain ZeroTier network IP");
}
/// <summary>
/// Terminate threads and stop the server
/// </summary>
public void Stop()
else if (Settings.UseP2P)
{
Logger?.Flush();
Logger?.Dispose();
_stopping = true;
_listenerThread.Join();
_playerUpdateTimer.Enabled = false;
_announceTimer.Enabled = false;
_worker.Dispose();
}
internal void QueueJob(Action job)
{
_worker.QueueJob(job);
Logger?.Warning("ZeroTier is not enabled, P2P connection may not work as expected.");
}
// Send a message to targets or all players
internal void ChatMessageReceived(string name, string message, Client sender = null)
// 623c92c287cc392406e7aaaac1c0f3b0 = RAGECOOP
NetPeerConfiguration config = new("623c92c287cc392406e7aaaac1c0f3b0")
{
if (message.StartsWith('/'))
{
string[] cmdArgs = message.Split(" ");
string cmdName = cmdArgs[0].Remove(0, 1);
QueueJob(() => API.Events.InvokeOnCommandReceived(cmdName, cmdArgs, sender));
return;
}
message = message.Replace("~", "");
Port = Settings.Port,
MaximumConnections = Settings.MaxPlayers,
EnableUPnP = false,
AutoFlushSendQueue = true,
PingInterval = 5
};
QueueJob(() => API.Events.InvokeOnChatMessage(message, sender));
config.EnableMessageType(NetIncomingMessageType.ConnectionApproval);
config.EnableMessageType(NetIncomingMessageType.UnconnectedData);
foreach (var c in ClientsByNetHandle.Values)
{
var msg = MainNetServer.CreateMessage();
var crypt = new Func<string, byte[]>((s) =>
{
return Security.Encrypt(s.GetBytes(), c.EndPoint);
});
new Packets.ChatMessage(crypt)
{
Username = name,
Message = message
}.Pack(msg);
MainNetServer.SendMessage(msg, c.Connection, NetDeliveryMethod.ReliableOrdered, (int)ConnectionChannel.Chat);
}
MainNetServer = new NetServer(config);
MainNetServer.Start();
BaseScript.API = API;
BaseScript.OnStart();
Resources.LoadAll();
_listenerThread.Start();
Logger?.Info("Listening for clients");
_playerUpdateTimer.Enabled = true;
if (Settings.AnnounceSelf) _announceTimer.Enabled = true;
if (Settings.AutoUpdate) _updateTimer.Enabled = true;
_antiAssholesTimer.Enabled = true;
}
/// <summary>
/// Terminate threads and stop the server
/// </summary>
public void Stop()
{
Logger?.Flush();
Logger?.Dispose();
_stopping = true;
_listenerThread.Join();
_playerUpdateTimer.Enabled = false;
_announceTimer.Enabled = false;
_worker.Dispose();
}
internal void QueueJob(Action job)
{
_worker.QueueJob(job);
}
// Send a message to targets or all players
internal void ChatMessageReceived(string name, string message, Client sender = null)
{
if (message.StartsWith('/'))
{
var cmdArgs = message.Split(" ");
var cmdName = cmdArgs[0].Remove(0, 1);
QueueJob(() => API.Events.InvokeOnCommandReceived(cmdName, cmdArgs, sender));
return;
}
internal void SendChatMessage(string name, string message, Client target)
message = message.Replace("~", "");
QueueJob(() => API.Events.InvokeOnChatMessage(message, sender));
foreach (var c in ClientsByNetHandle.Values)
{
if (target == null) { return; }
var msg = MainNetServer.CreateMessage();
new Packets.ChatMessage(new Func<string, byte[]>((s) =>
{
return Security.Encrypt(s.GetBytes(), target.EndPoint);
}))
var crypt = new Func<string, byte[]>(s => { return Security.Encrypt(s.GetBytes(), c.EndPoint); });
new Packets.ChatMessage(crypt)
{
Username = name,
Message = message,
Message = message
}.Pack(msg);
MainNetServer.SendMessage(msg, target.Connection, NetDeliveryMethod.ReliableOrdered, (int)ConnectionChannel.Chat);
MainNetServer.SendMessage(msg, c.Connection, NetDeliveryMethod.ReliableOrdered,
(int)ConnectionChannel.Chat);
}
}
internal void RegisterCommand(string name, string usage, short argsLength, Action<CommandContext> callback)
internal void SendChatMessage(string name, string message, Client target)
{
if (target == null) return;
var msg = MainNetServer.CreateMessage();
new Packets.ChatMessage(s => { return Security.Encrypt(s.GetBytes(), target.EndPoint); })
{
Command command = new(name) { Usage = usage, ArgsLength = argsLength };
Username = name,
Message = message
}.Pack(msg);
MainNetServer.SendMessage(msg, target.Connection, NetDeliveryMethod.ReliableOrdered,
(int)ConnectionChannel.Chat);
}
if (Commands.ContainsKey(command))
{
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
}
internal void RegisterCommand(string name, string usage, short argsLength, Action<CommandContext> callback)
{
Command command = new(name) { Usage = usage, ArgsLength = argsLength };
Commands.Add(command, callback);
if (Commands.ContainsKey(command))
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
Commands.Add(command, callback);
}
internal void RegisterCommand(string name, Action<CommandContext> callback)
{
Command command = new(name);
if (Commands.ContainsKey(command))
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
Commands.Add(command, callback);
}
internal void RegisterCommands<T>()
{
var commands = typeof(T).GetMethods().Where(method => method.GetCustomAttributes(typeof(Command), false).Any());
foreach (var method in commands)
{
var attribute = method.GetCustomAttribute<Command>(true);
RegisterCommand(attribute.Name, attribute.Usage, attribute.ArgsLength,
(Action<CommandContext>)Delegate.CreateDelegate(typeof(Action<CommandContext>), method));
}
internal void RegisterCommand(string name, Action<CommandContext> callback)
}
internal T GetResponse<T>(Client client, Packet request,
ConnectionChannel channel = ConnectionChannel.RequestResponse, int timeout = 5000) where T : Packet, new()
{
if (Thread.CurrentThread == _listenerThread)
throw new InvalidOperationException("Cannot wait for response from the listener thread!");
var received = new AutoResetEvent(false);
var response = new T();
var id = NewRequestID();
PendingResponses.Add(id, (type, m) =>
{
Command command = new(name);
response.Deserialize(m);
received.Set();
});
var msg = MainNetServer.CreateMessage();
msg.Write((byte)PacketType.Request);
msg.Write(id);
request.Pack(msg);
MainNetServer.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableOrdered, (int)channel);
if (received.WaitOne(timeout)) return response;
if (Commands.ContainsKey(command))
{
throw new Exception("Command \"" + command.Name + "\" was already been registered!");
}
return null;
}
Commands.Add(command, callback);
}
internal void SendFile(string path, string name, Client client, Action<float> updateCallback = null)
{
var fs = File.OpenRead(path);
SendFile(fs, name, client, NewFileID(), updateCallback);
fs.Close();
fs.Dispose();
}
internal void RegisterCommands<T>()
{
IEnumerable<MethodInfo> commands = typeof(T).GetMethods().Where(method => method.GetCustomAttributes(typeof(Command), false).Any());
foreach (MethodInfo method in commands)
{
Command attribute = method.GetCustomAttribute<Command>(true);
RegisterCommand(attribute.Name, attribute.Usage, attribute.ArgsLength, (Action<CommandContext>)Delegate.CreateDelegate(typeof(Action<CommandContext>), method));
}
}
internal T GetResponse<T>(Client client, Packet request, ConnectionChannel channel = ConnectionChannel.RequestResponse, int timeout = 5000) where T : Packet, new()
{
if (Thread.CurrentThread == _listenerThread)
{
throw new InvalidOperationException("Cannot wait for response from the listener thread!");
}
var received = new AutoResetEvent(false);
T response = new T();
var id = NewRequestID();
PendingResponses.Add(id, (type, m) =>
{
response.Deserialize(m);
received.Set();
});
var msg = MainNetServer.CreateMessage();
msg.Write((byte)PacketType.Request);
msg.Write(id);
request.Pack(msg);
MainNetServer.SendMessage(msg, client.Connection, NetDeliveryMethod.ReliableOrdered, (int)channel);
if (received.WaitOne(timeout))
{
return response;
}
return null;
}
internal void SendFile(string path, string name, Client client, Action<float> updateCallback = null)
{
var fs = File.OpenRead(path);
SendFile(fs, name, client, NewFileID(), updateCallback);
fs.Close();
fs.Dispose();
}
internal void SendFile(Stream stream, string name, Client client, int id = default, Action<float> updateCallback = null)
{
stream.Seek(0, SeekOrigin.Begin);
id = id == default ? NewFileID() : id;
var total = stream.Length;
Logger?.Debug($"Requesting file transfer:{name}, {total}");
if (GetResponse<Packets.FileTransferResponse>(client, new Packets.FileTransferRequest()
internal void SendFile(Stream stream, string name, Client client, int id = default,
Action<float> updateCallback = null)
{
stream.Seek(0, SeekOrigin.Begin);
id = id == default ? NewFileID() : id;
var total = stream.Length;
Logger?.Debug($"Requesting file transfer:{name}, {total}");
if (GetResponse<Packets.FileTransferResponse>(client, new Packets.FileTransferRequest
{
FileLength = total,
Name = name,
ID = id,
ID = id
}, ConnectionChannel.File)?.Response != FileResponse.NeedToDownload)
{
Logger?.Info($"Skipping file transfer \"{name}\" to {client.Username}");
return;
}
Logger?.Debug($"Initiating file transfer:{name}, {total}");
FileTransfer transfer = new()
{
ID = id,
Name = name
};
InProgressFileTransfers.Add(id, transfer);
var read = 0;
int thisRead;
do
{
// 4 KB chunk
var chunk = new byte[4096];
read += thisRead = stream.Read(chunk, 0, 4096);
if (thisRead != chunk.Length)
{
Logger?.Info($"Skipping file transfer \"{name}\" to {client.Username}");
return;
if (thisRead == 0) break;
Logger?.Trace($"Purging chunk:{thisRead}");
Array.Resize(ref chunk, thisRead);
}
Logger?.Debug($"Initiating file transfer:{name}, {total}");
FileTransfer transfer = new()
{
ID = id,
Name = name,
};
InProgressFileTransfers.Add(id, transfer);
int read = 0;
int thisRead;
do
{
// 4 KB chunk
byte[] chunk = new byte[4096];
read += thisRead = stream.Read(chunk, 0, 4096);
if (thisRead != chunk.Length)
{
if (thisRead == 0) { break; }
Logger?.Trace($"Purging chunk:{thisRead}");
Array.Resize(ref chunk, thisRead);
}
Send(
new Packets.FileTransferChunk()
Send(
new Packets.FileTransferChunk
{
ID = id,
FileChunk = chunk,
FileChunk = chunk
},
client, ConnectionChannel.File, NetDeliveryMethod.ReliableOrdered);
transfer.Progress = read / stream.Length;
if (updateCallback != null) { updateCallback(transfer.Progress); }
transfer.Progress = read / stream.Length;
if (updateCallback != null) updateCallback(transfer.Progress);
} while (thisRead > 0);
} while (thisRead > 0);
if (GetResponse<Packets.FileTransferResponse>(client, new Packets.FileTransferComplete()
if (GetResponse<Packets.FileTransferResponse>(client, new Packets.FileTransferComplete
{
ID = id,
ID = id
}, ConnectionChannel.File)?.Response != FileResponse.Completed)
{
Logger.Warning($"File trasfer to {client.Username} failed: " + name);
}
Logger?.Debug($"All file chunks sent:{name}");
InProgressFileTransfers.Remove(id);
}
internal int NewFileID()
{
int ID = 0;
while ((ID == 0)
|| InProgressFileTransfers.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
private int NewRequestID()
{
int ID = 0;
while ((ID == 0)
|| PendingResponses.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
internal void Send(Packet p, Client client, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, client.Connection, method, (int)channel);
}
internal void Forward(Packet p, Client except, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, except.Connection, method, (int)channel);
}
internal void SendToAll(Packet p, ConnectionChannel channel = ConnectionChannel.Default, NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
NetOutgoingMessage outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, method, (int)channel);
}
Logger.Warning($"File trasfer to {client.Username} failed: " + name);
Logger?.Debug($"All file chunks sent:{name}");
InProgressFileTransfers.Remove(id);
}
}
internal int NewFileID()
{
var ID = 0;
while (ID == 0
|| InProgressFileTransfers.ContainsKey(ID))
{
var rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
private int NewRequestID()
{
var ID = 0;
while (ID == 0
|| PendingResponses.ContainsKey(ID))
{
var rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
internal void Send(Packet p, Client client, ConnectionChannel channel = ConnectionChannel.Default,
NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
var outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendMessage(outgoingMessage, client.Connection, method, (int)channel);
}
internal void Forward(Packet p, Client except, ConnectionChannel channel = ConnectionChannel.Default,
NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
var outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, except.Connection, method, (int)channel);
}
internal void SendToAll(Packet p, ConnectionChannel channel = ConnectionChannel.Default,
NetDeliveryMethod method = NetDeliveryMethod.UnreliableSequenced)
{
var outgoingMessage = MainNetServer.CreateMessage();
p.Pack(outgoingMessage);
MainNetServer.SendToAll(outgoingMessage, method, (int)channel);
}
}

View File

@ -1,111 +1,107 @@
using RageCoop.Core;
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using RageCoop.Core;
namespace RageCoop.Server
namespace RageCoop.Server;
internal class Program
{
internal class Program
private static bool Stopping;
private static Logger mainLogger;
private static void Main(string[] args)
{
private static bool Stopping = false;
private static Logger mainLogger;
private static void Main(string[] args)
if (args.Length >= 2 && args[0] == "update")
{
if (args.Length >= 2 && args[0] == "update")
{
var target = args[1];
int i = 0;
while (i++ < 10)
var target = args[1];
var i = 0;
while (i++ < 10)
try
{
try
{
Console.WriteLine("Applying update to " + target);
Console.WriteLine("Applying update to " + target);
CoreUtils.CopyFilesRecursively(new(AppDomain.CurrentDomain.BaseDirectory), new(target));
Process.Start(Path.Combine(target, "RageCoop.Server"));
CoreUtils.CopyFilesRecursively(new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory),
new DirectoryInfo(target));
Process.Start(Path.Combine(target, "RageCoop.Server"));
Environment.Exit(0);
}
catch (Exception ex)
{
Console.WriteLine(ex.ToString());
Thread.Sleep(3000);
}
Environment.Exit(i);
}
AppDomain.CurrentDomain.UnhandledException += UnhandledException;
mainLogger = new Logger
{
Name = "Server"
};
mainLogger.Writers.Add(CoreUtils.OpenWriter("RageCoop.Server.log"));
try
{
Console.Title = "RAGECOOP";
var setting = Util.Read<Settings>("Settings.xml");
#if DEBUG
setting.LogLevel = 0;
#endif
var server = new Server(setting, mainLogger);
Console.CancelKeyPress += delegate(object sender, ConsoleCancelEventArgs e)
{
mainLogger.Info("Initiating shutdown sequence...");
mainLogger.Info("Press Ctrl+C again to commence an emergency shutdown.");
if (e.SpecialKey == ConsoleSpecialKey.ControlC)
{
if (!Stopping)
{
e.Cancel = true;
Stopping = true;
server.Stop();
mainLogger.Info("Server stopped.");
mainLogger.Dispose();
Thread.Sleep(1000);
Environment.Exit(0);
}
catch (Exception ex)
else
{
Console.WriteLine(ex.ToString());
Thread.Sleep(3000);
mainLogger.Flush();
Environment.Exit(1);
}
}
Environment.Exit(i);
}
AppDomain.CurrentDomain.UnhandledException += UnhandledException;
mainLogger = new Logger()
{
Name = "Server"
};
mainLogger.Writers.Add(CoreUtils.OpenWriter("RageCoop.Server.log"));
try
server.Start();
mainLogger?.Info("Please use CTRL + C if you want to stop the server!");
mainLogger?.Info("Type here to send chat messages or execute commands");
mainLogger?.Flush();
while (true)
{
Console.Title = "RAGECOOP";
var setting = Util.Read<Settings>("Settings.xml");
#if DEBUG
setting.LogLevel = 0;
#endif
var server = new Server(setting, mainLogger);
Console.CancelKeyPress += delegate (object sender, ConsoleCancelEventArgs e)
{
mainLogger.Info("Initiating shutdown sequence...");
mainLogger.Info("Press Ctrl+C again to commence an emergency shutdown.");
if (e.SpecialKey == ConsoleSpecialKey.ControlC)
{
if (!Stopping)
{
e.Cancel = true;
Stopping = true;
server.Stop();
mainLogger.Info("Server stopped.");
mainLogger.Dispose();
Thread.Sleep(1000);
Environment.Exit(0);
}
else
{
mainLogger.Flush();
Environment.Exit(1);
}
}
};
server.Start();
mainLogger?.Info("Please use CTRL + C if you want to stop the server!");
mainLogger?.Info("Type here to send chat messages or execute commands");
mainLogger?.Flush();
while (true)
{
var s = Console.ReadLine();
if (!Stopping && s != null)
{
server.ChatMessageReceived("Server", s, null);
}
Thread.Sleep(20);
}
}
catch (Exception e)
{
Fatal(e);
var s = Console.ReadLine();
if (!Stopping && s != null) server.ChatMessageReceived("Server", s);
Thread.Sleep(20);
}
}
private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
catch (Exception e)
{
mainLogger.Error($"Unhandled exception thrown from user thread", e.ExceptionObject as Exception);
mainLogger.Flush();
}
private static void Fatal(Exception e)
{
mainLogger.Error(e);
mainLogger.Error($"Fatal error occurred, server shutting down.");
mainLogger.Flush();
Thread.Sleep(5000);
Environment.Exit(1);
Fatal(e);
}
}
}
private static void UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
mainLogger.Error("Unhandled exception thrown from user thread", e.ExceptionObject as Exception);
mainLogger.Flush();
}
private static void Fatal(Exception e)
{
mainLogger.Error(e);
mainLogger.Error("Fatal error occurred, server shutting down.");
mainLogger.Flush();
Thread.Sleep(5000);
Environment.Exit(1);
}
}

View File

@ -1,7 +1,4 @@

using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Resources;
// General Information
@ -15,7 +12,6 @@ using System.Resources;
[assembly: AssemblyCulture("")]
// Version information
[assembly: AssemblyVersion("1.5.6.12")]
[assembly: AssemblyFileVersion("1.5.6.12")]
[assembly: NeutralResourcesLanguageAttribute( "en-US" )]
[assembly: AssemblyVersion("1.5.8.15")]
[assembly: AssemblyFileVersion("1.5.8.15")]
[assembly: NeutralResourcesLanguageAttribute("en-US")]

View File

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

View File

@ -1,96 +1,101 @@
using RageCoop.Core.Scripting;
using System;
using System;
using System.Linq;
using RageCoop.Core.Scripting;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
internal class BaseScript : ServerScript
{
internal class BaseScript : ServerScript
private readonly Server Server;
public BaseScript(Server server)
{
private readonly Server Server;
public BaseScript(Server server) { Server = server; }
public override void OnStart()
{
API.RegisterCustomEventHandler(CustomEvents.NativeResponse, NativeResponse);
API.RegisterCustomEventHandler(CustomEvents.OnVehicleDeleted, (e) =>
{
API.Entities.RemoveVehicle((int)e.Args[0]);
});
API.RegisterCustomEventHandler(CustomEvents.OnPedDeleted, (e) =>
{
API.Entities.RemovePed((int)e.Args[0]);
});
API.RegisterCustomEventHandler(CustomEvents.WeatherTimeSync, (e) =>
{
if (Server.Settings.WeatherTimeSync)
{
if (e.Client != API.Host) { e.Client.SendCustomEvent(CustomEvents.IsHost, false); return; }
Server = server;
}
foreach (var c in API.GetAllClients().Values)
{
if (c == e.Client)
{
continue;
}
c.SendCustomEventQueued(CustomEvents.WeatherTimeSync, e.Args);
}
public override void OnStart()
{
API.RegisterCustomEventHandler(CustomEvents.NativeResponse, NativeResponse);
API.RegisterCustomEventHandler(CustomEvents.OnVehicleDeleted,
e => { API.Entities.RemoveVehicle((int)e.Args[0]); });
API.RegisterCustomEventHandler(CustomEvents.OnPedDeleted, e => { API.Entities.RemovePed((int)e.Args[0]); });
API.RegisterCustomEventHandler(CustomEvents.WeatherTimeSync, e =>
{
if (Server.Settings.WeatherTimeSync)
{
if (e.Client != API.Host)
{
e.Client.SendCustomEvent(CustomEvents.IsHost, false);
return;
}
});
API.RegisterCustomEventHandler(CustomEvents.OnPlayerDied, (e) =>
{
API.SendCustomEventQueued(API.GetAllClients().Values.Where(x => x != e.Client).ToList(), CustomEvents.OnPlayerDied, e.Client.Username);
});
API.Events.OnChatMessage += (s, e) =>
Server.Logger?.Info((e.Client?.Username ?? e.ClaimedSender ?? "Unknown") + ": " + e.Message);
}
public override void OnStop()
{
}
public static void SetAutoRespawn(Client c, bool toggle)
{
c.SendCustomEvent(CustomEvents.SetAutoRespawn, toggle);
}
public void SetNameTag(Client c, bool toggle)
{
foreach (var other in API.GetAllClients().Values)
{
if (c == other) { continue; }
other.SendCustomEvent(CustomEvents.SetDisplayNameTag, c.Player.ID, toggle);
}
}
public void SendServerPropsTo(List<ServerProp> objects, List<Client> clients = null)
{
foreach (var obj in objects)
{
API.SendCustomEventQueued(clients, CustomEvents.ServerPropSync, obj.ID, obj.Model, obj.Position, obj.Rotation);
}
}
public void SendServerBlipsTo(List<ServerBlip> objects, List<Client> clients = null)
{
foreach (var obj in objects)
{
API.SendCustomEventQueued(clients, CustomEvents.ServerBlipSync, obj.ID, (ushort)obj.Sprite, (byte)obj.Color, obj.Scale, obj.Position, obj.Rotation, obj.Name);
}
}
private void NativeResponse(CustomEventReceivedArgs e)
{
try
{
int id = (int)e.Args[0];
lock (e.Client.Callbacks)
foreach (var c in API.GetAllClients().Values)
{
if (e.Client.Callbacks.TryGetValue(id, out Action<object> callback))
{
callback(e.Args[1]);
e.Client.Callbacks.Remove(id);
}
if (c == e.Client) continue;
c.SendCustomEventQueued(CustomEvents.WeatherTimeSync, e.Args);
}
}
catch (Exception ex)
});
API.RegisterCustomEventHandler(CustomEvents.OnPlayerDied,
e =>
{
API.Logger.Error("Failed to parse NativeResponse");
API.Logger.Error(ex);
}
API.SendCustomEventQueued(API.GetAllClients().Values.Where(x => x != e.Client).ToList(),
CustomEvents.OnPlayerDied, e.Client.Username);
});
API.Events.OnChatMessage += (s, e) =>
Server.Logger?.Info((e.Client?.Username ?? e.ClaimedSender ?? "Unknown") + ": " + e.Message);
}
public override void OnStop()
{
}
public static void SetAutoRespawn(Client c, bool toggle)
{
c.SendCustomEvent(CustomEvents.SetAutoRespawn, toggle);
}
public void SetNameTag(Client c, bool toggle)
{
foreach (var other in API.GetAllClients().Values)
{
if (c == other) continue;
other.SendCustomEvent(CustomEvents.SetDisplayNameTag, c.Player.ID, toggle);
}
}
}
public void SendServerPropsTo(List<ServerProp> objects, List<Client> clients = null)
{
foreach (var obj in objects)
API.SendCustomEventQueued(clients, CustomEvents.ServerPropSync, obj.ID, obj.Model, obj.Position,
obj.Rotation);
}
public void SendServerBlipsTo(List<ServerBlip> objects, List<Client> clients = null)
{
foreach (var obj in objects)
API.SendCustomEventQueued(clients, CustomEvents.ServerBlipSync, obj.ID, (ushort)obj.Sprite, (byte)obj.Color,
obj.Scale, obj.Position, obj.Rotation, obj.Name);
}
private void NativeResponse(CustomEventReceivedArgs e)
{
try
{
var id = (int)e.Args[0];
lock (e.Client.Callbacks)
{
if (e.Client.Callbacks.TryGetValue(id, out var callback))
{
callback(e.Args[1]);
e.Client.Callbacks.Remove(id);
}
}
}
catch (Exception ex)
{
API.Logger.Error("Failed to parse NativeResponse");
API.Logger.Error(ex);
}
}
}

View File

@ -1,102 +1,109 @@
using System;
using System.Net;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
/// <summary>
/// </summary>
public class ChatEventArgs : EventArgs
{
/// <summary>
///
/// The client that sent this message, will be null if sent from server
/// </summary>
public class ChatEventArgs : EventArgs
{
/// <summary>
/// The client that sent this message, will be null if sent from server
/// </summary>
public Client Client { get; set; }
/// <summary>
/// Message
/// </summary>
public string Message { get; set; }
public Client Client { get; set; }
/// <summary>
/// Only used when sending a message via <see cref="API.SendChatMessage(string, List{Client}, string, bool?)"/>
/// </summary>
public string ClaimedSender { get; set; }
}
/// <summary>
///
/// Message
/// </summary>
public class CustomEventReceivedArgs : EventArgs
{
/// <summary>
/// The <see cref="RageCoop.Server.Client"/> that triggered this event
/// </summary>
public Client Client { get; set; }
public string Message { get; set; }
/// <summary>
/// The event hash
/// </summary>
public int Hash { get; set; }
/// <summary>
/// Supported types: byte, short, ushort, int, uint, long, ulong, float, bool, string, Vector3, Quaternion, Vector2 <see cref="ServerObject.Handle"/>
/// </summary>
public object[] Args { get; set; }
}
/// <summary>
///
/// Only used when sending a message via <see cref="API.SendChatMessage(string, List{Client}, string, bool?)" />
/// </summary>
public class OnCommandEventArgs : EventArgs
{
/// <summary>
/// The <see cref="RageCoop.Server.Client"/> that executed this command, will be null if sent from server.
/// </summary>
public Client Client { get; set; }
/// <summary>
/// The name of executed command
/// </summary>
public string Name { get; set; }
/// <summary>
/// Arguments
/// </summary>
public string[] Args { get; set; }
/// <summary>
/// If this value was set to true, corresponding handler registered with <see cref="API.RegisterCommand(string, Action{CommandContext})"/> will not be invoked.
/// </summary>
public bool Cancel { get; set; } = false;
}
/// <summary>
///
/// </summary>
public class HandshakeEventArgs : EventArgs
{
/// <summary>
/// The player's ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// The claimed username
/// </summary>
public string Username { get; set; }
/// <summary>
/// The client password hashed with SHA256 algorithm.
/// </summary>
public string PasswordHash { get; set; }
/// <summary>
/// The <see cref="EndPoint"/> that sent the handshake request.
/// </summary>
public IPEndPoint EndPoint { get; set; }
/// <summary>
/// Deny the connection attempt
/// </summary>
/// <param name="reason"></param>
public void Deny(string reason)
{
DenyReason = reason;
Cancel = true;
}
internal string DenyReason { get; set; }
internal bool Cancel { get; set; } = false;
}
public string ClaimedSender { get; set; }
}
/// <summary>
/// </summary>
public class CustomEventReceivedArgs : EventArgs
{
/// <summary>
/// The <see cref="RageCoop.Server.Client" /> that triggered this event
/// </summary>
public Client Client { get; set; }
/// <summary>
/// The event hash
/// </summary>
public int Hash { get; set; }
/// <summary>
/// Supported types: byte, short, ushort, int, uint, long, ulong, float, bool, string, Vector3, Quaternion, Vector2
/// <see cref="ServerObject.Handle" />
/// </summary>
public object[] Args { get; set; }
}
/// <summary>
/// </summary>
public class OnCommandEventArgs : EventArgs
{
/// <summary>
/// The <see cref="RageCoop.Server.Client" /> that executed this command, will be null if sent from server.
/// </summary>
public Client Client { get; set; }
/// <summary>
/// The name of executed command
/// </summary>
public string Name { get; set; }
/// <summary>
/// Arguments
/// </summary>
public string[] Args { get; set; }
/// <summary>
/// If this value was set to true, corresponding handler registered with
/// <see cref="API.RegisterCommand(string, Action{CommandContext})" /> will not be invoked.
/// </summary>
public bool Cancel { get; set; } = false;
}
/// <summary>
/// </summary>
public class HandshakeEventArgs : EventArgs
{
/// <summary>
/// The player's ID
/// </summary>
public int ID { get; set; }
/// <summary>
/// The claimed username
/// </summary>
public string Username { get; set; }
/// <summary>
/// The client password hashed with SHA256 algorithm.
/// </summary>
public string PasswordHash { get; set; }
/// <summary>
/// The <see cref="EndPoint" /> that sent the handshake request.
/// </summary>
public IPEndPoint EndPoint { get; set; }
internal string DenyReason { get; set; }
internal bool Cancel { get; set; }
/// <summary>
/// Deny the connection attempt
/// </summary>
/// <param name="reason"></param>
public void Deny(string reason)
{
DenyReason = reason;
Cancel = true;
}
}

View File

@ -1,281 +1,283 @@
using DiscUtils.Iso9660;
using ICSharpCode.SharpZipLib.Zip;
using RageCoop.Core;
using System;
using System;
using System.IO;
using System.Threading.Tasks;
using DiscUtils.Iso9660;
using ICSharpCode.SharpZipLib.Zip;
using RageCoop.Core;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
internal class Resources
{
internal class Resources
private readonly Dictionary<string, Stream> ClientResources = new();
private readonly Logger Logger;
private readonly Dictionary<CDReader, Stream> Packages = new();
private readonly Dictionary<string, Stream> ResourceStreams = new();
private readonly Server Server;
public Dictionary<string, ServerResource> LoadedResources = new();
public Resources(Server server)
{
public Dictionary<string, ServerResource> LoadedResources = new();
private readonly Server Server;
private readonly Logger Logger;
public bool IsLoaded { get; private set; } = false;
public Resources(Server server)
Server = server;
Logger = server.Logger;
}
public bool IsLoaded { get; private set; }
public void LoadAll()
{
// Packages
{
Server = server;
Logger = server.Logger;
var path = Path.Combine("Resources", "Packages");
Directory.CreateDirectory(path);
foreach (var pkg in Directory.GetFiles(path, "*.respkg", SearchOption.AllDirectories))
try
{
Logger?.Debug($"Adding resources from package \"{Path.GetFileNameWithoutExtension(pkg)}\"");
var pkgStream = File.Open(pkg, FileMode.Open, FileAccess.Read, FileShare.Read);
var reader = new CDReader(pkgStream, true);
foreach (var e in reader.Root.GetDirectories("Client")[0].GetFiles())
{
var name = Path.GetFileNameWithoutExtension(e.Name);
if (!ClientResources.TryAdd(name, e.Open(FileMode.Open)))
{
Logger?.Warning($"Resource \"{name}\" already added, ignoring");
continue;
}
Logger?.Debug("Resource added: " + name);
}
foreach (var e in reader.Root.GetDirectories("Server")[0].GetFiles())
{
var name = Path.GetFileNameWithoutExtension(e.Name);
if (!ResourceStreams.TryAdd(name, e.Open(FileMode.Open)))
{
Logger?.Warning($"Resource \"{name}\" already loaded, ignoring");
continue;
}
Logger?.Debug("Resource added: " + name);
}
Packages.Add(reader, pkgStream);
}
catch (Exception ex)
{
Logger?.Error("Failed to read resources from package: " + pkg, ex);
}
}
private readonly Dictionary<CDReader, Stream> Packages = new();
private readonly Dictionary<string, Stream> ClientResources = new();
private readonly Dictionary<string, Stream> ResourceStreams = new();
public void LoadAll()
// Client
{
// Packages
{
var path = Path.Combine("Resources", "Packages");
Directory.CreateDirectory(path);
foreach (var pkg in Directory.GetFiles(path, "*.respkg", SearchOption.AllDirectories))
var path = Path.Combine("Resources", "Client");
var tmpDir = Path.Combine("Resources", "Temp", "Client");
Directory.CreateDirectory(path);
if (Directory.Exists(tmpDir)) Directory.Delete(tmpDir, true);
Directory.CreateDirectory(tmpDir);
var resourceFolders = Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly);
if (resourceFolders.Length != 0)
foreach (var resourceFolder in resourceFolders)
{
var zipPath = Path.Combine(tmpDir, Path.GetFileName(resourceFolder)) + ".res";
var name = Path.GetFileNameWithoutExtension(zipPath);
if (ClientResources.ContainsKey(name))
{
Logger?.Warning($"Client resource \"{name}\" already loaded, ignoring");
continue;
}
// Pack client side resource as a zip file
Logger?.Info("Packing client-side resource: " + resourceFolder);
try
{
Logger?.Debug($"Adding resources from package \"{Path.GetFileNameWithoutExtension(pkg)}\"");
var pkgStream = File.Open(pkg, FileMode.Open, FileAccess.Read, FileShare.Read);
var reader = new CDReader(pkgStream, true);
foreach (var e in reader.Root.GetDirectories("Client")[0].GetFiles())
using var zip = ZipFile.Create(zipPath);
zip.BeginUpdate();
foreach (var dir in Directory.GetDirectories(resourceFolder, "*", SearchOption.AllDirectories))
zip.AddDirectory(dir[(resourceFolder.Length + 1)..]);
foreach (var file in Directory.GetFiles(resourceFolder, "*", SearchOption.AllDirectories))
{
var name = Path.GetFileNameWithoutExtension(e.Name);
if (!ClientResources.TryAdd(name, e.Open(FileMode.Open)))
{
Logger?.Warning($"Resource \"{name}\" already added, ignoring");
continue;
}
Logger?.Debug("Resource added: " + name);
if (Path.GetFileName(file).CanBeIgnored()) continue;
zip.Add(file, file[(resourceFolder.Length + 1)..]);
}
foreach (var e in reader.Root.GetDirectories("Server")[0].GetFiles())
{
var name = Path.GetFileNameWithoutExtension(e.Name);
if (!ResourceStreams.TryAdd(name, e.Open(FileMode.Open)))
{
Logger?.Warning($"Resource \"{name}\" already loaded, ignoring");
continue;
}
Logger?.Debug("Resource added: " + name);
}
Packages.Add(reader, pkgStream);
zip.CommitUpdate();
zip.Close();
ClientResources.Add(name, File.OpenRead(zipPath));
}
catch (Exception ex)
{
Logger?.Error("Failed to read resources from package: " + pkg, ex);
}
}
}
// Client
{
var path = Path.Combine("Resources", "Client");
var tmpDir = Path.Combine("Resources", "Temp", "Client");
Directory.CreateDirectory(path);
if (Directory.Exists(tmpDir))
{
Directory.Delete(tmpDir, true);
}
Directory.CreateDirectory(tmpDir);
var resourceFolders = Directory.GetDirectories(path, "*", SearchOption.TopDirectoryOnly);
if (resourceFolders.Length != 0)
{
foreach (var resourceFolder in resourceFolders)
{
var zipPath = Path.Combine(tmpDir, Path.GetFileName(resourceFolder)) + ".res";
var name = Path.GetFileNameWithoutExtension(zipPath);
if (ClientResources.ContainsKey(name))
{
Logger?.Warning($"Client resource \"{name}\" already loaded, ignoring");
continue;
}
// Pack client side resource as a zip file
Logger?.Info("Packing client-side resource: " + resourceFolder);
try
{
using ZipFile zip = ZipFile.Create(zipPath);
zip.BeginUpdate();
foreach (var dir in Directory.GetDirectories(resourceFolder, "*", SearchOption.AllDirectories))
{
zip.AddDirectory(dir[(resourceFolder.Length + 1)..]);
}
foreach (var file in Directory.GetFiles(resourceFolder, "*", SearchOption.AllDirectories))
{
if (Path.GetFileName(file).CanBeIgnored()) { continue; }
zip.Add(file, file[(resourceFolder.Length + 1)..]);
}
zip.CommitUpdate();
zip.Close();
ClientResources.Add(name, File.OpenRead(zipPath));
}
catch (Exception ex)
{
Logger?.Error($"Failed to pack client resource:{resourceFolder}");
Logger?.Error(ex);
}
}
}
}
// Server
{
var path = Path.Combine("Resources", "Server");
var dataFolder = Path.Combine(path, "data");
Directory.CreateDirectory(path);
foreach (var resource in Directory.GetDirectories(path))
{
try
{
var name = Path.GetFileName(resource);
if (LoadedResources.ContainsKey(name))
{
Logger?.Warning($"Resource \"{name}\" has already been loaded, ignoring...");
continue;
}
if (name.ToLower() == "data") { continue; }
Logger?.Info($"Loading resource: {name}");
var r = ServerResource.LoadFrom(resource, dataFolder, Logger);
LoadedResources.Add(r.Name, r);
}
catch (Exception ex)
{
Logger?.Error($"Failed to load resource: {Path.GetFileName(resource)}");
Logger?.Error($"Failed to pack client resource:{resourceFolder}");
Logger?.Error(ex);
}
}
foreach (var res in ResourceStreams)
{
try
{
var name = res.Key;
if (LoadedResources.ContainsKey(name))
{
Logger?.Warning($"Resource \"{name}\" has already been loaded, ignoring...");
continue;
}
Logger?.Info($"Loading resource: " + name);
var r = ServerResource.LoadFrom(res.Value, name, Path.Combine("Resources", "Temp", "Server"), dataFolder, Logger);
LoadedResources.Add(r.Name, r);
}
catch (Exception ex)
{
Logger?.Error($"Failed to load resource: {res.Key}");
Logger?.Error(ex);
}
}
// Start scripts
lock (LoadedResources)
{
foreach (var r in LoadedResources.Values)
{
foreach (ServerScript s in r.Scripts.Values)
{
s.API = Server.API;
try
{
Logger?.Debug("Starting script:" + s.CurrentFile.Name);
s.OnStart();
}
catch (Exception ex) { Logger?.Error($"Failed to start resource: {r.Name}"); Logger?.Error(ex); }
}
}
}
IsLoaded = true;
}
}
public void UnloadAll()
// Server
{
var path = Path.Combine("Resources", "Server");
var dataFolder = Path.Combine(path, "data");
Directory.CreateDirectory(path);
foreach (var resource in Directory.GetDirectories(path))
try
{
var name = Path.GetFileName(resource);
if (LoadedResources.ContainsKey(name))
{
Logger?.Warning($"Resource \"{name}\" has already been loaded, ignoring...");
continue;
}
if (name.ToLower() == "data") continue;
Logger?.Info($"Loading resource: {name}");
var r = ServerResource.LoadFrom(resource, dataFolder, Logger);
LoadedResources.Add(r.Name, r);
}
catch (Exception ex)
{
Logger?.Error($"Failed to load resource: {Path.GetFileName(resource)}");
Logger?.Error(ex);
}
foreach (var res in ResourceStreams)
try
{
var name = res.Key;
if (LoadedResources.ContainsKey(name))
{
Logger?.Warning($"Resource \"{name}\" has already been loaded, ignoring...");
continue;
}
Logger?.Info("Loading resource: " + name);
var r = ServerResource.LoadFrom(res.Value, name, Path.Combine("Resources", "Temp", "Server"),
dataFolder, Logger);
LoadedResources.Add(r.Name, r);
}
catch (Exception ex)
{
Logger?.Error($"Failed to load resource: {res.Key}");
Logger?.Error(ex);
}
// Start scripts
lock (LoadedResources)
{
foreach (var d in LoadedResources.Values)
foreach (var r in LoadedResources.Values)
foreach (var s in r.Scripts.Values)
{
foreach (var s in d.Scripts.Values)
{
try
{
s.OnStop();
}
catch (Exception ex)
{
Logger?.Error(ex);
}
}
s.API = Server.API;
try
{
d.Dispose();
Logger?.Debug("Starting script:" + s.CurrentFile.Name);
s.OnStart();
}
catch (Exception ex)
{
Logger.Error($"Resource \"{d.Name}\" cannot be unloaded.");
Logger.Error(ex);
Logger?.Error($"Failed to start resource: {r.Name}");
Logger?.Error(ex);
}
}
LoadedResources.Clear();
}
foreach (var s in ClientResources.Values)
{
try
{
s.Close();
s.Dispose();
}
catch (Exception ex)
{
Logger?.Error("[Resources.CloseStream]", ex);
}
}
foreach (var s in ResourceStreams.Values)
{
try
{
s.Close();
s.Dispose();
}
catch (Exception ex)
{
Logger?.Error("[Resources.CloseStream]", ex);
}
}
foreach (var r in Packages)
{
r.Key.Dispose();
r.Value.Dispose();
}
}
public void SendTo(Client client)
{
Task.Run(() =>
{
try
{
if (ClientResources.Count != 0)
{
Logger?.Info($"Sending resources to client:{client.Username}");
foreach (var rs in ClientResources)
{
Logger?.Debug(rs.Key);
Server.SendFile(rs.Value, rs.Key + ".res", client);
}
Logger?.Info($"Resources sent to:{client.Username}");
}
if (Server.GetResponse<Packets.FileTransferResponse>(client, new Packets.AllResourcesSent(), ConnectionChannel.RequestResponse, 30000)?.Response == FileResponse.Loaded)
{
client.IsReady = true;
Server.API.Events.InvokePlayerReady(client);
}
else
{
Logger?.Warning($"Client {client.Username} failed to load resource.");
}
}
catch (Exception ex)
{
Logger.Error("Failed to send resource to client: " + client.Username, ex);
client.Kick("Resource error!");
}
});
IsLoaded = true;
}
}
}
public void UnloadAll()
{
lock (LoadedResources)
{
foreach (var d in LoadedResources.Values)
{
foreach (var s in d.Scripts.Values)
try
{
s.OnStop();
}
catch (Exception ex)
{
Logger?.Error(ex);
}
try
{
d.Dispose();
}
catch (Exception ex)
{
Logger.Error($"Resource \"{d.Name}\" cannot be unloaded.");
Logger.Error(ex);
}
}
LoadedResources.Clear();
}
foreach (var s in ClientResources.Values)
try
{
s.Close();
s.Dispose();
}
catch (Exception ex)
{
Logger?.Error("[Resources.CloseStream]", ex);
}
foreach (var s in ResourceStreams.Values)
try
{
s.Close();
s.Dispose();
}
catch (Exception ex)
{
Logger?.Error("[Resources.CloseStream]", ex);
}
foreach (var r in Packages)
{
r.Key.Dispose();
r.Value.Dispose();
}
}
public void SendTo(Client client)
{
Task.Run(() =>
{
try
{
if (ClientResources.Count != 0)
{
Logger?.Info($"Sending resources to client:{client.Username}");
foreach (var rs in ClientResources)
{
Logger?.Debug(rs.Key);
Server.SendFile(rs.Value, rs.Key + ".res", client);
}
Logger?.Info($"Resources sent to:{client.Username}");
}
if (Server.GetResponse<Packets.FileTransferResponse>(client, new Packets.AllResourcesSent(),
ConnectionChannel.RequestResponse, 30000)?.Response == FileResponse.Loaded)
{
client.IsReady = true;
Server.API.Events.InvokePlayerReady(client);
}
else
{
Logger?.Warning($"Client {client.Username} failed to load resource.");
}
}
catch (Exception ex)
{
Logger.Error("Failed to send resource to client: " + client.Username, ex);
client.Kick("Resource error!");
}
});
}
}

View File

@ -1,263 +1,279 @@
using GTA;
using GTA.Math;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Security.Cryptography;
using GTA;
using GTA.Math;
using RageCoop.Core;
using RageCoop.Core.Scripting;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
/// <summary>
/// Manipulate entities from the server
/// </summary>
public class ServerEntities
{
private readonly Server Server;
internal ServerEntities(Server server)
{
Server = server;
}
internal ConcurrentDictionary<int, ServerPed> Peds { get; set; } = new();
internal ConcurrentDictionary<int, ServerVehicle> Vehicles { get; set; } = new();
internal ConcurrentDictionary<int, ServerProp> ServerProps { get; set; } = new();
internal ConcurrentDictionary<int, ServerBlip> Blips { get; set; } = new();
/// <summary>
/// Manipulate entities from the server
/// Get a <see cref="ServerPed" /> by it's id
/// </summary>
public class ServerEntities
/// <param name="id"></param>
/// <returns></returns>
public ServerPed GetPedByID(int id)
{
private readonly Server Server;
internal ServerEntities(Server server)
return Peds.TryGetValue(id, out var ped) ? ped : null;
}
/// <summary>
/// Get a <see cref="ServerVehicle" /> by it's id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerVehicle GetVehicleByID(int id)
{
return Vehicles.TryGetValue(id, out var veh) ? veh : null;
}
/// <summary>
/// Get a <see cref="ServerProp" /> owned by server from it's ID.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerProp GetPropByID(int id)
{
return ServerProps.TryGetValue(id, out var obj) ? obj : null;
}
/// <summary>
/// Get a <see cref="ServerBlip" /> by it's id.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerBlip GetBlipByID(int id)
{
return Blips.TryGetValue(id, out var obj) ? obj : null;
}
/// <summary>
/// Create a static prop owned by server.
/// </summary>
/// <param name="model"></param>
/// <param name="pos"></param>
/// <param name="rot"></param>
/// <returns></returns>
public ServerProp CreateProp(Model model, Vector3 pos, Vector3 rot)
{
var id = RequestNetworkID();
ServerProp prop;
ServerProps.TryAdd(id, prop = new ServerProp(Server)
{
Server = server;
}
internal ConcurrentDictionary<int, ServerPed> Peds { get; set; } = new();
internal ConcurrentDictionary<int, ServerVehicle> Vehicles { get; set; } = new();
internal ConcurrentDictionary<int, ServerProp> ServerProps { get; set; } = new();
internal ConcurrentDictionary<int, ServerBlip> Blips { get; set; } = new();
ID = id,
Model = model,
_pos = pos,
_rot = rot
});
prop.Update();
return prop;
}
/// <summary>
/// Get a <see cref="ServerPed"/> by it's id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerPed GetPedByID(int id) => Peds.TryGetValue(id, out var ped) ? ped : null;
/// <summary>
/// Get a <see cref="ServerVehicle"/> by it's id
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerVehicle GetVehicleByID(int id) => Vehicles.TryGetValue(id, out var veh) ? veh : null;
/// <summary>
/// Get a <see cref="ServerProp"/> owned by server from it's ID.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerProp GetPropByID(int id) => ServerProps.TryGetValue(id, out var obj) ? obj : null;
/// <summary>
/// Get a <see cref="ServerBlip"/> by it's id.
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public ServerBlip GetBlipByID(int id) => Blips.TryGetValue(id, out var obj) ? obj : null;
/// <summary>
/// Create a static prop owned by server.
/// </summary>
/// <param name="model"></param>
/// <param name="pos"></param>
/// <param name="rot"></param>
/// <returns></returns>
public ServerProp CreateProp(Model model, Vector3 pos, Vector3 rot)
/// <summary>
/// Create a vehicle
/// </summary>
/// <param name="owner">Owner of this vehicle</param>
/// <param name="model">model</param>
/// <param name="pos">position</param>
/// <param name="heading">heading of this vehicle</param>
/// <returns></returns>
public ServerVehicle CreateVehicle(Client owner, Model model, Vector3 pos, float heading)
{
if (owner == null) throw new ArgumentNullException("Owner cannot be null");
ServerVehicle veh = new(Server)
{
int id = RequestNetworkID();
ServerProp prop;
ServerProps.TryAdd(id, prop = new ServerProp(Server)
{
ID = id,
Model = model,
_pos = pos,
_rot = rot
});
prop.Update();
return prop;
Owner = owner,
ID = RequestNetworkID(),
Model = model,
_pos = pos
};
owner.SendCustomEventQueued(CustomEvents.CreateVehicle, veh.ID, model, pos, heading);
Vehicles.TryAdd(veh.ID, veh);
return veh;
}
/// <summary>
/// Create a static <see cref="ServerBlip" /> owned by server.
/// </summary>
/// <param name="pos"></param>
/// <param name="rotation"></param>
/// <returns></returns>
public ServerBlip CreateBlip(Vector3 pos, int rotation)
{
var b = new ServerBlip(Server)
{
ID = RequestNetworkID(),
Position = pos,
Rotation = rotation
};
Blips.TryAdd(b.ID, b);
b.Update();
return b;
}
/// <summary>
/// Get all peds on this server
/// </summary>
/// <returns></returns>
public ServerPed[] GetAllPeds()
{
return Peds.Values.ToArray();
}
/// <summary>
/// Get all vehicles on this server
/// </summary>
/// <returns></returns>
public ServerVehicle[] GetAllVehicles()
{
return Vehicles.Values.ToArray();
}
/// <summary>
/// Get all static prop objects owned by server
/// </summary>
/// <returns></returns>
public ServerProp[] GetAllProps()
{
return ServerProps.Values.ToArray();
}
/// <summary>
/// Get all blips owned by server
/// </summary>
/// <returns></returns>
public ServerBlip[] GetAllBlips()
{
return Blips.Values.ToArray();
}
/// <summary>
/// Not thread safe
/// </summary>
internal void Update(Packets.PedSync p, Client sender)
{
if (!Peds.TryGetValue(p.ID, out var ped))
{
Peds.TryAdd(p.ID, ped = new ServerPed(Server));
ped.ID = p.ID;
}
/// <summary>
/// Create a vehicle
/// </summary>
/// <param name="owner">Owner of this vehicle</param>
/// <param name="model">model</param>
/// <param name="pos">position</param>
/// <param name="heading">heading of this vehicle</param>
/// <returns></returns>
public ServerVehicle CreateVehicle(Client owner, Model model, Vector3 pos, float heading)
ped._pos = p.Position;
ped.Owner = sender;
ped.Health = p.Health;
ped._rot = p.Rotation;
ped._isInvincible = p.Flags.HasPedFlag(PedDataFlags.IsInvincible);
if (p.Speed >= 4 && Vehicles.TryGetValue(p.VehicleID, out var v)) ped.LastVehicle = v;
if (ped.Owner != sender)
{
if (owner == null) { throw new ArgumentNullException("Owner cannot be null"); }
ServerVehicle veh = new(Server)
{
Owner = owner,
ID = RequestNetworkID(),
Model = model,
_pos = pos,
};
owner.SendCustomEventQueued(CustomEvents.CreateVehicle, veh.ID, model, pos, heading);
Vehicles.TryAdd(veh.ID, veh);
return veh;
}
/// <summary>
/// Create a static <see cref="ServerBlip"/> owned by server.
/// </summary>
/// <param name="pos"></param>
/// <param name="rotation"></param>
/// <returns></returns>
public ServerBlip CreateBlip(Vector3 pos, int rotation)
{
var b = new ServerBlip(Server)
{
ID = RequestNetworkID(),
Position = pos,
Rotation = rotation
};
Blips.TryAdd(b.ID, b);
b.Update();
return b;
}
/// <summary>
/// Get all peds on this server
/// </summary>
/// <returns></returns>
public ServerPed[] GetAllPeds() => Peds.Values.ToArray();
/// <summary>
/// Get all vehicles on this server
/// </summary>
/// <returns></returns>
public ServerVehicle[] GetAllVehicles() => Vehicles.Values.ToArray();
/// <summary>
/// Get all static prop objects owned by server
/// </summary>
/// <returns></returns>
public ServerProp[] GetAllProps() => ServerProps.Values.ToArray();
/// <summary>
/// Get all blips owned by server
/// </summary>
/// <returns></returns>
public ServerBlip[] GetAllBlips() => Blips.Values.ToArray();
/// <summary>
/// Not thread safe
/// </summary>
internal void Update(Packets.PedSync p, Client sender)
{
if (!Peds.TryGetValue(p.ID, out ServerPed ped))
{
Peds.TryAdd(p.ID, ped = new ServerPed(Server));
ped.ID = p.ID;
}
ped._pos = p.Position;
if (ped.Owner != null) ped.Owner.EntitiesCount--;
ped.Owner = sender;
ped.Health = p.Health;
ped._rot = p.Rotation;
ped._isInvincible = p.Flags.HasPedFlag(PedDataFlags.IsInvincible);
if (p.Speed >= 4 && Vehicles.TryGetValue(p.VehicleID, out var v))
{
ped.LastVehicle = v;
}
if (ped.Owner != sender)
{
if (ped.Owner != null)
{
ped.Owner.EntitiesCount--;
}
ped.Owner = sender;
sender.EntitiesCount++;
}
}
internal void Update(Packets.VehicleSync p, Client sender)
{
if (!Vehicles.TryGetValue(p.ID, out ServerVehicle veh))
{
Vehicles.TryAdd(p.ID, veh = new ServerVehicle(Server));
veh.ID = p.ID;
}
veh._pos = p.Position + p.Velocity * sender.Latency;
veh._quat = p.Quaternion;
if (veh.Owner != sender)
{
if (veh.Owner != null)
{
veh.Owner.EntitiesCount--;
}
veh.Owner = sender;
sender.EntitiesCount++;
}
}
internal void CleanUp(Client left)
{
// Server.Logger?.Trace("Removing all entities from: "+left.Username);
foreach (var pair in Peds)
{
if (pair.Value.Owner == left)
{
Server.QueueJob(() => Peds.TryRemove(pair.Key, out _));
}
}
foreach (var pair in Vehicles)
{
if (pair.Value.Owner == left)
{
Server.QueueJob(() => Vehicles.TryRemove(pair.Key, out _));
}
}
// Server.QueueJob(() =>
// Server.Logger?.Trace("Remaining entities: "+(Peds.Count+Vehicles.Count+ServerProps.Count)));
}
internal void RemoveVehicle(int id)
{
Vehicles.TryRemove(id, out var veh);
if (veh?.Owner != null)
{
veh.Owner.EntitiesCount--;
}
}
internal void RemoveProp(int id) => ServerProps.TryRemove(id, out _);
internal void RemoveServerBlip(int id) => Blips.TryRemove(id, out _);
internal void RemovePed(int id)
{
Peds.TryRemove(id, out var ped);
if (ped?.Owner != null)
{
ped.Owner.EntitiesCount--;
}
}
internal void Add(ServerPed ped)
{
if (Peds.ContainsKey(ped.ID))
{
Peds[ped.ID] = ped;
return;
}
Peds.TryAdd(ped.ID, ped);
}
internal int RequestNetworkID()
{
int ID = 0;
while ((ID == 0)
|| ServerProps.ContainsKey(ID)
|| Peds.ContainsKey(ID)
|| Vehicles.ContainsKey(ID)
|| Blips.ContainsKey(ID))
{
byte[] rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
sender.EntitiesCount++;
}
}
}
internal void Update(Packets.VehicleSync p, Client sender)
{
if (!Vehicles.TryGetValue(p.ID, out var veh))
{
Vehicles.TryAdd(p.ID, veh = new ServerVehicle(Server));
veh.ID = p.ID;
}
veh._pos = p.Position + p.Velocity * sender.Latency;
veh._quat = p.Quaternion;
if (veh.Owner != sender)
{
if (veh.Owner != null) veh.Owner.EntitiesCount--;
veh.Owner = sender;
sender.EntitiesCount++;
}
}
internal void CleanUp(Client left)
{
// Server.Logger?.Trace("Removing all entities from: "+left.Username);
foreach (var pair in Peds)
if (pair.Value.Owner == left)
Server.QueueJob(() => Peds.TryRemove(pair.Key, out _));
foreach (var pair in Vehicles)
if (pair.Value.Owner == left)
Server.QueueJob(() => Vehicles.TryRemove(pair.Key, out _));
// Server.QueueJob(() =>
// Server.Logger?.Trace("Remaining entities: "+(Peds.Count+Vehicles.Count+ServerProps.Count)));
}
internal void RemoveVehicle(int id)
{
Vehicles.TryRemove(id, out var veh);
if (veh?.Owner != null) veh.Owner.EntitiesCount--;
}
internal void RemoveProp(int id)
{
ServerProps.TryRemove(id, out _);
}
internal void RemoveServerBlip(int id)
{
Blips.TryRemove(id, out _);
}
internal void RemovePed(int id)
{
Peds.TryRemove(id, out var ped);
if (ped?.Owner != null) ped.Owner.EntitiesCount--;
}
internal void Add(ServerPed ped)
{
if (Peds.ContainsKey(ped.ID))
{
Peds[ped.ID] = ped;
return;
}
Peds.TryAdd(ped.ID, ped);
}
internal int RequestNetworkID()
{
var ID = 0;
while (ID == 0
|| ServerProps.ContainsKey(ID)
|| Peds.ContainsKey(ID)
|| Vehicles.ContainsKey(ID)
|| Blips.ContainsKey(ID))
{
var rngBytes = new byte[4];
RandomNumberGenerator.Create().GetBytes(rngBytes);
// Convert the bytes into an integer
ID = BitConverter.ToInt32(rngBytes, 0);
}
return ID;
}
}

View File

@ -1,420 +1,483 @@
using GTA;
using System;
using System.Threading;
using System.Threading.Tasks;
using GTA;
using GTA.Math;
using GTA.Native;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
/// <summary>
/// Server-side object controller
/// </summary>
public abstract class ServerObject
{
/// <summary>
/// Server-side object controller
/// Server that this object belongs to
/// </summary>
public abstract class ServerObject
internal readonly Server Server;
internal Vector3 _pos;
internal Vector3 _rot;
internal ServerObject(Server server)
{
/// <summary>
/// Server that this object belongs to
/// </summary>
internal readonly Server Server;
internal ServerObject(Server server) { Server = server; }
/// <summary>
/// Pass this as an argument in CustomEvent or NativeCall to convert this object to handle at client side.
/// </summary>
public Tuple<byte, byte[]> Handle
{
get
{
return new(GetTypeByte(), BitConverter.GetBytes(ID));
}
}
private byte GetTypeByte()
{
switch (this)
{
case ServerProp _:
return 50;
case ServerPed _:
return 51;
case ServerVehicle _:
return 52;
default:
throw new NotImplementedException();
}
}
/// <summary>
/// The client that owns this object, null if it's owned by server.
/// </summary>
public Client Owner { get; internal set; }
/// <summary>
/// Network ID of this object.
/// </summary>
public int ID { get; internal set; }
/// <summary>
/// The object's model
/// </summary>
public Model Model { get; internal set; }
/// <summary>
/// Gets or sets this object's position
/// </summary>
public virtual Vector3 Position
{
get => _pos;
set { _pos = value; Owner.SendNativeCall(Hash.SET_ENTITY_COORDS_NO_OFFSET, Handle, value.X, value.Y, value.Z, 1, 1, 1); }
}
internal Vector3 _pos;
/// <summary>
/// Gets or sets this object's rotation
/// </summary>
public virtual Vector3 Rotation
{
get => _rot;
set { _rot = value; Owner.SendNativeCall(Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z, 2, 1); }
}
internal Vector3 _rot;
/// <summary>
/// Send updated information to clients, would be called automatically.
/// </summary>
/// <summary>
/// Delete this object
/// </summary>
public virtual void Delete()
{
Owner?.SendCustomEvent(CustomEventFlags.Queued, CustomEvents.DeleteEntity, Handle);
}
/// <summary>
/// Freeze this object, will throw an exception if it's a ServerProp.
/// </summary>
/// <param name="toggle"></param>
/// <exception cref="InvalidOperationException"></exception>
public virtual void Freeze(bool toggle)
{
if (GetTypeByte() == 50)
{
throw new InvalidOperationException("Can't freeze or unfreeze static server object");
}
else
{
Owner.SendNativeCall(Hash.FREEZE_ENTITY_POSITION, Handle, toggle);
}
}
Server = server;
}
/// <summary>
/// Represents an prop owned by server.
/// Pass this as an argument in CustomEvent or NativeCall to convert this object to handle at client side.
/// </summary>
public class ServerProp : ServerObject
{
public Tuple<byte, byte[]> Handle => new Tuple<byte, byte[]>(GetTypeByte(), BitConverter.GetBytes(ID));
internal ServerProp(Server server) : base(server) { }
/// <summary>
/// Delete this prop
/// </summary>
public override void Delete()
{
Server.API.SendCustomEvent(CustomEventFlags.Queued, null, CustomEvents.DeleteServerProp, ID);
Server.API.Entities.RemoveProp(ID);
}
/// <summary>
/// Gets or sets this object's position
/// </summary>
public override Vector3 Position
{
get => _pos;
set { _pos = value; Server.API.SendNativeCall(null, Hash.SET_ENTITY_COORDS_NO_OFFSET, Handle, value.X, value.Y, value.Z, 1, 1, 1); }
}
/// <summary>
/// Gets or sets this object's rotation
/// </summary>
public override Vector3 Rotation
{
get => _rot;
set { _rot = value; Server.API.SendNativeCall(null, Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z, 2, 1); }
}
/// <summary>
/// Send updated information to clients, would be called automatically.
/// </summary>
internal void Update()
{
Server.API.Server.BaseScript.SendServerPropsTo(new() { this });
}
}
/// <summary>
/// Represents a ped from a client
/// The client that owns this object, null if it's owned by server.
/// </summary>
public class ServerPed : ServerObject
{
internal ServerPed(Server server) : base(server) { }
public Client Owner { get; internal set; }
/// <summary>
/// Get the ped's last vehicle
/// </summary>
public ServerVehicle LastVehicle { get; internal set; }
/// <summary>
/// Get the <see cref="PedBlip"/> attached to this ped.
/// </summary>
public PedBlip AttachedBlip { get; internal set; }
/// <summary>
/// Attach a blip to this ped.
/// </summary>
/// <returns></returns>
public PedBlip AddBlip()
{
AttachedBlip = new PedBlip(this);
AttachedBlip.Update();
return AttachedBlip;
}
/// <summary>
/// Health
/// </summary>
public int Health { get; internal set; }
internal bool _isInvincible;
/// <summary>
/// Get or set whether this ped is invincible
/// </summary>
public bool IsInvincible
{
get => _isInvincible;
set => Owner.SendNativeCall(Hash.SET_ENTITY_INVINCIBLE, Handle, value);
}
}
/// <summary>
/// Represents a vehicle from a client
/// Network ID of this object.
/// </summary>
public class ServerVehicle : ServerObject
public int ID { get; internal set; }
/// <summary>
/// The object's model
/// </summary>
public Model Model { get; internal set; }
/// <summary>
/// Gets or sets this object's position
/// </summary>
public virtual Vector3 Position
{
internal ServerVehicle(Server server) : base(server) { }
/// <summary>
/// Gets or sets vehicle rotation
/// </summary>
public override Vector3 Rotation
get => _pos;
set
{
get => _quat.ToEulerAngles().ToDegree();
set { Owner.SendNativeCall(Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z); }
}
internal Quaternion _quat;
/// <summary>
/// Get this vehicle's quaternion
/// </summary>
public Quaternion Quaternion
{
get => _quat;
set { _quat = value; Owner.SendNativeCall(Hash.SET_ENTITY_QUATERNION, Handle, value.X, value.Y, value.Z, value.W); }
_pos = value;
Owner.SendNativeCall(Hash.SET_ENTITY_COORDS_NO_OFFSET, Handle, value.X, value.Y, value.Z, 1, 1, 1);
}
}
/// <summary>
/// A static blip owned by server.
/// Gets or sets this object's rotation
/// </summary>
public class ServerBlip
public virtual Vector3 Rotation
{
private readonly Server Server;
internal ServerBlip(Server server)
get => _rot;
set
{
Server = server;
_rot = value;
Owner.SendNativeCall(Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z, 2, 1);
}
}
/// <summary>
/// Pass this as an argument in CustomEvent or NativeCall to convert this object to handle at client side.
/// </summary>
public Tuple<byte, byte[]> Handle
private byte GetTypeByte()
{
switch (this)
{
get
{
return new(60, BitConverter.GetBytes(ID));
}
}
/// <summary>
/// Network ID (not handle!)
/// </summary>
public int ID { get; internal set; }
internal BlipColor _color;
/// <summary>
/// Color of this blip
/// </summary>
public BlipColor Color
{
get => _color;
set { _color = value; Update(); }
}
internal BlipSprite _sprite = BlipSprite.Standard;
/// <summary>
/// Sprite of this blip
/// </summary>
public BlipSprite Sprite
{
get => _sprite;
set { _sprite = value; Update(); }
}
internal float _scale = 1;
/// <summary>
/// Scale of this blip
/// </summary>
public float Scale
{
get => _scale;
set { _scale = value; Update(); }
}
internal Vector3 _pos = new();
/// <summary>
/// Position of this blip
/// </summary>
public Vector3 Position
{
get => _pos;
set { _pos = value; Update(); }
}
internal int _rot;
/// <summary>
/// Rotation of this blip
/// </summary>
public int Rotation
{
get => _rot;
set { _rot = value; Update(); }
}
internal string _name = "Beeeeeee";
/// <summary>
/// Name of this blip
/// </summary>
public string Name
{
get => _name;
set { _name = value; Update(); }
}
/// <summary>
/// Delete this blip
/// </summary>
public void Delete()
{
Server.API.SendCustomEvent(CustomEventFlags.Queued, null, CustomEvents.DeleteServerBlip, ID);
Server.Entities.RemoveServerBlip(ID);
}
private bool _bouncing = false;
internal void Update()
{
// 5ms debounce
if (!_bouncing)
{
_bouncing = true;
Task.Run(() =>
{
Thread.Sleep(5);
DoUpdate();
_bouncing = false;
});
}
}
private void DoUpdate()
{
// Server.Logger?.Debug("bee");
// Serve-side blip
Server.BaseScript.SendServerBlipsTo(new() { this });
case ServerProp _:
return 50;
case ServerPed _:
return 51;
case ServerVehicle _:
return 52;
default:
throw new NotImplementedException();
}
}
/// <summary>
/// Represent a blip attached to ped.
/// Send updated information to clients, would be called automatically.
/// </summary>
public class PedBlip
/// <summary>
/// Delete this object
/// </summary>
public virtual void Delete()
{
/// <summary>
/// Get the <see cref="ServerPed"/> that this blip attached to.
/// </summary>
public ServerPed Ped { get; internal set; }
internal PedBlip(ServerPed ped)
Owner?.SendCustomEvent(CustomEventFlags.Queued, CustomEvents.DeleteEntity, Handle);
}
/// <summary>
/// Freeze this object, will throw an exception if it's a ServerProp.
/// </summary>
/// <param name="toggle"></param>
/// <exception cref="InvalidOperationException"></exception>
public virtual void Freeze(bool toggle)
{
if (GetTypeByte() == 50)
throw new InvalidOperationException("Can't freeze or unfreeze static server object");
Owner.SendNativeCall(Hash.FREEZE_ENTITY_POSITION, Handle, toggle);
}
}
/// <summary>
/// Represents an prop owned by server.
/// </summary>
public class ServerProp : ServerObject
{
internal ServerProp(Server server) : base(server)
{
}
/// <summary>
/// Gets or sets this object's position
/// </summary>
public override Vector3 Position
{
get => _pos;
set
{
Ped = ped;
_pos = value;
Server.API.SendNativeCall(null, Hash.SET_ENTITY_COORDS_NO_OFFSET, Handle, value.X, value.Y, value.Z, 1, 1,
1);
}
}
internal BlipColor _color;
/// <summary>
/// Color of this blip
/// </summary>
public BlipColor Color
/// <summary>
/// Gets or sets this object's rotation
/// </summary>
public override Vector3 Rotation
{
get => _rot;
set
{
get => _color;
set { _color = value; Update(); }
_rot = value;
Server.API.SendNativeCall(null, Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z, 2, 1);
}
}
internal BlipSprite _sprite = BlipSprite.Standard;
/// <summary>
/// Sprite of this blip
/// </summary>
public BlipSprite Sprite
{
get => _sprite;
set { _sprite = value; Update(); }
}
/// <summary>
/// Delete this prop
/// </summary>
public override void Delete()
{
Server.API.SendCustomEvent(CustomEventFlags.Queued, null, CustomEvents.DeleteServerProp, ID);
Server.API.Entities.RemoveProp(ID);
}
internal float _scale = 1;
/// <summary>
/// Scale of this blip
/// </summary>
public float Scale
{
get => _scale;
set { _scale = value; Update(); }
}
private bool _bouncing = false;
internal void Update()
{
// 5ms debounce
if (!_bouncing)
{
_bouncing = true;
Task.Run(() =>
{
Thread.Sleep(5);
DoUpdate();
_bouncing = false;
});
}
}
private void DoUpdate()
{
Ped.Owner.SendCustomEvent(CustomEventFlags.Queued, CustomEvents.UpdatePedBlip, Ped.Handle, (byte)Color, (ushort)Sprite, Scale);
/// <summary>
/// Send updated information to clients, would be called automatically.
/// </summary>
internal void Update()
{
Server.API.Server.BaseScript.SendServerPropsTo(new List<ServerProp> { this });
}
}
/// <summary>
/// Represents a ped from a client
/// </summary>
public class ServerPed : ServerObject
{
internal bool _isInvincible;
internal ServerPed(Server server) : base(server)
{
}
/// <summary>
/// Get the ped's last vehicle
/// </summary>
public ServerVehicle LastVehicle { get; internal set; }
/// <summary>
/// Get the <see cref="PedBlip" /> attached to this ped.
/// </summary>
public PedBlip AttachedBlip { get; internal set; }
/// <summary>
/// Health
/// </summary>
public int Health { get; internal set; }
/// <summary>
/// Get or set whether this ped is invincible
/// </summary>
public bool IsInvincible
{
get => _isInvincible;
set => Owner.SendNativeCall(Hash.SET_ENTITY_INVINCIBLE, Handle, value);
}
/// <summary>
/// Attach a blip to this ped.
/// </summary>
/// <returns></returns>
public PedBlip AddBlip()
{
AttachedBlip = new PedBlip(this);
AttachedBlip.Update();
return AttachedBlip;
}
}
/// <summary>
/// Represents a vehicle from a client
/// </summary>
public class ServerVehicle : ServerObject
{
internal Quaternion _quat;
internal ServerVehicle(Server server) : base(server)
{
}
/// <summary>
/// Gets or sets vehicle rotation
/// </summary>
public override Vector3 Rotation
{
get => _quat.ToEulerAngles().ToDegree();
set => Owner.SendNativeCall(Hash.SET_ENTITY_ROTATION, Handle, value.X, value.Y, value.Z);
}
/// <summary>
/// Get this vehicle's quaternion
/// </summary>
public Quaternion Quaternion
{
get => _quat;
set
{
_quat = value;
Owner.SendNativeCall(Hash.SET_ENTITY_QUATERNION, Handle, value.X, value.Y, value.Z, value.W);
}
}
}
/// <summary>
/// A static blip owned by server.
/// </summary>
public class ServerBlip
{
private readonly Server Server;
private bool _bouncing;
internal BlipColor _color;
internal string _name = "Beeeeeee";
internal Vector3 _pos;
internal int _rot;
internal float _scale = 1;
internal BlipSprite _sprite = BlipSprite.Standard;
internal ServerBlip(Server server)
{
Server = server;
}
/// <summary>
/// Pass this as an argument in CustomEvent or NativeCall to convert this object to handle at client side.
/// </summary>
public Tuple<byte, byte[]> Handle => new Tuple<byte, byte[]>(60, BitConverter.GetBytes(ID));
/// <summary>
/// Network ID (not handle!)
/// </summary>
public int ID { get; internal set; }
/// <summary>
/// Color of this blip
/// </summary>
public BlipColor Color
{
get => _color;
set
{
_color = value;
Update();
}
}
/// <summary>
/// Sprite of this blip
/// </summary>
public BlipSprite Sprite
{
get => _sprite;
set
{
_sprite = value;
Update();
}
}
/// <summary>
/// Scale of this blip
/// </summary>
public float Scale
{
get => _scale;
set
{
_scale = value;
Update();
}
}
/// <summary>
/// Position of this blip
/// </summary>
public Vector3 Position
{
get => _pos;
set
{
_pos = value;
Update();
}
}
/// <summary>
/// Rotation of this blip
/// </summary>
public int Rotation
{
get => _rot;
set
{
_rot = value;
Update();
}
}
/// <summary>
/// Name of this blip
/// </summary>
public string Name
{
get => _name;
set
{
_name = value;
Update();
}
}
/// <summary>
/// Delete this blip
/// </summary>
public void Delete()
{
Server.API.SendCustomEvent(CustomEventFlags.Queued, null, CustomEvents.DeleteServerBlip, ID);
Server.Entities.RemoveServerBlip(ID);
}
internal void Update()
{
// 5ms debounce
if (!_bouncing)
{
_bouncing = true;
Task.Run(() =>
{
Thread.Sleep(5);
DoUpdate();
_bouncing = false;
});
}
}
private void DoUpdate()
{
// Server.Logger?.Debug("bee");
// Serve-side blip
Server.BaseScript.SendServerBlipsTo(new List<ServerBlip> { this });
}
}
/// <summary>
/// Represent a blip attached to ped.
/// </summary>
public class PedBlip
{
private bool _bouncing;
internal BlipColor _color;
internal float _scale = 1;
internal BlipSprite _sprite = BlipSprite.Standard;
internal PedBlip(ServerPed ped)
{
Ped = ped;
}
/// <summary>
/// Get the <see cref="ServerPed" /> that this blip attached to.
/// </summary>
public ServerPed Ped { get; internal set; }
/// <summary>
/// Color of this blip
/// </summary>
public BlipColor Color
{
get => _color;
set
{
_color = value;
Update();
}
}
/// <summary>
/// Sprite of this blip
/// </summary>
public BlipSprite Sprite
{
get => _sprite;
set
{
_sprite = value;
Update();
}
}
/// <summary>
/// Scale of this blip
/// </summary>
public float Scale
{
get => _scale;
set
{
_scale = value;
Update();
}
}
internal void Update()
{
// 5ms debounce
if (!_bouncing)
{
_bouncing = true;
Task.Run(() =>
{
Thread.Sleep(5);
DoUpdate();
_bouncing = false;
});
}
}
private void DoUpdate()
{
Ped.Owner.SendCustomEvent(CustomEventFlags.Queued, CustomEvents.UpdatePedBlip, Ped.Handle, (byte)Color,
(ushort)Sprite, Scale);
}
}

View File

@ -1,234 +1,242 @@
using ICSharpCode.SharpZipLib.Zip;
using McMaster.NETCore.Plugins;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using ICSharpCode.SharpZipLib.Zip;
using McMaster.NETCore.Plugins;
using RageCoop.Core;
using RageCoop.Core.Scripting;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
/// <summary>
/// A class representing a server side resource, each resource is isolated from another and will be started alongside
/// the server.
/// </summary>
public class ServerResource : PluginLoader
{
/// <summary>
/// A class representing a server side resource, each resource is isolated from another and will be started alongside the server.
/// Get a <see cref="Logger" /> instance that can be used to show information in console.
/// </summary>
public class ServerResource : PluginLoader
public Logger Logger;
internal ServerResource(PluginConfig config) : base(config)
{
}
internal ServerResource(PluginConfig config) : base(config) { }
internal static ServerResource LoadFrom(string resDir, string dataFolder, Logger logger = null)
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
public string DataFolder { get; internal set; }
/// <summary>
/// Get all <see cref="ServerScript" /> instance in this resource
/// </summary>
public Dictionary<string, ServerScript> Scripts { get; internal set; } = new();
/// <summary>
/// Get all <see cref="ResourceFile" /> that can be used to acces files in this resource
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new();
internal static ServerResource LoadFrom(string resDir, string dataFolder, Logger logger = null)
{
var runtimeLibs = Path.Combine(resDir, "RuntimeLibs", CoreUtils.GetInvariantRID());
if (Directory.Exists(runtimeLibs))
{
var runtimeLibs = Path.Combine(resDir, "RuntimeLibs", CoreUtils.GetInvariantRID());
if (Directory.Exists(runtimeLibs))
{
logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID());
CoreUtils.CopyFilesRecursively(new(runtimeLibs), new(resDir));
}
logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID());
CoreUtils.CopyFilesRecursively(new DirectoryInfo(runtimeLibs), new DirectoryInfo(resDir));
}
runtimeLibs = Path.Combine(resDir, "RuntimeLibs", RuntimeInformation.RuntimeIdentifier);
if (Directory.Exists(runtimeLibs))
{
logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID());
CoreUtils.CopyFilesRecursively(new(runtimeLibs), new(resDir));
}
runtimeLibs = Path.Combine(resDir, "RuntimeLibs", RuntimeInformation.RuntimeIdentifier);
if (Directory.Exists(runtimeLibs))
{
logger?.Debug("Applying runtime libraries from " + CoreUtils.GetInvariantRID());
CoreUtils.CopyFilesRecursively(new DirectoryInfo(runtimeLibs), new DirectoryInfo(resDir));
}
var conf = new PluginConfig(Path.GetFullPath(Path.Combine(resDir, Path.GetFileName(resDir) + ".dll")))
{
PreferSharedTypes = true,
EnableHotReload = false,
IsUnloadable = false,
LoadInMemory = true,
};
ServerResource r = new(conf);
r.Logger = logger;
r.Name = Path.GetFileName(resDir);
if (!File.Exists(conf.MainAssemblyPath))
{
r.Dispose();
throw new FileNotFoundException($"Main assembly for resource \"{r.Name}\" cannot be found.");
}
r.Scripts = new();
r.DataFolder = Path.Combine(dataFolder, r.Name);
r.Reloaded += (s, e) => { r.Logger?.Info($"Resource: {r.Name} has been reloaded"); };
var conf = new PluginConfig(Path.GetFullPath(Path.Combine(resDir, Path.GetFileName(resDir) + ".dll")))
{
PreferSharedTypes = true,
EnableHotReload = false,
IsUnloadable = false,
LoadInMemory = true
};
ServerResource r = new(conf);
r.Logger = logger;
r.Name = Path.GetFileName(resDir);
if (!File.Exists(conf.MainAssemblyPath))
{
r.Dispose();
throw new FileNotFoundException($"Main assembly for resource \"{r.Name}\" cannot be found.");
}
Directory.CreateDirectory(r.DataFolder);
foreach (var dir in Directory.GetDirectories(resDir, "*", SearchOption.AllDirectories))
r.Scripts = new Dictionary<string, ServerScript>();
r.DataFolder = Path.Combine(dataFolder, r.Name);
r.Reloaded += (s, e) => { r.Logger?.Info($"Resource: {r.Name} has been reloaded"); };
Directory.CreateDirectory(r.DataFolder);
foreach (var dir in Directory.GetDirectories(resDir, "*", SearchOption.AllDirectories))
r.Files.Add(dir, new ResourceFile
{
r.Files.Add(dir, new ResourceFile()
{
IsDirectory = true,
Name = dir.Substring(resDir.Length + 1).Replace('\\', '/')
});
}
var assemblies = new Dictionary<ResourceFile, Assembly>();
foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories))
IsDirectory = true,
Name = dir.Substring(resDir.Length + 1).Replace('\\', '/')
});
var assemblies = new Dictionary<ResourceFile, Assembly>();
foreach (var file in Directory.GetFiles(resDir, "*", SearchOption.AllDirectories))
{
if (Path.GetFileName(file).CanBeIgnored())
{
if (Path.GetFileName(file).CanBeIgnored()) { try { File.Delete(file); } catch { } continue; }
var relativeName = file.Substring(resDir.Length + 1).Replace('\\', '/');
var rfile = new ResourceFile()
try
{
GetStream = () => { return new FileStream(file, FileMode.Open, FileAccess.Read); },
IsDirectory = false,
Name = relativeName
};
if (file.EndsWith(".dll") && !relativeName.Contains('/') && IsManagedAssembly(file))
{
assemblies.Add(rfile, r.LoadAssemblyFromPath(Path.GetFullPath(file)));
File.Delete(file);
}
r.Files.Add(relativeName, rfile);
}
foreach (var a in assemblies)
{
if (a.Key.Name.ToLower() == r.Name.ToLower() + ".dll")
catch
{
}
continue;
}
var relativeName = file.Substring(resDir.Length + 1).Replace('\\', '/');
var rfile = new ResourceFile
{
GetStream = () => { return new FileStream(file, FileMode.Open, FileAccess.Read); },
IsDirectory = false,
Name = relativeName
};
if (file.EndsWith(".dll") && !relativeName.Contains('/') && IsManagedAssembly(file))
assemblies.Add(rfile, r.LoadAssemblyFromPath(Path.GetFullPath(file)));
r.Files.Add(relativeName, rfile);
}
foreach (var a in assemblies)
if (a.Key.Name.ToLower() == r.Name.ToLower() + ".dll")
try
{
r.LoadScriptsFromAssembly(a.Key, a.Value);
}
catch (FileLoadException ex)
{
if (!ex.Message.EndsWith("Assembly with same name is already loaded"))
{
logger?.Warning("Failed to load assembly: " + a.Key.Name);
logger?.Trace(ex.Message);
}
}
return r;
}
internal static ServerResource LoadFrom(Stream input, string name, string tmpDir, string dataFolder,
Logger logger = null)
{
tmpDir = Path.Combine(tmpDir, name);
if (Directory.Exists(tmpDir)) Directory.Delete(tmpDir, true);
Directory.CreateDirectory(tmpDir);
new FastZip().ExtractZip(input, tmpDir, FastZip.Overwrite.Always, null, null, null, true, true);
return LoadFrom(tmpDir, dataFolder, logger);
}
private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly)
{
var count = 0;
try
{
// Find all script types in the assembly
foreach (var type in assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(ServerScript))))
{
var constructor = type.GetConstructor(Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
try
{
r.LoadScriptsFromAssembly(a.Key, a.Value);
// Invoke script constructor
var script = constructor.Invoke(null) as ServerScript;
script.CurrentResource = this;
script.CurrentFile = rfile;
Scripts.Add(script.GetType().FullName, script);
count++;
}
catch (FileLoadException ex)
catch (Exception ex)
{
if (!ex.Message.EndsWith("Assembly with same name is already loaded"))
{
logger?.Warning("Failed to load assembly: " + a.Key.Name);
logger?.Trace(ex.Message);
}
Logger?.Error($"Error occurred when loading script: {type.FullName}.");
Logger?.Error(ex);
}
}
else
Logger?.Error($"Script {type.FullName} has an invalid contructor.");
}
return r;
}
internal static ServerResource LoadFrom(Stream input, string name, string tmpDir, string dataFolder, Logger logger = null)
catch (ReflectionTypeLoadException ex)
{
tmpDir = Path.Combine(tmpDir, name);
if (Directory.Exists(tmpDir)) { Directory.Delete(tmpDir, true); }
Directory.CreateDirectory(tmpDir);
new FastZip().ExtractZip(input, tmpDir, FastZip.Overwrite.Always, null, null, null, true, true);
return LoadFrom(tmpDir, dataFolder, logger);
}
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
public string DataFolder { get; internal set; }
/// <summary>
/// Get all <see cref="ServerScript"/> instance in this resource
/// </summary>
public Dictionary<string, ServerScript> Scripts { get; internal set; } = new();
/// <summary>
/// Get all <see cref="ResourceFile"/> that can be used to acces files in this resource
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new Dictionary<string, ResourceFile>();
/// <summary>
/// Get a <see cref="Logger"/> instance that can be used to show information in console.
/// </summary>
public Logger Logger;
private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly)
{
int count = 0;
try
{
// Find all script types in the assembly
foreach (var type in assembly.GetTypes().Where(x => x.IsSubclassOf(typeof(ServerScript))))
{
ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
{
try
{
// Invoke script constructor
var script = constructor.Invoke(null) as ServerScript;
script.CurrentResource = this;
script.CurrentFile = rfile;
Scripts.Add(script.GetType().FullName, script);
count++;
}
catch (Exception ex)
{
Logger?.Error($"Error occurred when loading script: {type.FullName}.");
Logger?.Error(ex);
}
}
else
{
Logger?.Error($"Script {type.FullName} has an invalid contructor.");
}
}
}
catch (ReflectionTypeLoadException ex)
{
Logger?.Error($"Failed to load assembly {rfile.Name}: ");
Logger?.Error(ex);
foreach (var e in ex.LoaderExceptions)
{
Logger?.Error(e);
}
return false;
}
if (count != 0)
{
Logger?.Info($"Loaded {count} script(s) in {rfile.Name}");
}
return count != 0;
Logger?.Error($"Failed to load assembly {rfile.Name}: ");
Logger?.Error(ex);
foreach (var e in ex.LoaderExceptions) Logger?.Error(e);
return false;
}
internal new void Dispose()
if (count != 0) Logger?.Info($"Loaded {count} script(s) in {rfile.Name}");
return count != 0;
}
internal new void Dispose()
{
base.Dispose();
}
private static bool IsManagedAssembly(string filename)
{
try
{
base.Dispose();
}
private static bool IsManagedAssembly(string filename)
{
try
using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
if (file.Length < 64)
return false;
using (var bin = new BinaryReader(file))
{
if (file.Length < 64)
// PE header starts at offset 0x3C (60). Its a 4 byte header.
file.Position = 0x3C;
var offset = bin.ReadUInt32();
if (offset == 0)
offset = 0x80;
// Ensure there is at least enough room for the following structures:
// 24 byte PE Signature & Header
// 28 byte Standard Fields (24 bytes for PE32+)
// 68 byte NT Fields (88 bytes for PE32+)
// >= 128 byte Data Dictionary Table
if (offset > file.Length - 256)
return false;
using (BinaryReader bin = new BinaryReader(file))
{
// PE header starts at offset 0x3C (60). Its a 4 byte header.
file.Position = 0x3C;
uint offset = bin.ReadUInt32();
if (offset == 0)
offset = 0x80;
// Check the PE signature. Should equal 'PE\0\0'.
file.Position = offset;
if (bin.ReadUInt32() != 0x00004550)
return false;
// Ensure there is at least enough room for the following structures:
// 24 byte PE Signature & Header
// 28 byte Standard Fields (24 bytes for PE32+)
// 68 byte NT Fields (88 bytes for PE32+)
// >= 128 byte Data Dictionary Table
if (offset > file.Length - 256)
return false;
// Read PE magic number from Standard Fields to determine format.
file.Position += 20;
var peFormat = bin.ReadUInt16();
if (peFormat != 0x10b /* PE32 */ && peFormat != 0x20b /* PE32Plus */)
return false;
// Check the PE signature. Should equal 'PE\0\0'.
file.Position = offset;
if (bin.ReadUInt32() != 0x00004550)
return false;
// Read PE magic number from Standard Fields to determine format.
file.Position += 20;
var peFormat = bin.ReadUInt16();
if (peFormat != 0x10b /* PE32 */ && peFormat != 0x20b /* PE32Plus */)
return false;
// Read the 15th Data Dictionary RVA field which contains the CLI header RVA.
// When this is non-zero then the file contains CLI data otherwise not.
file.Position = offset + (peFormat == 0x10b ? 232 : 248);
return bin.ReadUInt32() != 0;
}
// Read the 15th Data Dictionary RVA field which contains the CLI header RVA.
// When this is non-zero then the file contains CLI data otherwise not.
file.Position = offset + (peFormat == 0x10b ? 232 : 248);
return bin.ReadUInt32() != 0;
}
}
catch
{
// This is likely not a valid assembly if any IO exceptions occur during reading
return false;
}
}
catch
{
// This is likely not a valid assembly if any IO exceptions occur during reading
return false;
}
}
}
}

View File

@ -1,86 +1,91 @@
using RageCoop.Core.Scripting;
using System;
using System;
using RageCoop.Core;
using RageCoop.Core.Scripting;
namespace RageCoop.Server.Scripting
namespace RageCoop.Server.Scripting;
/// <summary>
/// Inherit from this class, constructor will be called automatically, but other scripts might have yet been loaded and
/// <see cref="API" /> will be null, you should use <see cref="OnStart" />. to initiate your script.
/// </summary>
public abstract class ServerScript
{
/// <summary>
/// Inherit from this class, constructor will be called automatically, but other scripts might have yet been loaded and <see cref="API"/> will be null, you should use <see cref="OnStart"/>. to initiate your script.
/// Get the <see cref="Scripting.API" /> instance that can be used to control the server.
/// </summary>
public abstract class ServerScript
{
/// <summary>
/// This method would be called from listener thread after all scripts have been loaded.
/// </summary>
public abstract void OnStart();
/// <summary>
/// This method would be called from listener thread when the server is shutting down, you MUST terminate all background jobs/threads in this method.
/// </summary>
public abstract void OnStop();
/// <summary>
/// Get the <see cref="Scripting.API"/> instance that can be used to control the server.
/// </summary>
public API API { get; set; }
/// <summary>
/// Get the <see cref="ServerResource"/> this script belongs to, this property won't be initiated before <see cref="OnStart"/>.
/// </summary>
public ServerResource CurrentResource { get; internal set; }
/// <summary>
/// Get the <see cref="ResourceFile"/> that the script belongs to.
/// </summary>
public ResourceFile CurrentFile { get; internal set; }
/// <summary>
/// Eqivalent of <see cref="ServerResource.Logger"/> in <see cref="CurrentResource"/>
/// </summary>
public Core.Logger Logger => CurrentResource.Logger;
}
/// <summary>
/// Decorate your method with this attribute and use <see cref="API.RegisterCommands{T}"/> or <see cref="API.RegisterCommands(object)"/> to register commands.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class Command : Attribute
{
/// <summary>
/// Sets name of the command
/// </summary>
public string Name { get; set; }
/// <summary>
/// Set the Usage (Example: "Please use "/help"". ArgsLength required!)
/// </summary>
public string Usage { get; set; }
/// <summary>
/// Set the length of arguments (Example: 2 for "/message USERNAME MESSAGE". Usage required!)
/// </summary>
public short ArgsLength { get; set; }
/// <summary>
///
/// </summary>
/// <param name="name">Name of the command</param>
public Command(string name)
{
Name = name;
}
}
public API API { get; set; }
/// <summary>
/// The context containg command information.
/// Get the <see cref="ServerResource" /> this script belongs to, this property won't be initiated before
/// <see cref="OnStart" />.
/// </summary>
public class CommandContext
{
/// <summary>
/// Gets the client which executed the command
/// </summary>
public Client Client { get; internal set; }
public ServerResource CurrentResource { get; internal set; }
/// <summary>
/// Gets the arguments (Example: "/message USERNAME MESSAGE", Args[0] for USERNAME)
/// </summary>
public string[] Args { get; internal set; }
}
/// <summary>
/// Get the <see cref="ResourceFile" /> that the script belongs to.
/// </summary>
public ResourceFile CurrentFile { get; internal set; }
/// <summary>
/// Eqivalent of <see cref="ServerResource.Logger" /> in <see cref="CurrentResource" />
/// </summary>
public Logger Logger => CurrentResource.Logger;
/// <summary>
/// This method would be called from listener thread after all scripts have been loaded.
/// </summary>
public abstract void OnStart();
/// <summary>
/// This method would be called from listener thread when the server is shutting down, you MUST terminate all
/// background jobs/threads in this method.
/// </summary>
public abstract void OnStop();
}
/// <summary>
/// Decorate your method with this attribute and use <see cref="API.RegisterCommands{T}" /> or
/// <see cref="API.RegisterCommands(object)" /> to register commands.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = false)]
public class Command : Attribute
{
/// <summary>
/// </summary>
/// <param name="name">Name of the command</param>
public Command(string name)
{
Name = name;
}
/// <summary>
/// Sets name of the command
/// </summary>
public string Name { get; set; }
/// <summary>
/// Set the Usage (Example: "Please use "/help"". ArgsLength required!)
/// </summary>
public string Usage { get; set; }
/// <summary>
/// Set the length of arguments (Example: 2 for "/message USERNAME MESSAGE". Usage required!)
/// </summary>
public short ArgsLength { get; set; }
}
/// <summary>
/// The context containg command information.
/// </summary>
public class CommandContext
{
/// <summary>
/// Gets the client which executed the command
/// </summary>
public Client Client { get; internal set; }
/// <summary>
/// Gets the arguments (Example: "/message USERNAME MESSAGE", Args[0] for USERNAME)
/// </summary>
public string[] Args { get; internal set; }
}

View File

@ -1,72 +1,71 @@
using RageCoop.Core;
using System.IO;
using System.IO;
using System.Net;
using System.Security.Cryptography;
namespace RageCoop.Server
using RageCoop.Core;
namespace RageCoop.Server;
internal class Security
{
internal class Security
private readonly Logger Logger;
private readonly Dictionary<IPEndPoint, Aes> SecuredConnections = new();
public RSA RSA = RSA.Create(2048);
public Security(Logger logger)
{
private readonly Logger Logger;
public Security(Logger logger)
{
Logger = logger;
}
public RSA RSA = RSA.Create(2048);
private readonly Dictionary<IPEndPoint, Aes> SecuredConnections = new Dictionary<IPEndPoint, Aes>();
public bool HasSecuredConnection(IPEndPoint target)
{
return SecuredConnections.ContainsKey(target);
}
public byte[] Encrypt(byte[] data, IPEndPoint target)
{
var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, SecuredConnections[target].CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
}
return ms.ToArray();
}
public byte[] Decrypt(byte[] data, IPEndPoint target)
{
return new CryptoStream(new MemoryStream(data), SecuredConnections[target].CreateDecryptor(), CryptoStreamMode.Read).ReadToEnd();
}
public void AddConnection(IPEndPoint endpoint, byte[] cryptedKey, byte[] cryptedIV)
{
var key = RSA.Decrypt(cryptedKey, RSAEncryptionPadding.Pkcs1);
var iv = RSA.Decrypt(cryptedIV, RSAEncryptionPadding.Pkcs1);
// Logger?.Debug($"key:{key.Dump()}, iv:{iv.Dump()}");
var conAes = Aes.Create();
conAes.Key = key;
conAes.IV = iv;
if (!SecuredConnections.ContainsKey(endpoint))
{
SecuredConnections.Add(endpoint, conAes);
}
else
{
SecuredConnections[endpoint] = conAes;
}
}
public void RemoveConnection(IPEndPoint ep)
{
if (SecuredConnections.ContainsKey(ep))
{
SecuredConnections.Remove(ep);
}
}
public void GetPublicKey(out byte[] modulus, out byte[] exponent)
{
var key = RSA.ExportParameters(false);
modulus = key.Modulus;
exponent = key.Exponent;
}
public void ClearConnections()
{
SecuredConnections.Clear();
}
Logger = logger;
}
}
public bool HasSecuredConnection(IPEndPoint target)
{
return SecuredConnections.ContainsKey(target);
}
public byte[] Encrypt(byte[] data, IPEndPoint target)
{
var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, SecuredConnections[target].CreateEncryptor(), CryptoStreamMode.Write))
{
cs.Write(data, 0, data.Length);
}
return ms.ToArray();
}
public byte[] Decrypt(byte[] data, IPEndPoint target)
{
return new CryptoStream(new MemoryStream(data), SecuredConnections[target].CreateDecryptor(),
CryptoStreamMode.Read).ReadToEnd();
}
public void AddConnection(IPEndPoint endpoint, byte[] cryptedKey, byte[] cryptedIV)
{
var key = RSA.Decrypt(cryptedKey, RSAEncryptionPadding.Pkcs1);
var iv = RSA.Decrypt(cryptedIV, RSAEncryptionPadding.Pkcs1);
// Logger?.Debug($"key:{key.Dump()}, iv:{iv.Dump()}");
var conAes = Aes.Create();
conAes.Key = key;
conAes.IV = iv;
if (!SecuredConnections.ContainsKey(endpoint))
SecuredConnections.Add(endpoint, conAes);
else
SecuredConnections[endpoint] = conAes;
}
public void RemoveConnection(IPEndPoint ep)
{
if (SecuredConnections.ContainsKey(ep)) SecuredConnections.Remove(ep);
}
public void GetPublicKey(out byte[] modulus, out byte[] exponent)
{
var key = RSA.ExportParameters(false);
modulus = key.Modulus;
exponent = key.Exponent;
}
public void ClearConnections()
{
SecuredConnections.Clear();
}
}

View File

@ -1,128 +1,130 @@
namespace RageCoop.Server
namespace RageCoop.Server;
/// <summary>
/// Settings for RageCoop Server
/// </summary>
public class Settings
{
/// <summary>
/// Settings for RageCoop Server
/// Port to listen for incoming connections
/// </summary>
public class Settings
{
/// <summary>
/// Port to listen for incoming connections
/// </summary>
public int Port { get; set; } = 4499;
public int Port { get; set; } = 4499;
/// <summary>
/// Maximum number of players on this server
/// </summary>
public int MaxPlayers { get; set; } = 32;
/// <summary>
/// Maximum number of players on this server
/// </summary>
public int MaxPlayers { get; set; } = 32;
/// <summary>
/// Maximum latency allowed for a client, a client will be kicked if it's latency it's higher than this value
/// </summary>
public int MaxLatency { get; set; } = 500;
/// <summary>
/// Maximum latency allowed for a client, a client will be kicked if it's latency it's higher than this value
/// </summary>
public int MaxLatency { get; set; } = 500;
/// <summary>
/// The server name to be shown on master server
/// </summary>
public string Name { get; set; } = "RAGECOOP server";
/// <summary>
/// The server name to be shown on master server
/// </summary>
public string Name { get; set; } = "RAGECOOP server";
/// <summary>
/// The website address to be shown on master server
/// </summary>
public string Website { get; set; } = "https://ragecoop.online/";
/// <summary>
/// The website address to be shown on master server
/// </summary>
public string Website { get; set; } = "https://ragecoop.online/";
/// <summary>
/// The description to be shown on master server
/// </summary>
public string Description { get; set; } = "RAGECOOP server";
/// <summary>
/// The description to be shown on master server
/// </summary>
public string Description { get; set; } = "RAGECOOP server";
/// <summary>
/// The game mode to be shown on master server
/// </summary>
public string GameMode { get; set; } = "FreeRoam";
/// <summary>
/// The game mode to be shown on master server
/// </summary>
public string GameMode { get; set; } = "FreeRoam";
/// <summary>
/// The language to be shown on master server
/// </summary>
public string Language { get; set; } = "English";
/// <summary>
/// The language to be shown on master server
/// </summary>
public string Language { get; set; } = "English";
/// <summary>
/// The message to send when a client connected (not visible to others)
/// </summary>
public string WelcomeMessage { get; set; } = "Welcome on this server :)";
/// <summary>
/// The message to send when a client connected (not visible to others)
/// </summary>
public string WelcomeMessage { get; set; } = "Welcome on this server :)";
/// <summary>
/// Whether or not to announce this server so it'll appear on server list.
/// </summary>
public bool AnnounceSelf { get; set; } = false;
/// <summary>
/// Whether or not to announce this server so it'll appear on server list.
/// </summary>
public bool AnnounceSelf { get; set; } = false;
/// <summary>
/// Master server address, mostly doesn't need to be changed.
/// </summary>
public string MasterServer { get; set; } = "https://masterserver.ragecoop.online/";
/// <summary>
/// Master server address, mostly doesn't need to be changed.
/// </summary>
public string MasterServer { get; set; } = "https://masterserver.ragecoop.online/";
/// <summary>
/// See <see cref="Core.Logger.LogLevel"/>.
/// </summary>
public int LogLevel { get; set; } = 0;
/// <summary>
/// See <see cref="Core.Logger.LogLevel" />.
/// </summary>
public int LogLevel { get; set; } = 0;
/// <summary>
/// NPC data won't be sent to a player if their distance is greater than this value. -1 for unlimited.
/// </summary>
public float NpcStreamingDistance { get; set; } = 500;
/// <summary>
/// NPC data won't be sent to a player if their distance is greater than this value. -1 for unlimited.
/// </summary>
public float NpcStreamingDistance { get; set; } = 500;
/// <summary>
/// Player's data won't be sent to another player if their distance is greater than this value. -1 for unlimited.
/// </summary>
public float PlayerStreamingDistance { get; set; } = -1;
/// <summary>
/// Player's data won't be sent to another player if their distance is greater than this value. -1 for unlimited.
/// </summary>
public float PlayerStreamingDistance { get; set; } = -1;
/// <summary>
/// If enabled, all clients will have same weather and time as host
/// </summary>
public bool WeatherTimeSync { get; set; } = true;
/// <summary>
/// If enabled, all clients will have same weather and time as host
/// </summary>
public bool WeatherTimeSync { get; set; } = true;
/// <summary>
/// List of all allowed username characters
/// </summary>
public string AllowedUsernameChars { get; set; } = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_";
/// <summary>
/// List of all allowed username characters
/// </summary>
public string AllowedUsernameChars { get; set; } =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890-_";
/// <summary>
/// Whether to use direct connection between players to send entity information, <see cref="UseZeroTier"/> needs to be enabled if on WAN for this feature to function properly.
/// </summary>
public bool UseP2P { get; set; } = false;
/// <summary>
/// Whether to use direct connection between players to send entity information, <see cref="UseZeroTier" /> needs to be
/// enabled if on WAN for this feature to function properly.
/// </summary>
public bool UseP2P { get; set; } = false;
/// <summary>
/// Whether to enable zerotier VLAN functionality, allowing you to host a server behind NAT firewall, no port forward required.
/// </summary>
public bool UseZeroTier { get; set; } = false;
/// <summary>
/// Whether to enable zerotier VLAN functionality, allowing you to host a server behind NAT firewall, no port forward
/// required.
/// </summary>
public bool UseZeroTier { get; set; } = false;
/// <summary>
/// Use in-game voice chat to communicate with other players
/// </summary>
public bool UseVoice { get; set; } = false;
/// <summary>
/// Use in-game voice chat to communicate with other players
/// </summary>
public bool UseVoice { get; set; } = false;
/// <summary>
/// The zerotier network id to join, default value is zerotier's public Earth network.
/// </summary>
public string ZeroTierNetworkID { get; set; } = "8056c2e21c000001";
/// <summary>
/// The zerotier network id to join, default value is zerotier's public Earth network.
/// </summary>
public string ZeroTierNetworkID { get; set; } = "8056c2e21c000001";
/// <summary>
/// Automatically update to nightly build when an update is avalible, check is performed every 10 minutes.
/// </summary>
public bool AutoUpdate { get; set; } = false;
/// <summary>
/// Automatically update to nightly build when an update is avalible, check is performed every 10 minutes.
/// </summary>
public bool AutoUpdate { get; set; } = false;
/// <summary>
/// Kick godmode assholes
/// </summary>
public bool KickGodMode { get; set; } = false;
/// <summary>
/// Kick godmode assholes
/// </summary>
public bool KickGodMode { get; set; } = false;
/// <summary>
/// Kick spamming assholes
/// </summary>
public bool KickSpamming { get; set; } = true;
/// <summary>
/// Kick spamming assholes
/// </summary>
public bool KickSpamming { get; set; } = true;
/// <summary>
/// Player that spawned entities more than this amount will be kicked if <see cref="KickSpamming"/> is enabled.
/// </summary>
public int SpamLimit { get; set; } = 100;
}
}
/// <summary>
/// Player that spawned entities more than this amount will be kicked if <see cref="KickSpamming" /> is enabled.
/// </summary>
public int SpamLimit { get; set; } = 100;
}

View File

@ -1,5 +1,4 @@
global using System.Collections.Generic;
using Lidgren.Network;
using System;
using System.IO;
using System.Linq;
@ -7,148 +6,146 @@ using System.Net;
using System.Net.Http;
using System.Xml;
using System.Xml.Serialization;
namespace RageCoop.Server
{
internal static partial class Util
{
using Lidgren.Network;
public static string DownloadString(string url)
namespace RageCoop.Server;
internal static class Util
{
public static string DownloadString(string url)
{
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, url);
var response = client.Send(request);
using var reader = new StreamReader(response.Content.ReadAsStream());
var responseBody = reader.ReadToEnd();
return responseBody;
}
catch
{
return "";
}
}
public static List<NetConnection> Exclude(this IEnumerable<NetConnection> connections, NetConnection toExclude)
{
return new List<NetConnection>(connections.Where(e => e != toExclude));
}
public static T Read<T>(string file) where T : new()
{
XmlSerializer ser = new(typeof(T));
XmlWriterSettings settings = new()
{
Indent = true,
IndentChars = "\t",
OmitXmlDeclaration = true
};
var path = AppContext.BaseDirectory + file;
T data;
if (File.Exists(path))
try
{
// TLS only
ServicePointManager.Expect100Continue = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13 | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
using (var stream = XmlReader.Create(path))
{
data = (T)ser.Deserialize(stream);
}
HttpClient client = new();
HttpRequestMessage request = new(HttpMethod.Get, url);
HttpResponseMessage response = client.Send(request);
using var reader = new StreamReader(response.Content.ReadAsStream());
string responseBody = reader.ReadToEnd();
return responseBody;
using (var stream = XmlWriter.Create(path, settings))
{
ser.Serialize(stream, data);
}
}
catch
{
return "";
}
}
public static List<NetConnection> Exclude(this IEnumerable<NetConnection> connections, NetConnection toExclude)
{
return new(connections.Where(e => e != toExclude));
}
public static T Read<T>(string file) where T : new()
{
XmlSerializer ser = new(typeof(T));
XmlWriterSettings settings = new()
{
Indent = true,
IndentChars = ("\t"),
OmitXmlDeclaration = true
};
string path = AppContext.BaseDirectory + file;
T data;
if (File.Exists(path))
{
try
{
using (XmlReader stream = XmlReader.Create(path))
{
data = (T)ser.Deserialize(stream);
}
using (XmlWriter stream = XmlWriter.Create(path, settings))
{
ser.Serialize(stream, data);
}
}
catch
{
using (XmlWriter stream = XmlWriter.Create(path, settings))
{
ser.Serialize(stream, data = new T());
}
}
}
else
{
using (XmlWriter stream = XmlWriter.Create(path, settings))
using (var stream = XmlWriter.Create(path, settings))
{
ser.Serialize(stream, data = new T());
}
}
return data;
}
public static T Next<T>(this T[] values)
{
return values[new Random().Next(values.Length - 1)];
}
public static string GetFinalRedirect(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
int maxRedirCount = 8; // prevent infinite loops
string newUrl = url;
do
else
using (var stream = XmlWriter.Create(path, settings))
{
try
{
HttpClientHandler handler = new()
{
AllowAutoRedirect = false
};
HttpClient client = new(handler);
HttpRequestMessage request = new(HttpMethod.Head, url);
HttpResponseMessage response = client.Send(request);
ser.Serialize(stream, data = new T());
}
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return newUrl;
case HttpStatusCode.Redirect:
case HttpStatusCode.MovedPermanently:
case HttpStatusCode.RedirectKeepVerb:
case HttpStatusCode.RedirectMethod:
newUrl = response.Headers.Location.ToString();
if (newUrl == null)
return url;
string newUrlString = newUrl;
if (!newUrlString.Contains("://"))
{
// Doesn't have a URL Schema, meaning it's a relative or absolute URL
Uri u = new Uri(new Uri(url), newUrl);
newUrl = u.ToString();
}
break;
default:
return newUrl;
}
url = newUrl;
}
catch (WebException)
{
// Return the last known good URL
return newUrl;
}
catch
{
return null;
}
} while (maxRedirCount-- > 0);
return newUrl;
}
return data;
}
}
public static T Next<T>(this T[] values)
{
return values[new Random().Next(values.Length - 1)];
}
public static string GetFinalRedirect(string url)
{
if (string.IsNullOrWhiteSpace(url))
return url;
var maxRedirCount = 8; // prevent infinite loops
var newUrl = url;
do
{
try
{
HttpClientHandler handler = new()
{
AllowAutoRedirect = false
};
HttpClient client = new(handler);
HttpRequestMessage request = new(HttpMethod.Head, url);
var response = client.Send(request);
switch (response.StatusCode)
{
case HttpStatusCode.OK:
return newUrl;
case HttpStatusCode.Redirect:
case HttpStatusCode.MovedPermanently:
case HttpStatusCode.RedirectKeepVerb:
case HttpStatusCode.RedirectMethod:
newUrl = response.Headers.Location.ToString();
if (newUrl == null)
return url;
var newUrlString = newUrl;
if (!newUrlString.Contains("://"))
{
// Doesn't have a URL Schema, meaning it's a relative or absolute URL
var u = new Uri(new Uri(url), newUrl);
newUrl = u.ToString();
}
break;
default:
return newUrl;
}
url = newUrl;
}
catch (WebException)
{
// Return the last known good URL
return newUrl;
}
catch
{
return null;
}
} while (maxRedirCount-- > 0);
return newUrl;
}
}