diff --git a/Mods/Alpakit/Source/Alpakit.Automation/.gitignore b/Mods/Alpakit/Source/Alpakit.Automation/.gitignore
new file mode 100644
index 0000000000..7b19f74fde
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit.Automation/.gitignore
@@ -0,0 +1,401 @@
+*.props
+Scripts/
+
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.log
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+*.ncb
+*.aps
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
\ No newline at end of file
diff --git a/Mods/Alpakit/Source/Alpakit.Automation/Alpakit.Automation.csproj b/Mods/Alpakit/Source/Alpakit.Automation/Alpakit.Automation.csproj
new file mode 100644
index 0000000000..67ff7b91f7
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit.Automation/Alpakit.Automation.csproj
@@ -0,0 +1,53 @@
+
+
+
+
+
+ enable
+
+
+
+ Debug
+ AnyCPU
+ {A777BF44-017F-4E69-916A-A62D06D63556}
+ Library
+ Properties
+ Alpakit.Automation
+ Alpakit.Automation
+ net6.0
+ 512
+
+
+
+ AnyCPU
+ true
+ full
+ false
+ ..\..\Binaries\DotNET\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ ..\..\Binaries\DotNET\
+ TRACE
+ prompt
+ 4
+
+
+ ..\..\Binaries\DotNET\
+ true
+ pdbonly
+
+
+
+
+
+
+
+
+
+
diff --git a/Mods/Alpakit/Source/Alpakit.Automation/LaunchGame.cs b/Mods/Alpakit/Source/Alpakit.Automation/LaunchGame.cs
new file mode 100644
index 0000000000..60e33f7314
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit.Automation/LaunchGame.cs
@@ -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 });
+ }
+}
diff --git a/Mods/Alpakit/Source/Alpakit.Automation/PackagePlugin.cs b/Mods/Alpakit/Source/Alpakit.Automation/PackagePlugin.cs
new file mode 100644
index 0000000000..cfa9aa7dc9
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit.Automation/PackagePlugin.cs
@@ -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();
+ 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_{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 //Mods/
+ 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 /Saved/ArchivedPlugins/
+ 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);
+ }
+ }
+ }
+}
diff --git a/Mods/Alpakit/Source/Alpakit/Alpakit.Build.cs b/Mods/Alpakit/Source/Alpakit/Alpakit.Build.cs
index 5aed73b298..9c807e9c80 100644
--- a/Mods/Alpakit/Source/Alpakit/Alpakit.Build.cs
+++ b/Mods/Alpakit/Source/Alpakit/Alpakit.Build.cs
@@ -11,6 +11,7 @@ public Alpakit(ReadOnlyTargetRules Target) : base(Target)
PublicDependencyModuleNames.AddRange(new[] {
"Core",
+ "DesktopPlatform",
});
PrivateDependencyModuleNames.AddRange(new[] {
diff --git a/Mods/Alpakit/Source/Alpakit/Private/AlpakitInstance.cpp b/Mods/Alpakit/Source/Alpakit/Private/AlpakitInstance.cpp
index 0b5747947f..ed9c6d8f0d 100644
--- a/Mods/Alpakit/Source/Alpakit/Private/AlpakitInstance.cpp
+++ b/Mods/Alpakit/Source/Alpakit/Private/AlpakitInstance.cpp
@@ -19,7 +19,7 @@ FOnAlpakitInstanceSpawned FAlpakitInstance::OnNewInstanceSpawned;
FCriticalSection FAlpakitInstance::GlobalListCriticalSection;
TArray> FAlpakitInstance::GlobalList;
-FAlpakitInstance::FAlpakitInstance( const FString& InPluginName, ILauncherProfileRef InLauncherProfile ) : PluginName( InPluginName ), LauncherProfile( InLauncherProfile )
+FAlpakitInstance::FAlpakitInstance( const FString& InPluginName, TSharedRef InProfile ) : PluginName( InPluginName ), Profile( InProfile )
{
}
@@ -28,20 +28,14 @@ bool FAlpakitInstance::Start()
check( IsInGameThread() );
check( InstanceState == EAlpakitInstanceState::None || InstanceState == EAlpakitInstanceState::Completed );
- ILauncherServicesModule& ProjectLauncherServicesModule = FModuleManager::LoadModuleChecked("LauncherServices");
- ITargetDeviceServicesModule& TargetDeviceServicesModule = FModuleManager::LoadModuleChecked("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();
@@ -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 SharedSelf = AsShared();
AsyncTask( ENamedThreads::GameThread, [SharedSelf, Message]()
@@ -73,7 +67,7 @@ void FAlpakitInstance::OnWorkerMessageReceived( const FString& Message )
} );
}
-void FAlpakitInstance::OnWorkerCancelled( double Time )
+void FAlpakitInstance::OnWorkerCancelled()
{
const TSharedPtr SharedSelf = AsShared();
AsyncTask( ENamedThreads::GameThread, [SharedSelf]()
@@ -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 SharedSelf = AsShared();
+ bool bSuccess = ExitCode == 0;
+ double Duration = UATProcess->GetDuration().GetTotalSeconds();
AsyncTask( ENamedThreads::GameThread, [SharedSelf, bSuccess, Duration, ExitCode]()
{
SharedSelf->OnWorkerCompleted_GameThread(bSuccess, Duration, ExitCode);
@@ -167,7 +163,7 @@ void FAlpakitInstance::HandleCancelButtonClicked()
{
if ( SharedSelf->InstanceState == EAlpakitInstanceState::Running )
{
- SharedSelf->LauncherWorker->Cancel();
+ SharedSelf->UATProcess->Cancel();
}
} );
}
@@ -303,4 +299,4 @@ void FAlpakitInstance::RemoveFromGlobalList()
GlobalList.Remove( AsShared() );
}
-#undef LOCTEXT_NAMESPACE
\ No newline at end of file
+#undef LOCTEXT_NAMESPACE
diff --git a/Mods/Alpakit/Source/Alpakit/Private/AlpakitModEntry.cpp b/Mods/Alpakit/Source/Alpakit/Private/AlpakitModEntry.cpp
index a991332528..656ab9aa3c 100644
--- a/Mods/Alpakit/Source/Alpakit/Private/AlpakitModEntry.cpp
+++ b/Mods/Alpakit/Source/Alpakit/Private/AlpakitModEntry.cpp
@@ -166,19 +166,6 @@ FReply SAlpakitModEntry::OnEditModFinished(UModMetadataObject* MetadataObject)
return FReply::Handled();
}
-FString GetArgumentForLaunchType(EAlpakitStartGameType LaunchMode) {
- switch (LaunchMode) {
- case EAlpakitStartGameType::STEAM:
- return TEXT("-Steam");
- case EAlpakitStartGameType::EPIC_EARLY_ACCESS:
- return TEXT("-EpicEA");
- case EAlpakitStartGameType::EPIC_EXPERIMENTAL:
- return TEXT("-EpicExp");
- default:
- return TEXT("");
- }
-}
-
FText GetCurrentPlatformName() {
#if PLATFORM_WINDOWS
return LOCTEXT("PlatformName_Windows", "Windows");
@@ -204,49 +191,24 @@ void SAlpakitModEntry::PackageMod(const TArray>& Ne
ILauncherServicesModule& ProjectLauncherServicesModule = FModuleManager::LoadModuleChecked("LauncherServices");
// Create a temporary profile for packaging the mod
- const ILauncherProfileRef ProfileRef = ProjectLauncherServicesModule.GetProfileManager()->CreateUnsavedProfile( FString::Printf( TEXT("Alpakit [%s]"), *PluginName ) );
- ProfileRef->SetDescription( FString::Printf( TEXT("Packaging Mod %s"), *PluginName ) );
+ const TSharedRef ProfileRef = MakeShared( PluginName );
- if (Mod->GetDescriptor().Modules.Num() > 0) {
- ProfileRef->SetBuildMode( ELauncherProfileBuildModes::Auto );
- } else {
- ProfileRef->SetBuildMode( ELauncherProfileBuildModes::DoNotBuild );
- }
- ProfileRef->SetCookMode( ELauncherProfileCookModes::ByTheBook );
- ProfileRef->SetLaunchMode( ELauncherProfileLaunchModes::DoNotLaunch );
+ ProfileRef->bBuild = Mod->GetDescriptor().Modules.Num() > 0;
- ProfileRef->SetProjectPath( ProjectPath );
- ProfileRef->SetCreateDLC( true );
- ProfileRef->SetDLCName( PluginName );
- ProfileRef->SetDLCIncludeEngineContent( false );
+ ProfileRef->BuildConfiguration = Settings->GetBuildConfiguration();
+ // ProfileRef->CookedPlatforms = Settings->CookPlatforms;
+ ProfileRef->CookedPlatforms = {TEXT("Windows")}; // Only Windows is allowed for now
- // TODO @SML: I would like to pass an empty based on release version, but the cooker checks against it
- ProfileRef->SetBasedOnReleaseVersionName( TEXT("SMLNonExistentBasedOnReleaseVersion") );
-
- ProfileRef->SetUnversionedCooking( true );
- ProfileRef->SetCookOptions( TEXT("-AllowUncookedAssetReferences") );
-
- ProfileRef->SetDeployWithUnrealPak( true );
- ProfileRef->SetCompressed( true );
-
- ProfileRef->SetPackagingMode( ELauncherProfilePackagingModes::Locally );
- ProfileRef->SetDeploymentMode( ELauncherProfileDeploymentModes::DoNotDeploy );
+ if (Settings->bCopyModsToGame) {
+ FAlpakitProfileGameInfo& GameInfo = ProfileRef->PlatformGameInfo.FindOrAdd(TEXT("Windows"));
+ GameInfo.bCopyToGame = true;
+ GameInfo.GamePath = GamePath;
+ }
- ProfileRef->SetBuildConfiguration( Settings->GetBuildConfiguration() );
- for ( const FString& PlatformName : Settings->CookPlatforms )
- {
- ProfileRef->AddCookedPlatform( PlatformName );
+ if (Settings->LaunchGameAfterPacking != EAlpakitStartGameType::NONE && NextEntries.Num() == 0) {
+ FAlpakitProfileGameInfo& GameInfo = ProfileRef->PlatformGameInfo.FindOrAdd(TEXT("Windows"));
+ GameInfo.StartGameType = Settings->LaunchGameAfterPacking;
}
-
- /*FString AdditionalUATArguments;
- if (Settings->bCopyModsToGame) {
- AdditionalUATArguments.Append(TEXT("-CopyToGameDir "));
- }
- if (Settings->LaunchGameAfterPacking != EAlpakitStartGameType::NONE && NextEntries.Num() == 0) {
- AdditionalUATArguments.Append(TEXT("-LaunchGame "));
- AdditionalUATArguments.Append(GetArgumentForLaunchType(Settings->LaunchGameAfterPacking)).Append(TEXT(" "));
- }
- const FString LaunchGameArgument = GetArgumentForLaunchType(Settings->LaunchGameAfterPacking);*/
UE_LOG(LogAlpakit, Display, TEXT("Packaging plugin \"%s\". %d remaining"), *PluginName, NextEntries.Num());
diff --git a/Mods/Alpakit/Source/Alpakit/Private/AlpakitProfile.cpp b/Mods/Alpakit/Source/Alpakit/Private/AlpakitProfile.cpp
new file mode 100644
index 0000000000..6126d2cd7a
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit/Private/AlpakitProfile.cpp
@@ -0,0 +1,93 @@
+#include "AlpakitProfile.h"
+#include "PlatformInfo.h"
+
+FString FAlpakitProfile::MakeUATPlatformArgs() {
+ // Code below replicates the minimum configuration required from FLauncherWorker::CreateUATCommand
+ FString CommandLine;
+
+ FString ServerCommand = TEXT("");
+ FString ServerPlatforms = TEXT("");
+ FString Platforms = TEXT("");
+ FString PlatformCommand = TEXT("");
+ FString OptionalParams = TEXT("");
+ TSet OptionalTargetPlatforms;
+
+ for (const FString& PlatformName : CookedPlatforms)
+ {
+ // Platform info for the given platform
+ const PlatformInfo::FTargetPlatformInfo* PlatformInfo = PlatformInfo::FindPlatformInfo(FName(*PlatformName));
+
+ if (ensure(PlatformInfo))
+ {
+ // separate out Server platforms
+ FString& PlatformString = (PlatformInfo->PlatformType == EBuildTargetType::Server) ? ServerPlatforms : Platforms;
+
+ PlatformString += TEXT("+");
+ PlatformString += PlatformInfo->DataDrivenPlatformInfo->UBTPlatformString;
+
+ // Append any extra UAT flags specified for this platform flavor
+ if (!PlatformInfo->UATCommandLine.IsEmpty())
+ {
+ FString OptionalUATCommandLine = PlatformInfo->UATCommandLine;
+
+ OptionalParams += TEXT(" ");
+ OptionalParams += OptionalUATCommandLine;
+ }
+ }
+ }
+
+ // If both Client and Server are desired to be built avoid Server causing clients to not be built PlatformInfo wise
+ if (OptionalParams.Contains(TEXT("-client")) && OptionalParams.Contains(TEXT("-noclient")))
+ {
+ OptionalParams = OptionalParams.Replace(TEXT("-noclient"), TEXT(""));
+ }
+
+ if (ServerPlatforms.Len() > 0)
+ {
+ ServerCommand = TEXT(" -server -serverplatform=") + ServerPlatforms.RightChop(1);
+ if (Platforms.Len() == 0)
+ {
+ OptionalParams += TEXT(" -noclient");
+ }
+ }
+ if (Platforms.Len() > 0)
+ {
+ PlatformCommand = TEXT(" -platform=") + Platforms.RightChop(1);
+ }
+
+ CommandLine += PlatformCommand;
+ CommandLine += ServerCommand;
+ CommandLine += OptionalParams;
+
+ return CommandLine;
+}
+
+FString FAlpakitProfile::MakeUATCommandLine() {
+ const FString ProjectPath = FPaths::IsProjectFilePathSet()
+ ? FPaths::ConvertRelativePathToFull(FPaths::GetProjectFilePath())
+ : FPaths::RootDir() / FApp::GetProjectName() / FApp::GetProjectName() + TEXT(".uproject");
+
+ FString CommandLine = FString::Printf(TEXT("-ScriptsForProject=\"%s\" PackagePlugin -project=\"%s\" -clientconfig=%s -serverconfig=%s -utf8output -DLCName=%s"),
+ *ProjectPath,
+ *ProjectPath,
+ LexToString(BuildConfiguration),
+ LexToString(BuildConfiguration),
+ *PluginName);
+
+ if (bBuild) {
+ CommandLine += TEXT(" -build");
+ }
+
+ CommandLine += MakeUATPlatformArgs();
+
+ for (auto [Platform, GameInfo] : PlatformGameInfo) {
+ if (GameInfo.bCopyToGame) {
+ CommandLine += FString::Printf(TEXT(" -CopyToGameDirectory_%s=\"%s\""), *Platform, *GameInfo.GamePath);
+ }
+ if (GameInfo.StartGameType != EAlpakitStartGameType::NONE) {
+ CommandLine += FString::Printf(TEXT(" -LaunchGame_%s=%s"), *Platform, LexToString(GameInfo.StartGameType));
+ }
+ }
+
+ return CommandLine;
+}
diff --git a/Mods/Alpakit/Source/Alpakit/Private/AlpakitSettings.cpp b/Mods/Alpakit/Source/Alpakit/Private/AlpakitSettings.cpp
index cbc5292d7f..25fd274536 100644
--- a/Mods/Alpakit/Source/Alpakit/Private/AlpakitSettings.cpp
+++ b/Mods/Alpakit/Source/Alpakit/Private/AlpakitSettings.cpp
@@ -1,5 +1,18 @@
#include "AlpakitSettings.h"
+const TCHAR* LexToString(EAlpakitStartGameType StartGameType) {
+ switch (StartGameType) {
+ case EAlpakitStartGameType::STEAM:
+ return TEXT("Steam");
+ case EAlpakitStartGameType::EPIC_EARLY_ACCESS:
+ return TEXT("EpicEA");
+ case EAlpakitStartGameType::EPIC_EXPERIMENTAL:
+ return TEXT("EpicExp");
+ default:
+ return TEXT("");
+ }
+}
+
UAlpakitSettings* UAlpakitSettings::Get()
{
return GetMutableDefault();
@@ -14,10 +27,10 @@ TArray UAlpakitSettings::GetAllowedBuildConfigurations() const
{
return TArray
{
- LexToString(EBuildConfiguration::Debug ),
- LexToString(EBuildConfiguration::DebugGame ),
- LexToString(EBuildConfiguration::Development),
- LexToString(EBuildConfiguration::Test ),
+ // LexToString(EBuildConfiguration::Debug ),
+ // LexToString(EBuildConfiguration::DebugGame ),
+ // LexToString(EBuildConfiguration::Development),
+ // LexToString(EBuildConfiguration::Test ),
LexToString(EBuildConfiguration::Shipping ),
};
}
@@ -25,6 +38,6 @@ TArray UAlpakitSettings::GetAllowedBuildConfigurations() const
EBuildConfiguration UAlpakitSettings::GetBuildConfiguration() const
{
EBuildConfiguration ResultBuildConfiguration = EBuildConfiguration::Shipping;
- LexTryParseString( ResultBuildConfiguration, *BuildConfiguration );
+ // LexTryParseString( ResultBuildConfiguration, *BuildConfiguration );
return ResultBuildConfiguration;
}
diff --git a/Mods/Alpakit/Source/Alpakit/Public/AlpakitInstance.h b/Mods/Alpakit/Source/Alpakit/Public/AlpakitInstance.h
index 023f22f138..4c46aa129b 100644
--- a/Mods/Alpakit/Source/Alpakit/Public/AlpakitInstance.h
+++ b/Mods/Alpakit/Source/Alpakit/Public/AlpakitInstance.h
@@ -1,7 +1,9 @@
#pragma once
#include "CoreMinimal.h"
-#include "LauncherServices/Public/ILauncherWorker.h"
+#include "AlpakitProfile.h"
+#include "AlpakitSettings.h"
+#include "Misc/MonitoredProcess.h"
#include "Widgets/Notifications/SNotificationList.h"
enum class EAlpakitInstanceState
@@ -35,15 +37,15 @@ class ALPAKIT_API FAlpakitInstance : public TSharedFromThis
EAlpakitInstanceState InstanceState{EAlpakitInstanceState::None};
EAlpakitInstanceResult Result{EAlpakitInstanceResult::Undetermined};
FString PluginName;
- ILauncherProfilePtr LauncherProfile;
+ TSharedRef Profile;
TSharedPtr NotificationItem;
- ILauncherWorkerPtr LauncherWorker;
+ TSharedPtr UATProcess;
FOnAlpakitProcessCompleted OnProcessCompletedDelegate;
TArray MessageList;
FOnAlpakitMessageReceived OnMessageReceivedDelegate;
public:
- FAlpakitInstance( const FString& InPluginName, ILauncherProfileRef InLauncherProfile );
+ FAlpakitInstance( const FString& InPluginName, TSharedRef InProfile );
FORCEINLINE EAlpakitInstanceState GetInstanceState() const { return InstanceState; }
FORCEINLINE EAlpakitInstanceResult GetResult() const { return Result; }
@@ -64,9 +66,9 @@ class ALPAKIT_API FAlpakitInstance : public TSharedFromThis
static FCriticalSection GlobalListCriticalSection;
static TArray> GlobalList;
- void OnWorkerMessageReceived( const FString& Message );
- void OnWorkerCancelled( double Time );
- void OnWorkerCompleted( bool bSuccess, double Duration, int32 ExitCode );
+ void OnWorkerMessageReceived(FString Message);
+ void OnWorkerCancelled();
+ void OnWorkerCompleted(int32 ExitCode);
void OnWorkerMessageReceived_GameThread( const FString& Message );
void OnWorkerCancelled_GameThread();
@@ -86,4 +88,4 @@ class ALPAKIT_API FAlpakitInstance : public TSharedFromThis
void RegisterInGlobalList();
void RemoveFromGlobalList();
-};
\ No newline at end of file
+};
diff --git a/Mods/Alpakit/Source/Alpakit/Public/AlpakitProfile.h b/Mods/Alpakit/Source/Alpakit/Public/AlpakitProfile.h
new file mode 100644
index 0000000000..5d1f179fab
--- /dev/null
+++ b/Mods/Alpakit/Source/Alpakit/Public/AlpakitProfile.h
@@ -0,0 +1,26 @@
+#pragma once
+#include "AlpakitSettings.h"
+
+struct FAlpakitProfileGameInfo {
+ FAlpakitProfileGameInfo(): bCopyToGame(false), StartGameType(EAlpakitStartGameType::NONE) {}
+ FAlpakitProfileGameInfo(bool bInCopyToGame, FString InGamePath, EAlpakitStartGameType InStartGameType):
+ bCopyToGame(bInCopyToGame), GamePath(InGamePath), StartGameType(InStartGameType) {}
+
+ bool bCopyToGame;
+ FString GamePath;
+ EAlpakitStartGameType StartGameType;
+};
+
+struct FAlpakitProfile {
+ explicit FAlpakitProfile(FString InPluginName): BuildConfiguration(EBuildConfiguration::Shipping), PluginName(InPluginName) {}
+
+ bool bBuild{false};
+ EBuildConfiguration BuildConfiguration;
+ TArray CookedPlatforms;
+ FString PluginName;
+ TMap PlatformGameInfo;
+
+ FString MakeUATCommandLine();
+private:
+ FString MakeUATPlatformArgs();
+};
diff --git a/Mods/Alpakit/Source/Alpakit/Public/AlpakitSettings.h b/Mods/Alpakit/Source/Alpakit/Public/AlpakitSettings.h
index 82d4435d66..ff97321961 100644
--- a/Mods/Alpakit/Source/Alpakit/Public/AlpakitSettings.h
+++ b/Mods/Alpakit/Source/Alpakit/Public/AlpakitSettings.h
@@ -12,6 +12,8 @@ enum class EAlpakitStartGameType : uint8 {
EPIC_EXPERIMENTAL UMETA(DisplayName = "Epic: Experimental")
};
+ALPAKIT_API const TCHAR* LexToString(EAlpakitStartGameType StartGameType);
+
UCLASS(config=Game)
class ALPAKIT_API UAlpakitSettings : public UObject {
GENERATED_BODY()
@@ -22,6 +24,8 @@ class ALPAKIT_API UAlpakitSettings : public UObject {
/** Saves alpakit settings to configuration file */
void SaveSettings();
+ // Hide these fields for now, as only Windows Shipping is allowed
+ /*
// Name of the build configuration in which the mod should be built
UPROPERTY(EditAnywhere, config, Category = Config, meta = ( GetOptions = GetAllowedBuildConfigurations ))
FString BuildConfiguration;
@@ -29,6 +33,7 @@ class ALPAKIT_API UAlpakitSettings : public UObject {
// The configurations to cook the mods in (Windows, WindowsServer, and so on)
UPROPERTY(EditAnywhere, config, Category = Config)
TArray CookPlatforms;
+ */
UPROPERTY(EditAnywhere, config, Category = Config)
FDirectoryPath SatisfactoryGamePath;