Restructure solution
This commit is contained in:
209
Server/Client.cs
Normal file
209
Server/Client.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using Lidgren.Network;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using RageCoop.Server.Scripting;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Represent a player connected to this server.
|
||||
/// </summary>
|
||||
public class Client
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
10
Server/FileTransfer.cs
Normal file
10
Server/FileTransfer.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
internal class FileTransfer
|
||||
{
|
||||
public int ID { get; set; }
|
||||
public float Progress { get; set; }
|
||||
public string Name { get; set; }
|
||||
public bool Cancel { get; set; } = false;
|
||||
}
|
||||
}
|
3
Server/FodyWeavers.xml
Normal file
3
Server/FodyWeavers.xml
Normal file
@ -0,0 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Costura ExcludeAssemblies="RageCoop.Core" />
|
||||
</Weavers>
|
141
Server/FodyWeavers.xsd
Normal file
141
Server/FodyWeavers.xsd
Normal file
@ -0,0 +1,141 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
|
||||
<xs:element name="Weavers">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element name="Costura" minOccurs="0" maxOccurs="1">
|
||||
<xs:complexType>
|
||||
<xs:all>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with line breaks</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
<xs:element minOccurs="0" maxOccurs="1" name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with line breaks.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="CreateTemporaryAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This will copy embedded files to disk before loading them into memory. This is helpful for some scenarios that expected an assembly to be loaded from a physical file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeDebugSymbols" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if .pdbs for reference assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeReferences" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls if runtime assemblies are also embedded.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="UseRuntimeReferencePaths" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Controls whether the runtime assemblies are embedded with their full path or only with their assembly name.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCompression" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Embedded assemblies are compressed by default, and uncompressed when they are loaded. You can turn compression off with this option.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="DisableCleanup" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>As part of Costura, embedded assemblies are no longer included as part of the build. This cleanup can be turned off.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="LoadAtModuleInit" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura by default will load as part of the module initialization. This flag disables that behavior. Make sure you call CosturaUtility.Initialize() somewhere in your code.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IgnoreSatelliteAssemblies" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Costura will by default use assemblies with a name like 'resources.dll' as a satellite resource and prepend the output path. This flag disables that behavior.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="ExcludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to exclude from the default action of "embed all Copy Local references", delimited with |</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="IncludeRuntimeAssemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of runtime assembly names to include from the default action of "embed all Copy Local references", delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged32Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 32 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="Unmanaged64Assemblies" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A list of unmanaged 64 bit assembly names to include, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="PreloadOrder" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The order of preloaded assemblies, delimited with |.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="VerifyAssembly" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="GenerateXsd" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
</xs:schema>
|
7
Server/HolePunch.cs
Normal file
7
Server/HolePunch.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
internal class HolePunch
|
||||
{
|
||||
|
||||
}
|
||||
}
|
197
Server/Networking/Server.Background.cs
Normal file
197
Server/Networking/Server.Background.cs
Normal file
@ -0,0 +1,197 @@
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using Lidgren.Network;
|
||||
using Newtonsoft.Json;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Server.Scripting;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
public partial class Server
|
||||
{
|
||||
private const string _versionURL = "https://raw.githubusercontent.com/RAGECOOP/RAGECOOP-V/main/RageCoop.Server/Properties/AssemblyInfo.cs";
|
||||
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; };
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
Security.GetPublicKey(out var pModulus, out var pExpoenet);
|
||||
var serverInfo = new ServerInfo
|
||||
{
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
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 versionLine = HttpHelper.DownloadString(_versionURL).Split('\n', StringSplitOptions.RemoveEmptyEntries).Where(x => x.Contains("[assembly: AssemblyVersion(")).First();
|
||||
var start = versionLine.IndexOf('\"') + 1;
|
||||
var end = versionLine.LastIndexOf('\"');
|
||||
var latest = Version.Parse(versionLine.AsSpan(start, end - start));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
197
Server/Networking/Server.Connections.cs
Normal file
197
Server/Networking/Server.Connections.cs
Normal file
@ -0,0 +1,197 @@
|
||||
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
|
||||
{
|
||||
public partial class Server
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
78
Server/Networking/Server.EntitySync.cs
Normal file
78
Server/Networking/Server.EntitySync.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using Lidgren.Network;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Server.Scripting;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
public partial class Server
|
||||
{
|
||||
private void PedSync(Packets.PedSync packet, Client client)
|
||||
{
|
||||
QueueJob(() => Entities.Update(packet, client));
|
||||
|
||||
bool 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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
private void ProjectileSync(Packets.ProjectileSync packet, Client client)
|
||||
{
|
||||
if (Settings.UseP2P) { return; }
|
||||
Forward(packet, client, ConnectionChannel.ProjectileSync);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
29
Server/Networking/Server.HolePunch.cs
Normal file
29
Server/Networking/Server.HolePunch.cs
Normal file
@ -0,0 +1,29 @@
|
||||
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);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
247
Server/Networking/Server.Listener.cs
Normal file
247
Server/Networking/Server.Listener.cs
Normal file
@ -0,0 +1,247 @@
|
||||
using Lidgren.Network;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Server.Scripting;
|
||||
using System;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
public partial class Server
|
||||
{
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
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:
|
||||
{
|
||||
Packets.CustomEvent 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
412
Server/Networking/Server.cs
Normal file
412
Server/Networking/Server.cs
Normal file
@ -0,0 +1,412 @@
|
||||
using Lidgren.Network;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Server.Scripting;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// The instantiable RageCoop server class
|
||||
/// </summary>
|
||||
public partial class Server
|
||||
{
|
||||
/// <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;
|
||||
|
||||
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)
|
||||
{
|
||||
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());
|
||||
|
||||
|
||||
_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();
|
||||
|
||||
|
||||
_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()} => {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;
|
||||
|
||||
|
||||
}
|
||||
/// <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('/'))
|
||||
{
|
||||
string[] cmdArgs = message.Split(" ");
|
||||
string cmdName = cmdArgs[0].Remove(0, 1);
|
||||
QueueJob(() => API.Events.InvokeOnCommandReceived(cmdName, cmdArgs, sender));
|
||||
return;
|
||||
}
|
||||
message = message.Replace("~", "");
|
||||
|
||||
QueueJob(() => API.Events.InvokeOnChatMessage(message, sender));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
internal void SendChatMessage(string name, string message, Client target)
|
||||
{
|
||||
if (target == null) { return; }
|
||||
var msg = MainNetServer.CreateMessage();
|
||||
new Packets.ChatMessage(new Func<string, byte[]>((s) =>
|
||||
{
|
||||
return Security.Encrypt(s.GetBytes(), target.EndPoint);
|
||||
}))
|
||||
{
|
||||
Username = name,
|
||||
Message = message,
|
||||
}.Pack(msg);
|
||||
MainNetServer.SendMessage(msg, target.Connection, NetDeliveryMethod.ReliableOrdered, (int)ConnectionChannel.Chat);
|
||||
}
|
||||
|
||||
internal void RegisterCommand(string name, string usage, short argsLength, Action<CommandContext> callback)
|
||||
{
|
||||
Command command = new(name) { Usage = usage, ArgsLength = argsLength };
|
||||
|
||||
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>()
|
||||
{
|
||||
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()
|
||||
{
|
||||
FileLength = total,
|
||||
Name = name,
|
||||
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);
|
||||
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()
|
||||
{
|
||||
ID = id,
|
||||
FileChunk = chunk,
|
||||
},
|
||||
client, ConnectionChannel.File, NetDeliveryMethod.ReliableOrdered);
|
||||
transfer.Progress = read / stream.Length;
|
||||
if (updateCallback != null) { updateCallback(transfer.Progress); }
|
||||
|
||||
} while (thisRead > 0);
|
||||
if (GetResponse<Packets.FileTransferResponse>(client, new Packets.FileTransferComplete()
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
111
Server/Program.cs
Normal file
111
Server/Program.cs
Normal file
@ -0,0 +1,111 @@
|
||||
using RageCoop.Core;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
private static bool Stopping = false;
|
||||
private static Logger mainLogger;
|
||||
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
if (args.Length >= 2 && args[0] == "update")
|
||||
{
|
||||
var target = args[1];
|
||||
int i = 0;
|
||||
while (i++ < 10)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Applying update to " + target);
|
||||
|
||||
CoreUtils.CopyFilesRecursively(new(AppDomain.CurrentDomain.BaseDirectory), new(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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
21
Server/Properties/AssemblyInfo.cs
Normal file
21
Server/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,21 @@
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Resources;
|
||||
|
||||
// General Information
|
||||
[assembly: AssemblyTitle("RageCoop.Server")]
|
||||
[assembly: AssemblyDescription("RageCoop.Server")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("RAGECOOP")]
|
||||
[assembly: AssemblyProduct("RageCoop.Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2022")]
|
||||
[assembly: AssemblyTrademark("RAGECOOP")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information
|
||||
[assembly: AssemblyVersion("1.5.4.73")]
|
||||
[assembly: AssemblyFileVersion("1.5.4.73")]
|
||||
[assembly: NeutralResourcesLanguageAttribute( "en-US" )]
|
||||
|
45
Server/Properties/AssemblyInfo.tt
Normal file
45
Server/Properties/AssemblyInfo.tt
Normal file
@ -0,0 +1,45 @@
|
||||
<#@ template debug="true" hostspecific="true" language="C#" #>
|
||||
<#@ output extension=".cs" #>
|
||||
<#@ import namespace="System.IO" #>
|
||||
<#@ import namespace="System.Text.RegularExpressions" #>
|
||||
<#
|
||||
string output = File.ReadAllText(this.Host.ResolvePath("AssemblyInfo.cs"));
|
||||
Regex pattern = new Regex("AssemblyVersion\\(\"(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<revision>\\d+)\\.(?<build>\\d+)\"\\)");
|
||||
MatchCollection matches = pattern.Matches(output);
|
||||
if( matches.Count == 1 )
|
||||
{
|
||||
major = Convert.ToInt32(matches[0].Groups["major"].Value);
|
||||
minor = Convert.ToInt32(matches[0].Groups["minor"].Value);
|
||||
build = Convert.ToInt32(matches[0].Groups["build"].Value) + 1;
|
||||
revision = Convert.ToInt32(matches[0].Groups["revision"].Value);
|
||||
if( this.Host.ResolveParameterValue("-","-","BuildConfiguration") == "Release" )
|
||||
revision++;
|
||||
}
|
||||
#>
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Resources;
|
||||
|
||||
// General Information
|
||||
[assembly: AssemblyTitle("RageCoop.Server")]
|
||||
[assembly: AssemblyDescription("RageCoop.Server")]
|
||||
[assembly: AssemblyConfiguration("")]
|
||||
[assembly: AssemblyCompany("RAGECOOP")]
|
||||
[assembly: AssemblyProduct("RageCoop.Server")]
|
||||
[assembly: AssemblyCopyright("Copyright © 2022")]
|
||||
[assembly: AssemblyTrademark("RAGECOOP")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Version information
|
||||
[assembly: AssemblyVersion("<#= this.major #>.<#= this.minor #>.<#= this.revision #>.<#= this.build #>")]
|
||||
[assembly: AssemblyFileVersion("<#= this.major #>.<#= this.minor #>.<#= this.revision #>.<#= this.build #>")]
|
||||
[assembly: NeutralResourcesLanguageAttribute( "en-US" )]
|
||||
|
||||
<#+
|
||||
int major = 1;
|
||||
int minor = 0;
|
||||
int revision = 0;
|
||||
int build = 0;
|
||||
#>
|
109
Server/RageCoop.Server.csproj
Normal file
109
Server/RageCoop.Server.csproj
Normal file
@ -0,0 +1,109 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<AssemblyVersion></AssemblyVersion>
|
||||
<FileVersion></FileVersion>
|
||||
<RepositoryUrl>https://github.com/RAGECOOP/RAGECOOP-V</RepositoryUrl>
|
||||
<PackageProjectUrl>https://ragecoop.online/</PackageProjectUrl>
|
||||
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Product>$(AssemblyName)</Product>
|
||||
<PackageId>RageCoop.Server</PackageId>
|
||||
<Authors>RAGECOOP</Authors>
|
||||
<Version></Version>
|
||||
<DebugType>embedded</DebugType>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Description>An library for hosting a RAGECOOP server or API reference for developing a resource.</Description>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<PackageIcon>icon.png</PackageIcon>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<OutDir>..\bin\Debug\Server</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
<OutDir>..\bin\Release\Server</OutDir>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<NoWarn>1701;1702;1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="icon.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\images\icon.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="Properties\AssemblyInfo.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>AssemblyInfo.tt</DependentUpon>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Costura.Fody" Version="5.7.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Fody" Version="6.6.3">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="6.0.8" />
|
||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\RageCoop.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Lidgren.Network">
|
||||
<HintPath>..\libs\Lidgren.Network.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="McMaster.NETCore.Plugins">
|
||||
<HintPath>..\libs\McMaster.NETCore.Plugins.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>..\libs\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="ScriptHookVDotNet3">
|
||||
<HintPath>..\libs\ScriptHookVDotNet3.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Properties\AssemblyInfo.tt">
|
||||
<Generator>TextTemplatingFileGenerator</Generator>
|
||||
<LastGenOutput>AssemblyInfo.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Service Include="{508349b6-6b84-4df5-91f0-309beebad82d}" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\AssemblyInfo.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>AssemblyInfo.tt</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition=" '$(DevEnvDir)' != '*Undefined*'">
|
||||
<Exec Command=""$(DevEnvDir)TextTransform.exe" -a !!BuildConfiguration!$(Configuration) "$(ProjectDir)Properties\AssemblyInfo.tt"" />
|
||||
</Target>
|
||||
|
||||
|
||||
</Project>
|
6
Server/RageCoop.Server.csproj.user
Normal file
6
Server/RageCoop.Server.csproj.user
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<_LastSelectedProfileId>M:\SandBox-Shared\repos\RAGECOOP\RAGECOOP-V\RageCoop.Server\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
|
||||
</PropertyGroup>
|
||||
</Project>
|
438
Server/Scripting/API.cs
Normal file
438
Server/Scripting/API.cs
Normal file
@ -0,0 +1,438 @@
|
||||
using Lidgren.Network;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RageCoop.Server.Scripting
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
public class ServerEvents
|
||||
{
|
||||
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();
|
||||
|
||||
CommandContext ctx = new()
|
||||
{
|
||||
Client = sender,
|
||||
Args = argsWithoutCmd
|
||||
};
|
||||
|
||||
KeyValuePair<Command, Action<CommandContext>> command = Server.Commands.First(x => x.Key.Name == cmdName);
|
||||
command.Value.Invoke(ctx);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Server.SendChatMessage("Server", "Command not found!", sender);
|
||||
}
|
||||
}
|
||||
|
||||
internal void InvokeOnChatMessage(string msg, Client sender, string clamiedSender = null)
|
||||
{
|
||||
OnChatMessage?.Invoke(this, new ChatEventArgs()
|
||||
{
|
||||
Client = sender,
|
||||
Message = msg,
|
||||
ClaimedSender = clamiedSender
|
||||
});
|
||||
}
|
||||
internal void InvokePlayerConnected(Client client)
|
||||
{ OnPlayerConnected?.Invoke(this, client); }
|
||||
internal void InvokePlayerReady(Client client)
|
||||
{ OnPlayerReady?.Invoke(this, client); }
|
||||
internal void InvokePlayerDisconnected(Client client)
|
||||
{ OnPlayerDisconnected?.Invoke(this, client); }
|
||||
|
||||
internal void InvokeCustomEventReceived(Packets.CustomEvent p, Client sender)
|
||||
{
|
||||
var args = new CustomEventReceivedArgs() { Hash = p.Hash, Args = p.Args, Client = sender };
|
||||
if (CustomEventHandlers.TryGetValue(p.Hash, out List<Action<CustomEventReceivedArgs>> 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
|
||||
{
|
||||
internal readonly Server Server;
|
||||
internal readonly Dictionary<string, Func<Stream>> RegisteredFiles = new Dictionary<string, Func<System.IO.Stream>>();
|
||||
internal API(Server server)
|
||||
{
|
||||
Server = server;
|
||||
Events = new(server);
|
||||
Server.RequestHandlers.Add(PacketType.FileTransferRequest, (data, client) =>
|
||||
{
|
||||
var p = new Packets.FileTransferRequest();
|
||||
p.Deserialize(data);
|
||||
var id = Server.NewFileID();
|
||||
if (RegisteredFiles.TryGetValue(p.Name, out var s))
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
Server.SendFile(s(), p.Name, client, id);
|
||||
});
|
||||
return new Packets.FileTransferResponse()
|
||||
{
|
||||
ID = id,
|
||||
Response = FileResponse.Loaded
|
||||
};
|
||||
}
|
||||
return new Packets.FileTransferResponse()
|
||||
{
|
||||
ID = id,
|
||||
Response = FileResponse.LoadFailed
|
||||
};
|
||||
});
|
||||
}
|
||||
/// <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)
|
||||
{
|
||||
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
|
||||
};
|
||||
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
|
||||
}
|
||||
}
|
96
Server/Scripting/BaseScript.cs
Normal file
96
Server/Scripting/BaseScript.cs
Normal file
@ -0,0 +1,96 @@
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace RageCoop.Server.Scripting
|
||||
{
|
||||
internal class BaseScript : ServerScript
|
||||
{
|
||||
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; }
|
||||
|
||||
foreach (var c in API.GetAllClients().Values)
|
||||
{
|
||||
if (c == e.Client)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
c.SendCustomEventQueued(CustomEvents.WeatherTimeSync, e.Args);
|
||||
}
|
||||
}
|
||||
});
|
||||
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)
|
||||
{
|
||||
if (e.Client.Callbacks.TryGetValue(id, out Action<object> callback))
|
||||
{
|
||||
callback(e.Args[1]);
|
||||
e.Client.Callbacks.Remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
API.Logger.Error("Failed to parse NativeResponse");
|
||||
API.Logger.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
102
Server/Scripting/EventArgs/EventArgs.cs
Normal file
102
Server/Scripting/EventArgs/EventArgs.cs
Normal file
@ -0,0 +1,102 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
|
||||
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 Client Client { get; set; }
|
||||
/// <summary>
|
||||
/// Message
|
||||
/// </summary>
|
||||
public string Message { 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>
|
||||
///
|
||||
/// </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; }
|
||||
/// <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;
|
||||
}
|
||||
}
|
270
Server/Scripting/Resources.cs
Normal file
270
Server/Scripting/Resources.cs
Normal file
@ -0,0 +1,270 @@
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using RageCoop.Core;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
namespace RageCoop.Server.Scripting
|
||||
{
|
||||
internal class Resources
|
||||
{
|
||||
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;
|
||||
}
|
||||
private readonly Dictionary<string, Stream> ClientResources = new();
|
||||
private readonly Dictionary<string, Stream> ResourceStreams = new();
|
||||
public void LoadAll()
|
||||
{
|
||||
// Packages
|
||||
{
|
||||
var path = Path.Combine("Resources", "Packages");
|
||||
Directory.CreateDirectory(path);
|
||||
foreach (var pkg in Directory.GetFiles(path, "*.respkg", SearchOption.AllDirectories))
|
||||
{
|
||||
Logger?.Debug($"Adding resources from package \"{Path.GetFileNameWithoutExtension(pkg)}\"");
|
||||
var pkgZip = new ZipFile(pkg);
|
||||
foreach (ZipEntry e in pkgZip)
|
||||
{
|
||||
if (!e.IsFile) { continue; }
|
||||
if (e.Name.StartsWith("Client") && e.Name.EndsWith(".res"))
|
||||
{
|
||||
var stream = pkgZip.GetInputStream(e).ToMemStream();
|
||||
ClientResources.Add(Path.GetFileNameWithoutExtension(e.Name), stream);
|
||||
Logger?.Debug("Resource added: " + Path.GetFileNameWithoutExtension(e.Name));
|
||||
}
|
||||
else if (e.Name.StartsWith("Server") && e.Name.EndsWith(".res"))
|
||||
{
|
||||
var stream = pkgZip.GetInputStream(e).ToMemStream();
|
||||
ResourceStreams.Add(Path.GetFileNameWithoutExtension(e.Name), stream);
|
||||
Logger?.Debug("Resource added: " + Path.GetFileNameWithoutExtension(e.Name));
|
||||
}
|
||||
}
|
||||
pkgZip.Close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Pack client side resource as a zip file
|
||||
Logger?.Info("Packing client-side resource: " + resourceFolder);
|
||||
var zipPath = Path.Combine(tmpDir, Path.GetFileName(resourceFolder)) + ".res";
|
||||
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(Path.GetFileNameWithoutExtension(zipPath), File.OpenRead(zipPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.Error($"Failed to pack client resource:{resourceFolder}");
|
||||
Logger?.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
var packed = Directory.GetFiles(path, "*.res", SearchOption.TopDirectoryOnly);
|
||||
if (packed.Length > 0)
|
||||
{
|
||||
foreach (var file in packed)
|
||||
{
|
||||
ClientResources.Add(Path.GetFileNameWithoutExtension(file), File.OpenRead(file));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 Directory.GetFiles(path, "*.res", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
if (!ResourceStreams.TryAdd(Path.GetFileNameWithoutExtension(res), File.OpenRead(res)))
|
||||
{
|
||||
Logger?.Warning($"Resource \"{res}\" cannot be loaded, ignoring...");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
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()
|
||||
{
|
||||
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 ResourceStreams.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Close();
|
||||
s.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.Error("[Resources.CloseStream]", ex);
|
||||
}
|
||||
}
|
||||
foreach (var s in ClientResources.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
s.Close();
|
||||
s.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.Error("[Resources.CloseStream]", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
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())?.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!");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
263
Server/Scripting/ServerEntities.cs
Normal file
263
Server/Scripting/ServerEntities.cs
Normal file
@ -0,0 +1,263 @@
|
||||
using GTA;
|
||||
using GTA.Math;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
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>
|
||||
/// 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)
|
||||
{
|
||||
int id = RequestNetworkID();
|
||||
ServerProp prop;
|
||||
ServerProps.TryAdd(id, prop = new ServerProp(Server)
|
||||
{
|
||||
ID = id,
|
||||
Model = model,
|
||||
_pos = pos,
|
||||
_rot = rot
|
||||
});
|
||||
prop.Update();
|
||||
return prop;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
420
Server/Scripting/ServerObject.cs
Normal file
420
Server/Scripting/ServerObject.cs
Normal file
@ -0,0 +1,420 @@
|
||||
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
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Server-side object controller
|
||||
/// </summary>
|
||||
public abstract class ServerObject
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents an prop owned by server.
|
||||
/// </summary>
|
||||
public class ServerProp : ServerObject
|
||||
{
|
||||
|
||||
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
|
||||
/// </summary>
|
||||
public class ServerPed : ServerObject
|
||||
{
|
||||
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>
|
||||
/// 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
|
||||
/// </summary>
|
||||
public class ServerVehicle : ServerObject
|
||||
{
|
||||
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); }
|
||||
}
|
||||
|
||||
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); }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A static blip owned by server.
|
||||
/// </summary>
|
||||
public class ServerBlip
|
||||
{
|
||||
private readonly Server Server;
|
||||
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
|
||||
{
|
||||
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 });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represent a blip attached to ped.
|
||||
/// </summary>
|
||||
public class PedBlip
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the <see cref="ServerPed"/> that this blip attached to.
|
||||
/// </summary>
|
||||
public ServerPed Ped { get; internal set; }
|
||||
internal PedBlip(ServerPed ped)
|
||||
{
|
||||
Ped = ped;
|
||||
}
|
||||
|
||||
|
||||
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(); }
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
234
Server/Scripting/ServerResource.cs
Normal file
234
Server/Scripting/ServerResource.cs
Normal file
@ -0,0 +1,234 @@
|
||||
using ICSharpCode.SharpZipLib.Zip;
|
||||
using McMaster.NETCore.Plugins;
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
internal ServerResource(PluginConfig config) : base(config) { }
|
||||
internal static ServerResource LoadFrom(string resDir, string dataFolder, Logger logger = null)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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"); };
|
||||
|
||||
Directory.CreateDirectory(r.DataFolder);
|
||||
foreach (var dir in Directory.GetDirectories(resDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
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))
|
||||
{
|
||||
if (Path.GetFileName(file).CanBeIgnored()) { try { File.Delete(file); } 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);
|
||||
}
|
||||
/// <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;
|
||||
}
|
||||
|
||||
internal new void Dispose()
|
||||
{
|
||||
base.Dispose();
|
||||
}
|
||||
private static bool IsManagedAssembly(string filename)
|
||||
{
|
||||
try
|
||||
{
|
||||
using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
if (file.Length < 64)
|
||||
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;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// This is likely not a valid assembly if any IO exceptions occur during reading
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
86
Server/Scripting/ServerScript.cs
Normal file
86
Server/Scripting/ServerScript.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
|
||||
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>
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
}
|
72
Server/Security.cs
Normal file
72
Server/Security.cs
Normal file
@ -0,0 +1,72 @@
|
||||
using RageCoop.Core;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
internal class Security
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
128
Server/Settings.cs
Normal file
128
Server/Settings.cs
Normal file
@ -0,0 +1,128 @@
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
/// <summary>
|
||||
/// Settings for RageCoop Server
|
||||
/// </summary>
|
||||
public class Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Port to listen for incoming connections
|
||||
/// </summary>
|
||||
public int Port { get; set; } = 4499;
|
||||
|
||||
/// <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>
|
||||
/// 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 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 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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// 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>
|
||||
/// 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 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>
|
||||
/// 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>
|
||||
/// Kick godmode assholes
|
||||
/// </summary>
|
||||
public bool KickGodMode { get; set; } = false;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
}
|
154
Server/Util.cs
Normal file
154
Server/Util.cs
Normal file
@ -0,0 +1,154 @@
|
||||
global using System.Collections.Generic;
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Xml;
|
||||
using System.Xml.Serialization;
|
||||
namespace RageCoop.Server
|
||||
{
|
||||
internal static partial 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);
|
||||
HttpResponseMessage response = client.Send(request);
|
||||
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||
string responseBody = reader.ReadToEnd();
|
||||
|
||||
return responseBody;
|
||||
}
|
||||
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))
|
||||
{
|
||||
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
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpClientHandler handler = new()
|
||||
{
|
||||
AllowAutoRedirect = false
|
||||
};
|
||||
HttpClient client = new(handler);
|
||||
HttpRequestMessage request = new(HttpMethod.Head, url);
|
||||
HttpResponseMessage 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
BIN
Server/icon.ico
Normal file
BIN
Server/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Reference in New Issue
Block a user