Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 23 additions & 25 deletions SPTarkov.Server/Modding/EnumPatcher.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using Mono.Cecil;
using Mono.Cecil.Rocks;
using SPTarkov.Reflection.Patching;
using AsmResolver.DotNet;
using AsmResolver.PE.DotNet.Metadata.Tables;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Exceptions;

Expand All @@ -23,8 +22,7 @@ private static void ApplyEntry(ModuleDefinition module, EnumEntryDefinition entr
throw new ModLoaderException("An enum prepatch entry has no enumType.");
}

var cecilTypeName = entry.EnumType.Replace('+', '/');
var enumType = module.GetAllTypes().FirstOrDefault(type => string.Equals(type.FullName, cecilTypeName, StringComparison.Ordinal));
var enumType = module.GetAllTypes().FirstOrDefault(type => string.Equals(type.FullName, entry.EnumType, StringComparison.Ordinal));

if (enumType is null || !enumType.IsEnum)
{
Expand All @@ -36,39 +34,39 @@ private static void ApplyEntry(ModuleDefinition module, EnumEntryDefinition entr
throw new ModLoaderException($"Enum `{entry.EnumType}` already contains an entry named `{entry.ConstantName}`.");
}

if (enumType.Fields.Any(field => field.HasConstant && Convert.ToDecimal(field.Constant) == entry.ConstantValue))
if (enumType.Fields.Any(field => field.Constant?.InterpretData() is { } value && Convert.ToDecimal(value) == entry.ConstantValue))
{
throw new ModLoaderException($"Enum `{entry.EnumType}` already contains the value {entry.ConstantValue}.");
}

enumType.Fields.Add(
new FieldDefinition(
entry.ConstantName,
FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault,
enumType
)
{
Constant = ConvertConstant(enumType, entry),
}
var field = new FieldDefinition(
entry.ConstantName,
FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault,
enumType.ToTypeSignature()
);
field.Constant = ConvertConstant(enumType, entry);
enumType.Fields.Add(field);
module.TokenAllocator.AssignNextAvailableToken(field);
}

private static object ConvertConstant(TypeDefinition enumType, EnumEntryDefinition entry)
private static Constant ConvertConstant(TypeDefinition enumType, EnumEntryDefinition entry)
{
var underlyingType = enumType.Fields.First(field => field.Name == "value__").FieldType.MetadataType;
var underlyingType =
enumType.GetEnumUnderlyingType()?.ElementType
?? throw new ModLoaderException($"Enum `{entry.EnumType}` has no underlying type.");

try
{
return underlyingType switch
{
MetadataType.SByte => checked((sbyte)entry.ConstantValue),
MetadataType.Byte => checked((byte)entry.ConstantValue),
MetadataType.Int16 => checked((short)entry.ConstantValue),
MetadataType.UInt16 => checked((ushort)entry.ConstantValue),
MetadataType.Int32 => checked((int)entry.ConstantValue),
MetadataType.UInt32 => checked((uint)entry.ConstantValue),
MetadataType.Int64 => entry.ConstantValue,
MetadataType.UInt64 => checked((ulong)entry.ConstantValue),
ElementType.I1 => Constant.FromValue(checked((sbyte)entry.ConstantValue)),
ElementType.U1 => Constant.FromValue(checked((byte)entry.ConstantValue)),
ElementType.I2 => Constant.FromValue(checked((short)entry.ConstantValue)),
ElementType.U2 => Constant.FromValue(checked((ushort)entry.ConstantValue)),
ElementType.I4 => Constant.FromValue(checked((int)entry.ConstantValue)),
ElementType.U4 => Constant.FromValue(checked((uint)entry.ConstantValue)),
ElementType.I8 => Constant.FromValue(entry.ConstantValue),
ElementType.U8 => Constant.FromValue(checked((ulong)entry.ConstantValue)),
_ => throw new ModLoaderException($"Enum `{entry.EnumType}` has an unsupported underlying type `{underlyingType}`."),
};
}
Expand Down
57 changes: 10 additions & 47 deletions SPTarkov.Server/Modding/ModLoader.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using Mono.Cecil;
using Mono.Cecil.Cil;
using AsmResolver.DotNet;
using SPTarkov.Common.Models.Logging;
using SPTarkov.Reflection.Patching;
using SPTarkov.Server.Core.Models.Spt.Mod;
using SPTarkov.Server.Exceptions;

Expand All @@ -16,9 +14,7 @@ public sealed class ModLoader(ISptLogger<ModLoader> logger, ModValidator modVali
private readonly List<EnumPrepatch> _enumPrepatches = [];

private ModuleDefinition? _serverCoreModule;
private MemoryStream? _serverCoreModuleStream;
private MemoryStream? _serverCoreSymbolStream;
private bool _serverCoreHasSymbols;
private byte[]? _serverCoreSymbols;
private readonly List<PrepatchResultEntry> _prepatchResults = [];

private const string ModPath = "./user/mods/";
Expand Down Expand Up @@ -161,21 +157,8 @@ private async Task LoadMods(bool isPrepatchedProcess, CancellationToken cancella
return null;
}

using var patchedStream = new MemoryStream();
using var symbolStream = new MemoryStream();

var writerParameters = new WriterParameters();
if (_serverCoreHasSymbols)
{
writerParameters.WriteSymbols = true;
writerParameters.SymbolWriterProvider = new PortablePdbWriterProvider();
writerParameters.SymbolStream = symbolStream;
}

_serverCoreModule!.Write(patchedStream, writerParameters);

var assemblyBytes = patchedStream.ToArray();
var symbolBytes = _serverCoreHasSymbols ? symbolStream.ToArray() : null;
var assemblyBytes = _serverCoreModule!.Write();
var symbolBytes = _serverCoreSymbols;

await File.WriteAllBytesAsync(PatchedAssemblyName, assemblyBytes, cancellationToken);

Expand All @@ -189,11 +172,8 @@ private async Task LoadMods(bool isPrepatchedProcess, CancellationToken cancella
}
finally
{
DisposeServerCoreModule();
_serverCoreModuleStream?.Dispose();
_serverCoreModuleStream = null;
_serverCoreSymbolStream?.Dispose();
_serverCoreSymbolStream = null;
_serverCoreModule = null;
_serverCoreSymbols = null;
}
}

Expand Down Expand Up @@ -234,12 +214,6 @@ private bool RunEnumPrepatches(IReadOnlyCollection<SptMod>? validRuntimeMods)
return _prepatchResults.All(r => r.Succeeded);
}

private void DisposeServerCoreModule()
{
_serverCoreModule?.Dispose();
_serverCoreModule = null;
}

private static void ClearConsole()
{
if (Console.IsOutputRedirected)
Expand Down Expand Up @@ -357,9 +331,7 @@ private IModMetadata LoadModMetadata(IEnumerable<Assembly> assemblies, string pa

if (result == null)
{
throw new ModLoaderException(
$"Failed to load mod metadata for: {Path.GetFullPath(path)} \ndid you implement `IModMetadata`?"
);
throw new ModLoaderException($"Failed to load mod metadata for: {Path.GetFullPath(path)} \ndid you implement `IModMetadata`?");
}

return result;
Expand Down Expand Up @@ -422,22 +394,13 @@ private async Task<bool> TryLoadServerCoreBytes()
var serverCorePath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "SPTarkov.Server.Core.dll");
var symbolPath = Path.ChangeExtension(serverCorePath, ".pdb");

// Don't dispose the streams, keep them open, it will cause cecil to have a stroke if they're disposed of
_serverCoreModuleStream = new MemoryStream(await File.ReadAllBytesAsync(serverCorePath), writable: false);

var readerParameters = new ReaderParameters { ReadingMode = ReadingMode.Immediate, InMemory = true };
_serverCoreModule = ModuleDefinition.FromBytes(await File.ReadAllBytesAsync(serverCorePath));

// Read the symbols so the patched Core can emit a matching pdb and stay breakpointable
// Existing metadata tokens are preserved, so the original PDB remains valid for the patched assembly.
if (File.Exists(symbolPath))
{
_serverCoreSymbolStream = new MemoryStream(await File.ReadAllBytesAsync(symbolPath), writable: false);
readerParameters.ReadSymbols = true;
readerParameters.SymbolReaderProvider = new PortablePdbReaderProvider();
readerParameters.SymbolStream = _serverCoreSymbolStream;
_serverCoreHasSymbols = true;
_serverCoreSymbols = await File.ReadAllBytesAsync(symbolPath);
}

_serverCoreModule = ModuleDefinition.ReadModule(_serverCoreModuleStream, readerParameters);
}
catch (Exception e)
{
Expand Down
14 changes: 14 additions & 0 deletions SPTarkov.Server/Modding/PrepatchAssemblyWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using AsmResolver.DotNet;
using AsmResolver.DotNet.Builder;

namespace SPTarkov.Server.Modding;

internal static class PrepatchAssemblyWriter
{
public static byte[] Write(this ModuleDefinition module)
{
using var stream = new MemoryStream();
module.Write(stream, new ManagedPEImageBuilder(MetadataBuilderFlags.PreserveAll));
return stream.ToArray();
}
}
6 changes: 6 additions & 0 deletions SPTarkov.Server/SPTarkov.Server.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
<ProjectReference Include="..\Libraries\SPTarkov.Server.Assets\SPTarkov.Server.Assets.csproj" />
<ProjectReference Include="..\Libraries\SPTarkov.Server.Web\SPTarkov.Server.Web.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsmResolver.DotNet" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="UnitTests" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\**">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
Expand Down
Loading