Prepare for CoreCLR version
This commit is contained in:
@ -1,9 +1,9 @@
|
||||
using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
[assembly: DisableRuntimeMarshalling]
|
||||
[assembly: InternalsVisibleTo("RageCoop.Client")] // For debugging
|
||||
|
||||
namespace RageCoop.Client.Scripting
|
||||
@ -11,7 +11,28 @@ namespace RageCoop.Client.Scripting
|
||||
public static unsafe partial class APIBridge
|
||||
{
|
||||
static readonly ThreadLocal<char[]> _resultBuf = new(() => new char[4096]);
|
||||
static List<CustomEventHandler> _handlers = new();
|
||||
static readonly List<CustomEventHandler> _handlers = new();
|
||||
|
||||
static APIBridge()
|
||||
{
|
||||
if (SHVDN.Core.GetPtr == null)
|
||||
throw new InvalidOperationException("Game not running");
|
||||
|
||||
foreach(var fd in typeof(APIBridge).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
|
||||
{
|
||||
var importAttri = fd.GetCustomAttribute<ApiImportAttribute>();
|
||||
if (importAttri == null)
|
||||
continue;
|
||||
importAttri.EntryPoint ??= fd.Name;
|
||||
var key = $"RageCoop.Client.Scripting.API.{importAttri.EntryPoint}";
|
||||
var fptr = SHVDN.Core.GetPtr(key);
|
||||
if (fptr == default)
|
||||
throw new KeyNotFoundException($"Failed to find function pointer: {key}");
|
||||
|
||||
fd.SetValue(null,fptr);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copy content of string to a sequential block of memory
|
||||
/// </summary>
|
||||
@ -56,10 +77,13 @@ namespace RageCoop.Client.Scripting
|
||||
var argv = StringArrayToMemory(args.Select(JsonSerialize).ToArray());
|
||||
try
|
||||
{
|
||||
var resultLen = InvokeCommandAsJsonUnsafe(name, argc, argv);
|
||||
if (resultLen == 0)
|
||||
throw new Exception(GetLastResult());
|
||||
return GetLastResult();
|
||||
fixed(char* pName = name)
|
||||
{
|
||||
var resultLen = InvokeCommandAsJsonUnsafe(pName, argc, argv);
|
||||
if (resultLen == 0)
|
||||
throw new Exception(GetLastResult());
|
||||
return GetLastResult();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -77,7 +101,7 @@ namespace RageCoop.Client.Scripting
|
||||
var cbBufSize = _resultBuf.Value.Length * sizeof(char);
|
||||
fixed (char* pBuf = _resultBuf.Value)
|
||||
{
|
||||
if (GetLastResult(pBuf, cbBufSize) > 0)
|
||||
if (GetLastResultUnsafe(pBuf, cbBufSize) > 0)
|
||||
{
|
||||
return new string(pBuf);
|
||||
}
|
||||
@ -91,7 +115,7 @@ namespace RageCoop.Client.Scripting
|
||||
{
|
||||
var writer = GetWriter();
|
||||
CustomEvents.WriteObjects(writer, args);
|
||||
SendCustomEvent(flags, hash, writer.Address, writer.Position);
|
||||
SendCustomEventUnsafe(flags, hash, writer.Address, writer.Position);
|
||||
}
|
||||
|
||||
public static void RegisterCustomEventHandler(CustomEventHash hash, Action<CustomEventReceivedArgs> handler)
|
||||
@ -107,25 +131,31 @@ namespace RageCoop.Client.Scripting
|
||||
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);
|
||||
[ApiImport]
|
||||
public static delegate* unmanaged<char*, CustomEventHash> GetEventHash;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
internal static partial void SetLastResult(string msg);
|
||||
[ApiImport]
|
||||
private static delegate* unmanaged<char*,void> SetLastResult;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll")]
|
||||
private static partial int GetLastResult(char* buf, int cbBufSize);
|
||||
[ApiImport(EntryPoint = "GetLastResult")]
|
||||
private static delegate* unmanaged<char*, int, int> GetLastResultUnsafe;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll", EntryPoint = "InvokeCommand", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial int InvokeCommandAsJsonUnsafe(string name, int argc, char** argv);
|
||||
[ApiImport(EntryPoint = "InvokeCommand")]
|
||||
private static delegate* unmanaged<char*, int, char**, int> InvokeCommandAsJsonUnsafe;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll")]
|
||||
private static partial void SendCustomEvent(CustomEventFlags flags, int hash, byte* data, int cbData);
|
||||
[ApiImport(EntryPoint = "SendCustomEvent")]
|
||||
private static delegate* unmanaged<CustomEventFlags, int, byte*, int, void> SendCustomEventUnsafe;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll")]
|
||||
private static partial int GetLastResultLenInChars();
|
||||
[ApiImport]
|
||||
private static delegate* unmanaged<int> GetLastResultLenInChars;
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
public static partial void LogEnqueue(LogLevel level, string msg);
|
||||
[ApiImport]
|
||||
public static delegate* unmanaged<LogLevel, char*, void> LogEnqueue;
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Field)]
|
||||
class ApiImportAttribute : Attribute
|
||||
{
|
||||
public string EntryPoint;
|
||||
}
|
||||
}
|
@ -20,7 +20,8 @@ namespace RageCoop.Client.Scripting
|
||||
static unsafe ClientScript()
|
||||
{
|
||||
char* buf = stackalloc char[260];
|
||||
SHVDN.PInvoke.GetModuleFileNameW(SHVDN.Core.CurrentModule, buf, 260);
|
||||
// TODO: needs some fix up here
|
||||
// SHVDN.PInvoke.GetModuleFileNameW(SHVDN.Core.CurrentModule, buf, 260);
|
||||
if (Marshal.GetLastWin32Error() != 0)
|
||||
throw new Win32Exception("Failed to get path for current module");
|
||||
FullPath = new(buf);
|
||||
@ -37,12 +38,14 @@ namespace RageCoop.Client.Scripting
|
||||
{
|
||||
Logger.Warning("No file associated with curent script was found");
|
||||
}
|
||||
|
||||
Tick += DoQueuedJobs;
|
||||
}
|
||||
protected void QueueAction(Func<bool> action) => _jobQueue.Enqueue(action);
|
||||
protected void QueueAction(Action action) => QueueAction(() => { action(); return true; });
|
||||
|
||||
protected override void OnTick()
|
||||
{
|
||||
base.OnTick();
|
||||
DoQueuedJobs();
|
||||
}
|
||||
private void DoQueuedJobs()
|
||||
{
|
||||
while (_reAdd.TryDequeue(out var toAdd))
|
||||
|
@ -12,13 +12,16 @@ namespace RageCoop.Client.Scripting
|
||||
public static readonly ResourceLogger Default = new();
|
||||
public ResourceLogger()
|
||||
{
|
||||
FlushImmediately= true;
|
||||
FlushImmediately = true;
|
||||
OnFlush += FlushToMainModule;
|
||||
}
|
||||
|
||||
private void FlushToMainModule(LogLine line, string fomatted)
|
||||
private unsafe void FlushToMainModule(LogLine line, string fomatted)
|
||||
{
|
||||
APIBridge.LogEnqueue(line.LogLevel, line.Message);
|
||||
fixed (char* pMsg = line.Message)
|
||||
{
|
||||
APIBridge.LogEnqueue(line.LogLevel, pMsg);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GTA.UI;
|
||||
|
||||
namespace RageCoop.Client
|
||||
{
|
||||
internal enum TimeStamp
|
||||
{
|
||||
AddPeds,
|
||||
PedTotal,
|
||||
AddVehicles,
|
||||
VehicleTotal,
|
||||
SendPed,
|
||||
SendPedState,
|
||||
SendVehicle,
|
||||
SendVehicleState,
|
||||
UpdatePed,
|
||||
UpdateVehicle,
|
||||
CheckProjectiles,
|
||||
GetAllEntities,
|
||||
Receive,
|
||||
ProjectilesTotal
|
||||
}
|
||||
|
||||
internal static class Debug
|
||||
{
|
||||
public static Dictionary<TimeStamp, long> TimeStamps = new Dictionary<TimeStamp, long>();
|
||||
private static int _lastNfHandle;
|
||||
|
||||
static Debug()
|
||||
{
|
||||
foreach (TimeStamp t in Enum.GetValues<TimeStamp>()) TimeStamps.Add(t, 0);
|
||||
}
|
||||
|
||||
public static string Dump(this Dictionary<TimeStamp, long> d)
|
||||
{
|
||||
var s = "";
|
||||
foreach (var kvp in d) s += kvp.Key + ":" + kvp.Value + "\n";
|
||||
return s;
|
||||
}
|
||||
|
||||
public static void ShowTimeStamps()
|
||||
{
|
||||
Notification.Hide(_lastNfHandle);
|
||||
_lastNfHandle = Notification.Show(TimeStamps.Dump());
|
||||
}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using GTA;
|
||||
using GTA.Math;
|
||||
@ -34,7 +35,7 @@ namespace RageCoop.Client
|
||||
internal static Logger Log = null;
|
||||
internal static ulong Ticked = 0;
|
||||
internal static Vector3 PlayerPosition;
|
||||
internal static Resources MainRes = null;
|
||||
internal static Scripting.Resources MainRes = null;
|
||||
|
||||
public static Ped P;
|
||||
public static float FPS;
|
||||
@ -61,7 +62,8 @@ namespace RageCoop.Client
|
||||
|
||||
Log = new Logger()
|
||||
{
|
||||
Writers = new List<StreamWriter> { CoreUtils.OpenWriter(LogPath) },
|
||||
FlushImmediately = true,
|
||||
Writers = null,
|
||||
#if DEBUG
|
||||
LogLevel = 0,
|
||||
#else
|
||||
@ -70,26 +72,11 @@ namespace RageCoop.Client
|
||||
};
|
||||
Log.OnFlush += (line, formatted) =>
|
||||
{
|
||||
switch (line.LogLevel)
|
||||
{
|
||||
#if DEBUG
|
||||
// case LogLevel.Trace:
|
||||
case LogLevel.Debug:
|
||||
Console.PrintInfo(line.Message);
|
||||
break;
|
||||
#endif
|
||||
case LogLevel.Info:
|
||||
Console.PrintInfo(line.Message);
|
||||
break;
|
||||
case LogLevel.Warning:
|
||||
Console.PrintWarning(line.Message);
|
||||
break;
|
||||
case LogLevel.Error:
|
||||
Console.PrintError(line.Message);
|
||||
break;
|
||||
}
|
||||
SHVDN.Logger.Write(line.Message, (uint)line.LogLevel);
|
||||
};
|
||||
|
||||
// Run static constructor to register all function pointers and remoting entries
|
||||
RuntimeHelpers.RunClassConstructor(typeof(API).TypeHandle);
|
||||
}
|
||||
|
||||
protected override void OnAborted(AbortedEventArgs e)
|
||||
@ -120,7 +107,7 @@ namespace RageCoop.Client
|
||||
throw new NotSupportedException("Please update your GTA5 to v1.0.1290 or newer!");
|
||||
}
|
||||
|
||||
MainRes = new Resources();
|
||||
MainRes = new();
|
||||
|
||||
|
||||
|
||||
@ -220,6 +207,22 @@ namespace RageCoop.Client
|
||||
protected override void OnKeyUp(GTA.KeyEventArgs e)
|
||||
{
|
||||
base.OnKeyUp(e);
|
||||
|
||||
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("test");
|
||||
}
|
||||
#if CEF
|
||||
if (CefRunning)
|
||||
{
|
||||
@ -379,23 +382,23 @@ namespace RageCoop.Client
|
||||
|
||||
if (reason != "Abort")
|
||||
{
|
||||
Log.Info($">> Disconnected << reason: {reason}");
|
||||
Log.Info($">> Disconnected << reason: {reason}");
|
||||
Notification.Show("~r~Disconnected: " + reason);
|
||||
}
|
||||
|
||||
if (MainChat.Focused)
|
||||
if (MainChat?.Focused == true)
|
||||
{
|
||||
MainChat.Focused = false;
|
||||
}
|
||||
|
||||
PlayerList.Cleanup();
|
||||
MainChat.Clear();
|
||||
MainChat?.Clear();
|
||||
EntityPool.Cleanup();
|
||||
WorldThread.Traffic(true);
|
||||
Call(SET_ENABLE_VEHICLE_SLIPSTREAMING, false);
|
||||
CoopMenu.DisconnectedMenuSetting();
|
||||
LocalPlayerID = default;
|
||||
MainRes.Unload();
|
||||
MainRes?.Unload();
|
||||
Memory.RestorePatches();
|
||||
#if CEF
|
||||
if (CefRunning)
|
||||
|
@ -38,9 +38,9 @@ namespace RageCoop.Client
|
||||
{
|
||||
DiagnosticMenu.Clear();
|
||||
DiagnosticMenu.Add(new NativeItem("EntityPool", EntityPool.DumpDebug()));
|
||||
foreach (var pair in Debug.TimeStamps)
|
||||
DiagnosticMenu.Add(
|
||||
new NativeItem(pair.Key.ToString(), pair.Value.ToString(), pair.Value.ToString()));
|
||||
// foreach (var pair in Debug.TimeStamps)
|
||||
// DiagnosticMenu.Add(
|
||||
// new NativeItem(pair.Key.ToString(), pair.Value.ToString(), pair.Value.ToString()));
|
||||
};
|
||||
ShowNetworkInfoItem.CheckboxChanged += (s, e) =>
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<NoAotCompile>false</NoAotCompile>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>
|
||||
@ -63,9 +64,6 @@
|
||||
<PackageReference Include="SharpZipLib" Version="1.4.0" />
|
||||
<PackageReference Include="System.Diagnostics.DiagnosticSource" Version="4.3.0" />
|
||||
</ItemGroup>
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(SolutionDir)' != '*Undefined*' And '$(NoAotCompile)' != 'true'">
|
||||
<Exec Command="dotnet publish "$(ProjectPath)" -o "$(OutDir)\native" -p:PublishAOT=true -r win-x64 " />
|
||||
</Target>
|
||||
<PropertyGroup Condition="'$(SolutionDir)' != '*Undefined*' AND '$(PublishAot)' != 'true'">
|
||||
<PostBuildEvent Condition=" '$(DevEnvDir)' != '*Undefined*'">
|
||||
"$(DevEnvDir)TextTransform.exe" -a !!BuildConfiguration!$(Configuration) "$(ProjectDir)Properties\AssemblyInfo.tt"
|
||||
|
@ -3,6 +3,8 @@ using RageCoop.Core;
|
||||
using RageCoop.Core.Scripting;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Runtime.InteropServices;
|
||||
using static RageCoop.Core.Scripting.CustomEvents;
|
||||
@ -11,6 +13,23 @@ namespace RageCoop.Client.Scripting
|
||||
{
|
||||
internal static unsafe partial class API
|
||||
{
|
||||
static API()
|
||||
{
|
||||
RegisterFunctionPointers();
|
||||
}
|
||||
|
||||
static void RegisterFunctionPointers()
|
||||
{
|
||||
foreach (var method in typeof(API).GetMethods(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
var attri = method.GetCustomAttribute<UnmanagedCallersOnlyAttribute>();
|
||||
if (attri == null) continue;
|
||||
Debug.Assert(attri.EntryPoint == method.Name);
|
||||
SHVDN.Core.SetPtr($"{typeof(API).FullName}.{method.Name}", method.MethodHandle.GetFunctionPointer());
|
||||
Log.Debug($"Registered function pointer for {method.DeclaringType}.{method.Name}");
|
||||
}
|
||||
}
|
||||
|
||||
[ThreadStatic]
|
||||
static string _lastResult;
|
||||
|
||||
|
@ -465,8 +465,8 @@ namespace RageCoop.Client.Scripting
|
||||
[Remoting]
|
||||
public static void RegisterCustomEventHandler(CustomEventHash hash, CustomEventHandler handler)
|
||||
{
|
||||
if (handler.Module == default)
|
||||
throw new ArgumentException("Module not specified");
|
||||
if (handler.Directory == default)
|
||||
throw new ArgumentException("Script directory not specified");
|
||||
|
||||
if (handler.FunctionPtr == default)
|
||||
throw new ArgumentException("Function pointer not specified");
|
||||
|
@ -65,6 +65,8 @@ namespace RageCoop.Client.Scripting
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
/*
|
||||
// Unregister associated handler
|
||||
foreach (var handlers in API.CustomEventHandlers.Values)
|
||||
{
|
||||
@ -77,7 +79,7 @@ namespace RageCoop.Client.Scripting
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
LoadedResources.Clear();
|
||||
}
|
||||
|
||||
|
@ -14,9 +14,13 @@ namespace RageCoop.Client
|
||||
/// </summary>
|
||||
internal static class ThreadManager
|
||||
{
|
||||
private static List<Thread> _threads = new();
|
||||
private static Thread _watcher = new(() => _removeStopped());
|
||||
private static void _removeStopped()
|
||||
private static readonly List<Thread> _threads = new();
|
||||
private static readonly Thread _watcher = new(RemoveStopped);
|
||||
static ThreadManager()
|
||||
{
|
||||
_watcher.Start();
|
||||
}
|
||||
private static void RemoveStopped()
|
||||
{
|
||||
while (!IsUnloading)
|
||||
{
|
||||
@ -46,8 +50,10 @@ namespace RageCoop.Client
|
||||
{
|
||||
Log.Debug($"Thread stopped: " + Environment.CurrentManagedThreadId);
|
||||
}
|
||||
});
|
||||
created.Name = name;
|
||||
})
|
||||
{
|
||||
Name = name
|
||||
};
|
||||
Log.Debug($"Thread created: {name}, id: {created.ManagedThreadId}");
|
||||
_threads.Add(created);
|
||||
if (startNow) created.Start();
|
||||
|
@ -50,7 +50,7 @@ namespace RageCoop.Core
|
||||
|
||||
internal Logger()
|
||||
{
|
||||
Name = Process.GetCurrentProcess().Id.ToString();
|
||||
Name = Environment.ProcessId.ToString();
|
||||
if (!FlushImmediately)
|
||||
{
|
||||
LoggerThread = new Thread(() =>
|
||||
@ -164,7 +164,7 @@ namespace RageCoop.Core
|
||||
while (_queuedLines.TryDequeue(out var line))
|
||||
{
|
||||
var formatted = Format(line);
|
||||
Writers.ForEach(x =>
|
||||
Writers?.ForEach(x =>
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -38,7 +38,7 @@ namespace RageCoop.Core.Scripting
|
||||
public CustomEventHandler(IntPtr func) : this()
|
||||
{
|
||||
FunctionPtr = (ulong)func;
|
||||
Module = (ulong)SHVDN.Core.CurrentModule;
|
||||
Directory = SHVDN.Core.CurrentDirectory;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
@ -48,7 +48,7 @@ namespace RageCoop.Core.Scripting
|
||||
public ulong FunctionPtr { get; private set; }
|
||||
|
||||
[JsonProperty]
|
||||
public ulong Module { get; private set; }
|
||||
public string Directory { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
|
@ -2,6 +2,7 @@
|
||||
using GTA.Math;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
@ -290,7 +291,20 @@ namespace RageCoop.Core.Scripting
|
||||
return Args;
|
||||
}
|
||||
|
||||
[LibraryImport("RageCoop.Client.dll")]
|
||||
public static partial int IdToHandle(byte type, int id);
|
||||
static unsafe delegate* unmanaged<byte, int, int> _idToHandlePtr;
|
||||
public static unsafe int IdToHandle(byte type, int id)
|
||||
{
|
||||
if (_idToHandlePtr == default)
|
||||
{
|
||||
if (SHVDN.Core.GetPtr == default)
|
||||
throw new InvalidOperationException("Not client");
|
||||
|
||||
_idToHandlePtr = (delegate* unmanaged<byte, int, int>)SHVDN.Core.GetPtr("RageCoop.Client.Scripting.API.IdToHandle");
|
||||
if (_idToHandlePtr == default)
|
||||
throw new KeyNotFoundException("IdToHandle function not found");
|
||||
}
|
||||
|
||||
return _idToHandlePtr(type, id);
|
||||
}
|
||||
}
|
||||
}
|
@ -15,13 +15,13 @@ namespace RageCoop.Core
|
||||
{
|
||||
static Type JsonTypeCheck(Type type)
|
||||
{
|
||||
if (type.GetCustomAttribute<JsonDontSerialize>() != null)
|
||||
if (type?.GetCustomAttribute<JsonDontSerialize>() != null)
|
||||
throw new TypeAccessException($"The type {type} cannot be serialized");
|
||||
return type;
|
||||
}
|
||||
static object JsonTypeCheck(object obj)
|
||||
{
|
||||
JsonTypeCheck(obj.GetType());
|
||||
JsonTypeCheck(obj?.GetType());
|
||||
return obj;
|
||||
}
|
||||
public static readonly JsonSerializerSettings JsonSettings = new();
|
||||
|
Submodule libs/ScriptHookVDotNetCore updated: fc239b823e...f531165cd3
Reference in New Issue
Block a user