Unity Mod AutoPackage Utility
This is a packaging utility for class libraries targeting .NET Framework 4.8 for deployment in the Unity environment
This tool ensures...
🔥 The build output is clean (single DLL)
📦 All referenced assemblies are included as embedded resources
This includes implicit references that are not typically copied to the output directory
📥 Embedded assemblies are compressed
📄 MetaData for each assembly is included
🚫 Manually referenced DLL's are not included <Reference>
Things like UnityEngine.dll will not be added as an embedded resource
🚫 Project references are not included <ProjectReference>
Note
Common libraries like System.Memory are not copied to the output directory resulting in AssemblyLoad errors
These libraries will be included and packaged with your project
Caution
The embedded libraries are not automatically or magically loaded for you!
You must implement your own AppDomain.CurrentDomain.AssemblyResolve += handler;
For automatic dependency resolution, see here
This is a tool meant to run after assets.project.json is generated
On first build/restore, it will update the *.csproj file and changes may not take effect until the second build
Tip
It is recommended to add the following to your *.csproj
Be sure to include InitialTargets="GenerateNewTargets"
<?xml version =" 1.0" encoding =" utf-8" ?>
<Project Sdk =" Microsoft.NET.Sdk" InitialTargets =" GenerateNewTargets" >
<!-- Import constants/defintions from the main project file. -->
<Import Project =" $(ProjectDir)../../REPO_Mods/main.targets" />
<!-- This target is used to generate the new .targets file for the project. -->
<Target Name =" GenerateNewTargets" BeforeTargets =" Restore" >
<Exec Command =" $(DotNetToolsDirectory)$(PathSeparator)UnityModPackager --pre" WorkingDirectory =" $(ProjectDir)" />
</Target >
<!-- This target is used to copy the output files to the plugin directory after the build is complete. -->
<Target Name =" CustomPostBuild" AfterTargets =" Build" >
<ItemGroup >
<AllOutputFiles Include =" $(TargetDir)*.dll" />
</ItemGroup >
<Copy SourceFiles =" @(AllOutputFiles)" DestinationFolder =" $(CopyToDirOnBuild)%(RecursiveDir)"
OverwriteReadOnlyFiles =" true" SkipUnchangedFiles =" true" />
</Target >
</Project >
An example of main.targets:
<?xml version =" 1.0" encoding =" utf-8" ?>
<Project xmlns =" http://schemas.microsoft.com/developer/msbuild/2003" >
<PropertyGroup >
<DotNetToolsDirectory Condition =" '$([MSBuild]::IsOSUnixLike())' == 'true'" >$(HOME)/.dotnet/tools</DotNetToolsDirectory >
<DotNetToolsDirectory Condition =" '$([MSBuild]::IsOSUnixLike())' != 'true'" >$(USERPROFILE)\.dotnet\tools</DotNetToolsDirectory >
<PathSeparator Condition =" '$(OS)' == 'Windows_NT'" >\</PathSeparator >
<PathSeparator Condition =" '$(OS)' != 'Windows_NT'" >/</PathSeparator >
</PropertyGroup >
<PropertyGroup >
<CopyToDirOnBuild >/home/theguy920/.config/r2modmanPlus-local/REPO/profiles/Default/BepInEx/plugins/TheGuy920-SoundBoard/</CopyToDirOnBuild >
<REPODir >..\..\..\..\..\mnt\2TB_NVME\SteamLibrary\steamapps\common\REPO\</REPODir >
<HarmonyDir >..\..\..\.config\r2modmanPlus-local\REPO\profiles\Default\BepInEx\core\</HarmonyDir >
<BepInExDir >..\..\..\.config\r2modmanPlus-local\REPO\profiles\Default\BepInEx\core\</BepInExDir >
<MenuLibDir >..\..\..\.config\r2modmanPlus-local\REPO\profiles\Default\BepInEx\plugins\nickklmao-MenuLib\</MenuLibDir >
</PropertyGroup >
</Project >
Important
Make sure that UnityModPackager can be accessed at $USER_HOME$/.dotnet/tools/
<!-- Unix implementation using symbolic link -->
<Exec Command =" rm -f " $(DestExe)" && ln -sf " $(SourceExe)" " $(DestExe)" "
Condition =" $(IsUnix)" />
Here a symbolic link is created on build
Once all of the above is complete, you are ready to start modding!
If you want, you can take a peak at the Workflow below to see how this tool works
var workingDir = Directory . GetCurrentDirectory ( ) ;
var csprojFiles = Directory . GetFiles ( workingDir , "*.csproj" , SearchOption . TopDirectoryOnly ) ;
var csprojFile = csprojFiles . FirstOrDefault ( ) ;
Ensure Tag : CopyLocalLockFileAssemblies = false
Prevents references generated in assets.project.json [NuGet] from being copied to the output directory
userCsProj . AddTagToFirst ( "PropertyGroup" , "CopyLocalLockFileAssemblies" , false ) ;
Ensure Tag : AutoGenerateBindingRedirects = true
Ensures properly linking to referenced libraries to prevent versioning errors
userCsProj . AddTagToFirst ( "PropertyGroup" , "AutoGenerateBindingRedirects" , true ) ;
Ensure Tag : GenerateBindingRedirectsOutputType = true
Ensures the binding redirects are generated based on the project output type ( library )
userCsProj . AddTagToFirst ( "PropertyGroup" , "GenerateBindingRedirectsOutputType" , true ) ;
Ensure Attribute : Private = false
Ensures that ProjectReference and Reference are not copied to the output directory
PackageReference is not required because it is handled by the tags above
userCsProj . AddTag ( "Reference" , "Private" , false ) ;
userCsProj . AddAttribute ( "ProjectReference" , "Private" , false ) ;
// userCsProj.AddAttribute("PackageReference", "ExcludeAssets", "runtime");
Ensure Tag : <Import Project="obj/GeneratedResources.targets"/>
Ensures the auto-generated resource file is imported into the project
userCsProj . AddTagToRoot ( "Import" , ( "Project" , "obj/GeneratedResources.targets" ) ) ;
Working in assets.project.json
// Load all libraries in ./obj/project.assets.json
var projectAssetsPath = Path . Combine ( Path . GetDirectoryName ( csprojFile ) ?? "" , "obj" , "project.assets.json" ) ;
var projectAssetsJson = JsonNode . Parse ( File . ReadAllText ( projectAssetsPath ) ) ! ;
var jsonLibraries = projectAssetsJson [ "targets" ] ! [ ".NETFramework,Version=v4.8" ] ! ;
Locate all dependencies, including implicit (default behavior of assets.project.json)
var libName = jobj . Select ( kvp => kvp . Key ) . First ( ) ;
Warning
If our target is invalid _._ or is under ref, we must look for alternatives
if ( libName . EndsWith ( "_._" ) || libName . StartsWith ( "ref" ) )
Strongly prefer files found under lib and DO NOT include .NET Framework 4.5 as it has a history of causing fatal crashes in Unity
var alternatives = projectAssetsJson [ "libraries" ] ! [ key ] ! [ "files" ] ! . AsArray ( )
. Select ( i => i ? . AsValue ( ) . GetValue < string > ( ) )
. Where ( f => f is not null && (
f . StartsWith ( "lib/net4" ) ||
f . StartsWith ( "lib/netstandard" )
) && f . EndsWith ( ".dll" ) && ! f . StartsWith ( "lib/net45" ) ) . ToArray ( ) ;
If no candidates are found, fallback to files under ref
alternatives = projectAssetsJson [ "libraries" ] ! [ key ] ! [ "files" ] ! . AsArray ( )
. Select ( i => i ? . AsValue ( ) . GetValue < string > ( ) )
. Where ( f => f is not null && (
f . StartsWith ( "ref/net4" ) ||
f . StartsWith ( "ref/netstandard" )
) && f . EndsWith ( ".dll" ) && ! f . StartsWith ( "ref/net45" ) ) . ToArray ( ) ;
Allow for multiple versions of the same Assembly, so that if Assembly.Load fails, there are fallback assemblies to try
if ( alternatives . Length > 0 )
{
libraries . AddRange ( alternatives . Select ( alt =>
Path . Combine ( librariesBasePath , key . ToLower ( ) , alt ) ) ) ;
continue ;
}
Generate library metadata in place
var dllMeta = lib + ".dllmeta" ;
// if (!File.Exists(dllMeta))
{
var asm = File . ReadAllBytes ( lib ) ;
var lAsmName = GetAssemblyNameFromData ( asm ) ;
var nameMeta = SerializeAssemblyName ( lAsmName ) ;
File . WriteAllBytes ( dllMeta , nameMeta ) ;
Console . WriteLine ( "Generated Dll MetaData " + dllMeta ) ;
}
Compress library in place
using var inputStream = File . OpenRead ( lib ) ;
using var outputStream = File . Create ( compressedLib ) ;
using var gzipStream = new GZipStream ( outputStream , CompressionMode . Compress ) ;
inputStream . CopyTo ( gzipStream ) ;
compressedLibraries . Add ( lib ) ;
Generate .targets file
var fName = ( i ++ ) + "." + Path . GetFileName ( lib ) ;
xmlInclude . AppendLine ( $ "<EmbeddedResource Include=\" { lib } .gz\" LogicalName=\" BundledAssemblies\\ { fName } .gz\" Visible=\" false\" />") ;
xmlInclude . AppendLine ( $ "<EmbeddedResource Include=\" { lib } .dllmeta\" LogicalName=\" BundledAssemblies\\ { fName } .dllmeta\" Visible=\" false\" />") ;
Ensure the LogicalName (the name of the resource in the manifest) is unique under the current assembly
This allows for multiple versions of the same assembly for fallback purposes
Write file to obj/GeneratedResources.targets
Automatic Dependency Resolution
Note
This tool only generates the workflow for automatically embedding assemblies into your project
Repo.Shared Includes the necessary components for automatically loading, decompressing, and resolving these internal assemblies
Check out REPO.Shared.AssemblyResolver.cs to see how the library metadata is used