Skip to content

Commit

Permalink
Use custom BuildCookRun command for Alpakit
Browse files Browse the repository at this point in the history
  • Loading branch information
mircearoata committed Nov 8, 2023
1 parent 80d1ff4 commit 9df5785
Show file tree
Hide file tree
Showing 12 changed files with 859 additions and 84 deletions.
401 changes: 401 additions & 0 deletions Mods/Alpakit/Source/Alpakit.Automation/.gitignore

Large diffs are not rendered by default.

53 changes: 53 additions & 0 deletions Mods/Alpakit/Source/Alpakit.Automation/Alpakit.Automation.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(EngineDir)\Source\Programs\Shared\UnrealEngine.csproj.props" />

<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>

<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A777BF44-017F-4E69-916A-A62D06D63556}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Alpakit.Automation</RootNamespace>
<AssemblyName>Alpakit.Automation</AssemblyName>
<TargetFramework>net6.0</TargetFramework>
<FileAlignment>512</FileAlignment>
</PropertyGroup>

<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>..\..\Binaries\DotNET\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>..\..\Binaries\DotNET\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Development|AnyCPU' ">
<OutputPath>..\..\Binaries\DotNET\</OutputPath>
<DebugSymbols>true</DebugSymbols>
<DebugType>pdbonly</DebugType>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(EngineDir)\Source\Programs\AutomationTool\AutomationUtils\AutomationUtils.Automation.csproj" />
<ProjectReference Include="$(EngineDir)\Source\Programs\AutomationTool\Scripts\AutomationScripts.Automation.csproj" />
<ProjectReference Include="$(EngineDir)\Source\Programs\Shared\EpicGames.Build\EpicGames.Build.csproj" />
<ProjectReference Include="$(EngineDir)\Source\Programs\Shared\EpicGames.Core\EpicGames.Core.csproj" />
<ProjectReference Include="$(EngineDir)\Source\Programs\UnrealBuildTool\UnrealBuildTool.csproj" />
</ItemGroup>
</Project>
53 changes: 53 additions & 0 deletions Mods/Alpakit/Source/Alpakit.Automation/LaunchGame.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Diagnostics;
using AutomationTool;

namespace Alpakit.Automation;

public class LaunchGame
{
public enum LaunchType
{
Steam,
SteamDS,
EpicEA,
EpicExp,
EpicDS,
EpicDSExp,
Custom
}

private static string GetGameLaunchUrl(LaunchType LaunchType)
{
switch (LaunchType)
{
case LaunchType.Steam:
return "steam://rungameid/526870";
case LaunchType.SteamDS:
return "steam://rungameid/1690800";
case LaunchType.EpicEA:
return "com.epicgames.launcher://apps/CrabEA?action=launch&silent=true";
case LaunchType.EpicExp:
return "com.epicgames.launcher://apps/CrabTest?action=launch&silent=true";
case LaunchType.EpicDS:
return "com.epicgames.launcher://apps/CrabDedicatedServer?action=launch&silent=true";
case LaunchType.EpicDSExp:
// No more nice names
return "com.epicgames.launcher://apps/c509233193024c5f8124467d3aa36199?action=launch&silent=true";
default:
throw new AutomationException("Invalid Launch Type {0}", LaunchType);
}
}

public static void Launch(LaunchType Type, string? CustomLaunch)
{
if (Type == LaunchType.Custom)
{
if (CustomLaunch == null)
throw new AutomationException("Custom Launch Type requested, but no program to launch was specified");
Process.Start(CustomLaunch);
return;
}

Process.Start(new ProcessStartInfo(GetGameLaunchUrl(Type)) { UseShellExecute = true });
}
}
170 changes: 170 additions & 0 deletions Mods/Alpakit/Source/Alpakit.Automation/PackagePlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Linq;
using AutomationTool;
using EpicGames.Core;
using UnrealBuildTool;
using AutomationScripts;

namespace Alpakit.Automation;

public class PackagePlugin : BuildCookRun
{
public override void ExecuteBuild()
{
var projectParams = SetupParams();
projectParams.PreModifyDeploymentContextCallback = (ProjectParams, SC) =>
{
RemapCookedPluginsContentPaths(ProjectParams, SC);
};
projectParams.ModifyDeploymentContextCallback = (ProjectParams, SC) =>
{
ModifyModules(ProjectParams, SC);
};

DoBuildCookRun(projectParams);

var deploymentContexts = new List<DeploymentContext>();
if (!projectParams.NoClient)
deploymentContexts.AddRange(Project.CreateDeploymentContext(projectParams, false, false));
if (projectParams.DedicatedServer)
deploymentContexts.AddRange(Project.CreateDeploymentContext(projectParams, true, false));

foreach (var SC in deploymentContexts)
{
ArchiveStagedPlugin(projectParams, SC);
}

foreach (var SC in deploymentContexts)
{
var copyToGameDirectory = ParseOptionalDirectoryReferenceParam($"CopyToGameDirectory_{SC.FinalCookPlatform}");
var launchGameType = ParseOptionalEnumParam<LaunchGame.LaunchType>($"LaunchGame_{SC.FinalCookPlatform}");
var customLaunch = ParseOptionalStringParam($"CustomLaunch_{SC.FinalCookPlatform}");

if (copyToGameDirectory != null)
DeployStagedPlugin(projectParams, SC, copyToGameDirectory);

if (launchGameType != null)
LaunchGame.Launch(launchGameType.Value, customLaunch);
}
}

protected ProjectParams SetupParams()
{
LogInformation("Setting up ProjectParams for {0}", ProjectPath);

var Params = new ProjectParams
(
Command: this,
RawProjectPath: ProjectPath,

// Alpakit shared configuration
Cook: true,
AdditionalCookerOptions: "-AllowUncookedAssetReferences",
DLCIncludeEngineContent: false,
Compressed: true,
Pak: true,
Stage: true,

// TODO @SML: I would like to pass an empty based on release version, but the cooker checks against it
BasedOnReleaseVersion: "SMLNonExistentBasedOnReleaseVersion"
);

return Params;
}

private static void DeployStagedPlugin(ProjectParams ProjectParams, DeploymentContext SC, DirectoryReference GameDir)
{
// We only want to archive the staged files of the plugin, not the entire stage directory
var stagedPluginDirectory = Project.ApplyDirectoryRemap(SC, SC.GetStagedFileLocation(ProjectParams.DLCFile));
var fullStagedPluginDirectory = DirectoryReference.Combine(SC.StageDirectory, stagedPluginDirectory.Directory.Name);

var projectName = ProjectParams.RawProjectPath.GetFileNameWithoutAnyExtensions();

// Mods go into <GameDir>/<ProjectName>/Mods/<PluginName>
var gameModsDir = DirectoryReference.Combine(GameDir, projectName, "Mods");
var pluginName = ProjectParams.DLCFile.GetFileNameWithoutAnyExtensions();
var modDir = DirectoryReference.Combine(gameModsDir, pluginName);

if (DirectoryReference.Exists(modDir))
DirectoryReference.Delete(modDir, true);

CopyDirectory_NoExceptions(fullStagedPluginDirectory, modDir);
}

private static void ArchiveStagedPlugin(ProjectParams ProjectParams, DeploymentContext SC)
{
// Plugins will be archived under <Project>/Saved/ArchivedPlugins/<DLCName>
var baseArchiveDirectory = CombinePaths(SC.ProjectRoot.FullName, "Saved", "ArchivedPlugins");
var archiveDirectory = CombinePaths(baseArchiveDirectory, ProjectParams.DLCFile.GetFileNameWithoutAnyExtensions());

CreateDirectory(archiveDirectory);

var dlcName = ProjectParams.DLCFile.GetFileNameWithoutAnyExtensions();

var zipFileName = $"{dlcName}-{SC.FinalCookPlatform}.zip";

var zipFilePath = CombinePaths(archiveDirectory, zipFileName);

if (FileExists(zipFilePath))
DeleteFile(zipFilePath);

// We only want to archive the staged files of the plugin, not the entire stage directory
var stagedPluginDirectory = Project.ApplyDirectoryRemap(SC, SC.GetStagedFileLocation(ProjectParams.DLCFile));
var fullStagedPluginDirectory = DirectoryReference.Combine(SC.StageDirectory, stagedPluginDirectory.Directory.Name);

ZipFile.CreateFromDirectory(fullStagedPluginDirectory.FullName, zipFilePath);
}

private static string GetPluginPathRelativeToStageRoot(ProjectParams ProjectParams)
{
// All DLC paths are remapped to projectName/Mods/DLCName during RemapCookedPluginsContentPaths, regardless of nesting
// so the relative stage path is projectName/Mods/DLCName
var projectName = ProjectParams.RawProjectPath.GetFileNameWithoutAnyExtensions();
var dlcName = ProjectParams.DLCFile.GetFileNameWithoutAnyExtensions();
return CombinePaths(projectName, "Mods", dlcName);
}

private static void RemapCookedPluginsContentPaths(ProjectParams ProjectParams, DeploymentContext SC) {
//We need to make sure content paths will be relative to ../../../ProjectRoot/Mods/DLCFilename/Content,
//because that's what will be used in runtime as a content path for /DLCFilename/ mount point.
//But both project and engine plugins are actually cooked by different paths:
//Project plugins expect to be mounted under ../../../ProjectRoot/Plugins/DLCFilename/Content,
//and engine plugins expect to be mounted under ../../../Engine/Plugins/DLCFilename/Content
//Since changing runtime content path is pretty complicated and impractical,
//we remap cooked filenames to match runtime expectations. Since cooked assets only reference other assets
//using mount point-relative paths, it doesn't need any changes inside of the cooked assets
//deploymentContext.RemapDirectories.Add(Tuple.Create());

SC.RemapDirectories.Add(Tuple.Create(
SC.GetStagedFileLocation(ProjectParams.DLCFile).Directory,
new StagedDirectoryReference(GetPluginPathRelativeToStageRoot(ProjectParams))
));
}

private void ModifyModules(ProjectParams ProjectParams, DeploymentContext SC)
{
foreach (var target in SC.StageTargets)
{
// Handle .modules files explicitly by nulling out their BuildId since DLC cooks for mods are supposed to have relaxed compatibility requirements
var manifests = target.Receipt.BuildProducts.Where((Product => Product.Type == BuildProductType.RequiredResource && Product.Path.GetExtension() == ".modules" && Product.Path.IsUnderDirectory(ProjectParams.DLCFile.Directory)));
foreach (var manifest in manifests)
{
if (!ModuleManifest.TryRead(manifest.Path, out var moduleManifestFile))
throw new AutomationException(
$"Unable to read .modules build file at {manifest.Path} for DLC staging");

// Null out BuildId for Mod DLC cooks because they will be loaded by different game builds potentially
// The game specifically allows SML as BuildId to be loaded by any game build
moduleManifestFile.BuildId = "SML";

var intermediateModuleFilePath = FileReference.Combine(ProjectParams.DLCFile.Directory, "Intermediate",
"Staging", SC.FinalCookPlatform, manifest.Path.MakeRelativeTo(ProjectParams.DLCFile.Directory));
var outputFilePath = SC.GetStagedFileLocation(manifest.Path);
moduleManifestFile.Write(intermediateModuleFilePath);
SC.StageFile(StagedFileType.NonUFS, intermediateModuleFilePath, outputFilePath);
}
}
}
}
1 change: 1 addition & 0 deletions Mods/Alpakit/Source/Alpakit/Alpakit.Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public Alpakit(ReadOnlyTargetRules Target) : base(Target)

PublicDependencyModuleNames.AddRange(new[] {
"Core",
"DesktopPlatform",
});

PrivateDependencyModuleNames.AddRange(new[] {
Expand Down
36 changes: 16 additions & 20 deletions Mods/Alpakit/Source/Alpakit/Private/AlpakitInstance.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ FOnAlpakitInstanceSpawned FAlpakitInstance::OnNewInstanceSpawned;
FCriticalSection FAlpakitInstance::GlobalListCriticalSection;
TArray<TSharedPtr<FAlpakitInstance>> FAlpakitInstance::GlobalList;

FAlpakitInstance::FAlpakitInstance( const FString& InPluginName, ILauncherProfileRef InLauncherProfile ) : PluginName( InPluginName ), LauncherProfile( InLauncherProfile )
FAlpakitInstance::FAlpakitInstance( const FString& InPluginName, TSharedRef<FAlpakitProfile> InProfile ) : PluginName( InPluginName ), Profile( InProfile )
{
}

Expand All @@ -28,20 +28,14 @@ bool FAlpakitInstance::Start()
check( IsInGameThread() );
check( InstanceState == EAlpakitInstanceState::None || InstanceState == EAlpakitInstanceState::Completed );

ILauncherServicesModule& ProjectLauncherServicesModule = FModuleManager::LoadModuleChecked<ILauncherServicesModule>("LauncherServices");
ITargetDeviceServicesModule& TargetDeviceServicesModule = FModuleManager::LoadModuleChecked<ITargetDeviceServicesModule>("TargetDeviceServices");

static ILauncherRef Launcher = ProjectLauncherServicesModule.CreateLauncher();
LauncherWorker = Launcher->Launch( TargetDeviceServicesModule.GetDeviceProxyManager(), LauncherProfile.ToSharedRef() );
UATProcess = MakeShareable(new FSerializedUATProcess(Profile->MakeUATCommandLine()));

if ( !LauncherWorker.IsValid() )
{
return false;
}
LauncherWorker->OnOutputReceived().AddSP( this, &FAlpakitInstance::OnWorkerMessageReceived );
LauncherWorker->OnCanceled().AddSP( this, &FAlpakitInstance::OnWorkerCancelled );
LauncherWorker->OnCompleted().AddSP( this, &FAlpakitInstance::OnWorkerCompleted );
UATProcess->OnOutput().BindSP( this, &FAlpakitInstance::OnWorkerMessageReceived );
UATProcess->OnCanceled().BindSP( this, &FAlpakitInstance::OnWorkerCancelled );
UATProcess->OnCompleted().BindSP( this, &FAlpakitInstance::OnWorkerCompleted );

UATProcess->Launch();

InstanceState = EAlpakitInstanceState::Running;
Result = EAlpakitInstanceResult::Undetermined;
MessageList.Empty();
Expand All @@ -58,13 +52,13 @@ void FAlpakitInstance::Cancel() const
check( IsInGameThread() || IsInSlateThread() );
check( InstanceState == EAlpakitInstanceState::Running );

if ( LauncherWorker.IsValid() )
if ( UATProcess.IsValid() )
{
LauncherWorker->Cancel();
UATProcess->Cancel();
}
}

void FAlpakitInstance::OnWorkerMessageReceived( const FString& Message )
void FAlpakitInstance::OnWorkerMessageReceived( FString Message )
{
const TSharedPtr<FAlpakitInstance> SharedSelf = AsShared();
AsyncTask( ENamedThreads::GameThread, [SharedSelf, Message]()
Expand All @@ -73,7 +67,7 @@ void FAlpakitInstance::OnWorkerMessageReceived( const FString& Message )
} );
}

void FAlpakitInstance::OnWorkerCancelled( double Time )
void FAlpakitInstance::OnWorkerCancelled()
{
const TSharedPtr<FAlpakitInstance> SharedSelf = AsShared();
AsyncTask( ENamedThreads::GameThread, [SharedSelf]()
Expand All @@ -82,9 +76,11 @@ void FAlpakitInstance::OnWorkerCancelled( double Time )
} );
}

void FAlpakitInstance::OnWorkerCompleted( bool bSuccess, double Duration, int32 ExitCode )
void FAlpakitInstance::OnWorkerCompleted( int32 ExitCode )
{
const TSharedPtr<FAlpakitInstance> SharedSelf = AsShared();
bool bSuccess = ExitCode == 0;
double Duration = UATProcess->GetDuration().GetTotalSeconds();
AsyncTask( ENamedThreads::GameThread, [SharedSelf, bSuccess, Duration, ExitCode]()
{
SharedSelf->OnWorkerCompleted_GameThread(bSuccess, Duration, ExitCode);
Expand Down Expand Up @@ -167,7 +163,7 @@ void FAlpakitInstance::HandleCancelButtonClicked()
{
if ( SharedSelf->InstanceState == EAlpakitInstanceState::Running )
{
SharedSelf->LauncherWorker->Cancel();
SharedSelf->UATProcess->Cancel();
}
} );
}
Expand Down Expand Up @@ -303,4 +299,4 @@ void FAlpakitInstance::RemoveFromGlobalList()
GlobalList.Remove( AsShared() );
}

#undef LOCTEXT_NAMESPACE
#undef LOCTEXT_NAMESPACE
Loading

0 comments on commit 9df5785

Please sign in to comment.