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;