Client API remoting stuff

This commit is contained in:
Sardelka9515
2023-02-12 22:06:57 +08:00
parent fb654f0e56
commit ac07edfe68
34 changed files with 764 additions and 212 deletions

View File

@ -0,0 +1,45 @@
namespace RageCoop.Client.Scripting
{
public static unsafe partial class APIBridge
{
public static class Config
{
public static System.String Username => GetConfig<System.String>("Username");
public static System.Boolean EnableAutoRespawn => GetConfig<System.Boolean>("EnableAutoRespawn");
public static GTA.BlipColor BlipColor => GetConfig<GTA.BlipColor>("BlipColor");
public static GTA.BlipSprite BlipSprite => GetConfig<GTA.BlipSprite>("BlipSprite");
public static System.Single BlipScale => GetConfig<System.Single>("BlipScale");
public static System.Boolean ShowPlayerNameTag => GetConfig<System.Boolean>("ShowPlayerNameTag");
}
#region PROPERTIES
public static System.Int32 LocalPlayerID => GetProperty<System.Int32>("LocalPlayerID");
public static System.Boolean IsOnServer => GetProperty<System.Boolean>("IsOnServer");
public static System.Net.IPEndPoint ServerEndPoint => GetProperty<System.Net.IPEndPoint>("ServerEndPoint");
public static System.Boolean IsMenuVisible => GetProperty<System.Boolean>("IsMenuVisible");
public static System.Boolean IsChatFocused => GetProperty<System.Boolean>("IsChatFocused");
public static System.Boolean IsPlayerListVisible => GetProperty<System.Boolean>("IsPlayerListVisible");
public static System.Version CurrentVersion => GetProperty<System.Version>("CurrentVersion");
public static System.Collections.Generic.Dictionary<System.Int32, RageCoop.Client.Scripting.PlayerInfo> Players => GetProperty<System.Collections.Generic.Dictionary<System.Int32, RageCoop.Client.Scripting.PlayerInfo>>("Players");
#endregion
#region FUNCTIONS
public static void Connect(System.String address) => InvokeCommand("Connect", address);
public static void Disconnect() => InvokeCommand("Disconnect");
public static System.Collections.Generic.List<RageCoop.Core.ServerInfo> ListServers() => InvokeCommand<System.Collections.Generic.List<RageCoop.Core.ServerInfo>>("ListServers");
public static void LocalChatMessage(System.String from, System.String message) => InvokeCommand("LocalChatMessage", from, message);
public static void SendChatMessage(System.String message) => InvokeCommand("SendChatMessage", message);
public static RageCoop.Client.Scripting.ClientResource GetResource(System.String name) => InvokeCommand<RageCoop.Client.Scripting.ClientResource>("GetResource", name);
public static System.Object GetConfig(System.String name) => InvokeCommand<System.Object>("GetConfig", name);
public static void SetConfig(System.String name, System.String jsonVal) => InvokeCommand("SetConfig", name, jsonVal);
#endregion
}
}

View File

@ -0,0 +1,128 @@
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
[assembly: DisableRuntimeMarshalling]
[assembly: InternalsVisibleTo("RageCoop.Client")] // For debugging
namespace RageCoop.Client.Scripting
{
public static unsafe partial class APIBridge
{
static readonly ThreadLocal<char[]> _resultBuf = new(() => new char[4096]);
static readonly ThreadLocal<BufferWriter> _bufWriter = new(() => new(4096));
static readonly ThreadLocal<BufferReader> _bufReader = new(() => new());
/// <summary>
/// Copy content of string to a sequential block of memory
/// </summary>
/// <param name="strs"></param>
/// <returns>Pointer to the start of the block, can be used as argv</returns>
/// <remarks>Call <see cref="Marshal.FreeHGlobal(nint)"/> with the returned pointer when finished using</remarks>
internal static char** StringArrayToMemory(string[] strs)
{
var argc = strs.Length;
var cbSize = sizeof(IntPtr) * argc + strs.Sum(s => (s.Length + 1) * sizeof(char));
var result = (char**)Marshal.AllocHGlobal(cbSize);
var pCurStr = (char*)(result + argc);
for (int i = 0; i < argc; i++)
{
result[i] = pCurStr;
var len = strs[i].Length;
var cbStrSize = (len + 1) * sizeof(char); // null terminator
fixed (char* pStr = strs[i])
{
System.Buffer.MemoryCopy(pStr, pCurStr, cbStrSize, cbStrSize);
}
pCurStr += len + 1;
}
return result;
}
internal static void InvokeCommand(string name, params object[] args)
=> InvokeCommandAsJson(name, args);
internal static T InvokeCommand<T>(string name, params object[] args)
=> JsonDeserialize<T>(InvokeCommandAsJson(name, args));
/// <summary>
/// Invoke command and get the return value as json
/// </summary>
/// <param name="name"></param>
/// <param name="args"></param>
/// <returns>The json representation of returned object</returns>
/// <exception cref="Exception"></exception>
internal static string InvokeCommandAsJson(string name, params object[] args)
{
var argc = args.Length;
var argv = StringArrayToMemory(args.Select(JsonSerialize).ToArray());
try
{
var resultLen = InvokeCommandAsJsonUnsafe(name, argc, argv);
if (resultLen == 0)
throw new Exception(GetLastResult());
return GetLastResult();
}
finally
{
Marshal.FreeHGlobal((IntPtr)argv);
}
}
public static string GetLastResult()
{
var countCharsRequired = GetLastResultLenInChars() + 1;
if (countCharsRequired > _resultBuf.Value.Length)
{
_resultBuf.Value = new char[countCharsRequired];
}
var cbBufSize = _resultBuf.Value.Length * sizeof(char);
fixed (char* pBuf = _resultBuf.Value)
{
if (GetLastResult(pBuf, cbBufSize) > 0)
{
return new string(pBuf);
}
return null;
}
}
public static void SendCustomEvent(CustomEventFlags flags, CustomEventHash hash, params object[] args)
{
var writer = _bufWriter.Value;
writer.Reset();
CustomEvents.WriteObjects(writer, args);
SendCustomEvent(flags, hash, writer.Address, writer.Position);
}
internal static string GetPropertyAsJson(string name) => InvokeCommandAsJson("GetProperty", name);
internal static string GetConfigAsJson(string name) => InvokeCommandAsJson("GetConfig", name);
internal static T GetProperty<T>(string name) => JsonDeserialize<T>(GetPropertyAsJson(name));
internal static T GetConfig<T>(string name) => JsonDeserialize<T>(GetConfigAsJson(name));
internal static void SetProperty(string name, object val) => InvokeCommand("SetProperty", name, val);
internal static void SetConfig(string name, object val) => InvokeCommand("SetConfig", name, val);
[LibraryImport("RageCoop.Client.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial CustomEventHash GetEventHash(string name);
[LibraryImport("RageCoop.Client.dll", StringMarshalling = StringMarshalling.Utf16)]
internal static partial void SetLastResult(string msg);
[LibraryImport("RageCoop.Client.dll")]
private static partial int GetLastResult(char* buf, int cbBufSize);
[LibraryImport("RageCoop.Client.dll", EntryPoint = "InvokeCommand", StringMarshalling = StringMarshalling.Utf16)]
private static partial int InvokeCommandAsJsonUnsafe(string name, int argc, char** argv);
[LibraryImport("RageCoop.Client.dll")]
private static partial void SendCustomEvent(CustomEventFlags flags, int hash, byte* data, int cbData);
[LibraryImport("RageCoop.Client.dll")]
private static partial int GetLastResultLenInChars();
}
}

View File

@ -0,0 +1,9 @@
using GTA;
namespace RageCoop.Client.Scripting
{
[ScriptAttributes(NoDefaultInstance = true)]
public class ClientScript : Script
{
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\RageCoop.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
global using static RageCoop.Core.Shared;
global using static RageCoop.Client.Scripting.Shared;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Client.Scripting
{
internal class Shared
{
}
}

60
Client/Scripting/Types.cs Normal file
View File

@ -0,0 +1,60 @@
using RageCoop.Core.Scripting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Client.Scripting
{
/// <summary>
/// </summary>
public class ClientResource
{
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// Directory where the scripts is loaded from
/// </summary>
public string ScriptsDirectory { 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="ClientScript" /> instance in this resource.
/// </summary>
public List<ClientScript> Scripts { get; internal set; } = new List<ClientScript>();
/// <summary>
/// Get the <see cref="ResourceFile" /> where this script is loaded from.
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new Dictionary<string, ResourceFile>();
/// <summary>
/// A <see cref="Core.Logger" /> instance that can be used to debug your resource.
/// </summary>
public Core.Logger Logger { get; internal set; }
}
public class PlayerInfo
{
public byte HolePunchStatus { get; internal set; }
public bool IsHost { get; internal set; }
public string Username { get; internal set; }
public int ID { get; internal set; }
public int EntityHandle { get; internal set; }
public IPEndPoint InternalEndPoint { get; internal set; }
public IPEndPoint ExternalEndPoint { get; internal set; }
public float Ping { get; internal set; }
public float PacketTravelTime { get; internal set; }
public bool DisplayNameTag { get; internal set; }
public bool HasDirectConnection { get; internal set; }
}
}

View File

@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Reflection;
using System.Threading;
using GTA;
using GTA.Math;
@ -122,7 +123,7 @@ namespace RageCoop.Client
Resources = new Resources();
Logger.Info(
$"Main script initialized");
@ -135,7 +136,7 @@ namespace RageCoop.Client
Util.NativeMemory();
Counter.Restart();
}
protected override void OnTick()
{
@ -237,6 +238,23 @@ namespace RageCoop.Client
return;
}
if (e.KeyCode == Keys.U)
{
foreach (var prop in typeof(APIBridge).GetProperties(BindingFlags.Public | BindingFlags.Static))
{
Console.PrintInfo($"{prop.Name}: {JsonSerialize(prop.GetValue(null))}");
}
foreach (var prop in typeof(APIBridge.Config).GetProperties(BindingFlags.Public | BindingFlags.Static))
{
Console.PrintInfo($"{prop.Name}: {JsonSerialize(prop.GetValue(null))}");
}
}
if (e.KeyCode == Keys.I)
{
APIBridge.SendChatMessage("hello there");
}
#if CEF
if (CefRunning)
{
@ -263,14 +281,14 @@ namespace RageCoop.Client
if (Game.IsControlPressed(Control.FrontendPause))
{
Call(ACTIVATE_FRONTEND_MENU,
Call<int>(GET_HASH_KEY, "FE_MENU_VERSION_SP_PAUSE"), false, 0);
SHVDN.NativeMemory.GetHashKey("FE_MENU_VERSION_SP_PAUSE"), false, 0);
return;
}
if (Game.IsControlPressed(Control.FrontendPauseAlternate) && Settings.DisableAlternatePause)
{
Call(ACTIVATE_FRONTEND_MENU,
Call<int>(GET_HASH_KEY, "FE_MENU_VERSION_SP_PAUSE"), false, 0);
SHVDN.NativeMemory.GetHashKey("FE_MENU_VERSION_SP_PAUSE"), false, 0);
return;
}
}

View File

@ -37,7 +37,7 @@ namespace RageCoop.Client
{
if (File.Exists(AnimationsDataPath))
{
var anims = JsonConvert.DeserializeObject<AnimDic[]>(File.ReadAllText(AnimationsDataPath));
var anims = JsonDeserialize<AnimDic[]>(File.ReadAllText(AnimationsDataPath));
foreach (var anim in anims)
foreach (var a in anim.Animations)
if (Call<bool>(IS_ENTITY_PLAYING_ANIM, Main.P, anim.DictionaryName, a, 3))

View File

@ -58,7 +58,7 @@ namespace RageCoop.Client.Menus
List<ServerInfo> serverList = null;
var realUrl = Main.Settings.MasterServer;
serverList = null;
try { serverList = JsonConvert.DeserializeObject<List<ServerInfo>>(DownloadString(realUrl)); }
try { serverList = JsonDeserialize<List<ServerInfo>>(DownloadString(realUrl)); }
catch (Exception ex) { Main.Logger.Error(ex); }
// Need to be processed in main thread

View File

@ -4,6 +4,7 @@ using System.Drawing;
using GTA;
using GTA.UI;
using LemonUI.Menus;
using RageCoop.Client.Scripting;
namespace RageCoop.Client.Menus
{
@ -66,8 +67,7 @@ namespace RageCoop.Client.Menus
};
_showNametag.Activated += (s, e) =>
{
Main.Settings.ShowPlayerNameTag = _showNametag.Checked;
Util.SaveSettings();
API.Config.ShowPlayerNameTag = _showNametag.Checked;
};
Menu.Add(_disableTrafficItem);

View File

@ -5,6 +5,7 @@ using GTA;
using GTA.Math;
using GTA.Native;
using Lidgren.Network;
using Newtonsoft.Json;
using RageCoop.Client.Menus;
using RageCoop.Client.Scripting;
using RageCoop.Core;
@ -127,9 +128,21 @@ namespace RageCoop.Client
}
}
public class Player
internal class Player
{
internal float _latencyToServer;
internal bool ConnectWhenPunched { get; set; }
[JsonIgnore]
public Blip FakeBlip { get; internal set; }
[JsonIgnore]
public Vector3 Position { get; internal set; }
[JsonIgnore]
public SyncedPed Character { get; internal set; }
[JsonIgnore]
public NetConnection Connection { get; internal set; }
public byte HolePunchStatus { get; internal set; } = 1;
public bool IsHost { get; internal set; }
public string Username { get; internal set; }
@ -139,12 +152,10 @@ namespace RageCoop.Client
/// </summary>
public int ID { get; internal set; }
public int EntityHandle => Character?.MainPed?.Handle ?? 0;
public IPEndPoint InternalEndPoint { get; internal set; }
public IPEndPoint ExternalEndPoint { get; internal set; }
internal bool ConnectWhenPunched { get; set; }
public Blip FakeBlip { get; internal set; }
public Vector3 Position { get; internal set; }
public SyncedPed Character { get; internal set; }
/// <summary>
/// Player round-trip time in seconds, will be the rtt to server if not using P2P connection.
@ -157,7 +168,6 @@ namespace RageCoop.Client
: Networking.Latency + _latencyToServer;
public bool DisplayNameTag { get; set; } = true;
public NetConnection Connection { get; internal set; }
public bool HasDirectConnection => Connection?.Status == NetConnectionStatus.Connected;
}
}

View File

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<NoAotCompile>false</NoAotCompile>
<TargetFramework>net7.0</TargetFramework>
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
<OutputType>Library</OutputType>
<OutDir>..\..\bin\$(Configuration)\Client\Scripts</OutDir>
@ -21,6 +22,9 @@
<EmbeddedResource Remove="GUI\**" />
<None Remove="GUI\**" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Scripting\ClientScript.cs" />
</ItemGroup>
<ItemGroup>
<Compile Update="Properties\AssemblyInfo.cs">
<AutoGen>True</AutoGen>
@ -50,6 +54,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Core\RageCoop.Core.csproj" />
<ProjectReference Include="..\Scripting\RageCoop.Client.Scripting.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NAudio" Version="2.1.0" />

View File

@ -1,87 +1,94 @@
using Lidgren.Network;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using static RageCoop.Core.Scripting.CustomEvents;
namespace RageCoop.Client.Scripting
{
public static unsafe partial class API
internal static unsafe partial class API
{
[UnmanagedCallersOnly(EntryPoint = "Connect")]
public static void Connect(char* address) => Connect(new string(address));
[ThreadStatic]
static string _lastResult;
[UnmanagedCallersOnly(EntryPoint = "GetLocalPlayerID")]
public static int GetLocalPlayerID() => LocalPlayerID;
/// <summary>
/// Get configuration value
/// </summary>
/// <param name="szName">The name of the config</param>
/// <param name="buf">Buffer to store retrived value</param>
/// <param name="bufSize">Buffer size</param>
/// <returns>The string length of returned value, not including the null terminator</returns>
[UnmanagedCallersOnly(EntryPoint = "GetConfigValue")]
public static int GetConfigValue(char* szName, char* buf, int bufSize)
[UnmanagedCallersOnly(EntryPoint = nameof(GetLastResult))]
public static int GetLastResult(char* buf, int cbBufSize)
{
var name = new string(szName);
var value = name switch
{
nameof(Config.EnableAutoRespawn) => Config.EnableAutoRespawn.ToString(),
nameof(Config.Username) => Config.Username.ToString(),
nameof(Config.BlipColor) => Config.BlipColor.ToString(),
nameof(Config.BlipScale) => Config.BlipScale.ToString(),
nameof(Config.BlipSprite) => Config.BlipSprite.ToString(),
_ => null
};
if (value == null)
if (_lastResult == null)
return 0;
fixed (char* p = value)
fixed (char* pErr = _lastResult)
{
var cbRequired = (value.Length + 1) * sizeof(char);
Buffer.MemoryCopy(p, buf, bufSize, cbRequired);
return value.Length;
var cbToCopy = sizeof(char) * (_lastResult.Length + 1);
System.Buffer.MemoryCopy(pErr, buf, cbToCopy, Math.Min(cbToCopy, cbBufSize));
if (cbToCopy > cbBufSize && cbBufSize > 0)
{
buf[cbBufSize / sizeof(char) - 1] = '\0'; // Always add null terminator
}
return _lastResult.Length;
}
}
public static void SetLastResult(string msg) => _lastResult = msg;
[UnmanagedCallersOnly(EntryPoint = nameof(SetLastResult))]
public static void SetLastResult(char* msg)
{
try
{
SetLastResult(msg == null ? null : new string(msg));
}
catch (Exception ex)
{
SHVDN.PInvoke.MessageBoxA(default, ex.ToString(), "error", default);
}
}
[UnmanagedCallersOnly(EntryPoint = "SetConfigValue")]
public static void SetConfigValue(char* szName, char* szValue)
[UnmanagedCallersOnly(EntryPoint = nameof(GetEventHash))]
public static CustomEventHash GetEventHash(char* name) => new string(name);
[UnmanagedCallersOnly(EntryPoint = nameof(SendCustomEvent))]
public static void SendCustomEvent(CustomEventFlags flags, int hash, byte* data, int cbData)
{
var name = new string(szName);
var value = new string(szValue);
switch (name)
var payload = new byte[cbData];
Marshal.Copy((IntPtr)data, payload, 0, cbData);
Networking.Peer.SendTo(new Packets.CustomEvent()
{
case nameof(Config.EnableAutoRespawn): Config.EnableAutoRespawn = bool.Parse(value); break;
case nameof(Config.Username): Config.Username = value; break;
case nameof(Config.BlipColor): Config.BlipColor = Enum.Parse<BlipColor>(value); break;
case nameof(Config.BlipScale): Config.BlipScale = float.Parse(value); break;
case nameof(Config.BlipSprite): Config.BlipSprite = Enum.Parse<BlipSprite>(value); break;
};
Flags = flags,
Payload = payload,
Hash = hash
}, Networking.ServerConnection, ConnectionChannel.Event, NetDeliveryMethod.ReliableOrdered);
}
[UnmanagedCallersOnly(EntryPoint = "LocalChatMessage")]
public static void LocalChatMessage(char* from, char* msg) => LocalChatMessage(new string(from), new string(msg));
[UnmanagedCallersOnly(EntryPoint = "SendChatMessage")]
public static void SendChatMessage(char* msg) => SendChatMessage(new string(msg));
[UnmanagedCallersOnly(EntryPoint = "GetEventHash")]
public static CustomEventHash GetEventHash(char* name)=>new string(name);
public static void SendCustomEvent(int hash, CustomEventFlags flags, byte* data, int cbData)
[UnmanagedCallersOnly(EntryPoint = nameof(InvokeCommand))]
public static int InvokeCommand(char* name, int argc, char** argv)
{
try
{
var args = new string[argc];
for (int i = 0; i < argc; i++)
{
args[i] = new(argv[i]);
}
_lastResult = _invokeCommand(new string(name), args);
return _lastResult.Length;
}
catch (Exception ex)
{
Main.Logger.Error(ex);
SetLastResult(ex.ToString());
return 0;
}
}
[UnmanagedCallersOnly(EntryPoint = nameof(GetLastResultLenInChars))]
public static int GetLastResultLenInChars() => _lastResult?.Length ?? 0;
/// <summary>
/// Convert Entity ID to handle
/// </summary>
[UnmanagedCallersOnly(EntryPoint = "IdToHandle")]
[UnmanagedCallersOnly(EntryPoint = nameof(IdToHandle))]
public static int IdToHandle(byte type, int id)
{
return type switch

View File

@ -0,0 +1,55 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Client.Scripting
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
internal class RemotingAttribute : Attribute
{
public bool GenBridge = true;
}
/// <summary>
/// Some local remoting implementation with json-based serialization, somewhar slow, but convenient
/// </summary>
internal static unsafe partial class API
{
static readonly MethodInfo[] _apiEntries =
typeof(API).GetMethods(BindingFlags.Static | BindingFlags.Public);
static readonly Dictionary<string, MethodInfo> _commands =
new(typeof(API).GetMethods().
Where(md => md.CustomAttributes.
Any(attri => attri.AttributeType == typeof(RemotingAttribute))).
Select(x => new KeyValuePair<string, MethodInfo>(x.Name, x)));
static string _invokeCommand(string name, string[] argsJson)
{
if (_commands.TryGetValue(name, out var method))
{
var ps = method.GetParameters();
if (argsJson.Length != ps.Length)
throw new ArgumentException($"Parameter count mismatch, expecting {ps.Length} parameters, got {argsJson.Length}", nameof(argsJson));
object[] args = new object[ps.Length];
for (int i = 0; i < ps.Length; i++)
{
args[i] = JsonDeserialize(argsJson[i], ps[i].ParameterType);
}
var result = method.Invoke(null, args);
if (method.ReturnType == typeof(void))
{
return "void";
}
return JsonSerialize(result);
}
throw new KeyNotFoundException($"Command {name} was not found");
}
}
}

View File

@ -1,15 +1,17 @@
#undef DEBUG
using System;
using System.Collections.Generic;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading;
using GTA;
using Lidgren.Network;
using Newtonsoft.Json;
using RageCoop.Client.Menus;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System;
using System.Collections.Generic;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
[assembly: InternalsVisibleTo("CodeGen")] // For generating api bridge
namespace RageCoop.Client.Scripting
{
@ -31,7 +33,7 @@ namespace RageCoop.Client.Scripting
/// <summary>
/// Provides vital functionality to interact with RAGECOOP
/// </summary>
public static unsafe partial class API
internal static unsafe partial class API
{
#region INTERNAL
@ -78,6 +80,17 @@ namespace RageCoop.Client.Scripting
/// Get or set scale of player's blip
/// </summary>
public static float BlipScale { get; set; } = 1;
public static bool ShowPlayerNameTag
{
get => Main.Settings.ShowPlayerNameTag;
set
{
if (value == ShowPlayerNameTag) return;
Main.Settings.ShowPlayerNameTag = value;
Util.SaveSettings();
}
}
}
/// <summary>
@ -207,7 +220,7 @@ namespace RageCoop.Client.Scripting
/// <summary>
/// Get all players indexed by their ID
/// </summary>
public static Dictionary<int, Player> Players => new Dictionary<int, Player>(PlayerList.Players);
public static Dictionary<int, Player> Players => new(PlayerList.Players);
#endregion
@ -255,57 +268,6 @@ namespace RageCoop.Client.Scripting
WorldThread.QueueAction(a);
}
/// <summary>
/// Connect to a server
/// </summary>
/// <param name="address">Address of the server, e.g. 127.0.0.1:4499</param>
/// <exception cref="InvalidOperationException">When a connection is active or being established</exception>
public static void Connect(string address)
{
if (Networking.IsOnServer || Networking.IsConnecting)
throw new InvalidOperationException("Cannot connect to server when another connection is active");
Networking.ToggleConnection(address);
}
/// <summary>
/// Disconnect from current server or cancel the connection attempt.
/// </summary>
[UnmanagedCallersOnly(EntryPoint = "Disconnect")]
public static void Disconnect()
{
if (Networking.IsOnServer || Networking.IsConnecting) Networking.ToggleConnection(null);
}
/// <summary>
/// List all servers from master server address
/// </summary>
/// <returns></returns>
public static List<ServerInfo> ListServers()
{
return JsonConvert.DeserializeObject<List<ServerInfo>>(
HttpHelper.DownloadString(Main.Settings.MasterServer));
}
/// <summary>
/// Send a local chat message to this player
/// </summary>
/// <param name="from">Name of the sender</param>
/// <param name="message">The player's message</param>
public static void LocalChatMessage(string from, string message)
{
Main.MainChat.AddMessage(from, message);
}
/// <summary>
/// Send a chat message or command to server/other players
/// </summary>
/// <param name="message"></param>
public static void SendChatMessage(string message)
{
Networking.SendChatMessage(message);
}
/// <summary>
/// Send an event and data to the server.
/// </summary>
@ -315,13 +277,7 @@ namespace RageCoop.Client.Scripting
/// types
/// </param>
public static void SendCustomEvent(CustomEventHash eventHash, params object[] args)
{
Networking.Peer.SendTo(new Packets.CustomEvent
{
Args = args,
Hash = eventHash
}, Networking.ServerConnection, ConnectionChannel.Event, NetDeliveryMethod.ReliableOrdered);
}
=> SendCustomEvent(CustomEventFlags.None, eventHash, args);
/// <summary>
/// Send an event and data to the server
@ -346,8 +302,7 @@ namespace RageCoop.Client.Scripting
/// backgound thread, use <see cref="QueueAction(Action)" /> in the handler to dispatch code to script thread.
/// </summary>
/// <param name="hash">
/// An unique identifier of the event, you can hash your event name with
/// <see cref="Core.Scripting.CustomEvents.Hash(string)" />
/// 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 static void RegisterCustomEventHandler(CustomEventHash hash, Action<CustomEventReceivedArgs> handler)
@ -384,6 +339,102 @@ namespace RageCoop.Client.Scripting
});
}
/// <summary>
/// Connect to a server
/// </summary>
/// <param name="address">Address of the server, e.g. 127.0.0.1:4499</param>
/// <exception cref="InvalidOperationException">When a connection is active or being established</exception>
[Remoting]
public static void Connect(string address)
{
if (Networking.IsOnServer || Networking.IsConnecting)
throw new InvalidOperationException("Cannot connect to server when another connection is active");
Networking.ToggleConnection(address);
}
/// <summary>
/// Disconnect from current server or cancel the connection attempt.
/// </summary>
[Remoting]
public static void Disconnect()
{
if (Networking.IsOnServer || Networking.IsConnecting) Networking.ToggleConnection(null);
}
/// <summary>
/// List all servers from master server address
/// </summary>
/// <returns></returns>
[Remoting]
public static List<ServerInfo> ListServers()
{
return JsonDeserialize<List<ServerInfo>>(
HttpHelper.DownloadString(Main.Settings.MasterServer));
}
/// <summary>
/// Send a local chat message to this player
/// </summary>
/// <param name="from">Name of the sender</param>
/// <param name="message">The player's message</param>
[Remoting]
public static void LocalChatMessage(string from, string message)
{
Main.MainChat.AddMessage(from, message);
}
/// <summary>
/// Send a chat message or command to server/other players
/// </summary>
/// <param name="message"></param>
[Remoting]
public static void SendChatMessage(string message)
{
if (!IsOnServer)
throw new InvalidOperationException("Not on server");
Networking.SendChatMessage(message);
}
[Remoting]
public static ClientResource GetResource(string name)
{
if (Main.Resources.LoadedResources.TryGetValue(name, out var resource))
return resource;
return null;
}
[Remoting(GenBridge = false)]
public static object GetProperty(string name)
=> typeof(API).GetProperty(name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null);
[Remoting(GenBridge = false)]
public static void SetProperty(string name, string jsonVal)
{
var prop = typeof(API).GetProperty(name, BindingFlags.Static | BindingFlags.Public); ;
if (prop == null)
throw new KeyNotFoundException($"Property {name} was not found");
prop.SetValue(null, JsonDeserialize(jsonVal, prop.PropertyType));
}
[Remoting]
public static object GetConfig(string name)
=> typeof(Config).GetProperty(name, BindingFlags.Static | BindingFlags.Public)?.GetValue(null);
[Remoting]
public static void SetConfig(string name, string jsonVal)
{
var prop = typeof(Config).GetProperty(name, BindingFlags.Static | BindingFlags.Public);
if (prop == null)
throw new KeyNotFoundException($"Property {name} was not found");
prop.SetValue(null, JsonDeserialize(jsonVal, prop.PropertyType));
}
#endregion
}
}

View File

@ -10,47 +10,11 @@ using RageCoop.Core.Scripting;
namespace RageCoop.Client.Scripting
{
/// <summary>
/// </summary>
public class ClientResource
{
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// Directory where the scripts is loaded from
/// </summary>
public string ScriptsDirectory { 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="ClientScript" /> instance in this resource.
/// </summary>
public List<ClientScript> Scripts { get; internal set; } = new List<ClientScript>();
/// <summary>
/// Get the <see cref="ResourceFile" /> where this script is loaded from.
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new Dictionary<string, ResourceFile>();
/// <summary>
/// A <see cref="Core.Logger" /> instance that can be used to debug your resource.
/// </summary>
public Logger Logger { get; internal set; }
}
internal class Resources
{
public static string TempPath;
internal readonly ConcurrentDictionary<string, ClientResource> LoadedResources =
new ConcurrentDictionary<string, ClientResource>();
internal readonly ConcurrentDictionary<string, ClientResource> LoadedResources = new();
static Resources()
{
@ -86,7 +50,7 @@ namespace RageCoop.Client.Scripting
}
Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories).Where(x => x.CanBeIgnored())
.ForEach(x => File.Delete(x));
.ForEach(File.Delete);
// TODO Core.ScheduleLoad()...
}

View File

@ -4,7 +4,7 @@ global using static GTA.Native.Function;
global using static GTA.Native.Hash;
global using static RageCoop.Client.Shared;
global using Console = GTA.Console;
global using static SHVDN.PInvoke;
global using static RageCoop.Core.Shared;
using System.IO;
namespace RageCoop.Client

View File

@ -1,7 +1,9 @@
<linker>
<assembly fullname="RageCoop.Client" preserve="all" />
<assembly fullname="RageCoop.Client.Scripting" preserve="all" />
<assembly fullname="RageCoop.Core" preserve="all" />
<assembly fullname="System.Collections">
<type fullname="System.Collections.Generic.List`1" preserve="all" />
<type fullname="System.Collections.Generic.Dictionary`2" preserve="all" />
</assembly>
</linker>

View File

@ -107,12 +107,12 @@ namespace RageCoop.Client
Settings settings;
try
{
settings = JsonConvert.DeserializeObject<Settings>(File.ReadAllText(path));
settings = JsonDeserialize<Settings>(File.ReadAllText(path));
}
catch (Exception ex)
{
Main.Logger?.Error(ex);
File.WriteAllText(path, JsonConvert.SerializeObject(settings = new Settings(), Formatting.Indented));
File.WriteAllText(path, JsonSerialize(settings = new Settings()));
}
return settings;
@ -126,7 +126,7 @@ namespace RageCoop.Client
settings = settings ?? Main.Settings;
Directory.CreateDirectory(Directory.GetParent(path).FullName);
File.WriteAllText(path, JsonConvert.SerializeObject(settings, Formatting.Indented));
File.WriteAllText(path, JsonSerialize(settings));
return true;
}
catch (Exception ex)

View File

@ -25,14 +25,14 @@ namespace RageCoop.Client
static WeaponUtil()
{
// Parse and load to memory
foreach (var w in JsonConvert.DeserializeObject<VehicleWeaponInfo[]>(
foreach (var w in JsonDeserialize<VehicleWeaponInfo[]>(
File.ReadAllText(VehicleWeaponDataPath))) VehicleWeapons.Add(w.Hash, w);
Weapons = JsonConvert.DeserializeObject<Dictionary<uint, WeaponInfo>>(
Weapons = JsonDeserialize<Dictionary<uint, WeaponInfo>>(
File.ReadAllText(WeaponInfoDataPath));
if (File.Exists(WeaponFixDataPath))
WeaponFix = JsonConvert.DeserializeObject<WeaponFix>(File.ReadAllText(WeaponFixDataPath));
WeaponFix = JsonDeserialize<WeaponFix>(File.ReadAllText(WeaponFixDataPath));
else
Main.Logger.Warning("Weapon fix data not found");
}
@ -73,7 +73,7 @@ namespace RageCoop.Client
}
}
File.WriteAllText(path, JsonConvert.SerializeObject(fix, Formatting.Indented));
File.WriteAllText(path, JsonSerialize(fix));
P.IsInvincible = false;
}

View File

@ -115,7 +115,7 @@ namespace RageCoop.Client
}
#endregion
/*
public static Image CaptureWindow(IntPtr handle)
{
var windowDC = User32.GetWindowDC(handle);
@ -134,7 +134,7 @@ namespace RageCoop.Client
User32.ReleaseDC(handle, windowDC);
return image;
}
*/
public static void ClearLastError()
{
SetLastErrorEx(0, 0);