295 lines
7.6 KiB
C#
Raw Normal View History

using System.IO;
using RageCoop.Core.Scripting;
2022-07-01 12:22:31 +08:00
using RageCoop.Core;
using ICSharpCode.SharpZipLib.Zip;
2022-06-30 09:28:13 +08:00
using System;
2022-07-01 12:22:31 +08:00
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
namespace RageCoop.Client.Scripting
{
2022-07-01 13:54:18 +08:00
/// <summary>
///
/// </summary>
2022-07-01 12:22:31 +08:00
public class ClientResource
{
/// <summary>
/// Name of the resource
/// </summary>
public string Name { get; internal set; }
/// <summary>
/// A resource-specific folder that can be used to store your files.
/// </summary>
public string DataFolder { get; internal set; }
2022-07-01 13:54:18 +08:00
/// <summary>
/// Get all <see cref="ClientScript"/> instance in this resource.
/// </summary>
2022-07-01 12:22:31 +08:00
public List<ClientScript> Scripts { get; internal set; } = new List<ClientScript>();
2022-07-01 13:54:18 +08:00
/// <summary>
/// Get the <see cref="ResourceFile"/> where this script is loaded from.
/// </summary>
2022-07-01 12:22:31 +08:00
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; }
2022-07-01 12:22:31 +08:00
}
internal class Resources
{
2022-07-01 12:22:31 +08:00
public Resources(){
BaseScriptType = "RageCoop.Client.Scripting.ClientScript";
Logger = Main.Logger;
}
private void StartAll()
{
lock (LoadedResources)
{
foreach (var d in LoadedResources)
{
foreach (var s in d.Scripts)
{
2022-06-30 09:28:13 +08:00
try
{
2022-07-19 17:15:53 +08:00
s.CurrentResource=d;
2022-06-30 09:28:13 +08:00
s.OnStart();
}
catch(Exception ex)
{
Logger.Error("Error occurred when starting script:"+s.GetType().FullName);
Logger?.Error(ex);
}
}
}
}
}
private void StopAll()
{
lock (LoadedResources)
{
foreach (var d in LoadedResources)
{
foreach (var s in d.Scripts)
{
2022-06-30 09:28:13 +08:00
try
{
s.OnStop();
}
catch (Exception ex)
{
Logger.Error("Error occurred when stopping script:"+s.GetType().FullName);
Logger?.Error(ex);
}
}
}
}
}
2022-07-11 11:11:48 +08:00
public void Load(string path,string[] zips)
{
2022-07-20 07:34:53 +08:00
LoadedResources.Clear();
foreach (var zip in zips)
{
var zipPath=Path.Combine(path, zip);
Logger?.Info($"Loading resource: {Path.GetFileNameWithoutExtension(zip)}");
2022-07-01 12:22:31 +08:00
LoadResource(new ZipFile(zipPath),Path.Combine(path,"data"));
}
StartAll();
}
public void Unload()
{
StopAll();
if (LoadedResources.Count > 0)
{
2022-07-10 23:30:29 +08:00
API.QueueAction(()=>Util.Reload());
}
LoadedResources.Clear();
}
2022-07-01 12:22:31 +08:00
private List<string> ToIgnore = new List<string>
{
"RageCoop.Client.dll",
"RageCoop.Core.dll",
"RageCoop.Server.dll",
"ScriptHookVDotNet3.dll"
};
private List<ClientResource> LoadedResources = new List<ClientResource>();
private string BaseScriptType;
public Logger Logger { get; set; }
2022-07-01 13:54:18 +08:00
2022-07-01 12:22:31 +08:00
private void LoadResource(ZipFile file, string dataFolderRoot)
{
var r = new ClientResource()
{
2022-07-19 17:15:53 +08:00
Logger = Main.Logger,
2022-07-01 12:22:31 +08:00
Scripts = new List<ClientScript>(),
Name=Path.GetFileNameWithoutExtension(file.Name),
DataFolder=Path.Combine(dataFolderRoot, Path.GetFileNameWithoutExtension(file.Name))
};
Directory.CreateDirectory(r.DataFolder);
foreach (ZipEntry entry in file)
{
ResourceFile rFile;
r.Files.Add(entry.Name, rFile=new ResourceFile()
{
Name=entry.Name,
IsDirectory=entry.IsDirectory,
});
if (!entry.IsDirectory)
{
rFile.GetStream=() => { return file.GetInputStream(entry); };
if (entry.Name.EndsWith(".dll"))
{
var tmp = Path.GetTempFileName();
var f = File.OpenWrite(tmp);
rFile.GetStream().CopyTo(f);
f.Close();
LoadScriptsFromAssembly(rFile, tmp, r, false);
}
}
}
LoadedResources.Add(r);
2022-07-02 17:14:56 +08:00
file.Close();
2022-07-01 12:22:31 +08:00
}
private bool LoadScriptsFromAssembly(ResourceFile file, string path, ClientResource resource, bool shadowCopy = true)
{
lock (LoadedResources)
{
if (!IsManagedAssembly(path)) { return false; }
if (ToIgnore.Contains(file.Name)) { try { File.Delete(path); } catch { }; return false; }
Logger?.Debug($"Loading assembly {file.Name} ...");
Assembly assembly;
try
{
if (shadowCopy)
{
var temp = Path.GetTempFileName();
File.Copy(path, temp, true);
assembly = Assembly.LoadFrom(temp);
}
else
{
assembly = Assembly.LoadFrom(path);
}
}
catch (Exception ex)
{
Logger?.Error("Unable to load "+file.Name);
Logger?.Error(ex);
return false;
}
return LoadScriptsFromAssembly(file, assembly, path, resource);
}
}
private bool LoadScriptsFromAssembly(ResourceFile rfile, Assembly assembly, string filename, ClientResource toload)
{
int count = 0;
try
{
// Find all script types in the assembly
foreach (var type in assembly.GetTypes().Where(x => IsSubclassOf(x, BaseScriptType)))
{
ConstructorInfo constructor = type.GetConstructor(System.Type.EmptyTypes);
if (constructor != null && constructor.IsPublic)
{
try
{
// Invoke script constructor
var script = constructor.Invoke(null) as ClientScript;
// script.CurrentResource = toload;
script.CurrentFile=rfile;
2022-07-02 11:23:12 +08:00
script.CurrentResource=toload;
2022-07-01 12:22:31 +08:00
toload.Scripts.Add(script);
count++;
}
catch (Exception ex)
{
Logger?.Error($"Error occurred when loading script: {type.FullName}.");
Logger?.Error(ex);
}
}
else
{
Logger?.Error($"Script {type.FullName} has an invalid contructor.");
}
}
}
catch (ReflectionTypeLoadException ex)
{
Logger?.Error($"Failed to load assembly {rfile.Name}: ");
Logger?.Error(ex);
foreach (var e in ex.LoaderExceptions)
{
Logger?.Error(e);
}
return false;
}
Logger?.Info($"Loaded {count} script(s) in {rfile.Name}");
return count != 0;
}
private bool IsManagedAssembly(string filename)
{
try
{
using (Stream file = new FileStream(filename, FileMode.Open, FileAccess.Read))
{
if (file.Length < 64)
return false;
using (BinaryReader bin = new BinaryReader(file))
{
// PE header starts at offset 0x3C (60). Its a 4 byte header.
file.Position = 0x3C;
uint offset = bin.ReadUInt32();
if (offset == 0)
offset = 0x80;
// Ensure there is at least enough room for the following structures:
// 24 byte PE Signature & Header
// 28 byte Standard Fields (24 bytes for PE32+)
// 68 byte NT Fields (88 bytes for PE32+)
// >= 128 byte Data Dictionary Table
if (offset > file.Length - 256)
return false;
// Check the PE signature. Should equal 'PE\0\0'.
file.Position = offset;
if (bin.ReadUInt32() != 0x00004550)
return false;
// Read PE magic number from Standard Fields to determine format.
file.Position += 20;
var peFormat = bin.ReadUInt16();
if (peFormat != 0x10b /* PE32 */ && peFormat != 0x20b /* PE32Plus */)
return false;
// Read the 15th Data Dictionary RVA field which contains the CLI header RVA.
// When this is non-zero then the file contains CLI data otherwise not.
file.Position = offset + (peFormat == 0x10b ? 232 : 248);
return bin.ReadUInt32() != 0;
}
}
}
catch
{
// This is likely not a valid assembly if any IO exceptions occur during reading
return false;
}
}
private bool IsSubclassOf(Type type, string baseTypeName)
{
for (Type t = type.BaseType; t != null; t = t.BaseType)
if (t.FullName == baseTypeName)
return true;
return false;
}
}
}