Basically working resource system

This commit is contained in:
Sardelka9515
2023-02-27 11:54:02 +08:00
parent 6e2b4aff2f
commit 0e5271b322
21 changed files with 332 additions and 95 deletions

View File

@ -36,10 +36,10 @@ namespace RageCoop.Client.Scripting
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 RageCoop.Client.Scripting.ClientResource GetResouceFromFilePath(System.String filePath) => InvokeCommand<RageCoop.Client.Scripting.ClientResource>("GetResouceFromFilePath", filePath);
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);
public static void RegisterCustomEventHandler(RageCoop.Core.Scripting.CustomEventHash hash, RageCoop.Core.Scripting.CustomEventHandler handler) => InvokeCommand("RegisterCustomEventHandler", hash, handler);
#endregion
}
}

View File

@ -11,7 +11,7 @@ namespace RageCoop.Client.Scripting
public static unsafe partial class APIBridge
{
static readonly ThreadLocal<char[]> _resultBuf = new(() => new char[4096]);
static List<CustomEventHandler> _handlers = new();
/// <summary>
/// Copy content of string to a sequential block of memory
/// </summary>
@ -85,6 +85,8 @@ namespace RageCoop.Client.Scripting
}
}
public static void SendCustomEvent(CustomEventHash hash, params object[] args)
=> SendCustomEvent(CustomEventFlags.None, hash, args);
public static void SendCustomEvent(CustomEventFlags flags, CustomEventHash hash, params object[] args)
{
var writer = GetWriter();
@ -92,6 +94,9 @@ namespace RageCoop.Client.Scripting
SendCustomEvent(flags, hash, writer.Address, writer.Position);
}
public static void RegisterCustomEventHandler(CustomEventHash hash, Action<CustomEventReceivedArgs> handler)
=> RegisterCustomEventHandler(hash, (CustomEventHandler)handler);
internal static string GetPropertyAsJson(string name) => InvokeCommandAsJson("GetProperty", name);
internal static string GetConfigAsJson(string name) => InvokeCommandAsJson("GetConfig", name);
@ -120,9 +125,7 @@ namespace RageCoop.Client.Scripting
[LibraryImport("RageCoop.Client.dll")]
private static partial int GetLastResultLenInChars();
[LibraryImport("RageCoop.Client.dll")]
[return: MarshalAs(UnmanagedType.I1)]
public static partial bool RegisterCustomEventHandler(CustomEventHash hash, IntPtr ptrHandler);
[LibraryImport("RageCoop.Client.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial void LogEnqueue(LogLevel level, string msg);
}
}

View File

@ -1,4 +1,5 @@
using RageCoop.Core.Scripting;
using Newtonsoft.Json;
using RageCoop.Core.Scripting;
using System;
using System.Collections.Generic;
using System.Linq;
@ -9,5 +10,19 @@ namespace RageCoop.Client.Scripting
{
public class ClientFile : ResourceFile
{
public ClientFile() {
GetStream = GetStreamMethod;
}
[JsonProperty]
public string FullPath { get; internal set; }
Stream GetStreamMethod()
{
if (IsDirectory)
{
return File.Open(FullPath, FileMode.Open);
}
throw new InvalidOperationException("Cannot open directory as file");
}
}
}

View File

@ -17,35 +17,38 @@ namespace RageCoop.Client.Scripting
/// <summary>
/// Name of the resource
/// </summary>
[JsonProperty]
public string Name { get; internal set; }
/// <summary>
/// Directory where the scripts is loaded from
/// </summary>
[JsonProperty]
public string ScriptsDirectory { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
[JsonProperty]
public string DataFolder { get; internal set; }
/// <summary>
/// Get the <see cref="ResourceFile" /> where this script is loaded from.
/// Get the <see cref="ClientFile" /> where this script is loaded from.
/// </summary>
public Dictionary<string, ResourceFile> Files { get; internal set; } = new Dictionary<string, ResourceFile>();
[JsonProperty]
public Dictionary<string, ClientFile> Files { get; internal set; } = new Dictionary<string, ClientFile>();
/// <summary>
/// List of the path of loaded modules, don't modify
/// </summary>
[JsonProperty]
public List<string> Modules = new();
/// <summary>
/// A <see cref="Core.Logger" /> instance that can be used to debug your resource.
/// </summary>
[JsonIgnore]
// TODO: call the api and use logging sinks
public Core.Logger Logger => throw new NotImplementedException();
/// <summary>
/// Get all <see cref="ClientScript" /> instance in this resource.
/// </summary>
[JsonIgnore]
public List<ClientScript> Scripts { get; } = SHVDN.Core.ListScripts().OfType<ClientScript>().ToList();
public ResourceLogger Logger => ResourceLogger.Default;
}

View File

@ -1,24 +1,72 @@
using GTA;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using System.Collections.Concurrent;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace RageCoop.Client.Scripting
{
[JsonDontSerialize]
[ScriptAttributes(NoDefaultInstance = true)]
public abstract class ClientScript : Script
{
ConcurrentQueue<Func<bool>> _jobQueue = new();
Queue<Func<bool>> _reAdd = new();
/// <summary>
/// Get the <see cref="ResourceFile" /> instance where this script is loaded from.
/// Fully qualified path to the module that the current script runs in.
/// </summary>
public ClientFile CurrentFile { get; internal set; }
public static readonly string FullPath;
static unsafe ClientScript()
{
char* buf = stackalloc char[260];
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);
}
public ClientScript()
{
CurrentResource = APIBridge.GetResouceFromFilePath(FullPath);
if (CurrentResource == null)
throw new Exception("No resource associated with this script is found");
CurrentFile = CurrentResource.Files.Values.FirstOrDefault(x => x?.FullPath?.ToLower() == FullPath.ToLower());
if (CurrentFile == null)
{
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; });
private void DoQueuedJobs()
{
while (_reAdd.TryDequeue(out var toAdd))
_jobQueue.Enqueue(toAdd);
while (_jobQueue.TryDequeue(out var job))
{
if (!job())
_reAdd.Enqueue(job);
}
}
/// <summary>
/// Get the <see cref="ClientFile" /> instance where this script is loaded from.
/// </summary>
public ClientFile CurrentFile { get; }
/// <summary>
/// Get the <see cref="ClientResource" /> that this script belongs to.
/// </summary>
public ClientResource CurrentResource { get; internal set; }
public ClientResource CurrentResource { get; }
/// <summary>
/// Eqivalent of <see cref="ClientResource.Logger" /> in <see cref="Script.CurrentResource" />
/// Eqivalent of <see cref="ClientResource.Logger" /> in <see cref="CurrentResource" />
/// </summary>
public Core.Logger Logger => CurrentResource.Logger;
public ResourceLogger Logger => CurrentResource.Logger;
}
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace RageCoop.Client.Scripting
{
public class ResourceLogger : Core.Logger
{
public static readonly ResourceLogger Default = new();
public ResourceLogger()
{
FlushImmediately= true;
OnFlush += FlushToMainModule;
}
private void FlushToMainModule(LogLine line, string fomatted)
{
APIBridge.LogEnqueue(line.LogLevel, line.Message);
}
}
}

View File

@ -46,7 +46,7 @@ namespace RageCoop.Client
/// <summary>
/// The key to open menu
/// </summary>
public Keys MenuKey { get; set; } = Keys.F9;
public Keys MenuKey { get; set; } = Keys.F7;
/// <summary>
/// The key to enter a vehicle as passenger.

View File

@ -52,7 +52,7 @@ public static class EntryPoint
}
[UnmanagedCallersOnly(EntryPoint = "OnKeyboard")]
public unsafe static void OnKeyboard(int key, ushort repeats, bool scanCode, bool isExtended, bool isWithAlt, bool wasDownBefore, bool isUpNow)
public unsafe static void OnKeyboard(uint key, ushort repeats, bool scanCode, bool isExtended, bool isWithAlt, bool wasDownBefore, bool isUpNow)
{
try
{

View File

@ -86,26 +86,6 @@ namespace RageCoop.Client.Scripting
[UnmanagedCallersOnly(EntryPoint = nameof(GetLastResultLenInChars))]
public static int GetLastResultLenInChars() => _lastResult?.Length ?? 0;
[UnmanagedCallersOnly(EntryPoint = nameof(RegisterCustomEventHandler))]
public static bool RegisterCustomEventHandler(CustomEventHash hash, IntPtr ptrHandler)
{
try
{
lock (CustomEventHandlers)
{
if (!CustomEventHandlers.TryGetValue(hash, out var handlers))
CustomEventHandlers.Add(hash, handlers = new());
handlers.Add(new(ptrHandler));
}
return true;
}
catch (Exception ex)
{
Log.Error(nameof(RegisterCustomEventHandler), ex);
return false;
}
}
/// <summary>
/// Convert Entity ID to handle
/// </summary>
@ -121,5 +101,16 @@ namespace RageCoop.Client.Scripting
_ => 0,
};
}
/// <summary>
/// Enqueue a message to the main logger
/// </summary>
/// <param name="level"></param>
/// <param name="msg"></param>
[UnmanagedCallersOnly(EntryPoint = nameof(LogEnqueue))]
public static void LogEnqueue(LogLevel level, char* msg)
{
Log.Enqueue((int)level, new(msg));
}
}
}

View File

@ -4,8 +4,10 @@ using Newtonsoft.Json;
using RageCoop.Client.Menus;
using RageCoop.Core;
using RageCoop.Core.Scripting;
using SHVDN;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Runtime.CompilerServices;
@ -309,14 +311,8 @@ namespace RageCoop.Client.Scripting
/// </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)
{
lock (CustomEventHandlers)
{
if (!CustomEventHandlers.TryGetValue(hash, out var handlers))
CustomEventHandlers.Add(hash, handlers = new());
handlers.Add(handler);
}
}
=> RegisterCustomEventHandler(hash, (CustomEventHandler)handler);
/// <summary>
/// </summary>
@ -400,6 +396,11 @@ namespace RageCoop.Client.Scripting
Networking.SendChatMessage(message);
}
/// <summary>
/// Get the <see cref="ClientResource"/> with this name
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[Remoting]
public static ClientResource GetResource(string name)
{
@ -409,6 +410,21 @@ namespace RageCoop.Client.Scripting
return null;
}
/// <summary>
/// Get <see cref="ClientResource"/> that contains the specified file
/// </summary>
/// <returns></returns>
[Remoting]
public static ClientResource GetResouceFromFilePath(string filePath)
{
foreach (var res in MainRes.LoadedResources)
{
if (res.Value.Files.Any(file => file.Value.FullPath.ToLower() == filePath.ToLower()))
return res.Value;
}
return null;
}
[Remoting(GenBridge = false)]
public static object GetProperty(string name)
@ -436,6 +452,33 @@ namespace RageCoop.Client.Scripting
prop.SetValue(null, JsonDeserialize(jsonVal, prop.PropertyType));
}
/// <summary>
/// Register an handler to the specifed event hash, one event can have multiple handlers. This will be invoked from
/// 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
/// </param>
/// <param name="handler">An handler to be invoked when the event is received from the server. </param>
[Remoting]
public static void RegisterCustomEventHandler(CustomEventHash hash, CustomEventHandler handler)
{
if (handler.Module == default)
throw new ArgumentException("Module not specified");
if (handler.FunctionPtr == default)
throw new ArgumentException("Function pointer not specified");
lock (CustomEventHandlers)
{
if (!CustomEventHandlers.TryGetValue(hash, out var handlers))
CustomEventHandlers.Add(hash, handlers = new());
handlers.Add(handler);
}
}
#endregion

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@ -33,7 +34,7 @@ namespace RageCoop.Client.Scripting
}
public void Load(string path, string[] zips)
public unsafe void Load(string path, string[] zips)
{
LoadedResources.Clear();
foreach (var zip in zips)
@ -42,19 +43,45 @@ namespace RageCoop.Client.Scripting
Log?.Info($"Loading resource: {Path.GetFileNameWithoutExtension(zip)}");
Unpack(zipPath, Path.Combine(path, "Data"));
}
Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories).Where(x => x.CanBeIgnored())
.ForEach(File.Delete);
// TODO: Core.ScheduleLoad()...
}
public void Unload()
public unsafe void Unload()
{
// TODO: Core.ScheduleUnload()...
HashSet<IntPtr> modules = new();
foreach (var res in LoadedResources.Values)
{
foreach (var module in res.Modules)
{
fixed (char* pModulePath = module)
{
Log.Debug($"Unloading module: {module}");
SHVDN.Core.ScheduleUnload(pModulePath);
var hModule = Util.GetModuleHandleW(module);
if (hModule == IntPtr.Zero)
Log.Warning("Failed to get module handler for " + Path.GetFileName(module));
else
modules.Add(hModule);
}
}
}
// Unregister associated handler
foreach (var handlers in API.CustomEventHandlers.Values)
{
foreach (var handler in handlers.ToArray())
{
if (modules.Contains((IntPtr)handler.Module))
{
Log.Debug($"Unregister handler from module {handler.Module}");
handlers.Remove(handler);
}
}
}
LoadedResources.Clear();
}
private ClientResource Unpack(string zipPath, string dataFolderRoot)
private unsafe ClientResource Unpack(string zipPath, string dataFolderRoot)
{
var r = new ClientResource
{
@ -73,10 +100,11 @@ namespace RageCoop.Client.Scripting
foreach (var dir in Directory.GetDirectories(scriptsDir, "*", SearchOption.AllDirectories))
r.Files.Add(dir, new ResourceFile
r.Files.Add(dir, new ClientFile
{
IsDirectory = true,
Name = dir.Substring(scriptsDir.Length + 1).Replace('\\', '/')
Name = dir.Substring(scriptsDir.Length + 1).Replace('\\', '/'),
FullPath = dir
});
foreach (var file in Directory.GetFiles(scriptsDir, "*", SearchOption.AllDirectories))
{
@ -99,9 +127,18 @@ namespace RageCoop.Client.Scripting
var rfile = new ClientFile
{
IsDirectory = false,
Name = relativeName
Name = relativeName,
FullPath = file
};
r.Files.Add(relativeName, rfile);
if (file.EndsWith(".dll"))
{
fixed (char* pModulePath = file)
{
SHVDN.Core.ScheduleLoad(pModulePath);
r.Modules.Add(file);
}
}
}
LoadedResources.TryAdd(r.Name, r);

View File

@ -17,7 +17,7 @@ using Font = GTA.UI.Font;
namespace RageCoop.Client
{
internal static class Util
internal static partial class Util
{
/// <summary>
/// The location of the cursor on screen between 0 and 1.
@ -206,10 +206,12 @@ namespace RageCoop.Client
Call(DELETE_ENTITY, &handle);
}
[DllImport("kernel32.dll")]
public static extern ulong GetTickCount64();
[LibraryImport("kernel32.dll")]
public static partial ulong GetTickCount64();
[LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
public static partial IntPtr GetModuleHandleW([MarshalAs(UnmanagedType.LPWStr)] string lpModuleName);
#region -- POINTER --
private static int _steeringAngleOffset { get; set; }

View File

@ -9,6 +9,7 @@ using System.Net.Sockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Security.Cryptography;
using System.Text;
using System.Xml;
@ -30,9 +31,9 @@ namespace RageCoop.Core
{
internal static class CoreUtils
{
private static readonly Random random = new Random();
private static readonly Random random = new();
private static readonly HashSet<string> ToIgnore = new HashSet<string>
private static readonly HashSet<string> ToIgnore = new()
{
"RageCoop.Client",
"RageCoop.Client.Loader",
@ -41,7 +42,8 @@ namespace RageCoop.Core
"RageCoop.Server",
"ScriptHookVDotNet2",
"ScriptHookVDotNet3",
"ScriptHookVDotNet"
"ScriptHookVDotNet",
"ScriptHookVDotNetCore"
};
public static string FormatToSharpStyle(string input, int offset)
@ -106,7 +108,20 @@ namespace RageCoop.Core
public static bool CanBeIgnored(this string name)
{
return ToIgnore.Contains(Path.GetFileNameWithoutExtension(name));
name = Path.GetFileNameWithoutExtension(name);
return ToIgnore.Contains(name) || AssemblyLoadContext.Default.Assemblies.Any(x => x.GetName().Name == name);
}
public static void ForceLoadAllAssemblies()
{
foreach (var a in AssemblyLoadContext.Default.Assemblies)
LoadAllReferencedAssemblies(a.GetName());
}
public static void LoadAllReferencedAssemblies(this AssemblyName assembly)
{
foreach (var child in Assembly.Load(assembly).GetReferencedAssemblies())
LoadAllReferencedAssemblies(child);
}
public static string ToFullPath(this string path)

View File

@ -46,7 +46,7 @@ namespace RageCoop.Core
public string Name = "Logger";
private bool Stopping;
public List<StreamWriter> Writers = new List<StreamWriter> { new StreamWriter(Console.OpenStandardOutput()) };
public List<StreamWriter> Writers = new() { new StreamWriter(Console.OpenStandardOutput()) };
internal Logger()
{
@ -166,18 +166,29 @@ namespace RageCoop.Core
var formatted = Format(line);
Writers.ForEach(x =>
{
x.WriteLine(formatted);
x.Flush();
try
{
x.WriteLine(formatted);
x.Flush();
}
catch (Exception ex)
{
HandleError(ex);
}
});
OnFlush?.Invoke(line, formatted);
}
}
catch
catch (Exception ex)
{
HandleError(ex);
}
}
}
void HandleError(Exception ex)
{
Console.WriteLine($"Logger {this} flush error: {ex}");
}
public class LogLine
{
public LogLevel LogLevel;

View File

@ -1,4 +1,5 @@
using System.Runtime.InteropServices;
using Newtonsoft.Json;
using System.Runtime.InteropServices;
namespace RageCoop.Core.Scripting
{
@ -22,20 +23,32 @@ namespace RageCoop.Core.Scripting
}
public unsafe class CustomEventHandler
{
// Make sure the handler doesn't get GC'd
static List<CustomEventHandler> _handlers = new();
[ThreadStatic]
static object _tag;
public CustomEventHandler(IntPtr func)
public CustomEventHandler()
{
FunctionPtr = func;
if (Path.GetFileName(Environment.ProcessPath).ToLower() == "gtav.exe")
lock (_handlers)
{
Module = SHVDN.Core.CurrentModule;
_handlers.Add(this);
}
}
public CustomEventHandler(IntPtr func) : this()
{
FunctionPtr = (ulong)func;
Module = (ulong)SHVDN.Core.CurrentModule;
}
[JsonIgnore]
private CustomEventHandlerDelegate _managedHandler; // Used to keep GC reference
public IntPtr FunctionPtr { get; }
public IntPtr Module { get; }
[JsonProperty]
public ulong FunctionPtr { get; private set; }
[JsonProperty]
public ulong Module { get; private set; }
/// <summary>
///

View File

@ -11,11 +11,13 @@ namespace RageCoop.Core.Scripting
/// <summary>
/// Full name with relative path of this file
/// </summary>
[JsonProperty]
public string Name { get; internal set; }
/// <summary>
/// Whether this is a directory
/// </summary>
[JsonProperty]
public bool IsDirectory { get; internal set; }
/// <summary>

View File

@ -1,11 +1,29 @@
global using static RageCoop.Core.Shared;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using System;
using System.Reflection;
namespace RageCoop.Core
{
public class JsonDontSerialize : Attribute
{
}
internal class Shared
{
static Type JsonTypeCheck(Type type)
{
if (type.GetCustomAttribute<JsonDontSerialize>() != null)
throw new TypeAccessException($"The type {type} cannot be serialized");
return type;
}
static object JsonTypeCheck(object obj)
{
JsonTypeCheck(obj.GetType());
return obj;
}
public static readonly JsonSerializerSettings JsonSettings = new();
static Shared()
{
@ -16,12 +34,12 @@ namespace RageCoop.Core
public static object JsonDeserialize(string text, Type type)
{
return JsonConvert.DeserializeObject(text, type, JsonSettings);
return JsonConvert.DeserializeObject(text, JsonTypeCheck(type), JsonSettings);
}
public static T JsonDeserialize<T>(string text) => (T)JsonDeserialize(text, typeof(T));
public static string JsonSerialize(object obj) => JsonConvert.SerializeObject(obj, JsonSettings);
public static string JsonSerialize(object obj) => JsonConvert.SerializeObject(JsonTypeCheck(obj), JsonSettings);
/// <summary>
/// Shortcut to <see cref="BufferReader.ThreadLocal"/>

View File

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

View File

@ -22,6 +22,11 @@ internal class Resources
Logger = server.Logger;
}
static Resources()
{
CoreUtils.ForceLoadAllAssemblies();
}
public bool IsLoaded { get; private set; }
public void LoadAll()
@ -263,7 +268,7 @@ internal class Resources
}
if (Server.GetResponse<Packets.FileTransferResponse>(client, new Packets.AllResourcesSent(),
ConnectionChannel.RequestResponse, 30000)?.Response == FileResponse.Loaded)
ConnectionChannel.File, 30000)?.Response == FileResponse.Loaded)
{
client.IsReady = true;
Server.API.Events.InvokePlayerReady(client);

View File

@ -47,6 +47,10 @@ public class ServerResource : PluginLoader
internal static ServerResource LoadFrom(string resDir, string dataFolder, Logger logger = null)
{
string mainAssemblyPath = Path.Combine(resDir, Path.GetFileName(resDir) + ".dll");
if (!File.Exists(mainAssemblyPath))
throw new FileNotFoundException($"Main assemby not found: {mainAssemblyPath}");
var runtimeLibs = Path.Combine(resDir, "RuntimeLibs", CoreUtils.GetInvariantRID());
if (Directory.Exists(runtimeLibs))
{
@ -61,16 +65,18 @@ public class ServerResource : PluginLoader
CoreUtils.CopyFilesRecursively(new DirectoryInfo(runtimeLibs), new DirectoryInfo(resDir));
}
var conf = new PluginConfig(Path.GetFullPath(Path.Combine(resDir, Path.GetFileName(resDir) + ".dll")))
var conf = new PluginConfig(Path.GetFullPath(mainAssemblyPath))
{
PreferSharedTypes = true,
EnableHotReload = false,
IsUnloadable = false,
LoadInMemory = true
};
ServerResource r = new(conf);
r.Logger = logger;
r.Name = Path.GetFileName(resDir);
ServerResource r = new(conf)
{
Logger = logger,
Name = Path.GetFileName(resDir)
};
if (!File.Exists(conf.MainAssemblyPath))
{
r.Dispose();