Skip to content

Repository files navigation

Unity Mod AutoPackage Utility

This is a packaging utility for class libraries targeting .NET Framework 4.8 for deployment in the Unity environment

What does this do?

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"?>
<ProjectSdk="Microsoft.NET.Sdk"InitialTargets="GenerateNewTargets">
<!-- Import constants/defintions from the main project file. -->
<ImportProject="$(ProjectDir)../../REPO_Mods/main.targets" />
<!-- This target is used to generate the new .targets file for the project. -->
<TargetName="GenerateNewTargets"BeforeTargets="Restore">
<ExecCommand="$(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. -->
<TargetName="CustomPostBuild"AfterTargets="Build">
<ItemGroup>
<AllOutputFilesInclude="$(TargetDir)*.dll" />
</ItemGroup>
<CopySourceFiles="@(AllOutputFiles)"DestinationFolder="$(CopyToDirOnBuild)%(RecursiveDir)"
OverwriteReadOnlyFiles="true"SkipUnchangedFiles="true" />
</Target>
</Project>

An example of main.targets:

<?xml version="1.0" encoding="utf-8"?>
<Projectxmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<DotNetToolsDirectoryCondition="'$([MSBuild]::IsOSUnixLike())' == 'true'">$(HOME)/.dotnet/tools</DotNetToolsDirectory>
<DotNetToolsDirectoryCondition="'$([MSBuild]::IsOSUnixLike())' != 'true'">$(USERPROFILE)\.dotnet\tools</DotNetToolsDirectory>
<PathSeparatorCondition="'$(OS)' == 'Windows_NT'">\</PathSeparator>
<PathSeparatorCondition="'$(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 -->
<ExecCommand="rm -f &quot;$(DestExe)&quot;&amp;&amp; ln -sf &quot;$(SourceExe)&quot;&quot;$(DestExe)&quot;"
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

Workflow

Working in *.csproj

varworkingDir=Directory.GetCurrentDirectory();
varcsprojFiles=Directory.GetFiles(workingDir,"*.csproj",SearchOption.TopDirectoryOnly);
varcsprojFile=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
varprojectAssetsPath=Path.Combine(Path.GetDirectoryName(csprojFile)??"","obj","project.assets.json");
varprojectAssetsJson=JsonNode.Parse(File.ReadAllText(projectAssetsPath))!;
varjsonLibraries=projectAssetsJson["targets"]![".NETFramework,Version=v4.8"]!;

  • Locate all dependencies, including implicit (default behavior of assets.project.json)
    varlibName=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
    varalternatives=projectAssetsJson["libraries"]![key]!["files"]!.AsArray()
    .Select(i =>i?.AsValue().GetValue<string>())
    .Where(f =>fis 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 =>fis 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

    vardllMeta=lib+".dllmeta";
    // if (!File.Exists(dllMeta))
    {
    varasm=File.ReadAllBytes(lib);
    varlAsmName=GetAssemblyNameFromData(asm);
    varnameMeta=SerializeAssemblyName(lAsmName);
    File.WriteAllBytes(dllMeta,nameMeta);
    Console.WriteLine("Generated Dll MetaData "+dllMeta);
    }

  • Compress library in place

    usingvarinputStream=File.OpenRead(lib);
    usingvaroutputStream=File.Create(compressedLib);
    usingvargzipStream=newGZipStream(outputStream,CompressionMode.Compress);
    inputStream.CopyTo(gzipStream);
    compressedLibraries.Add(lib);

  • Generate .targets file

    varfName=(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

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages