Skip to content

Unreal engine support #2721

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<PackageVersion Include="Avalonia.Labs.Panels" Version="11.2.0" />
<PackageVersion Include="Avalonia.Skia" Version="11.2.4" />
<PackageVersion Include="AvaloniaEdit.TextMate" Version="11.1.0" />
<PackageVersion Include="CUE4Parse" Version="1.2.1" />
<PackageVersion Include="Jitbit.FastCache" Version="1.1.0" />
<PackageVersion Include="K4os.Compression.LZ4" Version="1.3.8" />
<PackageVersion Include="Bannerlord.ModuleManager" Version="6.0.246" />
Expand Down
15 changes: 5 additions & 10 deletions NexusMods.App.sln
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Networking.GitHub
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Networking.GitHub.Tests", "tests\NexusMods.Networking.GitHub.Tests\NexusMods.Networking.GitHub.Tests.csproj", "{5B201091-811F-4931-8FD2-0F64EFD5A6CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NexusMods.Games.UnrealEngine", "src\Games\NexusMods.Games.UnrealEngine\NexusMods.Games.UnrealEngine.csproj", "{D67088E5-9677-4B03-9D8C-70A47E11E803}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -780,14 +782,8 @@ Global
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2}.Release|Any CPU.Build.0 = Release|Any CPU
{16847A6C-EADC-45E3-8354-52C7DE356941}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{16847A6C-EADC-45E3-8354-52C7DE356941}.Debug|Any CPU.Build.0 = Debug|Any CPU
{16847A6C-EADC-45E3-8354-52C7DE356941}.Release|Any CPU.ActiveCfg = Release|Any CPU
{16847A6C-EADC-45E3-8354-52C7DE356941}.Release|Any CPU.Build.0 = Release|Any CPU
{5B201091-811F-4931-8FD2-0F64EFD5A6CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5B201091-811F-4931-8FD2-0F64EFD5A6CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5B201091-811F-4931-8FD2-0F64EFD5A6CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5B201091-811F-4931-8FD2-0F64EFD5A6CA}.Release|Any CPU.Build.0 = Release|Any CPU
{D67088E5-9677-4B03-9D8C-70A47E11E803}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D67088E5-9677-4B03-9D8C-70A47E11E803}.Debug|Any CPU.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -925,8 +921,7 @@ Global
{E2DB1DF4-9934-4119-BA90-196FDDB0904A} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{71D8CF63-A287-45AB-B251-676A54576C1D} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
{653CF228-7CD2-4C1B-8B4F-1332B66A33A2} = {0CB73565-1207-4A56-A79F-6A8E9BBD795C}
{16847A6C-EADC-45E3-8354-52C7DE356941} = {D7E9D8F5-8AC8-4ADA-B219-C549084AD84C}
{5B201091-811F-4931-8FD2-0F64EFD5A6CA} = {897C4198-884F-448A-B0B0-C2A6D971EAE0}
{D67088E5-9677-4B03-9D8C-70A47E11E803} = {70D38D24-79AE-4600-8E83-17F3C11BA81F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {9F9F8352-34DD-42C0-8564-EE9AF34A3501}
Expand Down
80 changes: 80 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/AUnrealEngineGame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Versions;
using Microsoft.Extensions.DependencyInjection;
using NexusMods.Abstractions.Diagnostics.Emitters;
using NexusMods.Abstractions.Diagnostics.Values;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.GameLocators.GameCapabilities;
using NexusMods.Abstractions.Games;
using NexusMods.Abstractions.IO;
using NexusMods.Abstractions.Library.Installers;
using NexusMods.Games.UnrealEngine.Emitters;
using NexusMods.Games.UnrealEngine.Installers;
using NexusMods.Games.UnrealEngine.Interfaces;
using NexusMods.Paths;

namespace NexusMods.Games.UnrealEngine;

public abstract class AUnrealEngineGame(IServiceProvider provider) : AGame(provider), IUnrealEngineGameAddon
{
private readonly IServiceProvider _serviceProvider = provider;

// The relative path to the folder containing the content and binaries directories.
// this is usually some sort of codename (e.g. Avowed => Alabama)
public virtual RelativePath RelPathGameName => new("");

// The default location for pak mods (not to be confused with blueprint mods) - usually the ~mods folder.
public virtual RelativePath? RelPathPakMods => RelPathGameName.Join(new RelativePath("Content/Paks/~mods"));

// The link which the user should follow in order to install UE4SS.
public virtual NamedLink UE4SSLink => Helpers.UE4SSLink;

// The game engine version - CUE4Parse will try to detect this automatically when the
// default version container is provided.
public virtual VersionContainer? VersionContainer => VersionContainer.DEFAULT_VERSION_CONTAINER;

// AES keys used to decrypt the game's pak files.
public virtual IEnumerable<FAesKey>? AESKeys => null;

// Certain games (e.g. Palworld) require different offsets for its variables.
public virtual IStreamFactory? MemberVariableTemplate => null;

// The architecture segment in the path to the game's binaries. Usually Win64 or WinGDK.
public virtual Func<GameLocatorResult, RelativePath>? ArchitectureSegmentRetriever
{
get
{
return (installation) => installation.Store == GameStore.XboxGamePass
? new RelativePath("WinGDK")
: new RelativePath("Win64");
}
}

// Retrieve the standard locations for the game.
// We currently offer support for:
// - Pak mods
// - Blueprint/Logic mods
// - Lua mods
// - Config files
// - Save files
protected override IReadOnlyDictionary<LocationId, AbsolutePath> GetLocations(IFileSystem fileSystem, GameLocatorResult installation)
=> Utils.StandardUnrealEngineLocations(fileSystem, installation, this);

// By default, all UE games support UE4SS, its LUA scripting system and pak mods.
public override ILibraryItemInstaller[] LibraryItemInstallers =>
[
_serviceProvider.GetRequiredService<ScriptingSystemInstaller>(),
_serviceProvider.GetRequiredService<ScriptingSystemLuaInstaller>(),
_serviceProvider.GetRequiredService<UnrealEnginePakModInstaller>(),
];

public override List<IModInstallDestination> GetInstallDestinations(IReadOnlyDictionary<LocationId, AbsolutePath> locations)
=> ModInstallDestinationHelpers.GetCommonLocations(locations);

public override IDiagnosticEmitter[] DiagnosticEmitters =>
[
_serviceProvider.GetRequiredService<AssetConflictDiagnosticEmitter>(),
_serviceProvider.GetRequiredService<ModOverwritesGameFilesEmitter>(),
_serviceProvider.GetRequiredService<MissingScriptingSystemEmitter>(),
];
}
50 changes: 50 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Avowed/AvowedGame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using CUE4Parse.Encryption.Aes;
using CUE4Parse.UE4.Versions;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Abstractions.GameLocators.Stores.Steam;
using NexusMods.Abstractions.Games;
using NexusMods.Abstractions.IO;
using NexusMods.Abstractions.IO.StreamFactories;
using NexusMods.Abstractions.Loadouts.Synchronizers;
using JetBrains.Annotations;
using NexusMods.Abstractions.NexusWebApi.Types;
using NexusMods.Abstractions.NexusWebApi.Types.V2;
using NexusMods.Abstractions.Diagnostics.Values;
using NexusMods.Games.UnrealEngine.Emitters;
using NexusMods.Paths;

namespace NexusMods.Games.UnrealEngine.Avowed;

[UsedImplicitly]
public class AvowedGame(IServiceProvider provider) : AUnrealEngineGame(provider), ISteamGame
{
public override RelativePath RelPathGameName => "Alabama";
public override NamedLink UE4SSLink => Helpers.UE4SSLink;
public override IEnumerable<FAesKey> AESKeys => new List<FAesKey>
{
new ("0xDFA62F3EE8304BBF7A6E153F2F88203F823C47BF0A690D3D4793FB3EFA624F3F"),
new ("0x8B5C24306F12833AA69B443ABD3786356F12833A6F12833A6F12833A00000040"),
};

public override VersionContainer? VersionContainer => new (EGame.GAME_UE5_3);

public static GameId GameIdStatic => GameId.From(7325);
public static GameDomain DomainStatic => GameDomain.From("avowed");

public override string Name => "Avowed";
public override GameId GameId => GameIdStatic;
public override SupportType SupportType => SupportType.Official;
public override GamePath GetPrimaryFile(GameStore store) => new(LocationId.Game, "Avowed.exe");
public IEnumerable<uint> SteamIds => [2457220u];

public override IStreamFactory Icon =>
new EmbededResourceStreamFactory<AvowedGame>("NexusMods.Games.UnrealEngine.Resources.Avowed.icon.png");

public override IStreamFactory GameImage =>
new EmbededResourceStreamFactory<AvowedGame>("NexusMods.Games.UnrealEngine.Resources.Avowed.icon.png");

protected override ILoadoutSynchronizer MakeSynchronizer(IServiceProvider provider)
{
return new UESynchronizer<AvowedSettings>(provider, GameIdStatic);
}
}
26 changes: 26 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Avowed/AvowedSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using NexusMods.Abstractions.Settings;

namespace NexusMods.Games.UnrealEngine.Avowed;

public class AvowedSettings : ISettings
{
/// <summary>
/// If true, the contents of the Content folder will not be backed up. If the game updates
/// the loadout may become invalid. If mods are installed into this folder via the app they
/// will still be backed up as needed
/// </summary>
public bool DoFullGameBackup { get; set; } = false;

public static ISettingsBuilder Configure(ISettingsBuilder settingsBuilder)
{
return settingsBuilder.AddToUI<AvowedSettings>(builder => builder
.AddPropertyToUI(x => x.DoFullGameBackup, propertyBuilder => propertyBuilder
.AddToSection(Sections.Experimental)
.WithDisplayName("Full game backup: Avowed")
.WithDescription("Backup all game folders, including the game asset folders. Please note that this will increase disk space usage.")
.UseBooleanContainer()
)
);

}
}
43 changes: 43 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Constants.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using NexusMods.Paths;
using NexusMods.Abstractions.GameLocators;
using System.Collections.Immutable;

namespace NexusMods.Games.UnrealEngine;

public static partial class Constants
{
public const string TxtExtValue = ".txt";
public const string JsonExtValue = ".json";

public static readonly LocationId GameMainLocationId = LocationId.From("UE_GameMain");
public static readonly LocationId PakModsLocationId = LocationId.From("UE_PakMods");
public static readonly LocationId LogicModsLocationId = LocationId.From("UE_LogicMods");
public static readonly LocationId LuaModsLocationId = LocationId.From("UE_LuaMods");
public static readonly LocationId BinariesLocationId = LocationId.From("UE_Binaries");
public static readonly LocationId ConfigLocationId = LocationId.From("UE_ConfigPath");

public static readonly GamePath LuaModsLoadOrderFileTxt = new(LuaModsLocationId, "Mods.txt");
public static readonly GamePath LuaModsLoadOrderFileJson = new(LuaModsLocationId, "Mods.json");

public static readonly GamePath EnginePath = new(GameMainLocationId, "Engine");
public static readonly GamePath ResourcesPath = new(GameMainLocationId, "Resources");

public static readonly Extension ExeExt = new(".exe");
public static readonly Extension DllExt = new(".dll");
public static readonly Extension SaveExt = new(".sav");
public static readonly Extension ConfigExt = new(".ini");
public static readonly Extension LuaExt = new(".lua");
public static readonly Extension TxtExt = new(TxtExtValue);
public static readonly Extension JsonExt = new(JsonExtValue);
public static readonly Extension PakExt = new(".pak");
public static readonly Extension SigExt = new(".sig");
public static readonly Extension UassetExt = new(".uasset");
public static readonly Extension UexpExt = new(".uexp");
public static readonly Extension UbulkExt = new(".ubulk");
public static readonly Extension UcasExt = new(".ucas");
public static readonly Extension UtocExt = new(".utoc");

public static readonly string ScriptingSystemFileName = "dwmapi.dll";

public static readonly ImmutableHashSet<Extension> ContentExts = ImmutableHashSet.Create(PakExt, UcasExt, UtocExt);
}
81 changes: 81 additions & 0 deletions src/Games/NexusMods.Games.UnrealEngine/Diagnostics.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using JetBrains.Annotations;
using NexusMods.Abstractions.Diagnostics;
using NexusMods.Abstractions.Diagnostics.References;
using NexusMods.Abstractions.Diagnostics.Values;
using NexusMods.Abstractions.GameLocators;
using NexusMods.Generators.Diagnostics;
using NexusMods.Paths;

namespace NexusMods.Games.UnrealEngine;

internal static partial class Diagnostics
{
private const string Source = "NexusMods.Games.UnrealEngine";

[DiagnosticTemplate]
[UsedImplicitly]
internal static IDiagnosticTemplate UnrealEngineAssetConflictTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, number: 1))
.WithTitle("Asset Conflict")
.WithSeverity(DiagnosticSeverity.Warning)
.WithSummary("Mods {ConflictingItems} are modifying the same asset '{ModifiedUEAsset}'")
.WithDetails("""
Check that mods aren't mutually exclusive, otherwise disable all but one of them.
""")
.WithMessageData(messageBuilder => messageBuilder
.AddValue<string>("ConflictingItems")
.AddValue<string>("ModifiedUEAsset")
)
.Finish();

[DiagnosticTemplate]
[UsedImplicitly]
internal static IDiagnosticTemplate ModOverwritesGameFilesTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, number: 2))
.WithTitle("Mod overwrites game files")
.WithSeverity(DiagnosticSeverity.Suggestion)
.WithSummary("Mod {GroupName} overwrites game files")
.WithDetails("""
Mod {GroupName} overwrites game files. This can cause compatibility issues and have other
unintended side-effects.

You can resolve this diagnostic by replacing {Group} with a different mod which doesn't
overwrite game files.
""")
.WithMessageData(messageBuilder => messageBuilder
.AddDataReference<LoadoutItemGroupReference>("Group")
.AddValue<string>("GroupName")
)
.Finish();

[DiagnosticTemplate]
[UsedImplicitly]
internal static IDiagnosticTemplate ScriptingSystemRequiredButNotInstalledTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, number: 3))
.WithTitle("UE4SS is not installed")
.WithSeverity(DiagnosticSeverity.Warning)
.WithSummary("UE4SS is required for {ModCount} Mod(s) but it's not installed")
.WithDetails("You can download the latest UE4SS version from {NexusModsUE4SSUri}.")
.WithMessageData(messageBuilder => messageBuilder
.AddValue<int>("ModCount")
.AddValue<NamedLink>("NexusModsUE4SSUri")
)
.Finish();

[DiagnosticTemplate]
[UsedImplicitly]
internal static IDiagnosticTemplate ScriptingSystemRequiredButDisabledTemplate = DiagnosticTemplateBuilder
.Start()
.WithId(new DiagnosticId(Source, number: 4))
.WithTitle("UE4SS is not enabled")
.WithSeverity(DiagnosticSeverity.Warning)
.WithSummary("UE4SS is required for {ModCount} Mod(s) but it's not enabled")
.WithoutDetails()
.WithMessageData(messageBuilder => messageBuilder
.AddValue<int>("ModCount")
)
.Finish();
}
Loading