Skip to content

TGO-Inc/UnityModPackager

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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"?>
<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 &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

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

    UnityModPackager/Program.cs

    Lines 118 to 127 in b231e56

    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

    UnityModPackager/Program.cs

    Lines 136 to 140 in b231e56

    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

    UnityModPackager/Program.cs

    Lines 156 to 158 in 3a6a691

    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

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages