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

@ -3,11 +3,11 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>True</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Core\RageCoop.Core.csproj" />
<ProjectReference Include="..\..\Core\RageCoop.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using System;
global using static RageCoop.Core.Shared;
global using static RageCoop.Client.Scripting.Shared;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -6,7 +7,7 @@ using System.Threading.Tasks;
namespace RageCoop.Client.Scripting
{
internal class ClientScript
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);

View File

@ -20,6 +20,7 @@ using RageCoop.Core.Scripting;
[assembly: InternalsVisibleTo("RageCoop.Server")]
[assembly: InternalsVisibleTo("RageCoop.Client")]
[assembly: InternalsVisibleTo("RageCoop.Client.Scripting")]
[assembly: InternalsVisibleTo("RageCoop.Client.Installer")]
[assembly: InternalsVisibleTo("DataDumper")]
[assembly: InternalsVisibleTo("UnitTest")]
@ -289,7 +290,7 @@ namespace RageCoop.Core
throw new Exception($"IPv4 request failed! [{(int)response.StatusCode}/{response.ReasonPhrase}]");
var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
return JsonConvert.DeserializeObject<IpInfo>(content);
return JsonDeserialize<IpInfo>(content);
}
public static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target)

59
Core/JsonCoverters.cs Normal file
View File

@ -0,0 +1,59 @@
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Core
{
class IPAddressConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPAddress));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
return IPAddress.Parse((string)reader.Value);
}
}
class IPEndPointConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return (objectType == typeof(IPEndPoint));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
IPEndPoint ep = (IPEndPoint)value;
JObject jo = new()
{
{ "Address", JToken.FromObject(ep.Address, serializer) },
{ "Port", ep.Port }
};
jo.WriteTo(writer);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null) return null;
JObject jo = JObject.Load(reader);
IPAddress address = jo["Address"].ToObject<IPAddress>(serializer);
int port = (int)jo["Port"];
return new IPEndPoint(address, port);
}
}
}

View File

@ -8,7 +8,6 @@ namespace RageCoop.Core
[Serializable]
public class ServerInfo
{
#pragma warning disable 1591
public string address { get; set; }
public int port { get; set; }
public string name { get; set; }

View File

@ -96,6 +96,7 @@ namespace RageCoop.Core.Scripting
}
/// <summary>
/// Common processing for custome client\server events
/// </summary>
public static partial class CustomEvents
{

26
Core/Shared.cs Normal file
View File

@ -0,0 +1,26 @@
global using static RageCoop.Core.Shared;
using Newtonsoft.Json;
using System;
namespace RageCoop.Core
{
internal class Shared
{
public static readonly JsonSerializerSettings JsonSettings = new();
static Shared()
{
JsonSettings.Converters.Add(new IPAddressConverter());
JsonSettings.Converters.Add(new IPEndPointConverter());
JsonSettings.Formatting = Formatting.Indented;
}
public static object JsonDeserialize(string text, Type type)
{
return JsonConvert.DeserializeObject(text, type, JsonSettings);
}
public static T JsonDeserialize<T>(string text) => (T)JsonDeserialize(text, typeof(T));
public static string JsonSerialize(object obj) => JsonConvert.SerializeObject(obj, JsonSettings);
}
}

View File

@ -24,9 +24,9 @@ namespace RageCoop.Core
}
Console.WriteLine("Deserializing");
var anims = JsonConvert.DeserializeObject<AnimDic[]>(File.ReadAllText(input));
var anims = JsonDeserialize<AnimDic[]>(File.ReadAllText(input));
Console.WriteLine("Serializing");
File.WriteAllText(output, JsonConvert.SerializeObject(anims, Formatting.Indented));
File.WriteAllText(output, JsonSerialize(anims));
return anims;
}
}
@ -113,12 +113,11 @@ namespace RageCoop.Core
}
Console.WriteLine("Deserializing");
var infos = JsonConvert.DeserializeObject<VehicleInfo[]>(File.ReadAllText(input));
var infos = JsonDeserialize<VehicleInfo[]>(File.ReadAllText(input));
Console.WriteLine("Serializing");
File.WriteAllText(output,
JsonConvert.SerializeObject(
infos.Select(FromVehicle).Where(x => x != null),
Formatting.Indented));
JsonSerialize(
infos.Select(FromVehicle).Where(x => x != null)));
}
public static VehicleWeaponInfo FromVehicle(VehicleInfo info)

View File

@ -19,7 +19,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{70A1F09D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DataDumper", "Tools\DataDumper\DataDumper.csproj", "{6387D897-09AF-4464-B440-80438E3BB8D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Client.Scripting", "RageCoop.Client.Scripting\RageCoop.Client.Scripting.csproj", "{FE47AFBA-0613-4378-B318-892DEB7B3D88}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RageCoop.Client.Scripting", "Client\Scripting\RageCoop.Client.Scripting.csproj", "{FE47AFBA-0613-4378-B318-892DEB7B3D88}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UnitTest", "Tools\UnitTest\UnitTest.csproj", "{4FE96671-3DC5-4394-B2E3-584399E57310}"
EndProject
@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lidgren.Network", "libs\Lid
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ScriptHookVDotNetCore.Generator", "libs\ScriptHookVDotNetCore\src\ScriptHookVDotNetCore.Generator\ScriptHookVDotNetCore.Generator.csproj", "{2C08A5AF-C189-4350-B54F-3D23379E9E1D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodeGen", "Tools\CodeGen\CodeGen.csproj", "{C4CF8A98-7393-42BD-97A1-2E850D12890A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
API|Any CPU = API|Any CPU
@ -164,6 +166,18 @@ Global
{2C08A5AF-C189-4350-B54F-3D23379E9E1D}.Release|Any CPU.Build.0 = Release|Any CPU
{2C08A5AF-C189-4350-B54F-3D23379E9E1D}.Release|x64.ActiveCfg = Release|Any CPU
{2C08A5AF-C189-4350-B54F-3D23379E9E1D}.Release|x64.Build.0 = Release|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.API|Any CPU.ActiveCfg = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.API|Any CPU.Build.0 = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.API|x64.ActiveCfg = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.API|x64.Build.0 = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Debug|x64.ActiveCfg = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Debug|x64.Build.0 = Debug|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Release|Any CPU.Build.0 = Release|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Release|x64.ActiveCfg = Release|Any CPU
{C4CF8A98-7393-42BD-97A1-2E850D12890A}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -177,6 +191,7 @@ Global
{B15EDABB-30AF-475A-823D-ACB9F75CFE13} = {1AF84C35-B86B-46BB-9FDB-ACB7787E582A}
{02616B5A-2A68-42AA-A91E-311EF95FCF44} = {1AF84C35-B86B-46BB-9FDB-ACB7787E582A}
{2C08A5AF-C189-4350-B54F-3D23379E9E1D} = {1AF84C35-B86B-46BB-9FDB-ACB7787E582A}
{C4CF8A98-7393-42BD-97A1-2E850D12890A} = {70A1F09D-648D-4C8B-8947-E920B1A587A3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6CC7EA75-E4FF-4534-8EB6-0AEECF2620B7}

View File

@ -1,7 +0,0 @@
namespace RageCoop.Client.Scripting
{
public static class API
{
}
}

View File

@ -79,8 +79,7 @@ public partial class Server
if (!CanAnnounce)
{
var existing = JsonConvert
.DeserializeObject<List<ServerInfo>>(
var existing = JsonDeserialize<List<ServerInfo>>(
HttpHelper.DownloadString(Util.GetFinalRedirect(Settings.MasterServer)))
.Where(x => x.address == IpInfo.Address).FirstOrDefault();
if (existing != null)
@ -117,7 +116,7 @@ public partial class Server
publicKeyModulus = Convert.ToBase64String(pModulus),
publicKeyExponent = Convert.ToBase64String(pExpoenet)
};
var msg = JsonConvert.SerializeObject(serverInfo);
var msg = JsonSerialize(serverInfo);
var realUrl = Util.GetFinalRedirect(Settings.MasterServer);
response = httpClient.PostAsync(realUrl, new StringContent(msg, Encoding.UTF8, "application/json"))

View File

@ -15,7 +15,7 @@ using System.Resources;
[assembly: AssemblyCulture("")]
// Version information
[assembly: AssemblyVersion("1.6.0.28")]
[assembly: AssemblyFileVersion("1.6.0.28")]
[assembly: AssemblyVersion("1.6.0.30")]
[assembly: AssemblyFileVersion("1.6.0.30")]
[assembly: NeutralResourcesLanguageAttribute( "en-US" )]

9
Server/Shared.cs Normal file
View File

@ -0,0 +1,9 @@
global using static RageCoop.Core.Shared;
global using static RageCoop.Server.Shared;
global using RageCoop.Core;
namespace RageCoop.Server
{
internal class Shared
{
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<OutDir>$(SolutionDir)bin\Tools\CodeGen</OutDir>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Client\Scripts\RageCoop.Client.csproj" />
</ItemGroup>
</Project>

82
Tools/CodeGen/Program.cs Normal file
View File

@ -0,0 +1,82 @@
using RageCoop.Client.Scripting;
using System.Linq;
using System.Reflection;
using System.Text;
namespace CodeGen
{
internal static class Program
{
static void Main(string[] args)
{
Directory.SetCurrentDirectory(@"..\..\..\");
var props = new StringBuilder();
var config = new StringBuilder();
var funcs = new StringBuilder();
foreach (var prop in typeof(API.Config).GetProperties(BindingFlags.Public | BindingFlags.Static))
{
config.AppendLine($"public static {prop.PropertyType.ToTypeName()} {prop.Name} => GetConfig<{prop.PropertyType.ToTypeName()}>(\"{prop.Name}\");");
}
foreach (var prop in typeof(API).GetProperties(BindingFlags.Public | BindingFlags.Static))
{
props.AppendLine($"public static {prop.PropertyType.ToTypeName()} {prop.Name} => GetProperty<{prop.PropertyType.ToTypeName()}>(\"{prop.Name}\");");
}
foreach (var f in
typeof(API).GetMethods(BindingFlags.Public | BindingFlags.Static).
Where(x =>
{
var attri = x.GetCustomAttribute<RemotingAttribute>();
if (attri ==null) return false;
return attri.GenBridge;
}))
{
var ret = f.ReturnType.ToTypeName();
var gReturn = $"<{ret}>";
if (ret == "System.Void") { ret = "void"; gReturn = string.Empty; }
var ps = f.GetParameters();
var paras = string.Join(',', ps.Select(x => $"{x.ParameterType.ToTypeName()} {x.Name}"));
var parasNoType = string.Join(',', ps.Select(x => x.Name));
if (ps.Length > 0)
{
parasNoType = "," + parasNoType;
}
funcs.AppendLine($"public static {ret} {f.Name}({paras}) => InvokeCommand{gReturn}(\"{f.Name}\"{parasNoType});");
}
var code = $@"namespace RageCoop.Client.Scripting
{{
public static unsafe partial class APIBridge
{{
public static class Config
{{
{config}
}}
#region PROPERTIES
{props}
#endregion
#region FUNCTIONS
{funcs}
#endregion
}}
}}
";
File.WriteAllText(@"Client\Scripting\APIBridge.Generated.cs", code);
}
static string ToTypeName(this Type type)
{
var name = type.ToString();
if (type.GenericTypeArguments.Length > 0)
{
name = $"{name.Substring(0, name.IndexOf('`'))}<{string.Join(',', type.GenericTypeArguments.Select(ToTypeName))}>";
}
name = name.Replace("RageCoop.Client.Player", "RageCoop.Client.Scripting.PlayerInfo");
return name;
}
}
}