├── .gitignore ├── .gitmodules ├── ReadMe.md ├── doc ├── BuildPlugin.md ├── Cleanup.md ├── DataSync.md ├── GetLatest.md ├── GitSetup.md ├── Package.md ├── PackageConfig.md ├── PluginPackage.md ├── RebuildLightmaps.md ├── Release.md └── UEInstall.md ├── inc ├── buildtargets.ps1 ├── filetools.ps1 ├── itch.ps1 ├── packageconfig.ps1 ├── platform.ps1 ├── pluginconfig.ps1 ├── pluginversion.ps1 ├── projectversion.ps1 ├── steam.ps1 ├── ueeditor.ps1 ├── uplugin.ps1 └── uproject.ps1 ├── packageconfig_template.json ├── pluginconfig_template.json ├── ue-blueprint-recompile.ps1 ├── ue-build-plugin.ps1 ├── ue-build.ps1 ├── ue-cleanup.ps1 ├── ue-cook.ps1 ├── ue-datasync.ps1 ├── ue-get-latest.ps1 ├── ue-git-setup.ps1 ├── ue-package.ps1 ├── ue-plugin-package.ps1 ├── ue-rebuild-lightmaps.ps1 ├── ue-release.ps1 └── ue-updatelyra.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/launch.json 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "GitScripts"] 2 | path = GitScripts 3 | url = git@github.com:sinbad/GitScripts.git 4 | -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | # Steve's Unreal Scripts 2 | 3 | ## Summary 4 | 5 | These scripts are to help with various tasks in [Unreal Engine](https://www.unrealengine.com). 6 | They're written on the basis of using Git / Git LFS rather than Perforce (many of Unreal's own 7 | automation tools assume P4, which can be inconvenient). 8 | 9 | These scripts support **UE4 and UE5** and will detect which one your project is using 10 | 11 | * [Setting up a project for Git / Git-LFS](./doc/GitSetup.md): including LFS locking 12 | * [Managing Git LFS Locking Tasks](https://github.com/sinbad/GitScripts): push and unlock, release locks you don't need any more 13 | * [Packaging a Game](./doc/Package.md): building, cooking, archiving 14 | * [Releasing a Game](./doc/Release.md): e.g. uploading to Itch, Steam 15 | * [Rebuilding Lightmaps](./doc/RebuildLightmaps.md): build lighting on the command line easily (supporting git-lfs locking, no P4 dependency like RunUAT) 16 | * [Getting Latest for Artists](./doc/GetLatest.md): pulls from git and builds so C++ changes are automatically updated 17 | * [Synchronising BuiltData Files outside of Git](./doc/DataSync.md) 18 | * [Cleaning Up](./doc/Cleanup.md): Deleting unneeded Hot Reload DLLs etc 19 | * [Packaging a Marketplace Plugin](./doc/PluginPackage.md) 20 | 21 | 22 | ## Prerequisites 23 | 24 | * Powershell Core 7+ 25 | * Almost everything is compatible with Win10 built-in PS 5.1 but 7 is better, and platform independent 26 | * PsIni module installed (library for reading INI files easily) 27 | * Run `Install-Module PsIni` in a Powershell console 28 | * Itch's [`butler` tool](https://itch.io/docs/butler/) if you wish to release to Itch.io 29 | * The [Steamworks SDK](https://partner.steamgames.com/doc/sdk) if you wish to release on Steam 30 | 31 | -------------------------------------------------------------------------------- /doc/BuildPlugin.md: -------------------------------------------------------------------------------- 1 | # Building Plugins 2 | 3 | If you want to build a plugin so that you can test it locally as if it was a 4 | Marketplace plugin (before you [package it](PluginPackage.md)), the 5 | `ue-build-plugin.ps1` script can help make it easier. 6 | 7 | The plugin will be built for the current platform only, using the engine version 8 | specified in the .uplugin file. 9 | 10 | 11 | ``` 12 | Usage: 13 | ue-build-plugin.ps1 [[-src:]sourcefolder] [Options] 14 | 15 | -src : Source folder (current folder if omitted) 16 | : (should be root of project) 17 | -allplatforms : Build for all platforms, not just the current one 18 | -allversions : Build for all supported UE versions, not just the current one" 19 | : (specified in pluginconfig.json, only works with lancher-installed UE)" 20 | -uever:5.x.x : Build for a specific UE version, not the current one (launcher only)" 21 | -dryrun : Don't perform any actual actions, just report on what you would do 22 | -help : Print this help 23 | 24 | Environment Variables: 25 | UEINSTALL : Use a specific Unreal install. 26 | : Default is to find one based on project version, under UEROOT 27 | UEROOT : Parent folder of all binary Unreal installs (detects version). 28 | : Default C:\Program Files\Epic Games 29 | ``` 30 | 31 | This script operates based on a `pluginconfig.json` file which must be present 32 | in the root of your plugin, next to the .uplugin file. The options are: 33 | 34 | ```json 35 | { 36 | "PackageDir": "C:\\Users\\Steve\\Marketplace", 37 | "BuildDir": "C:\\Users\\Steve\\Builds\\MyPlugin", 38 | "PluginFile": "OptionalPluginFilenameWillDetectInDirOtherwise.uplugin", 39 | 40 | "EngineVersions": 41 | [ 42 | "5.1.0", 43 | "5.2.0" 44 | ] 45 | 46 | 47 | } 48 | ``` 49 | 50 | Only `BuildDir` is required. 51 | 52 | The `-allversions` option only works with Launcher installed engines, 53 | since the path is derived from UEROOT. If using non-Launcher engines, or you 54 | need to change some other environmental options per version (e.g. setting 55 | `LINUX_MULTIARCH_ROOT` environment var), then you're recommended to instead 56 | use the `-uever:` option to build one version at a time, and set the environment 57 | (including `UEINSTALL`) specifically for each version. 58 | 59 | This script will, however, handle changing the EngineVersion in the .uplugin 60 | during the build, and resetting it afterwards. -------------------------------------------------------------------------------- /doc/Cleanup.md: -------------------------------------------------------------------------------- 1 | # Cleanup tool 2 | 3 | Mostly this script cleans up Hot Reload DLLs that often get left over. It used 4 | to also call `git lfs prune` but has stopped doing that for now because 5 | of a previous bug in Git LFS which would delete stashed LFS files. 6 | 7 | I don't use this script very much any more because I'm using Live Coding now. 8 | The script also cleans up Live Coding patches but there's fewer of those. 9 | 10 | ``` 11 | ue-cleanup.ps1 [[-src:]sourcefolder] [Options] 12 | 13 | -src : Source folder (current folder if omitted) 14 | : (should be root of project) 15 | -nocloseeditor : Don't close Unreal editor (this will prevent DLL cleanup) 16 | -lfsprune : Call 'git lfs prune' to delete old LFS files as well 17 | -dryrun : Don't perform any actual actions, just report on what you would do 18 | -help : Print this help 19 | ``` 20 | 21 | -------------------------------------------------------------------------------- /doc/DataSync.md: -------------------------------------------------------------------------------- 1 | # Synchronising "BuiltData" Files Outside Git 2 | 3 | ## Why? 4 | 5 | Unreal stores lighting and other built data for your maps in a separate file. 6 | If your level is called "YourMap.umap", then lighting data will be saved in 7 | "YourMap_BuiltData.uasset". 8 | 9 | This data is entirely derived from the .umap file, and is also very large. Given 10 | this, it's a prime candidate for exclusion from version control; there's no 11 | need to fill up your repository with large data that can be rebuilt from the data 12 | already tracked in it. 13 | 14 | However, there's a problem with doing this: 15 | 16 | 1. Anyone else on the team won't see lighting you've built 17 | 1. Even if they take the time to rebuild lighting themselves, this process wants 18 | to alter the base .umap file as well, since there are cross-references 19 | 20 | Most of the time you can get away with not including the BuiltData in the repository, 21 | each person building lighting locally and deliberately NOT saving their map changes 22 | when prompted. However, building the lighting gets more time consuming over time, 23 | and remembering not to save is a pain. 24 | 25 | ## Isn't there a standard solution? 26 | 27 | The solution normally presented is to use Perforce, where you can tell Perforce 28 | to only keep the latest version of a given pattern of file. In that scenario 29 | BuiltData files are tracked in the repository and versions other than the latest 30 | are just deleted. 31 | 32 | Vanilla Git can't do this. Git LFS can, but only if you write some pruning code 33 | specific to your LFS server. It's not ideal. 34 | 35 | ## My solution 36 | 37 | My solution is to write a tool to make it easier to share BuiltData files 38 | as a side-channel to the Git LFS repository. You simply provide a file share 39 | (ideally a network drive, or a synced folder like Google Drive / Dropbox if you 40 | don't mind a little duplication) and use my script `ue-datasync.ps1` to 41 | sync lighting data between team members. 42 | 43 | Using Git LFS is a prerequisite, because it uses the OIDs from the .umap files 44 | (which must be tracked in Git LFS) as a corresponding identifier to make sure 45 | the matching version of the BuiltData is used. 46 | 47 | ## Details 48 | 49 | Note: this script will automatically close the Unreal editor if you have the 50 | project open, in order to prevent accidental issues such as unsaved changes or 51 | locked files. 52 | 53 | ``` 54 | Usage: 55 | ue-datasync.ps1 [-mode:] [[-path:]syncpath] [Options] 56 | 57 | -mode : Whether to push or pull the built data from your filesystem 58 | -root : Root folder to sync files to/from. Project name will be appended to this path. 59 | : Can be blank if specified in UESYNCROOT 60 | -src : Source folder (current folder if omitted) 61 | : (should be root of project) 62 | -prune : Clean up versions of the data older than the latest 63 | -force : Copy ALL BuiltData files regardless of size/timestamp checks 64 | -nocloseeditor : Don't close Unreal editor (this will prevent download of updated files) 65 | -dryrun : Don't perform any actual actions, just report on what you would do 66 | -verbose : Print more information 67 | -help : Print this help 68 | 69 | Environment Variables: 70 | UESYNCROOT : Root path to sync data. Subfolders for each project name. 71 | UEINSTALL : Use a specific Unreal install. 72 | : Default is to find one based on project version, under UEROOT 73 | UEROOT : Parent folder of all binary Unreal installs (detects version). 74 | : Default C:\Program Files\Epic Games 75 | ``` 76 | 77 | You must tell the sync tool where the shared drive is, either using the `-root` 78 | argument, or defining the `UESYNCROOT` environment variable. A project folder 79 | will be added below that, based on the name of your .uproject file, so that 80 | you can use the same root folder for multiple projects. 81 | 82 | The tools works in "push" or "pull" mode, and processes all .umap files which 83 | are tracked in Git LFS (others are ignored). It's worth explaining exactly 84 | what happens in each mode. 85 | 86 | ### Push mode 87 | 88 | > Example: `ue-datasync.ps1 push` 89 | > 90 | > Assuming you run this in your project root and have defined the environment variable UESYNCROOT 91 | 92 | In push mode, you want to upload BuiltData files you've updated, probably because of a 93 | change to the .umap. You have to have committed your changes to the .umap first, 94 | the tool won't allow you to push changes if they're uncommitted (to avoid drifting changes). 95 | 96 | For each umap file, the Git LFS OID (basically a SHA256 of the umap file) for your 97 | current version is used to derive a version-specific filename, e.g. "YourMap_BuiltData_112233445567.uasset". 98 | Your local copy of the BuiltData file will be copied to the shared drive with this 99 | name (under the subfolder Project/ContentPath). If there's already a file named 100 | this in the shared folder, and the size & date/timestamp match, then nothing will happen, 101 | unless you use the `-force` argument. 102 | 103 | Because you're not allowed to have local uncommitted changes to the umap files, 104 | and the BuiltData is tagged with the SHA of the umap, this means other people can 105 | get a 'safe' copy of your current lighting build corresponding to the state of the 106 | umap on this shared drive, without it being in the git repo. 107 | 108 | ### Pull mode 109 | 110 | > Example: `ue-datasync.ps1 pull` 111 | > 112 | > Assuming you run this in your project root and have defined the environment variable UESYNCROOT 113 | 114 | In pull mode, the script tries to find the BuiltData files corresponding to your 115 | umap files on the shared drive. Again, you can't have any uncommitted changes to 116 | umap files. 117 | 118 | In the same way as push, the script uses the OID of the umap file to look up 119 | the versions on the shared drive. However, if a lighting build with that 120 | OID doesn't exist, pull checks the git log and finds the latest lighting build 121 | available for that umap file. This is to deal with the case where changes have 122 | been made to the umap file since the last lighting build, but the lighting build 123 | is still OK to use (either the umap changes didn't affect lighting, or the 124 | differences are "good enough" for the moment). 125 | 126 | If an appropriate BuiltData file is found on the shared drive, and 127 | you don't have a newer local version, then the BuiltData file is copied into 128 | place in your local project folder. Next time you open the editor you'll 129 | have the lighting data that your team mate built. 130 | 131 | ## Pruning 132 | 133 | By default, the different versions of BuiltData would build up on the shared 134 | drive, one per OID of .umap file. To clean up and only keep the latest, 135 | add the `-prune` option (this isn't enabled by default because destructive actions 136 | should always be opt-in). 137 | 138 | The prune routine looks at all your tracked .umap files, and then deletes any 139 | files on the shared drive for this umap that have OIDs *other than* the current 140 | version, and which have an older modification date (to prevent accidental deletion 141 | of newer versions someone else has recently pushed after the version you're on). 142 | 143 | Although you can provide `-prune` to any invocation of this script, I'd recommend 144 | you only do it for the `push` variant. 145 | 146 | ## Automating this 147 | 148 | You can use `ue-datasync.ps1` manually, calling it in `push` mode just after 149 | you push any map changes (assuming you've built the lighting), and in `pull` mode 150 | on demand, as and when you know you want to pick up new lighting data that others 151 | have built. 152 | 153 | Alternatively you could add these commands to your git hooks, perhaps `pre-push` 154 | and `post-checkout` for `push` and `pull` respectively. Given the lower frequency 155 | of lighting build changes though, the manual approach is probably fine. -------------------------------------------------------------------------------- /doc/GetLatest.md: -------------------------------------------------------------------------------- 1 | # Getting Latest for Artists 2 | 3 | We've found it useful to provide a simple script which can be run on artists' 4 | machines to get the latest from Git, and make sure all the C++ components are 5 | built. 6 | 7 | It now also automatically calls `ue-datasync.ps1 pull` if `UESYNCROOT` is defined 8 | in the environment. 9 | 10 | While the UE editor can sometimes do this successfully on startup as well, 11 | it's just nicer to do it as part of the update process - the artist can then 12 | just double-click a shortcut on their desktop and let it run while getting 13 | coffee or something. 14 | 15 | The script also automatically closes the UE editor if it's open on the same 16 | project to make sure the build is successful. 17 | 18 | ``` 19 | ue-get-latest.ps1 [[-src:]sourcefolder] [Options] 20 | 21 | -src : Source folder (current folder if omitted) 22 | : (should be root of project) 23 | -nocloseeditor : Don't close Unreal editor (this will prevent DLL cleanup) 24 | -dryrun : Don't perform any actual actions, just report on what you would do 25 | -help : Print this help 26 | ``` 27 | 28 | -------------------------------------------------------------------------------- /doc/GitSetup.md: -------------------------------------------------------------------------------- 1 | # Git Setup 2 | 3 | We use Git LFS with Unreal, including [Git LFS Locking](https://github.com/git-lfs/git-lfs/wiki/File-Locking) which is now supported by 4 | most Git hosts, including self-hosted options like [Gitea](https://gitea.io/). Locking 5 | is important because Unreal uses a lot of binary `.uasset` files which are not mergeable. 6 | 7 | To make best use of git, LFS and locking, you really want to be using the 8 | [Git LFS 2 plugin](https://github.com/SRombauts/UE4GitPlugin) for the Unreal Editor. I 9 | also have a [fork of this plugin](https://github.com/sinbad/UE4GitPlugin) with some 10 | improvements which haven't been merged yet. I'm still making improvements so 11 | you might want to keep an eye on that. 12 | 13 | We use a this content workflow in our UE game repositories: 14 | 15 | 1. All content creation files in `$REPO/MediaSrc` (subfolders by type) 16 | * These are typically in formats e.g. Blender that Unreal doesn't read directly, so outside `Content` 17 | * These files are added to Git 18 | * They are also tracked as Git LFS files 19 | * They are NOT marked as lockable, simply because the tooling for managing locking 20 | isn't very good outside of Unreal right now 21 | 1. When exporting, output (`FBX`, `PNG`, `WAV` etc) goes in `$REPO/Content` (and subfolders) 22 | * These files are *ignored* in Git because they are derived data 23 | * Unreal imports them to a `.uasset` which contains all their contents anyway 24 | 1. Imported content becomes `.uasset` in `$REPO/Content` 25 | * These files are added to Git 26 | * They are also tracked as Git LFS files 27 | * These are also marked as *lockable* in Git LFS 28 | 29 | Together the script below configures all of this automatically. 30 | 31 | ## The script 32 | 33 | You run the script from a Powershell prompt, in the root of your Unreal project. 34 | 35 | ``` 36 | ue-git-setup.ps1 [[-src:]sourcefolder] [Options] 37 | 38 | -src : Source folder (current folder if omitted) 39 | : (should be root of your Unreal project) 40 | -dryrun : Don't perform any actual actions, just report on what you would do 41 | -help : Print this help 42 | ``` 43 | 44 | See the notes below for some practical details of running it. 45 | 46 | ## Notes 47 | 48 | 1. The script works for projects with no git repo yet, or those with an existing git repo 49 | 1. For existing repos, it's better if you have not committed any large files to Git yet 50 | * If you have, it is *highly recommended* you use `git lfs migrate` to re-write your repository history to be LFS compatible 51 | * `git lfs migrate import --everything --include="*.uasset,*.umap,"` 52 | * This repo will need to be re-cloned by everyone but it's MUCH cleaner than changing to LFS mid-history 53 | 1. Make a note of any custom .gitignore entries you have, the script will replace it 54 | 1. Run `ue-git-setup.ps1` in the root project folder 55 | 1. Add back any specialised .gitignores we didn't cover (might not need any) 56 | 1. Push ALL BRANCHES of this new repo to the host of your choice 57 | 58 | -------------------------------------------------------------------------------- /doc/Package.md: -------------------------------------------------------------------------------- 1 | # Packaging Script 2 | 3 | The `ue-package.ps1` script builds, cooks and packages your game into a folder, 4 | much like using "File > Package Project" in the UE editor. However, it offers a 5 | number of other features. 6 | 7 | This script operates based on a `packageconfig.json` file which must be present 8 | in the root of your Unreal project. Please see the [Package Config File docs](PackageConfig.md) 9 | for a full description of this file. 10 | 11 | ``` 12 | ue-package.ps1 [-src:sourcefolder] [-out:folder] [-major|-minor|-patch|-hotfix] [-keepversion] [-variant=VariantName] [-test] [-dryrun] 13 | 14 | -src : Source folder (current folder if omitted), must contain packageconfig.json 15 | -out : Overrides OutputDir in packageconfig.json 16 | -major : Increment major version i.e. [x++].0.0.0 17 | -minor : Increment minor version i.e. x.[x++].0.0 18 | -patch : Increment patch version i.e. x.x.[x++].0 (default) 19 | -hotfix : Increment hotfix version i.e. x.x.x.[x++] 20 | -keepversion : Keep current version number, doesn't tag unless -forcetag 21 | -nightly : Nightly build, doesn't tag, doesn't commit, re-uses same nightly folder, appends git rev version 22 | -variants Name1,Name2,Name3 23 | : Build only named variants instead of DefaultVariants from packageconfig.json 24 | -test : Testing mode, separate builds, allow dirty working copy 25 | -browse : After packaging, browse the output folder 26 | -dryrun : Don't perform any actual actions, just report on what you would do 27 | -help : Print this help 28 | 29 | Environment Variables: 30 | UEINSTALL : Use a specific Unreal install. 31 | : Default is to find one based on project version, under UEROOT 32 | UEROOT : Parent folder of all binary Unreal installs (detects version). 33 | : Default C:\Program Files\Epic Games 34 | ``` 35 | 36 | ## What the Script Does 37 | 38 | ### 1. Check Working Copy 39 | 40 | If you're using Git, as a safety check the script doesn't allow you to package 41 | builds from a working copy with uncommitted changes. This ensures that your builds 42 | are always from a known version. 43 | 44 | ### 2. Locate UE Install 45 | 46 | The script can locate your Unreal install automatically. You may need to customise 47 | this on non-Windows platforms or if you use a source build. 48 | 49 | See [How Scripts Locate the Unreal Install](UEInstall.md) for more details. 50 | 51 | ### 3. Close the UE Editor 52 | 53 | If you have this project open in UE, the script will close the editor. This is 54 | to ensure that it won't interfere with any build actions. 55 | 56 | ### 4. Increment Project Version 57 | 58 | The version number of the project will be increased automatically, by default 59 | as a "patch" release (meaning the 3rd number in the version string). As you 60 | can see, you can supply arguments `-major`, `-minor` or `-hotfix` instead to 61 | increment a different part of the version number. 62 | 63 | This will edit the `DefaultGame.ini` file and replace the `ProjectVersion` 64 | setting. This change will be committed automatically before the build if you're using Git. 65 | 66 | If you don't want to change the version number, you can provide `-keepversion` on 67 | the command line instead. 68 | 69 | ### 5. Tags Git Repository 70 | 71 | If you're using Git and the version number was incremented, the repository will 72 | be tagged with the new version number. 73 | 74 | ### 6. Cook Maps 75 | 76 | Based on your settings in [packageconfig.json](PackageConfig.md), the tools knows 77 | which maps to cook into your packages. You can tell it to cook all of them automatically, 78 | only a specific list, or all *excluding* a chosen few. 79 | 80 | ### 7. Package Variants 81 | 82 | Rather than building / packaging just a single way, `ue-package.ps1` supports 83 | packaging multiple variants of your project. The variations can be: 84 | 85 | * **Platform**: lets you build for Windows, Linux, Mac etc 86 | * **Build Configuration**: so you can build a private version as Development, public version as Shipping for example 87 | * **Build Arguments**: If you want to toggle on/off compiled-in features that are triggered by build arguments, you can add them for different variants 88 | * **Release Destinations**: If you have one build for Itch, and a different one for Steam etc 89 | * **Cultures**: For if you want to include specific cultures in a build 90 | 91 | Variants are defined in [packageconfig.json](PackageConfig.md) in the root of 92 | your project. You can either specify which variants you want to build on the command line, 93 | or you can just use the defaults as defined in your config. 94 | 95 | ### 8. Unique Package Folder 96 | 97 | The destination of the package operation is generated from a combination of: 98 | 99 | * The `OutputDir` setting in your [packageconfig.json](PackageConfig.md) 100 | * The version number 101 | * The variant name 102 | 103 | Therefore if you're building variant "PublicSteamWin64" at version 1.1.2.0, the 104 | package output will be in `$OutputDir/1.1.2.0/PublicSteamWin64/` 105 | 106 | ### Optionally Rename EXE 107 | 108 | Sometimes you want your packaged EXE to be called something other than your main 109 | target game module; unfortunately UE doesn't allow you to change it in the project 110 | settings (without renaming your module, which is very inconvenient); but simply 111 | renaming the EXE after building works fine, and means you can present a more pleasing 112 | EXE name in your build. 113 | 114 | Set `RenameExe` to the name you want your EXE to have, without the `.exe` extension. 115 | 116 | > Technically speaking the main EXE in the root of your package dir is a boostrapper 117 | > for another EXE inside your package, which is still called the same name as the 118 | > main target module. However, the display name of this process is set in your 119 | > Project Settings, so it looks OK in e.g. Windows Task Manager. Using lower level 120 | > process listing tools will reveal the EXE is named after the target module name 121 | > though. If you don't like this, you'll have to rename your main target module, 122 | > or create a small wrapper module which solely acts as the main target. 123 | 124 | ### 9. Optionally Zip Packaged Build 125 | 126 | If you've enabled the `Zip` option for a given variant in [packageconfig.json](PackageConfig.md), 127 | the package output folder will also be zipped up, into the `ZipDir` directory 128 | as given in that same config file. 129 | 130 | The files are named `ProjectName_Version_Variant[_PlatformType].zip`, e.g. 131 | `MyGame_1.1.2.0_PublicSteamWin64.zip`. 132 | 133 | > We zip the contents of the *subfolder* of the package output, e.g. `WindowsNoEditor`, 134 | so that the root of the zip is your game executable. 135 | 136 | > The `_PlatformType` suffix is usually omitted; it will only be there if there is 137 | more than one subfolder in the package folder, which is only the case when you 138 | build a dedicated client & server. In that case there will be separate zips for 139 | each, e.g. `MyGame_1.1.2.0_PublicSteamWin64_WindowsClient.zip` and `MyGame_1.1.2.0_PublicSteamWin64_WindowsServer.zip` 140 | 141 | ### 10. Optionally Browse Packaged Output 142 | 143 | If you supply the optional argument `-browse`, your file manager will be asked to 144 | open the folder containing the newly packaged output, if it completed successfully. -------------------------------------------------------------------------------- /doc/PackageConfig.md: -------------------------------------------------------------------------------- 1 | # The packageconfig.json File 2 | 3 | ## Overview 4 | 5 | Many of the tools in this repo, such as the [Packaging Script](./Package.md) 6 | and the [Release Script](./Release.md), depend on a configuration file named 7 | `packageconfig.json`. 8 | 9 | This file should be in the root of your Unreal project. It's contents are set out 10 | in detail later in this document, but but here's an example demonstrating many of the features: 11 | 12 | ```json 13 | { 14 | "OutputDir": "C:\\Users\\Steve\\Projects\\Builds\\Game1", 15 | "ZipDir": "C:\\Users\\Steve\\Projects\\Archives", 16 | 17 | "Target": "Game1", 18 | "CookAllMaps": true, 19 | "MapsExcluded": [ 20 | "TestMap", 21 | ], 22 | "UsePak": true, 23 | 24 | "DefaultVariants": [ 25 | "Win64Private", 26 | "Win64Itch", 27 | "Win64Steam" 28 | ], 29 | 30 | "Variants": [ 31 | { 32 | "Name": "Win64Private", 33 | "Platform": "Win64", 34 | "Configuration": "Development", 35 | "ExtraBuildArguments": "-EnableDebugPanel", 36 | "Zip": true 37 | }, 38 | { 39 | "Name": "Win64Steam", 40 | "Platform": "Win64", 41 | "Configuration": "Shipping", 42 | "ReleaseTo": [ 43 | "Steam" 44 | ], 45 | "SteamAppId": "783465", 46 | "SteamDepotId": "1238594", 47 | "SteamLogin": "MySteamLogin", 48 | "ExtraBuildArguments": "-EnableSteamworks" 49 | }, 50 | { 51 | "Name": "Win64Itch", 52 | "Platform": "Win64", 53 | "Configuration": "Shipping", 54 | "ReleaseTo": [ 55 | "Itch" 56 | ], 57 | "ItchAppId": "my-itch-user/game1", 58 | "ItchChannel": "win64" 59 | } 60 | ], 61 | } 62 | 63 | ``` 64 | 65 | ## Overall File Structure 66 | 67 | The `packageconfig.json` has 2 main parts: 68 | 69 | * Global properties 70 | * A list of Variants 71 | 72 | Variants are there to allow you to build / package for multiple scenarios, such as 73 | builds for your private use, builds for certain stores, different platforms, and so on. 74 | 75 | Global properties apply everywhere, whilst properties contained in each Variant section 76 | apply only to that specific build variant. 77 | 78 | ## Global Properties 79 | 80 | ### `OutputDir` 81 | *Mandatory Setting - string* 82 | 83 | This is the root folder in which packaged games are placed. 84 | Subfolders will be created for version numbers and variants, see the [Packaging Script docs](Package.md) 85 | for more information. 86 | 87 | ### `Target` 88 | *Mandatory Setting - string* 89 | 90 | The name of the target which you will package, which is usually your game name 91 | and often the same name as the `.uproject` file. 92 | 93 | ### `Variants` 94 | *Mandatory Setting - array of [PackageVariants](#package-variants)* 95 | 96 | This list is where you define the way you want to package your game. See 97 | the of [PackageVariant](#package-variants) documentation below for more details. 98 | 99 | 100 | ### `CookAllMaps` 101 | *Optional Setting - boolean: Default=false* 102 | 103 | If true, script will locate all `.umap` files in your Content folder and cook 104 | then when packaging. You can exclude some using the [`MapsExcluded`](#mapsexcluded) 105 | option if you need to. 106 | 107 | If false, either the contents of [`MapsIncluded`](#mapsincluded) will be cooked, 108 | or if that isn't specified, the maps to cook in your project packaging settings 109 | (DefaultGame.ini) will be used. 110 | 111 | So if you set this to false and don't provide `MapsIncluded` then your project 112 | settings continue to control the maps which are cooked. 113 | 114 | ### `MapsExcluded` 115 | *Optional Setting - array of strings* 116 | 117 | If [`CookAllMaps`](#cookallmaps) is true, any maps named in this array will be 118 | excluded from the cooking process. Do not include the folder name or extension of 119 | the map file, just the map name. 120 | 121 | ### `MapsIncluded` 122 | *Optional Setting - array of strings* 123 | 124 | If [`CookAllMaps`](#cookallmaps) is false and you include this setting, 125 | only the maps listed here will be cooked. Do not include the folder name or extension of 126 | the map file, just the map name. 127 | 128 | ### `DefaultVariants` 129 | *Optional Setting - array of strings* 130 | 131 | This is a list of [Variant](#package-variant) names - unless otherwise specified on the command line, 132 | this is the set of variants which will be packaged by the [Packaging Script](Package.md). 133 | This just makes it faster / less error prone to perform your regular packaging tasks. 134 | 135 | ### `UsePak` 136 | *Optional Setting - boolean: Default=true* 137 | 138 | If true, combine packaged files into a .pak file. 139 | 140 | ### `ZipDir` 141 | *Optional Setting - string* 142 | 143 | For variants which enable the [`Zip`](#zip) option, this is the directory that 144 | zipped packages will be created in. 145 | 146 | ### `ProjectFile` 147 | *Optional Setting - string* 148 | 149 | By default, scripts will locate your `.uproject` file automatically in the root of 150 | your Unreal project folder. If for any reason you have more than one, you can 151 | specify which to use with this setting. 152 | 153 | ## Package Variants 154 | 155 | The [`Variants`](#variants) property contains a list of ways you want to 156 | build and package your game. You can specify the [default list](#defaultvariants) of variants you 157 | want to package, or name one or more on the command line explicitly. 158 | 159 | Each entry has these properties: 160 | 161 | ### `Name` 162 | *Mandatory Setting - string* 163 | 164 | The name of the variant. This can be whatever you want, it just identifies this 165 | variant and also forms the basis of folder / filenames related to its packaging. 166 | 167 | ### `Platform` 168 | *Mandatory Setting - string* 169 | 170 | The platform this variant will be built for; must be one of those supported by 171 | Unreal, e.g. "Win64", "Linux" etc 172 | 173 | ### `Configuration` 174 | *Mandatory Setting - string* 175 | 176 | The build configuration for this variant as defined by Unreal, e.g. "Development" or "Shipping". 177 | 178 | ### `ExtraBuildArguments` 179 | *Optional Setting - string* 180 | 181 | If you need to supply any additional arguments to the build / packaging step for 182 | this variant, you can include them here (as one combined string). 183 | 184 | ### `Zip` 185 | *Optional setting - boolean: Default=false* 186 | 187 | Set this option to true if you would like this packaged build to be zipped up 188 | into an archive. It will be placed in the [`ZipDir`](#zipdir) folder, see the 189 | [Packaging Script](./Package.md) for more details about naming. 190 | 191 | ### `ReleaseTo` 192 | *Optional Setting - array of strings* 193 | 194 | Which services you want to be able to release this package to. Currently the 195 | only supported options are "Itch" and "Steam". You can list more than one on the 196 | same variant if the same build is released to multiple stores. 197 | 198 | Packaged builds are released using the [Release Script](./Release.md) which uses 199 | this setting. 200 | 201 | Each of the release stores has its own set of additional parameters which 202 | you'll need to also provide in the variant: 203 | 204 | #### Steam: 205 | * `SteamAppId`: the application ID of your app on Steam (numeric string) 206 | * `SteamDepotId`: the depot ID for this particular variant (numeric string) 207 | * `SteamLogin`: the username which you use to upload (string) 208 | 209 | #### Itch 210 | * `ItchAppId`: the application identifier on Itch e.g. "username/app" 211 | * `ItchChannel`: the channel to publish this variant on e.g. "windows" 212 | 213 | ### `Cultures` 214 | *Optional Setting - array of strings* 215 | 216 | If supplied, cooks a specific set of cultures (e.g. "en-us") into this particular 217 | variant. If not supplied, the project packaging settings are used. -------------------------------------------------------------------------------- /doc/PluginPackage.md: -------------------------------------------------------------------------------- 1 | # Packaging a Plugin for the Marketplace 2 | 3 | To distribute a plugin on the marketplace, you need to zip it up and make sure 4 | you only include approved files. The `ue-plugin-package.ps1` script is here 5 | to make that job easier. 6 | 7 | > **Note:** This script will update your .uplugin file to record the new version number, 8 | > and manipulate EngineVersion for each build. Its state will be restored afterwards 9 | > (apart from the version number). 10 | > 11 | > Unfortunately the first time, this will probably mess with indenting because 12 | > of a difference of opinion between JSON libraries. But it's harmless. 13 | 14 | ``` 15 | Usage: 16 | ue-plugin-package.ps1 [-src:sourcefolder] [-major|-minor|-patch|-hotfix] [options...] 17 | 18 | -src : Source folder (current folder if omitted), must contain pluginconfig.json 19 | -major : Increment major version i.e. [x++].0.0.0 20 | -minor : Increment minor version i.e. x.[x++].0.0 21 | -patch : Increment patch version i.e. x.x.[x++].0 (default) 22 | -hotfix : Increment hotfix version i.e. x.x.x.[x++] 23 | -keepversion : Keep current version number, doesn't tag 24 | -notag : Don't tag even if updating version 25 | -test : Testing mode, separate builds, allow dirty working copy 26 | -browse : After packaging, browse the output folder 27 | -dryrun : Don't perform any actual actions, just report on what you would do 28 | -help : Print this help 29 | ``` 30 | 31 | This script operates based on a `pluginconfig.json` file which must be present 32 | in the root of your plugin, next to the .uplugin file. The options are: 33 | 34 | ```json 35 | { 36 | "PackageDir": "C:\\Users\\Steve\\MarketplaceBuilds", 37 | "PluginFile": "OptionalPluginFilenameWillDetectInDirOtherwise.uplugin", 38 | "EngineVersions": 39 | [ 40 | "5.0.0", 41 | "5.1.0", 42 | "5.2.0" 43 | ] 44 | } 45 | ``` 46 | 47 | `PackageDir` and `EngineVersions` are required. 48 | 49 | ## Engine Versions 50 | 51 | When submitting code plugins to the Marketplace, you're only allowed to include 52 | a single supported `EngineVersion` in each version you upload. Even though you 53 | don't submit built binaries to the Marketplace, the publisher portal requires 54 | that the .uplugin has a single `EngineVersion` entry. 55 | 56 | Therefore to support multiple engine versions, you have to upload several essentially 57 | identical source archives, with each one having a different `EngineVersion` specified 58 | in the .uplugin. 59 | 60 | This script helps you do that; for each entry in `EngineVersions` in the `pluginconfig.json`, 61 | a separate zip archive is generated, with the correct version set in the .uplugin. 62 | 63 | > It seems you should always use ".0" as the 3rd version digit. 64 | 65 | ## Excluding Files 66 | 67 | By default, the plugin packaging process automatically excludes common 68 | files and directories that shouldn't be there: 69 | 70 | * ./.git/ 71 | * ./.git* 72 | * ./Binaries/ 73 | * ./Intermediate/ 74 | * ./Saved/ 75 | * ./pluginconfig.json 76 | 77 | If you'd like to exclude other things, create a file called `packageexclusions.txt` 78 | in the root of the plugin, listing files/folders you want to exclude (one per line). 79 | -------------------------------------------------------------------------------- /doc/RebuildLightmaps.md: -------------------------------------------------------------------------------- 1 | # Rebuilding Lightmaps 2 | 3 | This script is a more convenient alternative to calling `RunUAT RebuildLightmaps`, which 4 | has the downside of being unnecessarily dependent on Perforce. 5 | 6 | This script is compatible with Git (if you're using it), and specifically copes 7 | with Git LFS file locking. When building lighting the `.umap` needs to be locked 8 | for writing, this script will do that if necessary. The RunUAT version fails if 9 | you don't have Perforce even if you've already locked the file in LFS! Very silly. 10 | 11 | This script uses the [packaging configuration file](./Package.md) and can 12 | automatically determine which maps to rebuild if you want, or you can 13 | explicitly list them as arguments: 14 | 15 | ``` 16 | ue-rebuild-lightmaps.ps1 [-src:sourcefolder] [-quality:(preview|medium|high|production)] [-maps Map1,Map2,Map3] [-dryrun] 17 | 18 | -src : Source folder (current folder if omitted) 19 | -quality : Lightmap quality, preview/medium/high/production 20 | : (Default: production) 21 | -maps : List of maps to rebuild. If omitted, will derive which ones to 22 | rebuild based on cooked maps in packageconfig.json 23 | -dryrun : Don't perform any actual actions, just report on what you would do 24 | -help : Print this help 25 | 26 | Environment Variables: 27 | UEINSTALL : Use a specific Unreal install. 28 | : Default is to find one based on project version, under UEROOT 29 | UEROOT : Parent folder of all binary Unreal installs (detects version). 30 | : Default C:\Program Files\Epic Games 31 | ``` -------------------------------------------------------------------------------- /doc/Release.md: -------------------------------------------------------------------------------- 1 | # Release Script 2 | 3 | The release script `ue-release.ps1` takes previously packaged builds (see 4 | [Packaging Script](./Package.md)) and uploads them to publishing services; 5 | currently Itch.io and Steam. 6 | 7 | You will need to install the [Steamworks SDK](https://partner.steamgames.com/doc/sdk) 8 | to release on Steam, and the [Itch Butler tool](https://itch.io/docs/butler/) to 9 | release on Itch. 10 | 11 | This script uses configuration stored in [`packageconfig.json`](./PackageConfig.md). 12 | 13 | ``` 14 | ue-release.ps1 [-version:ver|-latest] -variants:v1,v2 -services:steam,itch [-src:sourcefolder] [-dryrun] 15 | 16 | -version:ver : Version to release; must have been packaged already 17 | -latest : Instead of an explicit version, release one identified in project settings 18 | -variants:var1,var2 : Name of variants to release. Omit to use DefaultVariants 19 | -services:s1,s2 : Name of services to release to. Can omit and rely on ReleaseTo 20 | setting of variant in packageconfig.json 21 | -src : Source folder (current folder if omitted), must contain packageconfig.json 22 | -dryrun : Don't perform any actual actions, just report what would happen 23 | -help : Print this help 24 | ``` 25 | 26 | 27 | ## Uploading all builds at once 28 | 29 | The only mandatory argument is the version number, which you can specify explicitly, 30 | or use the `-latest` option to take the version from project settings. With only that argument, 31 | the script will process all the [default variants](./PackageConfig.md#defaultvariants) 32 | for this project and release any which have [release settings](./PackageConfig.md#defaultvariants). 33 | This allows you to push all your builds for a given version at once. 34 | 35 | ## Uploading more selectively 36 | 37 | Alternatively you can limit the packages you upload using the `-variants` and 38 | `-services` arguments; if supplied, only matching variants and publishing services 39 | will be processed. 40 | 41 | ## Authentication 42 | 43 | This script will call the Itch `butler` tool and/or the Steam `steamcmd` tool. 44 | These have their own authentication; you will be prompted as needed but if you 45 | want to guarantee no prompts during running, you should log in to the services 46 | on the command line beforehand. 47 | 48 | ### Logging in to Itch 49 | 50 | ``` 51 | butler login 52 | ``` 53 | 54 | Your Itch authentication will be stored for future use after logging in. 55 | 56 | ### Logging in to Steam 57 | 58 | ``` 59 | steamcmd +login user_name 60 | ``` 61 | 62 | The `SteamLogin` setting in [`packageconfig.json`](./PackageConfig.md) 63 | should be the same as the username you log in with here. 64 | 65 | ## When builds go live 66 | 67 | There is some variation on when players can see your uploaded packages: 68 | 69 | * **Itch**: Packages go live as soon as Itch finishes processing them 70 | * **Steam**: Packages sit in the release queue until you explicitly release them 71 | via the Steamworks web interface 72 | -------------------------------------------------------------------------------- /doc/UEInstall.md: -------------------------------------------------------------------------------- 1 | # How Scripts Locate the Unreal Install 2 | 3 | If you're using an installed version of Unreal, the script reads your project file 4 | and automatically finds the location of the tools. 5 | 6 | If you're using a source version of UE, or have installed in a non-standard location, 7 | you can define the following environment variables instead: 8 | 9 | * **UEROOT** : Set the root directory of installed versions of Unreal (instead of the default e.g. C:\Program Files\Epic Games). The script will find the correct version in subfolders e.g. UE_4.27, UE_5.0 10 | * **UEINSTALL**: Explicitly set the location of the Unreal build you want to use. 11 | The script will just use this directly and assume that the folder it points to on disk contains e.g. Engine/Build/BatchFiles 12 | 13 | 14 | -------------------------------------------------------------------------------- /inc/buildtargets.ps1: -------------------------------------------------------------------------------- 1 | function Find-DefaultTarget { 2 | param ( 3 | [string]$srcfolder, 4 | # Game, Editor, Server, Client 5 | [string]$preferred = "Editor" 6 | ) 7 | 8 | $sourcefolder = Join-Path (Resolve-Path $srcfolder) "Source" 9 | 10 | # Enumerate the Target.cs files in Source folder and use the default one 11 | # This lets us not assume what the modules are called exactly 12 | $targetFiles = Get-ChildItem "$sourcefolder\*.Target.cs" 13 | 14 | foreach ($file in $targetfiles) { 15 | if ($file.Name -like "*$preferred.Target.cs") { 16 | return $file.Name.SubString(0, $file.Name.Length - 10) 17 | } 18 | } 19 | 20 | # Fall back on Game if nothing else 21 | foreach ($file in $targetfiles) { 22 | if ($file.Name -like "*Game.Target.cs") { 23 | return $file.Name.SubString(0, $file.Name.Length - 10) 24 | } 25 | } 26 | 27 | throw "Unable to find default build target ending in $preferred" 28 | 29 | } 30 | -------------------------------------------------------------------------------- /inc/filetools.ps1: -------------------------------------------------------------------------------- 1 | function Find-Files { 2 | param ( 3 | [string]$startDir, 4 | [string]$pattern, 5 | [bool]$includeByDefault, 6 | [array]$includeBaseNames, 7 | [array]$excludeBaseNames 8 | ) 9 | 10 | $basenames = [System.Collections.ArrayList]::New() 11 | $fullpaths = [System.Collections.ArrayList]::New() 12 | Get-ChildItem -Path $startDir -Filter $pattern -Recurse | ForEach-Object { 13 | if ($includeByDefault) { 14 | if ($excludeBaseNames -notcontains $_.BaseName) { 15 | $basenames.Add($_.BaseName) > $null 16 | $fullpaths.Add($_.FullName) > $null 17 | } 18 | } else { 19 | if ($includeBaseNames -contains $_.BaseName) { 20 | $basenames.Add($_.BaseName) > $null 21 | $fullpaths.Add($_.FullName) > $null 22 | } 23 | } 24 | } 25 | 26 | return [PSCustomObject]@{ 27 | BaseNames = $basenames 28 | FullNames = $fullpaths 29 | } 30 | 31 | } 32 | 33 | # Get the root package output dir for a version / variant 34 | function Get-Package-Dir { 35 | param ( 36 | [PackageConfig]$config, 37 | [string]$versionNumber, 38 | [string]$variantName 39 | ) 40 | 41 | return Join-Path $config.OutputDir "$versionNumber/$variantName" 42 | } 43 | 44 | # Get the dir where the client build is for a packaged version / variant 45 | # This is as Get-Package-Dir except with one extra level e.g. WindowsNoEditor 46 | function Get-Package-Client-Dir { 47 | param ( 48 | [PackageConfig]$config, 49 | [string]$versionNumber, 50 | [string]$variantName, 51 | [string]$ueVersion 52 | ) 53 | 54 | $root = Get-Package-Dir -config:$config -versionNumber:$versionNumber -variantName:$variantName 55 | $variant = $config.Variants | Where-Object { $_.Name -eq $variantName } | Select-Object -First 1 56 | 57 | if (-not $variant) { 58 | throw "Unknown variant $variantName" 59 | } 60 | 61 | $isUE5 = $ueVersion.StartsWith("5.") 62 | # Note, currently only supporting "Game" platform type, not separate client / server 63 | $subfolder = switch ($variant.Platform) { 64 | "Win32" { if ($isUE5) { "Windows" } else { "WindowsNoEditor" } } 65 | "Win64" { if ($isUE5) { "Windows" } else { "WindowsNoEditor" } } 66 | "Linux" { if ($isUE5) { "Linux" } else { "LinuxNoEditor" } } 67 | "Mac" { if ($isUE5) { "Mac" } else { "MacNoEditor" } } 68 | Default { throw "Unsupported platform $($variant.Platform)" } 69 | } 70 | 71 | return Join-Path $root $subfolder 72 | } 73 | 74 | # Return whether 2 files seem to be the same based on their size and date/time 75 | # Does not compare their contents! 76 | function Compare-Files-Quick { 77 | param ( 78 | [string]$filePathA, 79 | [string]$filePathB 80 | ) 81 | 82 | if (-not (Test-Path $filePathA -PathType Leaf)) { 83 | return $false 84 | } 85 | if (-not (Test-Path $filePathB -PathType Leaf)) { 86 | return $false 87 | } 88 | $propsA = Get-ItemProperty -Path $filePathA 89 | $propsB = Get-ItemProperty -Path $filePathB 90 | 91 | if ($propsA.Length -ne $propsB.Length) { 92 | return $false 93 | } 94 | 95 | $timediff = New-TimeSpan $propsA.LastWriteTime $propsB.LastWriteTime 96 | # Allow a 2s difference 97 | if ($timediff.Seconds -gt 2) { 98 | return $false 99 | } 100 | 101 | return $true 102 | } -------------------------------------------------------------------------------- /inc/itch.ps1: -------------------------------------------------------------------------------- 1 | function Release-Itch { 2 | param ( 3 | [PackageConfig]$config, 4 | [PackageVariant]$variant, 5 | [string]$sourcefolder, 6 | [string]$version, 7 | [switch]$dryrun = $false 8 | ) 9 | 10 | Write-Output ">>>--- Itch Upload Start ---<<<" 11 | 12 | $appid = $variant.ItchAppId 13 | $channel = $variant.ItchChannel 14 | 15 | if (-not $appid) { 16 | throw "Missing property ItchAppId in $($variant.Name)" 17 | } 18 | if (-not $channel) { 19 | throw "Missing property ItchChannel in $($variant.Name)" 20 | } 21 | 22 | $target = "$($appid):$channel" 23 | 24 | if ($dryrun) { 25 | Write-Output "Would have run butler command:" 26 | Write-Output " > butler push --userversion=$version '$sourcefolder' $target" 27 | } else { 28 | Write-Output "Releasing version $version to Itch.io at $target" 29 | Write-Output " Source: $sourcefolder" 30 | 31 | butler push --userversion=$version "$sourcefolder" $target 32 | if (!$?) { 33 | throw "Itch butler tool failed!" 34 | } 35 | } 36 | 37 | Write-Output ">>>--- Itch Upload Done! ---<<<" 38 | Write-Output "" 39 | 40 | } -------------------------------------------------------------------------------- /inc/packageconfig.ps1: -------------------------------------------------------------------------------- 1 | 2 | class PackageVariant { 3 | # Name of the variant (can be anything) 4 | [string]$Name 5 | # Platform name (must be one supported by Unreal e.g. Win64) 6 | [string]$Platform 7 | # Configuration name i.e. Development, Shipping 8 | [string]$Configuration 9 | # List of cultures to cook into this variant. If omitted, use the project packaging settings 10 | [array]$Cultures 11 | # Additional arguments to send to the build command line 12 | [string]$ExtraBuildArguments 13 | # Whether to create a zip of this package (default false) 14 | [bool]$Zip 15 | # List of services this variant should be released to ("steam", "itch" currently supported) 16 | [array]$ReleaseTo 17 | # The Steam application ID, if you intend to send this variant to Steam 18 | [string]$SteamAppId 19 | # The Steam depot ID, if you intend to send this variant to Steam 20 | [string]$SteamDepotId 21 | # Steam login to use to deploy to Steam (if you haven't cached your credential already you'll get a login prompt) 22 | [string]$SteamLogin 23 | # Itch application identifier e.g. your-account/game-name, if you intend to send this variant to Itch 24 | [string]$ItchAppId 25 | # Itch channel, if you intend to send this variant to Itch (usually a platform) 26 | [string]$ItchChannel 27 | 28 | PackageVariant() { 29 | $this.Configuration = "Development" 30 | $this.Zip = $false 31 | } 32 | PackageVariant([PSCustomObject]$obj) { 33 | $this.Configuration = "Development" 34 | $this.Zip = $false 35 | 36 | # Override just properties that are set 37 | $obj.PSObject.Properties | ForEach-Object { 38 | try { 39 | $this.$($_.Name) = $_.Value 40 | } catch { 41 | Write-Host "Invalid property for package variant: $($_.Name) = $($_.Value)" 42 | } 43 | } 44 | 45 | } 46 | } 47 | 48 | # Our config for both building and releasing 49 | # Note that environment variables also have an effect: 50 | # - UEINSTALL: a specific UE install to use (default blank, find a version in UEROOT) 51 | # - UEROOT: Parent folder of all binary UE installs (default C:\Program Files\Epic Games) 52 | class PackageConfig { 53 | # The root of the folder structure which will contain packaged output 54 | # Will be structured $OutputDir/$version/$variant 55 | # If relative, will be considered relative to source folder 56 | [string]$OutputDir 57 | # Optional name to rename the output EXE to 58 | [string]$RenameExe 59 | # Folder to place zipped releases (named $target_$platform_$variant_$version.zip) 60 | # If relative, will be considered relative to source folder 61 | [string]$ZipDir 62 | # Optional project file name (relative or absolute). If missing will detect .uproject in source folder 63 | [string]$ProjectFile 64 | # Target name: this will usually be the name of your game 65 | [string]$Target 66 | # Whether to cook all maps (default true) 67 | [bool]$CookAllMaps 68 | # If CookAllMaps=false, list the map names you want to cook 69 | [array]$MapsIncluded 70 | # If CookAllMaps=true, list the map names you want to exclude from cooking 71 | [array]$MapsExcluded 72 | # Whether to combine assets into a pak file (default true) 73 | [bool]$UsePak 74 | # List of PackageVariant entries 75 | [array]$Variants 76 | # Names of the default variant(s) to package / release if unspecified 77 | [array]$DefaultVariants 78 | 79 | PackageConfig([PSCustomObject]$obj) { 80 | # Construct from JSON object 81 | $this.CookAllMaps = $false 82 | $this.UsePak = $true 83 | $this.Variants = @() 84 | 85 | # Override just properties that are set 86 | $obj.PSObject.Properties | ForEach-Object { 87 | if ($_.Name -ne "Variants") { 88 | try { 89 | # Nested array dealt with below 90 | $this.$($_.Name) = $_.Value 91 | } catch { 92 | Write-Host "Invalid property in root package config: $($_.Name) = $($_.Value)" 93 | } 94 | } 95 | } 96 | 97 | $this.Variants = $obj.Variants | ForEach-Object { 98 | [PackageVariant]::New($_) 99 | } 100 | } 101 | 102 | 103 | } 104 | 105 | # Read packageconfig.json file from a source location and return PackageConfig instance 106 | function Read-Package-Config { 107 | param ( 108 | [string]$srcfolder 109 | ) 110 | 111 | $configfile = Resolve-Path "$srcfolder\packageconfig.json" 112 | if (-not (Test-Path $configfile -PathType Leaf)) { 113 | throw "$srcfolder\packageconfig.json does not exist!" 114 | } 115 | 116 | $obj = (Get-Content $configfile) | ConvertFrom-Json 117 | 118 | return [PackageConfig]::New($obj) 119 | 120 | } 121 | -------------------------------------------------------------------------------- /inc/platform.ps1: -------------------------------------------------------------------------------- 1 | # Simplify platform checks for Powershell < 6 2 | if (-not $PSVersionTable.Platform) { 3 | # This is Windows-only powershell 4 | $global:IsWindows = $true 5 | $global:IsLinux = $false 6 | $global:IsMacOS = $false 7 | } 8 | 9 | 10 | $exeSuffix = "" 11 | $batchSuffix = ".sh" 12 | if ($IsWindows) { 13 | $exeSuffix = ".exe" 14 | } 15 | if ($IsWindows) { 16 | $batchSuffix = ".bat" 17 | } 18 | 19 | 20 | function Get-Platform { 21 | if ($IsWindows) { 22 | return "Win64" 23 | } elseif ($IsLinux) { 24 | return "Linux" 25 | } else { 26 | return "Mac" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /inc/pluginconfig.ps1: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PluginConfig { 4 | [string]$PackageDir 5 | [string]$BuildDir 6 | [string]$PluginFile 7 | [array]$EngineVersions 8 | 9 | PluginConfig([PSCustomObject]$obj) { 10 | # Construct from JSON object 11 | 12 | # Override just properties that are set 13 | $obj.PSObject.Properties | ForEach-Object { 14 | try { 15 | # Nested array dealt with below 16 | $this.$($_.Name) = $_.Value 17 | } catch { 18 | Write-Host "Invalid property in plugin config: $($_.Name) = $($_.Value)" 19 | } 20 | } 21 | 22 | } 23 | 24 | 25 | } 26 | 27 | # Read pluginconfig.json file from a source location and return PluginConfig instance 28 | function Read-Plugin-Config { 29 | param ( 30 | [string]$srcfolder 31 | ) 32 | 33 | $configfile = Resolve-Path "$srcfolder\pluginconfig.json" 34 | if (-not (Test-Path $configfile -PathType Leaf)) { 35 | throw "$srcfolder\pluginconfig.json does not exist!" 36 | } 37 | 38 | $obj = (Get-Content $configfile) | ConvertFrom-Json 39 | 40 | return [PluginConfig]::New($obj) 41 | 42 | } 43 | -------------------------------------------------------------------------------- /inc/pluginversion.ps1: -------------------------------------------------------------------------------- 1 | 2 | function Get-NextPluginVersion { 3 | 4 | param ( 5 | [string]$currentVersion, 6 | [bool]$major, 7 | [bool]$minor, 8 | [bool]$patch, 9 | [bool]$hotfix 10 | ) 11 | 12 | if (($major + $minor + $patch + $hotfix) -gt 1) { 13 | throw "Can't set more than one of major/minor/patch/hotfix at the same time!" 14 | } 15 | 16 | 17 | # Regex features: 18 | # - Can read 2-4 version components but will pad with 0s up to 4 when writing 19 | # - captures pre- and post-fix text and retains 20 | $regex = "([^\d]*)(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)" 21 | $matches = $currentVersion | Select-String -Pattern $regex 22 | # 1 = prefix 23 | # 2-5 = version number components 24 | # 6 = postfix 25 | if (($matches.Matches.Count -gt 0) -and ($matches.Matches[0].Groups.Count -eq 7)) { 26 | $prefix = $matches.Matches[0].Groups[1].Value 27 | $postfix = $matches.Matches[0].Groups[6].Value 28 | 29 | $intversions = $matches.Matches[0].Groups[2..5] | ForEach-Object { 30 | if ($_.Value -ne "") { 31 | [int]$_.Value 32 | } else { 33 | # We fill in the version numbers to 4 digits always 34 | 0 35 | } 36 | 37 | } 38 | 39 | $versionDigit = 2; 40 | if ($major) { 41 | $versionDigit = 0 42 | } elseif ($minor) { 43 | $versionDigit = 1 44 | } elseif ($patch) { 45 | $versionDigit = 2 46 | } elseif ($hotfix) { 47 | $versionDigit = 3 48 | } 49 | # increment then zero anything after 50 | $intversions[$versionDigit]++ 51 | for ($d = $versionDigit + 1; $d -lt $intversions.Length; $d++) { 52 | $intversions[$d] = 0 53 | } 54 | 55 | $newver = "$prefix$($intversions[0]).$($intversions[1]).$($intversions[2]).$($intversions[3])$postfix" 56 | Write-Verbose "[version++] Bumping version to $newver" 57 | 58 | return "$newver" 59 | 60 | } else { 61 | throw "[version++] Error: unable to read current version" 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /inc/projectversion.ps1: -------------------------------------------------------------------------------- 1 | Import-Module PsIni 2 | 3 | function Get-Project-Version-Ini-Filename { 4 | param ( 5 | [string]$srcfolder 6 | ) 7 | 8 | return Join-Path $srcfolder "Config/DefaultGame.ini" -Resolve 9 | } 10 | 11 | function Get-Project-Version { 12 | param ( 13 | [string]$srcfolder 14 | ) 15 | 16 | $file = Get-Project-Version-Ini-Filename $srcfolder 17 | $gameIni = Get-IniContent $file 18 | 19 | return $gameIni["/Script/EngineSettings.GeneralProjectSettings"].ProjectVersion 20 | 21 | } 22 | function Get-ProjectVersionComponents { 23 | param ( 24 | [string]$srcfolder 25 | ) 26 | 27 | $versionString = Get-Project-Version $srcfolder 28 | # Regex features: 29 | # - Can read 2-4 version components but will pad with 0s up to 4 when writing 30 | # - captures pre- and post-fix text and retains 31 | $regex = "([^\d]*)(\d+)\.(\d+)(?:\.(\d+))?(?:\.(\d+))?(.*)" 32 | $matches = $versionString | Select-String -Pattern $regex 33 | # 1 = prefix 34 | # 2-5 = version number components 35 | # 6 = postfix 36 | 37 | if (($matches.Matches.Count -gt 0) -and ($matches.Matches[0].Groups.Count -eq 7)) { 38 | $prefix = $matches.Matches[0].Groups[1].Value 39 | $postfix = $matches.Matches[0].Groups[6].Value 40 | 41 | $intversions = $matches.Matches[0].Groups[2..5] | ForEach-Object { 42 | if ($_.Value -ne "") { 43 | [int]$_.Value 44 | } else { 45 | # We fill in the version numbers to 4 digits always 46 | 0 47 | } 48 | 49 | } 50 | 51 | return New-Object PsObject -Property @{prefix=$prefix ; postfix=$postfix; digits=$intversions} 52 | } else { 53 | return New-Object PsObject -Property @{prefix="" ; postfix=""; digits=@(1,0,0,0)} 54 | } 55 | } 56 | function Write-ProjectVersionFromObject { 57 | param ( 58 | [string]$srcfolder, 59 | [object]$versionObj, 60 | [bool]$dryrun = $false 61 | ) 62 | 63 | $newver = "$($versionObj.prefix)$($versionObj.digits[0]).$($versionObj.digits[1]).$($versionObj.digits[2]).$($versionObj.digits[3])$($versionObj.postfix)" 64 | Write-Project-Version -srcfolder:$srcfolder -newversion:$newver -dryrun:$dryrun 65 | 66 | } 67 | 68 | function Write-Project-Version { 69 | param ( 70 | [string]$srcfolder, 71 | [string]$newversion, 72 | [bool]$dryrun = $false 73 | ) 74 | 75 | $gameIniFile = Get-Project-Version-Ini-Filename $srcfolder 76 | 77 | if ($dryrun) { 78 | Write-Verbose "[version] dryrun: would have set $gameIniFile version: $newversion" 79 | } else { 80 | # We don't use PsIni to write, because it can screw up some nested non-trivial properties :( 81 | #$gameIni["/Script/EngineSettings.GeneralProjectSettings"].ProjectVersion = $newver 82 | #Out-IniFile -Force -InputObject $gameIni -FilePath $gameIniFile 83 | 84 | $verlineregex = "ProjectVersion=.*" 85 | $matches = Select-String -Path "$gameIniFile" -Pattern $verlineregex 86 | 87 | if ($matches.Matches.Count -gt 0) { 88 | $origline = $matches.Matches[0].Value 89 | $newline = "ProjectVersion=$newversion" 90 | 91 | (Get-Content "$gameIniFile").replace($origline, $newline) | Set-Content "$gameIniFile" 92 | Write-Verbose "[version++] Success! Version is now $newversion" 93 | 94 | } else { 95 | throw "[version++] Error: unable to substitute current version, unable to find '$verlineregex'" 96 | } 97 | 98 | 99 | } 100 | 101 | } 102 | function Increment-Project-Version { 103 | 104 | param ( 105 | [string]$srcfolder, 106 | [bool]$major, 107 | [bool]$minor, 108 | [bool]$patch, 109 | [bool]$hotfix, 110 | [bool]$dryrun = $false 111 | ) 112 | 113 | if (($major + $minor + $patch + $hotfix) -gt 1) { 114 | throw "Can't set more than one of major/minor/patch/hotfix at the same time!" 115 | } 116 | 117 | $versionobj = Get-ProjectVersionComponents $srcfolder 118 | 119 | $gameIniFile = Get-Project-Version-Ini-Filename $srcfolder 120 | 121 | Write-Verbose "[version++] M:$major m:$minor p:$patch h:$hotfix" 122 | 123 | # We have to use Write-Verbose now that we're using the return value, Write-Output 124 | # appends to the return value. Write-Verbose works but doesn't appear by default 125 | # Unless user sets $VerbosePreference="Continue" 126 | 127 | # Bump the version number of the build 128 | Write-Verbose "[inc_version] Updating $gameIniFile" 129 | 130 | Write-Verbose "[version++] Current version is $($versionObj.digits[0]).$($versionObj.digits[1]).$($versionObj.digits[2]).$($versionObj.digits[3])" 131 | 132 | $versionDigit = 2; 133 | if ($major) { 134 | $versionDigit = 0 135 | } elseif ($minor) { 136 | $versionDigit = 1 137 | } elseif ($patch) { 138 | $versionDigit = 2 139 | } elseif ($hotfix) { 140 | $versionDigit = 3 141 | } 142 | # increment then zero anything after 143 | $versionObj.digits[$versionDigit]++ 144 | for ($d = $versionDigit + 1; $d -lt $versionObj.digits.Length; $d++) { 145 | $versionObj.digits[$d] = 0 146 | } 147 | 148 | $newver = "$($versionObj.prefix)$($versionObj.digits[0]).$($versionObj.digits[1]).$($versionObj.digits[2]).$($versionObj.digits[3])$($versionObj.postfix)" 149 | Write-Verbose "[version++] Bumping version to $newver" 150 | 151 | Write-Project-Version -srcfolder:$srcfolder -newversion:$newver -dryrun:$dryrun 152 | 153 | return "$newver" 154 | 155 | } 156 | 157 | -------------------------------------------------------------------------------- /inc/steam.ps1: -------------------------------------------------------------------------------- 1 | function Release-Steam { 2 | param ( 3 | [PackageConfig]$config, 4 | [PackageVariant]$variant, 5 | [string]$sourcefolder, 6 | [string]$version, 7 | [switch]$dryrun = $false 8 | ) 9 | 10 | Write-Output ">>>--- Steam Upload Start ---<<<" 11 | 12 | $appid = $variant.SteamAppId 13 | $depotid = $variant.SteamDepotId 14 | $login = $variant.SteamLogin 15 | 16 | if (-not $appid) { 17 | throw "Missing property SteamAppId in $($variant.Name)" 18 | } 19 | if (-not $depotid) { 20 | throw "Missing property SteamDepotId in $($variant.Name)" 21 | } 22 | if (-not $login) { 23 | throw "Missing property SteamLogin in $($variant.Name)" 24 | } 25 | 26 | 27 | $steamconfigdir = Join-Path (Get-Item $sourcefolder).Parent "SteamConfig" 28 | New-Item -ItemType Directory $steamconfigdir -Force > $null 29 | 30 | # Preview mode in Steam build just outputs logs so it's dryrun 31 | $preview = if($dryrun) { "1" } else { "0"} 32 | 33 | # Use the UE platform as Steam target 34 | $target = $variant.Platform 35 | 36 | # write app file up to depot section then fill that in as we do depots 37 | $appfile = "$steamconfigdir\app_build_$($appid).vdf" 38 | Write-Output "Creating app build config $appfile" 39 | Remove-Item $appfile -Force -ErrorAction SilentlyContinue 40 | $appfp = New-Object -TypeName System.IO.FileStream( 41 | $appfile, 42 | [System.IO.FileMode]::Create, 43 | [System.IO.FileAccess]::Write) 44 | $appstream = New-Object System.IO.StreamWriter ($appfp, [System.Text.Encoding]::UTF8) 45 | 46 | $appstream.WriteLine("`"appbuild`"") 47 | $appstream.WriteLine("{") 48 | $appstream.WriteLine(" `"appid`" `"$appid`"") 49 | $appstream.WriteLine(" `"desc`" `"$version`"") 50 | $appstream.WriteLine(" `"buildoutput`" `".\steamcmdbuild`"") 51 | # we don't set contentroot in app file, we specify in depot files 52 | $appstream.WriteLine(" `"setlive`" `"`"") # never try to set live 53 | $appstream.WriteLine(" `"preview`" `"$preview`"") 54 | $appstream.WriteLine(" `"local`" `"`"") 55 | $appstream.WriteLine(" `"depots`"") 56 | $appstream.WriteLine(" {") 57 | 58 | # Depots inline 59 | # Just one in this case 60 | $depotfilerel = "depot_${target}_${depotid}.vdf" 61 | $depotfile = "$steamconfigdir\$depotfilerel" 62 | Write-Output "Creating depot build config $depotfile" 63 | Remove-Item $depotfile -Force -ErrorAction SilentlyContinue 64 | $depotfp = New-Object -TypeName System.IO.FileStream( 65 | $depotfile, 66 | [System.IO.FileMode]::Create, 67 | [System.IO.FileAccess]::Write) 68 | $depotstream = New-Object System.IO.StreamWriter($depotfp, [System.Text.Encoding]::UTF8) 69 | $depotstream.WriteLine("`"DepotBuildConfig`"") 70 | $depotstream.WriteLine("{") 71 | $depotstream.WriteLine(" `"DepotID`" `"$depotid`"") 72 | # We'll set ContentRoot specifically for 73 | $depotstream.WriteLine(" `"ContentRoot`" `"$sourcefolder`"") 74 | $depotstream.WriteLine(" `"FileMapping`"") 75 | $depotstream.WriteLine(" {") 76 | $depotstream.WriteLine(" `"LocalPath`" `"*`"") 77 | $depotstream.WriteLine(" `"DepotPath`" `".`"") 78 | $depotstream.WriteLine(" `"recursive`" `"1`"") 79 | $depotstream.WriteLine(" }") 80 | $depotstream.WriteLine(" `"FileExclusion`" `"*.pdb`"") 81 | $depotstream.WriteLine("}") 82 | $depotstream.Close() 83 | $depotfp.Close() 84 | 85 | # Now write depot entry to in-progress app file, relative file (same folder) 86 | $appstream.WriteLine(" `"$depotid`" `"$depotfilerel`"") 87 | 88 | # Finish the app file 89 | $appstream.WriteLine(" }") 90 | $appstream.WriteLine("}") 91 | $appstream.Close() 92 | 93 | if ($dryrun) { 94 | Write-Output "Would have run Steam command:" 95 | Write-Output " > steamcmd +login $($login) +run_app_build_http $appfile +quit" 96 | } else { 97 | Write-Output "Releasing version $version to Steam ($appid)" 98 | steamcmd +login $($login) +run_app_build_http $appfile +quit 99 | if (!$?) { 100 | throw "Steam upload tool failed!" 101 | } 102 | } 103 | 104 | Write-Output ">>>--- Steam Upload Done ---<<<" 105 | Write-Output "" 106 | if (-not $dryrun) { 107 | Write-Output "-- Remember to release in Steamworks Admin --" 108 | } 109 | 110 | } -------------------------------------------------------------------------------- /inc/ueeditor.ps1: -------------------------------------------------------------------------------- 1 | 2 | function Close-UE-Editor { 3 | param ( 4 | [string]$uprojectname, 5 | [bool]$dryrun 6 | ) 7 | 8 | # Filter by project name in main window title, it's always called "Project - Unreal Editor" 9 | $ue4proc = Get-Process UE4Editor -ErrorAction SilentlyContinue | Where-Object {$_.MainWindowTitle -like "$uprojectname*" } 10 | if ($ue4proc) { 11 | if ($dryrun) { 12 | Write-Output "UE4 project is currently open in editor, would have closed" 13 | } else { 14 | Write-Output "UE4 project is currently open in editor, closing..." 15 | $ue4proc.CloseMainWindow() > $null 16 | Start-Sleep 5 17 | if (!$ue4proc.HasExited) { 18 | throw "Couldn't close UE4 gracefully, aborting!" 19 | } 20 | } 21 | } 22 | Remove-Variable ue4proc 23 | 24 | # Also close UE5 25 | $ue5proc = Get-Process UnrealEditor -ErrorAction SilentlyContinue | Where-Object {$_.MainWindowTitle -like "$uprojectname*" } 26 | if ($ue5proc) { 27 | if ($dryrun) { 28 | Write-Output "UE5 project is currently open in editor, would have closed" 29 | } else { 30 | Write-Output "UE5 project is currently open in editor, closing..." 31 | $ue5proc.CloseMainWindow() > $null 32 | Start-Sleep 5 33 | if (!$ue5proc.HasExited) { 34 | throw "Couldn't close UE5 gracefully, aborting!" 35 | } 36 | } 37 | } 38 | Remove-Variable ue5proc 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /inc/uplugin.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\packageconfig.ps1 2 | 3 | 4 | function Get-Uplugin-Filename { 5 | param ( 6 | [string]$srcfolder, 7 | [PluginConfig]$config 8 | ) 9 | 10 | $projfile = "" 11 | if ($config -and $config.ProjectFile) { 12 | if (-not [System.IO.Path]::IsPathRooted($config.PluginFile)) { 13 | $projfile = Join-Path $srcfolder $config.PluginFile 14 | } else { 15 | $projfile = Resolve-Path $config.PluginFile 16 | } 17 | 18 | if (-not (Test-Path $projfile)) { 19 | throw "Invalid ProfileFile setting, $($config.PluginFile) does not exist." 20 | } 21 | 22 | } else { 23 | # can return multiple results, pick the first one 24 | $matchedfile = @(Get-ChildItem -Path $srcfolder -Filter *.uplugin)[0] 25 | $projfile = $matchedfile.FullName 26 | } 27 | 28 | # Resolve to absolute (do it here and not in join so missing file is friendlier error) 29 | if ($projfile) { 30 | return Resolve-Path $projfile 31 | } else { 32 | return $projfile 33 | } 34 | } 35 | 36 | function Update-UpluginUeVersion { 37 | [string]$srcfolder, 38 | [PluginConfig]$config, 39 | [string]$version 40 | 41 | $pluginfile = Get-Uplugin-Filename $srcfolder $config 42 | $plugincontents = (Get-Content $pluginfile) | ConvertFrom-Json 43 | $proj.EngineVersion = $version 44 | $newjson = ($plugincontents | ConvertTo-Json -depth 100) 45 | # Need to explicitly set to UTF8, Out-File now converts to UTF16-LE?? 46 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 47 | [System.IO.File]::WriteAllLines($pluginfile, $newjson, $Utf8NoBomEncoding) 48 | } -------------------------------------------------------------------------------- /inc/uproject.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\packageconfig.ps1 2 | 3 | 4 | function Get-Uproject-Filename { 5 | param ( 6 | [string]$srcfolder, 7 | [PackageConfig]$config 8 | ) 9 | 10 | $projfile = "" 11 | if ($config -and $config.ProjectFile) { 12 | if (-not [System.IO.Path]::IsPathRooted($config.ProjectFile)) { 13 | $projfile = Join-Path $srcfolder $config.ProjectFile 14 | } else { 15 | $projfile = Resolve-Path $config.ProjectFile 16 | } 17 | 18 | if (-not (Test-Path $projfile)) { 19 | throw "Invalid ProfileFile setting, $($config.ProjectFile) does not exist." 20 | } 21 | 22 | } else { 23 | # can return multiple results, pick the first one 24 | $matchedfile = @(Get-ChildItem -Path $srcfolder -Filter *.uproject)[0] 25 | $projfile = $matchedfile.FullName 26 | } 27 | 28 | # Resolve to absolute (do it here and not in join so missing file is friendlier error) 29 | if ($projfile) { 30 | return Resolve-Path $projfile 31 | } else { 32 | return $projfile 33 | } 34 | } 35 | 36 | # Read the uproject file and return as a PSCustomObject 37 | # Haven't defined this as a custom class because we don't control it 38 | function Read-Uproject { 39 | param ( 40 | [string]$uprojectfile 41 | ) 42 | 43 | # uproject is just JSON 44 | return (Get-Content $uprojectfile) | ConvertFrom-Json 45 | 46 | } 47 | 48 | function Get-UE-Version { 49 | param ( 50 | # the uproject object from Read-Uproject 51 | [psobject]$uproject 52 | ) 53 | 54 | if ($uproject.EngineAssociation) { 55 | $assoc = $uproject.EngineAssociation 56 | } else { 57 | # Plugin 58 | $assoc = $uproject.EngineVersion 59 | } 60 | 61 | # If this is a GUID "{A1234786-..}" then it's a source build, we need to resolve it via registry 62 | if ($assoc.StartsWith("{")) { 63 | # Look up the source dir from registry setting 64 | $srcdir = Get-ItemPropertyValue 'Registry::HKEY_CURRENT_USER\Software\Epic Games\Unreal Engine\Builds' -Name $assoc 65 | # In source build, read Build.version JSON 66 | $buildverfile = Join-Path $srcdir "Engine/Build/Build.version" 67 | $buildjson = (Get-Content $buildverfile) | ConvertFrom-Json 68 | return "$($buildjson.MajorVersion).$($buildjson.MinorVersion)" 69 | } else { 70 | return $assoc 71 | } 72 | } 73 | 74 | function Get-Is-UE5 { 75 | param ( 76 | # the uproject object from Read-Uproject 77 | [string]$ueVersion 78 | ) 79 | 80 | return $ueVersion.StartsWith("5.") 81 | } 82 | 83 | function Get-UE-Install { 84 | param ( 85 | [string]$ueVersion 86 | ) 87 | 88 | # UEINSTALL env var should point at the root of the *specific version* of 89 | # UE you want to use. This is mainly for use in source builds, default is 90 | # to build it from version number and root of all UE binary installs 91 | $uinstall = $Env:UEINSTALL 92 | # Backwards compat 93 | if (-not $uinstall) { 94 | $uinstall = $Env:UE4INSTALL 95 | } 96 | 97 | if (-not $uinstall) { 98 | # UEROOT should be the parent folder of all UE versions 99 | $uroot = $Env:UEROOT 100 | # Bakwards compat 101 | if (-not $uroot) { 102 | $uroot = $Env:UE4ROOT 103 | } 104 | if (-not $uroot) { 105 | $uroot = "C:\Program Files\Epic Games" 106 | } 107 | 108 | # When using $ueVersion, strip off 3rd digit if any 109 | $regex = "(\d+\.\d+)(\.\d+)?" 110 | $match = $ueVersion | Select-String -Pattern $regex 111 | 112 | $ueVersionTrimmed = $match.Matches[0].Groups[1].Value 113 | 114 | $uinstall = Join-Path $uroot "UE_$ueVersionTrimmed" 115 | } 116 | 117 | # Test we can find RunUAT.bat 118 | $batchfolder = Join-Path "$uinstall" "Engine\Build\BatchFiles" 119 | $buildbat = Join-Path "$batchfolder" "RunUAT.bat" 120 | if (-not (Test-Path $buildbat -PathType Leaf)) { 121 | throw "RunUAT.bat missing at $buildbat : Not a valid UE install" 122 | } 123 | 124 | return $uinstall 125 | } 126 | 127 | function Get-UEEditorCmd { 128 | param ( 129 | [string]$ueVersion, 130 | [string]$ueInstall 131 | ) 132 | 133 | if ((Get-Is-UE5 $ueVersion)) { 134 | return Join-Path $ueInstall "Engine/Binaries/Win64/UnrealEditor-Cmd$exeSuffix" 135 | 136 | } else { 137 | return Join-Path $ueInstall "Engine/Binaries/Win64/UE4Editor-Cmd$exeSuffix" 138 | } 139 | 140 | } 141 | 142 | -------------------------------------------------------------------------------- /packageconfig_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputDir": "/Path/To/Output/Parent/Dir", 3 | "ZipDir": "/Optional/Path/To/Zipped/Releases/Folder", 4 | 5 | "ProjectFile": "OptionalProjectFilenameWillDetectInDirOtherwise.uproject", 6 | 7 | "Target": "GameTargetName", 8 | "RenameExe": "NewExeNameWithNoExtension", 9 | "CookAllMaps": true, 10 | "MapsIncluded": [ 11 | "IfCookAllMapsIsFalse", 12 | "ListMapsToCookHere" 13 | ], 14 | "MapsExcluded": [ 15 | "IfCookAllMapsIsTrue", 16 | "ListMapsToExcludeHere" 17 | ], 18 | "UsePak": true, 19 | 20 | "DefaultVariants": [ 21 | "PrivateWin64Build" 22 | ], 23 | 24 | "Variants": [ 25 | { 26 | "Name": "PrivateWin64Build", 27 | "Platform": "Win64", 28 | "Configuration": "Development", 29 | "Zip": true, 30 | "ExtraBuildArguments": "-Any -Custom -Args=ToRunUAT -OrOtherCommandlets" 31 | }, 32 | { 33 | "Name": "PublicWin64SteamBuild", 34 | "Platform": "Win64", 35 | "Configuration": "Shipping", 36 | "ReleaseTo": [ 37 | "Steam" 38 | ], 39 | "SteamAppId": "YourSteamAppId", 40 | "SteamDepotId": "YourWindowsDepotId", 41 | "SteamLogin": "YourSteamReleaseUser", 42 | "Zip": false, 43 | "ExtraBuildArguments": "-EnableSteamworks", 44 | "Cultures": [ 45 | "ListOfCulturesToInclude", 46 | "IfNotSpecified", 47 | "WillUseProjectPackageSettings" 48 | ] 49 | }, 50 | { 51 | "Name": "PublicWin64Build", 52 | "Platform": "Win64", 53 | "Configuration": "Shipping", 54 | "ReleaseTo": [ 55 | "Itch", 56 | "SomeOtherService" 57 | ], 58 | "ItchAppId": "itch-user/app-name", 59 | "ItchChannel": "win64", 60 | "Zip": false 61 | } 62 | ], 63 | 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /pluginconfig_template.json: -------------------------------------------------------------------------------- 1 | { 2 | "OutputDir": "/Path/To/Output/Parent/Dir", 3 | 4 | "PluginFile": "OptionalPluginFilenameWillDetectInDirOtherwise.uplugin", 5 | 6 | "EngineVersions": 7 | [ 8 | "5.0", 9 | "5.1", 10 | "5.2" 11 | ] 12 | 13 | } 14 | 15 | 16 | -------------------------------------------------------------------------------- /ue-blueprint-recompile.ps1: -------------------------------------------------------------------------------- 1 | # Blueprint bulk recompile helper 2 | [CmdletBinding()] # Fail on unknown args 3 | param ( 4 | # Optional source folder, assumed current folder 5 | [string]$src, 6 | # Optional subfolder of Content to parse, default "Blueprints" 7 | [string]$bpdir = "Blueprints", 8 | # Dry-run; does nothing but report what *would* have happened 9 | [switch]$dryrun = $false, 10 | [switch]$help = $false 11 | ) 12 | 13 | . $PSScriptRoot\inc\platform.ps1 14 | . $PSScriptRoot\inc\packageconfig.ps1 15 | . $PSScriptRoot\inc\projectversion.ps1 16 | . $PSScriptRoot\inc\uproject.ps1 17 | . $PSScriptRoot\inc\filetools.ps1 18 | 19 | # Include Git tools locking 20 | . $PSScriptRoot\GitScripts\inc\locking.ps1 21 | 22 | function Write-Usage { 23 | Write-Output "Steve's Unreal Blueprint recompile tool" 24 | Write-Output "Usage:" 25 | Write-Output " ue-blueprint-recompile.ps1 [-src:sourcefolder] [-bpdir:blueprintdir] [-dryrun]" 26 | Write-Output " " 27 | Write-Output " -src : Source folder (current folder if omitted)" 28 | Write-Output " -bpdir : Path to Blueprints relative to your Content dir, defaults to 'Blueprints'" 29 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 30 | Write-Output " -help : Print this help" 31 | Write-Output " " 32 | Write-Output "Environment Variables:" 33 | Write-Output " UEINSTALL : Use a specific UE install." 34 | Write-Output " : Default is to find one based on project version, under UEROOT" 35 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 36 | Write-Output " : Default C:\Program Files\Epic Games" 37 | Write-Output " " 38 | } 39 | 40 | if ($src.Length -eq 0) { 41 | $src = "." 42 | Write-Verbose "-src not specified, assuming current directory" 43 | } 44 | 45 | $ErrorActionPreference = "Stop" 46 | 47 | if ($help) { 48 | Write-Usage 49 | Exit 0 50 | } 51 | 52 | Write-Output "~-~-~ Unreal Blueprint Recompile Start ~-~-~" 53 | 54 | try { 55 | 56 | $config = Read-Package-Config -srcfolder:$src 57 | $projfile = Get-Uproject-Filename -srcfolder:$src -config:$config 58 | $proj = Read-Uproject $projfile 59 | $ueVersion = Get-UE-Version $proj 60 | $ueinstall = Get-UE-Install $ueVersion 61 | 62 | Write-Output "" 63 | Write-Output "Project File : $projfile" 64 | Write-Output "UE Version : $ueVersion" 65 | Write-Output "UE Install : $ueinstall" 66 | Write-Output "Blueprint Dir : Content/$bpdir" 67 | Write-Output "" 68 | 69 | $bpfullpath = Join-Path $src "Content/$bpdir" -Resolve 70 | 71 | $argList = [System.Collections.ArrayList]@() 72 | $argList.Add("`"$projfile`"") > $null 73 | $argList.Add("-run=ResavePackages") > $null 74 | $argList.Add("-packagefolder=`"$bpfullpath`"") > $null 75 | $argList.Add("-autocheckout") > $null 76 | 77 | $ueEditorCmd = Get-UEEditorCmd $ueVersion $ueinstall 78 | 79 | if ($dryrun) { 80 | Write-Output "Would have run:" 81 | Write-Output "> $ueEditorCmd $($argList -join " ")" 82 | 83 | } else { 84 | $proc = Start-Process $ueEditorCmd $argList -Wait -PassThru -NoNewWindow 85 | if ($proc.ExitCode -ne 0) { 86 | throw "Blueprint recompile build failed!" 87 | } 88 | 89 | } 90 | 91 | } catch { 92 | Write-Output $_.Exception.Message 93 | Write-Output "~-~-~ Unreal Blueprint Recompile FAILED ~-~-~" 94 | Exit 9 95 | 96 | } 97 | 98 | 99 | Write-Output "~-~-~ Unreal Blueprint Recompile OK ~-~-~" 100 | if (!$dryrun) { 101 | Write-Output "Reminder: You may need to commit and unlock Blueprint files" 102 | } 103 | -------------------------------------------------------------------------------- /ue-build-plugin.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$mode, 4 | [string]$src, 5 | [switch]$allplatforms = $false, 6 | [switch]$allversions = $false, 7 | [string]$uever = "", 8 | [switch]$nocloseeditor = $false, 9 | [switch]$dryrun = $false, 10 | [switch]$help = $false 11 | ) 12 | 13 | . $PSScriptRoot\inc\platform.ps1 14 | . $PSScriptRoot\inc\pluginconfig.ps1 15 | . $PSScriptRoot\inc\pluginversion.ps1 16 | . $PSScriptRoot\inc\uproject.ps1 17 | . $PSScriptRoot\inc\uplugin.ps1 18 | . $PSScriptRoot\inc\filetools.ps1 19 | 20 | function Print-Usage { 21 | Write-Output "Steve's Unreal Plugin Build Tool" 22 | Write-Output "Usage:" 23 | Write-Output " ue-build-plugin.ps1 [[-src:]sourcefolder] [Options]" 24 | Write-Output " " 25 | Write-Output " -src : Source folder (current folder if omitted)" 26 | Write-Output " : (should be root of project)" 27 | Write-Output " -allplatforms : Build for all platforms, not just the current one" 28 | Write-Output " -allversions : Build for all supported UE versions, not just the current one" 29 | Write-Output " : (specified in pluginconfig.json, only works with lancher-installed UE)" 30 | Write-Output " -uever:5.x.x : Build for a specific UE version, not the current one (launcher only)" 31 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 32 | Write-Output " -help : Print this help" 33 | Write-Output " " 34 | Write-Output "Environment Variables:" 35 | Write-Output " UEINSTALL : Use a specific Unreal install." 36 | Write-Output " : Default is to find one based on project version, under UEROOT" 37 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 38 | Write-Output " : Default C:\Program Files\Epic Games" 39 | Write-Output " " 40 | 41 | } 42 | 43 | $ErrorActionPreference = "Stop" 44 | 45 | if ($src.Length -eq 0) { 46 | $src = "." 47 | Write-Verbose "-src not specified, assuming current directory" 48 | } 49 | 50 | if ($help) { 51 | Print-Usage 52 | Exit 0 53 | } 54 | 55 | 56 | $result = 0 57 | 58 | try { 59 | if ($src -ne ".") { Push-Location $src } 60 | 61 | Write-Output "-- Build plugin process starting --" 62 | 63 | $config = Read-Plugin-Config -srcfolder:$src 64 | 65 | # Locate Unreal project file 66 | $pluginfile = Get-Uplugin-Filename -srcfolder:$src -config:$config 67 | if (-not $pluginfile) { 68 | throw "Not in a uplugin dir!" 69 | } 70 | 71 | $proj = Read-Uproject $pluginfile 72 | $origUeVersion = Get-UE-Version $proj 73 | if ($allversions) { 74 | $ueVersions = $config.EngineVersions 75 | } elseif ($uever.Length -gt 0) { 76 | $ueVersions = @($uever) 77 | } else { 78 | $ueVersions = @($origUeVersion) 79 | } 80 | 81 | 82 | Write-Output "" 83 | Write-Output "Project File : $pluginfile" 84 | Write-Output "UE Version(s) : $($ueVersions -join `", `")" 85 | Write-Output "Output Folder : $($config.BuildDir)" 86 | Write-Output "" 87 | 88 | foreach ($ver in $ueVersions) { 89 | 90 | Write-Output "Building for UE Version $ver" 91 | $ueinstall = Get-UE-Install $ver 92 | $outputDir = Join-Path $config.BuildDir $ver 93 | 94 | # Need to change the version in the plugin while we build 95 | if (-not $dryrun -and ($allversions -or $ueVer.Length -gt 0)) { 96 | Update-UpluginUeVersion $src $config $ver 97 | } 98 | 99 | $runUAT = Join-Path $ueinstall "Engine/Build/BatchFiles/RunUAT$batchSuffix" 100 | 101 | $argList = [System.Collections.ArrayList]@() 102 | $argList.Add("BuildPlugin") > $null 103 | $argList.Add("-Plugin=`"$pluginfile`"") > $null 104 | $argList.Add("-Package=`"$outputDir`"") > $null 105 | $argList.Add("-Rocket") > $null 106 | 107 | if (-not $allplatforms) { 108 | $targetPlatform = Get-Platform 109 | $argList.Add("-TargetPlatforms=$targetPlatform") > $null 110 | } 111 | 112 | if ($dryrun) { 113 | Write-Output "" 114 | Write-Output "Would have run:" 115 | Write-Output "> $runUAT $($argList -join " ")" 116 | Write-Output "" 117 | 118 | } else { 119 | $proc = Start-Process $runUAT $argList -Wait -PassThru -NoNewWindow 120 | if ($proc.ExitCode -ne 0) { 121 | # Reset the plugin back to the original UE version 122 | if ($allversions -and -not $dryrun) { 123 | Update-UpluginUeVersion $src $config $origUeVersion 124 | } 125 | 126 | throw "RunUAT failed!" 127 | } 128 | } 129 | } 130 | 131 | # Reset the plugin back to the original UE version 132 | if ($allversions -and -not $dryrun) { 133 | Update-UpluginUeVersion $src $config $origUeVersion 134 | } 135 | 136 | Write-Output "-- Build plugin process finished OK --" 137 | 138 | } catch { 139 | Write-Output "ERROR: $($_.Exception.Message)" 140 | $result = 9 141 | } finally { 142 | if ($src -ne ".") { Pop-Location } 143 | } 144 | 145 | 146 | Exit $result 147 | -------------------------------------------------------------------------------- /ue-build.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$mode, 4 | [string]$src, 5 | [switch]$nocloseeditor = $false, 6 | [switch]$dryrun = $false, 7 | [switch]$help = $false 8 | ) 9 | 10 | function Print-Usage { 11 | Write-Output "Steve's Unreal Build Tool" 12 | Write-Output "Usage:" 13 | Write-Output " ue-build.ps1 [[-mode:]] [[-src:]sourcefolder] [Options]" 14 | Write-Output " " 15 | Write-Output " -mode : Build mode" 16 | Write-Output " : dev = build Development Editor, dlls only (default)" 17 | Write-Output " : cleandev = build Development Editor CLEANLY" 18 | Write-Output " : test = build Development and pacakge for test" 19 | Write-Output " : prod = build Shipping and package for production" 20 | Write-Output " -src : Source folder (current folder if omitted)" 21 | Write-Output " : (should be root of project)" 22 | Write-Output " -nocloseeditor : Don't close Unreal editor (this will prevent DLL cleanup)" 23 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 24 | Write-Output " -help : Print this help" 25 | Write-Output " " 26 | Write-Output "Environment Variables:" 27 | Write-Output " UEINSTALL : Use a specific Unreal install." 28 | Write-Output " : Default is to find one based on project version, under UEROOT" 29 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 30 | Write-Output " : Default C:\Program Files\Epic Games" 31 | Write-Output " " 32 | 33 | } 34 | 35 | $ErrorActionPreference = "Stop" 36 | 37 | 38 | if ($help) { 39 | Print-Usage 40 | Exit 0 41 | } 42 | 43 | if (-not $mode) { 44 | $mode = "dev" 45 | } 46 | 47 | if ($src.Length -eq 0) { 48 | $src = "." 49 | Write-Verbose "-src not specified, assuming current directory" 50 | } 51 | 52 | if (-not ($mode -in @('dev', 'cleandev', 'test', 'prod'))) { 53 | Print-Usage 54 | Write-Output "ERROR: Invalid mode argument: $mode" 55 | Exit 3 56 | 57 | } 58 | 59 | . $PSScriptRoot\inc\buildtargets.ps1 60 | 61 | $result = 0 62 | 63 | try { 64 | if ($src -ne ".") { Push-Location $src } 65 | 66 | Write-Output "-- Build process starting --" 67 | 68 | # Locate Unreal project file 69 | $uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name 70 | if (-not $uprojfile) { 71 | throw "No Unreal project file found in $(Get-Location)! Aborting." 72 | } 73 | if ($uprojfile -is [array]) { 74 | throw "Multiple Unreal project files found in $(Get-Location)! Aborting." 75 | } 76 | 77 | # In PS 6.0+ we could use Split-Path -LeafBase but let's stick with built-in PS 5.1 78 | $uprojname = [System.IO.Path]::GetFileNameWithoutExtension($uprojfile) 79 | if ($dryrun) { 80 | Write-Output "Would build $uprojname for $mode" 81 | } else { 82 | Write-Output "Building $uprojname for $mode" 83 | } 84 | 85 | # Check version number of Unreal project so we know which version to run 86 | # We can read this from .uproject which is JSON 87 | $uproject = Get-Content $uprojfile | ConvertFrom-Json 88 | $uversion = $uproject.EngineAssociation 89 | 90 | Write-Output "Engine version is $uversion" 91 | 92 | # UEINSTALL env var should point at the root of the *specific version* of 93 | # Unreal you want to use. This is mainly for use in source builds, default is 94 | # to build it from version number and root of all UE binary installs 95 | $uinstall = $Env:UEINSTALL 96 | 97 | # Backwards compat with old env var 98 | if (-not $uinstall) { 99 | $uinstall = $Env:UE4INSTALL 100 | } 101 | 102 | if (-not $uinstall) { 103 | # UEROOT should be the parent folder of all UE versions 104 | $uroot = $Env:UEROOT 105 | # Backwards compat with old env var 106 | if (-not $uroot) { 107 | $uroot = $Env:UE4ROOT 108 | } 109 | if (-not $uroot) { 110 | $uroot = "C:\Program Files\Epic Games" 111 | } 112 | 113 | $uinstall = Join-Path $uroot "UE_$uversion" 114 | } 115 | 116 | # Test we can find Build.bat 117 | $batchfolder = Join-Path "$uinstall" "Engine\Build\BatchFiles" 118 | $buildbat = Join-Path "$batchfolder" "Build.bat" 119 | if (-not (Test-Path $buildbat -PathType Leaf)) { 120 | throw "Build.bat missing at $buildbat : Aborting" 121 | } 122 | 123 | $buildargs = "" 124 | 125 | switch ($mode) { 126 | 'dev' { 127 | # Stolen from the VS project settings because boy is this badly documented 128 | # The -Project seems to be needed, as is the -FromMsBuild 129 | # -Project has to point at the ABSOLUTE PATH of the uproject 130 | $uprojfileabs = Join-Path "$(Get-Location)" $uprojfile 131 | $target = Find-DefaultTarget $src "Editor" 132 | $buildargs = "$target Win64 Development -Project=`"${uprojfileabs}`" -WaitMutex -FromMsBuild" 133 | } 134 | 'cleandev' { 135 | $uprojfileabs = Join-Path "$(Get-Location)" $uprojfile 136 | $target = Find-DefaultTarget $src "Editor" 137 | $buildargs = "$target Win64 Development -Project=`"${uprojfileabs}`" -WaitMutex -FromMsBuild -clean" 138 | } 139 | 'test' { 140 | $uprojfileabs = Join-Path "$(Get-Location)" $uprojfile 141 | $target = Find-DefaultTarget $src "Game" 142 | $buildargs = "$target Win64 Test -Project=`"${uprojfileabs}`" -WaitMutex -FromMsBuild -clean" 143 | } 144 | 'prod' { 145 | $uprojfileabs = Join-Path "$(Get-Location)" $uprojfile 146 | $target = Find-DefaultTarget $src "Game" 147 | $buildargs = "$target Win64 Shipping -Project=`"${uprojfileabs}`" -WaitMutex -FromMsBuild -clean" 148 | } 149 | default { 150 | # TODO 151 | # We probably want to use custom launch profiles for this 152 | Write-Output "Mode '$mode' is not supported yet" 153 | } 154 | } 155 | 156 | if ($dryrun) { 157 | Write-Output "Would run: build.bat $buildargs" 158 | } else { 159 | Write-Verbose "Running $buildbat $buildargs" 160 | 161 | $proc = Start-Process $buildbat $buildargs -Wait -PassThru -NoNewWindow 162 | if ($proc.ExitCode -ne 0) { 163 | $code = $proc.ExitCode 164 | throw "*** Build exited with code $code, see above" 165 | } 166 | } 167 | 168 | Write-Output "-- Build process finished OK --" 169 | 170 | } catch { 171 | Write-Output "ERROR: $($_.Exception.Message)" 172 | $result = 9 173 | } finally { 174 | if ($src -ne ".") { Pop-Location } 175 | } 176 | 177 | 178 | Exit $result 179 | -------------------------------------------------------------------------------- /ue-cleanup.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$src, 4 | [switch]$nocloseeditor = $false, 5 | [switch]$dryrun = $false, 6 | [switch]$help = $false 7 | ) 8 | 9 | function Print-Usage { 10 | Write-Output "Steve's Unreal Project Cleanup Tool" 11 | Write-Output " Clean up hot-reload DLLs & prune LFS to free space. Will close Unreal editor!" 12 | Write-Output "Usage:" 13 | Write-Output " ue-cleanup.ps1 [[-src:]sourcefolder] [Options]" 14 | Write-Output " " 15 | Write-Output " -src : Source folder (current folder if omitted)" 16 | Write-Output " : (should be root of project)" 17 | Write-Output " -nocloseeditor : Don't close Unreal editor (this will prevent DLL cleanup)" 18 | Write-Output " -lfsprune : Call 'git lfs prune' to delete old LFS files as well" 19 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 20 | Write-Output " -help : Print this help" 21 | Write-Output " " 22 | } 23 | 24 | function Cleanup-DLLs($cleanupdir, $projname, $dryrun) { 25 | if ($dryrun) { 26 | Write-Output "Would clean up temporary DLLs/PDBs in $cleanupdir for $projname" 27 | } else { 28 | Write-Output "Cleaning up temporary DLLs/PDBs in $cleanupdir for $projname" 29 | } 30 | # Hot Reload files - UE4 31 | $cleanupfiles = @(Get-ChildItem "$cleanupdir\UE4Editor-$projname-????.dll" | Select-Object -Expand Name) 32 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UE4Editor-$projname-????.pdb" | Select-Object -Expand Name) 33 | # Live Coding files - UE4 34 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UE4Editor-$projname.exe.patch_*" | Select-Object -Expand Name) 35 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UE4Editor-$projname.pdb.patch_*" | Select-Object -Expand Name) 36 | # Hot Reload files - UE5 37 | $cleanupfiles = @(Get-ChildItem "$cleanupdir\UnrealEditor-$projname-????.dll" | Select-Object -Expand Name) 38 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UnrealEditor-$projname-????.pdb" | Select-Object -Expand Name) 39 | # Live Coding files - UE5 40 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UnrealEditor-$projname.exe.patch_*" | Select-Object -Expand Name) 41 | $cleanupfiles += @(Get-ChildItem "$cleanupdir\UnrealEditor-$projname.pdb.patch_*" | Select-Object -Expand Name) 42 | foreach ($cf in $cleanupfiles) { 43 | if ($dryrun) { 44 | Write-Output "Would have deleted $cleanupdir\$cf" 45 | } else { 46 | Write-Verbose "Deleting $cleanupdir\$cf" 47 | Remove-Item "$cleanupdir\$cf" -Force 48 | } 49 | } 50 | 51 | 52 | } 53 | 54 | . $PSScriptRoot\inc\ueeditor.ps1 55 | 56 | $ErrorActionPreference = "Stop" 57 | 58 | if ($help) { 59 | Print-Usage 60 | Exit 0 61 | } 62 | 63 | $result = 0 64 | 65 | try { 66 | if ($src -ne ".") { Push-Location $src } 67 | 68 | # Locate UE project file 69 | $uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name 70 | if (-not $uprojfile) { 71 | throw "No Unreal project file found in $(Get-Location)! Aborting." 72 | } 73 | if ($uprojfile -is [array]) { 74 | throw "Multiple Unreal project files found in $(Get-Location)! Aborting." 75 | } 76 | 77 | # In PS 6.0+ we could use Split-Path -LeafBase but let's stick with built-in PS 5.1 78 | $uprojname = [System.IO.Path]::GetFileNameWithoutExtension($uprojfile) 79 | if ($dryrun) { 80 | Write-Output "Would clean up $uprojname" 81 | } else { 82 | Write-Output "Cleaning up $uprojname" 83 | } 84 | 85 | # Close UE as early as possible 86 | if (-not $nocloseeditor) { 87 | # Check if UE is running, if so try to shut it gracefully 88 | Close-UE-Editor $uprojname $dryrun 89 | 90 | # Find all the modules in the project 91 | $ujson = Get-Content $uprojfile | ConvertFrom-Json 92 | foreach ($module in $ujson.Modules) { 93 | # Because we know editor is closed, Hot Reload DLLs are OK to clean up 94 | Cleanup-DLLs ".\Binaries\Win64" $module.Name $dryrun 95 | } 96 | 97 | # Also clean up SOURCE plugins, since they will be rebuilt 98 | # This is not the same list as $ujson.Plugins, those are the binary ones 99 | $plugins = Get-ChildItem -Path .\Plugins -Filter *.uplugin -Recurse -ErrorAction SilentlyContinue -Force 100 | foreach ($pluginfile in $plugins) { 101 | $pluginname = [System.IO.Path]::GetFileNameWithoutExtension($pluginfile.FullName) 102 | if ($dryrun) { 103 | Write-Output "Would clean up plugin $pluginname" 104 | } else { 105 | Write-Output "Cleaning up plugin $pluginname" 106 | } 107 | $pluginroot = Resolve-Path $pluginfile.DirectoryName -Relative 108 | $pluginbinaries = Join-Path $pluginroot "Binaries\Win64" 109 | Cleanup-DLLs $pluginbinaries $pluginname $dryrun 110 | } 111 | 112 | } 113 | 114 | 115 | $isGit = Test-Path .git 116 | if ($isGit) { 117 | if ($lfsprune) { 118 | if ($dryrun) { 119 | Write-Output "Would have pruned LFS files" 120 | git lfs prune --dry-run 121 | } else { 122 | Write-Output "Pruning Git LFS files" 123 | git lfs prune 124 | } 125 | } 126 | } 127 | Write-Output "-- Cleanup finished OK --" 128 | 129 | 130 | } catch { 131 | Write-Output "ERROR: $($_.Exception.Message)" 132 | $result = 9 133 | } finally { 134 | if ($src -ne ".") { Pop-Location } 135 | } 136 | 137 | Exit $result 138 | 139 | -------------------------------------------------------------------------------- /ue-cook.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$src, 4 | [switch]$nocloseeditor = $false, 5 | [switch]$dryrun = $false, 6 | [switch]$help = $false 7 | ) 8 | 9 | . $PSScriptRoot\inc\platform.ps1 10 | . $PSScriptRoot\inc\packageconfig.ps1 11 | . $PSScriptRoot\inc\projectversion.ps1 12 | . $PSScriptRoot\inc\uproject.ps1 13 | . $PSScriptRoot\inc\filetools.ps1 14 | 15 | function Print-Usage { 16 | Write-Output "Steve's Unreal Cook Tool" 17 | Write-Output "Usage:" 18 | Write-Output " ue-cook.ps1 [[-src:]sourcefolder] [Options]" 19 | Write-Output " " 20 | Write-Output " -src : Source folder (current folder if omitted)" 21 | Write-Output " : (should be root of project)" 22 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 23 | Write-Output " -help : Print this help" 24 | Write-Output " " 25 | Write-Output "Environment Variables:" 26 | Write-Output " UEINSTALL : Use a specific Unreal install." 27 | Write-Output " : Default is to find one based on project version, under UEROOT" 28 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 29 | Write-Output " : Default C:\Program Files\Epic Games" 30 | Write-Output " " 31 | 32 | } 33 | 34 | $ErrorActionPreference = "Stop" 35 | 36 | 37 | if ($help) { 38 | Print-Usage 39 | Exit 0 40 | } 41 | 42 | if ($src.Length -eq 0) { 43 | $src = "." 44 | Write-Verbose "-src not specified, assuming current directory" 45 | } 46 | 47 | 48 | $result = 0 49 | 50 | try { 51 | if ($src -ne ".") { Push-Location $src } 52 | 53 | Write-Output "-- Cook process starting --" 54 | 55 | # Locate Unreal project file 56 | $uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name 57 | if (-not $uprojfile) { 58 | throw "No Unreal project file found in $(Get-Location)! Aborting." 59 | } 60 | if ($uprojfile -is [array]) { 61 | throw "Multiple Unreal project files found in $(Get-Location)! Aborting." 62 | } 63 | 64 | # In PS 6.0+ we could use Split-Path -LeafBase but let's stick with built-in PS 5.1 65 | $uprojname = [System.IO.Path]::GetFileNameWithoutExtension($uprojfile) 66 | if ($dryrun) { 67 | Write-Output "Would cook $uprojname" 68 | } else { 69 | Write-Output "Cooking $uprojname" 70 | } 71 | 72 | # Check version number of Unreal project so we know which version to run 73 | # We can read this from .uproject which is JSON 74 | $uproject = Get-Content $uprojfile | ConvertFrom-Json 75 | $uversion = $uproject.EngineAssociation 76 | 77 | Write-Output "Engine version is $uversion" 78 | 79 | # UEINSTALL env var should point at the root of the *specific version* of 80 | # Unreal you want to use. This is mainly for use in source builds, default is 81 | # to build it from version number and root of all UE binary installs 82 | $uinstall = $Env:UEINSTALL 83 | 84 | # Backwards compat with old env var 85 | if (-not $uinstall) { 86 | $uinstall = $Env:UE4INSTALL 87 | } 88 | 89 | if (-not $uinstall) { 90 | # UEROOT should be the parent folder of all UE versions 91 | $uroot = $Env:UEROOT 92 | # Backwards compat with old env var 93 | if (-not $uroot) { 94 | $uroot = $Env:UE4ROOT 95 | } 96 | if (-not $uroot) { 97 | $uroot = "C:\Program Files\Epic Games" 98 | } 99 | 100 | $uinstall = Join-Path $uroot "UE_$uversion" 101 | } 102 | 103 | # Test we can find RunUAT 104 | $ueEditorCmd = Get-UEEditorCmd $uversion $uinstall 105 | $runUAT = Join-Path $uinstall "Engine/Build/BatchFiles/RunUAT$batchSuffix" 106 | 107 | $absuprojectfile = Resolve-Path $uprojfile 108 | $platform = Get-Platform 109 | 110 | $argList = [System.Collections.ArrayList]@() 111 | $argList.Add("-ScriptsForProject=`"$absuprojectfile`"") > $null 112 | $argList.Add("BuildCookRun") > $null 113 | $argList.Add("-skipbuildeditor") > $null 114 | $argList.Add("-nocompileeditor") > $null 115 | #$argList.Add("-installed") > $null # don't think we need this, seems to be detected 116 | $argList.Add("-nop4") > $null 117 | $argList.Add("-project=`"$absuprojectfile`"") > $null 118 | $argList.Add("-cook") > $null 119 | $argList.Add("-skipstage") > $null 120 | $argList.Add("-nocompile") > $null 121 | $argList.Add("-nocompileuat") > $null 122 | if ((Get-Is-UE5 $uversion)) { 123 | $argList.Add("-unrealexe=`"$ueEditorCmd`"") > $null 124 | } else { 125 | $argList.Add("-ue4exe=`"$ueEditorCmd`"") > $null 126 | } 127 | $argList.Add("-platform=$($platform)") > $null 128 | $argList.Add("-target=$($uprojname)") > $null 129 | $argList.Add("-utf8output") > $null 130 | if ($maps.Count) { 131 | $argList.Add("-Map=$($maps -join "+")") > $null 132 | } 133 | 134 | if ($dryrun) { 135 | Write-Output "" 136 | Write-Output "Would have run:" 137 | Write-Output "> $runUAT $($argList -join " ")" 138 | Write-Output "" 139 | 140 | } else { 141 | $proc = Start-Process $runUAT $argList -Wait -PassThru -NoNewWindow 142 | if ($proc.ExitCode -ne 0) { 143 | throw "RunUAT failed!" 144 | } 145 | 146 | } 147 | 148 | Write-Output "-- Cook process finished OK --" 149 | 150 | } catch { 151 | Write-Output "ERROR: $($_.Exception.Message)" 152 | $result = 9 153 | } finally { 154 | if ($src -ne ".") { Pop-Location } 155 | } 156 | 157 | 158 | Exit $result 159 | -------------------------------------------------------------------------------- /ue-datasync.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$mode, 4 | [string]$root, 5 | [string]$src, 6 | [switch]$prune = $false, 7 | [switch]$force = $false, 8 | [switch]$nocloseeditor = $false, 9 | [switch]$dryrun = $false, 10 | [switch]$help = $false 11 | ) 12 | 13 | function Print-Usage { 14 | Write-Output "Steve's UE Map BuiltData Sync Tool" 15 | Write-Output " Avoid storing Map_BuiltData.uasset files in source control, sync them directly instead" 16 | Write-Output "Usage:" 17 | Write-Output " ue-datasync.ps1 [-mode:] [[-path:]syncpath] [Options]" 18 | Write-Output " " 19 | Write-Output " -mode : Whether to push or pull the built data from your filesystem" 20 | Write-Output " -root : Root folder to sync files to/from. Project name will be appended to this path." 21 | Write-Output " : Can be blank if specified in UESYNCROOT" 22 | Write-Output " -src : Source folder (current folder if omitted)" 23 | Write-Output " : (should be root of project)" 24 | Write-Output " -prune : Clean up versions of the data older than the latest" 25 | Write-Output " -force : Copy ALL BuiltData files regardless of size/timestamp checks" 26 | Write-Output " -nocloseeditor : Don't close Unreal editor before pulling (may prevent success)" 27 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 28 | Write-Output " -help : Print this help" 29 | Write-Output " " 30 | Write-Output "Environment Variables:" 31 | Write-Output " UESYNCROOT : Root path to sync data. Subfolders for each project name." 32 | Write-Output " UEINSTALL : Use a specific Unreal install." 33 | Write-Output " : Default is to find one based on project version, under UEROOT" 34 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 35 | Write-Output " : Default C:\Program Files\Epic Games" 36 | Write-Output " " 37 | 38 | } 39 | 40 | . $PSScriptRoot\inc\ueeditor.ps1 41 | . $PSScriptRoot\inc\filetools.ps1 42 | 43 | function Get-Current-Umaps { 44 | # Find all umaps which are tracked in git and get their LFS SHAs 45 | $umapsOutput = git lfs ls-files -l -I *.umap 46 | # Output is of the form 47 | # b75b42e082ffb0deeb3fc7b40b2a221ded62872a2289bf6b63e275372849447b * Content/Maps/Subfolder/MyLevel.umap 48 | foreach ($line in $umapsOutput) { 49 | if ($line -match "^([a-f0-9]+)\s+\*\s+(.+)$") { 50 | $oid = $matches[1] 51 | $filename = $matches[2].Trim() 52 | 53 | # returns multiple entries here 54 | # use property bag for convenience 55 | New-Object PSObject -Property @{Filename=$filename;Oid=$oid} 56 | } 57 | } 58 | } 59 | 60 | function Get-Remote-Builtdata-Path { 61 | param ( 62 | [string]$filename, 63 | [string]$oid, 64 | [string]$syncdir 65 | ) 66 | 67 | $subdir = [System.IO.Path]::GetDirectoryName($filename) 68 | $basename = [System.IO.Path]::GetFileNameWithoutExtension($filename) 69 | 70 | $remotesubdir = Join-Path $syncdir $subdir 71 | $remotebuiltdata = Join-Path $remotesubdir "${basename}_BuiltData_${oid}.uasset" 72 | 73 | return $remotebuiltdata 74 | 75 | } 76 | 77 | function Get-Builtdata-Paths { 78 | param ( 79 | [object]$umap, 80 | [string]$syncdir 81 | ) 82 | 83 | $subdir = [System.IO.Path]::GetDirectoryName($umap.Filename) 84 | $basename = [System.IO.Path]::GetFileNameWithoutExtension($umap.Filename) 85 | 86 | $localbuiltdata = Join-Path $subdir "${basename}_BuiltData.uasset" 87 | $remotesubdir = Join-Path $syncdir $subdir 88 | $remotebuiltdata = Join-Path $remotesubdir "${basename}_BuiltData_$($umap.Oid).uasset" 89 | 90 | return $localbuiltdata, $remotebuiltdata 91 | 92 | } 93 | 94 | $ErrorActionPreference = "Stop" 95 | 96 | 97 | if ($help) { 98 | Print-Usage 99 | Exit 0 100 | } 101 | 102 | if ($mode -ne "push" -and $mode -ne "pull") { 103 | Print-Usage 104 | Write-Output "ERROR: Mode must be 'push' or 'pull'" 105 | Exit 3 106 | 107 | } 108 | 109 | if (-not $root) { 110 | $root = $Env:UESYNCROOT 111 | } 112 | # Backwards compat 113 | if (-not $root) { 114 | $root = $Env:UE4SYNCROOT 115 | } 116 | 117 | if (-not $root) { 118 | Print-Usage 119 | Write-Output "ERROR: Missing '-root' argument and no UESYNCROOT env var" 120 | Exit 3 121 | } 122 | 123 | if (-not (Test-Path $root -PathType Container)) { 124 | Print-Usage 125 | Write-Output "ERROR: root path $root does not exist" 126 | Exit 3 127 | } 128 | 129 | # confirm that umap files are tracked in LFS so SHAs are already there 130 | $lfsTrackOutput = git lfs track 131 | if (!$?) { 132 | Write-Output "ERROR: failed to call 'git lfs track'" 133 | Exit 5 134 | } 135 | $umapsOK = $false 136 | foreach ($line in $lfsTrackOutput) { 137 | if ($line -match "^\s+\*\.umap\s+.*$") { 138 | $umapsOK = $true 139 | break 140 | } 141 | } 142 | if (-not $umapsOK) { 143 | Write-Output "ERROR: .umap files are not tracked in LFS, cannot continue" 144 | Exit 5 145 | } 146 | 147 | # Check for changes, ONLY to .umap files 148 | $statusOutput = git status -uno --porcelain *.umap 149 | $modifiedMaps = [System.Collections.ArrayList]@() 150 | 151 | foreach ($line in $statusOutput) { 152 | if ($line -match "^(?: [^\s]|[^\s] |[^\s][^\s])\s+(.+)$") { 153 | $filename = $matches[1] 154 | $modifiedMaps.Add($filename) > $null 155 | 156 | Write-Warning "Uncommitted change: $filename (will be skipped)" 157 | } 158 | } 159 | 160 | $result = 0 161 | 162 | try { 163 | if ($src -ne ".") { Push-Location $src } 164 | 165 | Write-Output "-- Sync process starting --" 166 | 167 | # Locate UE project file 168 | $uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name 169 | if (-not $uprojfile) { 170 | throw "No Unreal project file found in $(Get-Location)! Aborting." 171 | } 172 | if ($uprojfile -is [array]) { 173 | throw "Multiple Unreal project files found in $(Get-Location)! Aborting." 174 | } 175 | 176 | # In PS 6.0+ we could use Split-Path -LeafBase but let's stick with built-in PS 5.1 177 | $uprojname = [System.IO.Path]::GetFileNameWithoutExtension($uprojfile) 178 | if ($dryrun) { 179 | Write-Output "Would sync $uprojname" 180 | } else { 181 | Write-Output "Syncing $uprojname" 182 | } 183 | 184 | # Close UE as early as possible in pull mode 185 | if ($mode -eq "pull" -and -not $nocloseeditor) { 186 | # Check if UE is running, if so try to shut it gracefully 187 | if ($dryrun) { 188 | Write-Output "Would have closed UE Editor" 189 | } else { 190 | Close-UE-Editor $uprojname $dryrun 191 | } 192 | } 193 | 194 | # Create project sync dir if necessary when pushing 195 | $syncdir = Join-Path $root $uprojname 196 | if ($mode -eq "push") { 197 | New-Item -ItemType Directory $syncdir -Force > $null 198 | } elseif (-not (Test-Path $syncdir)) { 199 | # Abort, no need to pull anything 200 | Write-Output "No sync dir at $syncdir, aborting" 201 | Exit 0 202 | } 203 | 204 | Write-Output "Sync project folder: $syncdir" 205 | 206 | $umaps = Get-Current-Umaps 207 | foreach ($umap in $umaps) { 208 | 209 | $filename = $umap.Filename 210 | $oid = $umap.Oid 211 | 212 | Write-Verbose "Checking $filename ($oid)" 213 | 214 | if ($modifiedMaps.Contains($filename)) { 215 | Write-Verbose "Skipping $filename, uncommitted changes" 216 | continue 217 | } 218 | 219 | $localbuiltdata, $remotebuiltdata = Get-Builtdata-Paths $umap $syncdir 220 | 221 | $same = Compare-Files-Quick $localbuiltdata $remotebuiltdata 222 | 223 | if ($same -and -not $force) { 224 | Write-Verbose "Skipping $filename, matches" 225 | continue 226 | } 227 | 228 | if ($mode -eq "push") { 229 | Write-Verbose "$localbuiltdata -> $remotebuiltdata" 230 | 231 | # In push mode, we only upload our builtdata if there is no existing 232 | # entry for that OID by default (safest). Or, if forced to do so 233 | if (-not (Test-Path $localbuiltdata -PathType Leaf)) { 234 | Write-Warning "Skipping $filename, local file missing" 235 | continue 236 | } 237 | 238 | if ($dryrun) { 239 | Write-Output "Would have pushed: $filename ($oid)" 240 | } else { 241 | Write-Output "Push: $filename ($oid)" 242 | $remotedir = [System.IO.Path]::GetDirectoryName($remotebuiltdata) 243 | New-Item -ItemType Directory -Path $remotedir -Force > $null 244 | Copy-Item $localbuiltdata $remotebuiltdata 245 | } 246 | 247 | } else { 248 | Write-Verbose("$remotebuiltdata -> $localbuiltdata") 249 | # In pull mode, we always pull if not same, or forced (checked already above) 250 | 251 | if (-not (Test-Path $remotebuiltdata -PathType Leaf)) { 252 | 253 | # If we don't have lighting data for this specific OID, we 254 | # look back at the file history of the umap and use the latest 255 | # one that does exist instead. E.g. lighting build may have been done, then 256 | # small changes made to the umap afterward which would stop it matching 257 | # but the lighting build for the previous OID was fine 258 | # We don't use the latest file because that could be ahead of us 259 | $foundInHistory = $false 260 | $logOutput = git log -p --oneline -- $filename 261 | Write-Verbose "No data for $filename HEAD revision, checking for latest available" 262 | foreach ($line in $logOutput) { 263 | if ($line -match "^\+oid sha256:([0-9a-f]*)$") { 264 | $logoid = $matches[1] 265 | 266 | # Ignore the latest one, we've already tried 267 | if ($logoid -ne $oid) { 268 | $testremotefile = Get-Remote-Builtdata-Path $filename $logoid $syncdir 269 | 270 | if (Test-Path $testremotefile -PathType Leaf) { 271 | $foundInHistory = $true 272 | $remotebuiltdata = $testremotefile 273 | $oid = $logoid 274 | Write-Verbose "Found latest for $filename ($logoid)" 275 | break 276 | } 277 | } 278 | } 279 | } 280 | 281 | if ($foundInHistory) { 282 | $same = Compare-Files-Quick $localbuiltdata $remotebuiltdata 283 | 284 | if ($same -and -not $force) { 285 | Write-Verbose "Skipping $filename, matches" 286 | continue 287 | } 288 | 289 | } else { 290 | Write-Warning "Skipping $filename, remote file missing" 291 | continue 292 | } 293 | } 294 | 295 | if ($dryrun) { 296 | Write-Output "Would have pulled: $filename ($oid)" 297 | } else { 298 | Write-Output "Pull: $filename ($oid)" 299 | $subdir = [System.IO.Path]::GetDirectoryName($localbuiltdata) 300 | New-Item -ItemType Directory -Path $subdir -Force > $null 301 | Copy-Item $remotebuiltdata $localbuiltdata 302 | } 303 | } 304 | } 305 | 306 | if ($prune) { 307 | # Only keep latest for each map file 308 | # We derive that from the current oids, which we always keep, and date 309 | 310 | 311 | Write-Output "Pruning..." 312 | foreach ($umap in $umaps) { 313 | # We want to delete any files for this map which have a different OID 314 | # and which are older (to prevent deletion if you're behind) 315 | 316 | # Get our current one 317 | $localfile, $remotefile = Get-Builtdata-Paths $umap $syncdir 318 | 319 | $remotedir = [System.IO.Path]::GetDirectoryName($remotefile) 320 | $basename = [System.IO.Path]::GetFileNameWithoutExtension($umap.Filename) 321 | $matchingremotefiles = Get-ChildItem $remotedir -filter "${basename}_BuiltData_*.uasset" -ErrorAction Continue 322 | 323 | if (-not (Test-Path $remotefile -PathType Leaf)) { 324 | Write-Verbose "Skipping pruning old versions for $($umap.Filename) since our version isn't on remote" 325 | continue 326 | } 327 | $ourfileprops = Get-ItemProperty -Path $remotefile 328 | 329 | foreach ($file in $matchingremotefiles) { 330 | Write-Verbose "Considering $($file.Name) for deletion" 331 | if ($file.Name -notlike "*$($umap.Oid).uasset") { 332 | # This is not our OID, check date 333 | if ($file.LastWriteTime -le $ourfileprops.LastWriteTime) { 334 | if ($dryrun) { 335 | Write-Output "Would have pruned $($file.FullName)" 336 | } else { 337 | Write-Output "Pruning $($file.FullName)" 338 | Remove-Item -Path $file.FullName -Force 339 | } 340 | } else { 341 | Write-Verbose "Not pruning $($file.Name), date/time is later than ours" 342 | } 343 | } else { 344 | Write-Verbose "Not pruning $($file.Name), this is our latest" 345 | } 346 | } 347 | 348 | } 349 | 350 | } 351 | 352 | 353 | 354 | 355 | Write-Output "-- Sync process finished OK --" 356 | 357 | 358 | } catch { 359 | Write-Output "ERROR: $($_.Exception.Message)" 360 | $result = 9 361 | } finally { 362 | if ($src -ne ".") { Pop-Location } 363 | } 364 | 365 | 366 | Exit $result 367 | -------------------------------------------------------------------------------- /ue-get-latest.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$src, 4 | [switch]$nocloseeditor = $false, 5 | [switch]$dryrun = $false, 6 | [switch]$help = $false 7 | ) 8 | 9 | function Print-Usage { 10 | Write-Output "Steve's Unreal Get Latest Tool" 11 | Write-Output " Get latest from repo and build for dev. Will close Unreal editor!" 12 | Write-Output "Usage:" 13 | Write-Output " ue-get-latest.ps1 [[-src:]sourcefolder] [Options]" 14 | Write-Output " " 15 | Write-Output " -src : Source folder (current folder if omitted)" 16 | Write-Output " : (should be root of project)" 17 | Write-Output " -nocloseeditor : Don't close Unreal editor (this will prevent DLL cleanup)" 18 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 19 | Write-Output " -help : Print this help" 20 | Write-Output " " 21 | Write-Output "Environment Variables:" 22 | Write-Output " UEINSTALL : Use a specific Unreal install." 23 | Write-Output " : Default is to find one based on project version, under UEROOT" 24 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 25 | Write-Output " : Default C:\Program Files\Epic Games" 26 | Write-Output " " 27 | 28 | } 29 | 30 | $ErrorActionPreference = "Stop" 31 | 32 | if ($help) { 33 | Print-Usage 34 | Exit 0 35 | } 36 | 37 | $result = 0 38 | 39 | try { 40 | if ($src -ne ".") { Push-Location $src } 41 | 42 | # Make sure we're running in the root of the project 43 | $uprojfile = Get-ChildItem *.uproject | Select-Object -expand Name 44 | if (-not $uprojfile) { 45 | throw "No Unreal project file found in $(Get-Location)! Aborting." 46 | } 47 | if ($uprojfile -is [array]) { 48 | throw "Multiple Unreal project files found in $(Get-Location)! Aborting." 49 | } 50 | 51 | $isGit = Test-Path .git 52 | 53 | if ($isGit) { 54 | git diff --ignore-submodules --no-patch --exit-code > $null 55 | $unstagedChanges = ($LASTEXITCODE -ne 0) 56 | git diff --ignore-submodules --no-patch --cached --exit-code > $null 57 | $stagedChanges = ($LASTEXITCODE -ne 0) 58 | 59 | if ($unstagedChanges -or $stagedChanges) { 60 | if ($dryrun) { 61 | Write-Output "Changes present, would have run 'git stash push'" 62 | } else { 63 | Write-Output "Working copy has changes, saving them in stash" 64 | git stash push -q -m "Saved changes during Get Latest" 65 | if ($LASTEXITCODE -ne 0) { 66 | Write-Output "ERROR: git stash push failed, aborting" 67 | exit 5 68 | } 69 | } 70 | } 71 | 72 | # Actually don't clean up anymore, no longer needed 73 | # # Run cleanup tool 74 | # $cleanupargs = @() 75 | # if ($nocloseeditor) { 76 | # $cleanupargs += "-nocloseeditor" 77 | # } 78 | # if ($dryrun) { 79 | # $cleanupargs += "-dryrun" 80 | # } 81 | # # Use Invoke-Expression so we can use a string as options 82 | # Invoke-Expression "&'$PSScriptRoot/ue-cleanup.ps1' $cleanupargs" 83 | 84 | # Stopped using rebase because it's a PITA when it goes wrong 85 | Write-Output "Pulling latest from Git..." 86 | # I know Linus says we shouldn't accept the default merge message but pfft 87 | # No artist wan't to see that git merge message pop-up, come on, it's obvious why 88 | git pull --recurse-submodules --no-edit 89 | if ($LASTEXITCODE -ne 0) { 90 | Write-Output "ERROR: git pull failed!" 91 | exit 5 92 | } 93 | 94 | if ($unstagedChanges -or $stagedChanges) { 95 | Write-Output "Re-applying your saved changes..." 96 | git stash pop > $null 97 | if ($LASTEXITCODE -ne 0) { 98 | Write-Output "ERROR: Failed to re-apply your changes, resolve manually from stash!" 99 | exit 5 100 | } 101 | } 102 | } else { 103 | # Support Perforce? 104 | 105 | throw "Get Latest only supports Git right now" 106 | } 107 | 108 | # Now build 109 | $cmdargs = @() 110 | if ($nocloseeditor) { 111 | $cmdargs += "-nocloseeditor" 112 | } 113 | if ($dryrun) { 114 | $cmdargs += "-dryrun" 115 | } 116 | # Use Invoke-Expression so we can use a string as options 117 | Invoke-Expression "&'$PSScriptRoot/ue-build.ps1' dev $cmdargs" 118 | 119 | if ($LASTEXITCODE -ne 0) { 120 | throw "Build process failed, see above" 121 | } 122 | 123 | # Automatically pull lighting builds, if environment variable defined 124 | if ($Env:UESYNCROOT -or $Env:UE4SYNCROOT) { 125 | $cmdargs = @() 126 | if ($nocloseeditor) { 127 | $cmdargs += "-nocloseeditor" 128 | } 129 | if ($dryrun) { 130 | $cmdargs += "-dryrun" 131 | } 132 | # Use Invoke-Expression so we can use a string as options 133 | Invoke-Expression "&'$PSScriptRoot/ue-datasync.ps1' pull $cmdargs" 134 | 135 | } 136 | 137 | Write-Output "-- Get Latest finished OK --" 138 | 139 | 140 | } catch { 141 | Write-Output "ERROR: $($_.Exception.Message)" 142 | $result = 9 143 | } finally { 144 | if ($src -ne ".") { Pop-Location } 145 | } 146 | 147 | Exit $result 148 | 149 | -------------------------------------------------------------------------------- /ue-git-setup.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$src, 4 | # Ignore project structure problems 5 | [switch]$skipstructurecheck = $false, 6 | [switch]$overwriteprops = $false, 7 | [switch]$dryrun = $false, 8 | [switch]$help = $false 9 | ) 10 | 11 | function Print-Usage { 12 | Write-Output "Steve's Unreal Git Repo Setup Tool" 13 | Write-Output " Run this on your just-created Unreal project folder." 14 | Write-Output " .gitattributes and .gitignore will be overwritten" 15 | Write-Output "Usage:" 16 | Write-Output " ue-git-setup.ps1 [[-src:]sourcefolder] [Options]" 17 | Write-Output " " 18 | Write-Output " -src : Source folder (current folder if omitted)" 19 | Write-Output " : (should be root of trunk in new repo)" 20 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 21 | Write-Output " -help : Print this help" 22 | } 23 | 24 | $gitPluginURL = "git@github.com:sinbad/UE4GitPlugin.git" 25 | $gitPluginBranch = "ue4_24-fixes" 26 | 27 | $gitlfs_notlocked = @" 28 | *.fbx 29 | *.zip 30 | *.bmp 31 | *.afphoto 32 | *.exr 33 | *.dll 34 | *.mlt 35 | *.png 36 | *.jpg 37 | *.afdesign 38 | *.blend 39 | *.wav 40 | *.pdf 41 | *.tiff 42 | *.mp3 43 | "@ 44 | 45 | $gitlfs_locked = @" 46 | *.uasset 47 | *.umap 48 | "@ 49 | 50 | $gitignore = @" 51 | # Visual Studio 52 | .vs/ 53 | 54 | # Compiled Object files 55 | *.slo 56 | *.lo 57 | *.o 58 | *.obj 59 | 60 | # Precompiled Headers 61 | *.gch 62 | *.pch 63 | 64 | # Compiled Dynamic libraries 65 | *.so 66 | *.dylib 67 | *.dll 68 | 69 | # Fortran module files 70 | *.mod 71 | 72 | # Compiled Static libraries 73 | *.lai 74 | *.la 75 | *.a 76 | *.lib 77 | 78 | # Executables 79 | *.exe 80 | *.out 81 | *.app 82 | *.ipa 83 | 84 | # These project files can be generated by the engine 85 | *.xcodeproj 86 | *.xcworkspace 87 | *.sln 88 | *.suo 89 | *.opensdf 90 | *.sdf 91 | *.VC.db 92 | *.VC.opendb 93 | 94 | # Binary Files 95 | Binaries/* 96 | Plugins/*/Binaries/* 97 | 98 | # Builds 99 | Build/* 100 | 101 | # Whitelist PakBlacklist-.txt files 102 | !Build/*/ 103 | Build/*/** 104 | !Build/*/PakBlacklist*.txt 105 | 106 | # Don't ignore icon files in Build 107 | !Build/**/*.ico 108 | 109 | # Built data for maps 110 | *_BuiltData.uasset 111 | 112 | # Configuration files generated by the Editor 113 | Saved/* 114 | 115 | # Compiled source files for the engine to use 116 | Intermediate/* 117 | Plugins/*/Intermediate/* 118 | 119 | # Cache files for the editor to use 120 | DerivedDataCache/* 121 | 122 | # Localisation intermediate / export files 123 | Content/Localization/**/*.csv 124 | Content/Localization/**/*.po 125 | Content/Localization/**/*.archive 126 | 127 | # Temp files 128 | *.blend1 129 | 130 | # Exported files in Content 131 | Content/**/*.bmp 132 | Content/**/*.png 133 | Content/**/*.jpg 134 | Content/**/*.tif 135 | Content/**/*.tiff 136 | Content/**/*.tga 137 | Content/**/*.fbx 138 | Content/**/*.exr 139 | Content/**/*.mp3 140 | Content/**/*.wav 141 | "@ 142 | 143 | 144 | if ($help) { 145 | Print-Usage 146 | Exit 0 147 | } 148 | 149 | if (-not (Get-Module -ListAvailable -Name PsIni)) { 150 | Write-Output "Missing module: PsIni" 151 | Write-Output "This script uses PsIni to update DefaultEngine.ini" 152 | Write-Output "Install it using 'Install-Module PsIni [-Scope CurrentUser]'" 153 | Exit 2 154 | } 155 | 156 | if ($src.Length -eq 0) { 157 | $src = "." 158 | Write-Verbose "-src not specified, assuming current directory" 159 | } 160 | 161 | $ErrorActionPreference = "Stop" 162 | 163 | if ($src -ne ".") { 164 | Push-Location $src 165 | if ($LASTEXITCODE -ne 0) { 166 | Write-Output "ERROR: Unable to change directory to '$src', exiting" 167 | Exit 1 168 | } 169 | } 170 | 171 | 172 | try { 173 | # Check if we're a git repo already 174 | if (-not (Test-Path ".git")) { 175 | Write-Output "Creating git repo" 176 | git init 177 | } 178 | 179 | # Add .gitignore FIRST before LFS 180 | # Otherwise lockable files that are ignored can be made read-only 181 | Write-Output "Creating .gitignore" 182 | Set-Content .gitignore $gitignore 183 | 184 | # init LFS 185 | Write-Output "Enabling Git LFS" 186 | git lfs install 187 | 188 | # track, including lockable types 189 | Write-Output "Tracking Git LFS Files" 190 | foreach($f in $gitlfs_notlocked -split "\r?\n") { 191 | git lfs track "$f" 192 | } 193 | foreach($f in $gitlfs_locked -split "\r?\n") { 194 | git lfs track --lockable "$f" 195 | } 196 | 197 | # Configure DefaultEngine.ini to make Git LFS operations MUCH faster 198 | # [SystemSettingsEditor] 199 | # r.Editor.SkipSourceControlCheckForEditablePackages = 1 200 | # Seriously, without this saving a locked LFS file takes ~5s vs 0.5s 201 | Import-Module PsIni 202 | $engineIni = Get-IniContent "Config/DefaultEngine.ini" 203 | $engineIni["SystemSettingsEditor"] = @{"r.Editor.SkipSourceControlCheckForEditablePackages" = "1"} 204 | Out-IniFile -Force -InputObject $engineIni -FilePath "Config/DefaultEngine.ini" 205 | 206 | # git submodule UE4Plugin 207 | if (!(Test-Path "Plugins/UE4GitPlugin/GitSourceControl.uplugin")) { 208 | Write-Output "Checking out $gitPluginURL/$gitPluginBranch..." 209 | if (!(Test-Path Plugins)) { 210 | New-Item -ItemType Directory Plugins > $null 211 | } 212 | git submodule add -b $gitPluginBranch $gitPluginURL Plugins/UE4GitPlugin 213 | } 214 | 215 | 216 | } catch { 217 | Write-Output $_.Exception.Message 218 | Exit 9 219 | } 220 | 221 | 222 | 223 | if ($src -ne ".") { Pop-Location } 224 | -------------------------------------------------------------------------------- /ue-package.ps1: -------------------------------------------------------------------------------- 1 | # Packaging helper 2 | # Bumps versions, builds, cooks, packages variants 3 | # Put packageconfig.json in your project folder to configure 4 | # See packageconfig_template.json 5 | [CmdletBinding()] # Fail on unknown args 6 | param ( 7 | [string]$src, 8 | [string]$out, 9 | [switch]$major = $false, 10 | [switch]$minor = $false, 11 | [switch]$patch = $false, 12 | [switch]$hotfix = $false, 13 | [switch]$nightly = $false, 14 | # Don't increment version 15 | [switch]$keepversion = $false, 16 | # Name of variant to build (optional, uses DefaultVariants from packageconfig.json if unspecified) 17 | [array]$variants, 18 | # Testing mode; skips clean checks, tags 19 | [switch]$test = $false, 20 | # Browse the output directory in file explorer after packaging 21 | [switch]$browse = $false, 22 | # Dry-run; does nothing but report what *would* have happened 23 | [switch]$dryrun = $false, 24 | [switch]$help = $false 25 | ) 26 | 27 | . $PSScriptRoot\inc\platform.ps1 28 | . $PSScriptRoot\inc\packageconfig.ps1 29 | . $PSScriptRoot\inc\projectversion.ps1 30 | . $PSScriptRoot\inc\uproject.ps1 31 | . $PSScriptRoot\inc\ueeditor.ps1 32 | . $PSScriptRoot\inc\filetools.ps1 33 | 34 | 35 | function Write-Usage { 36 | Write-Output "Steve's Unreal packaging tool" 37 | Write-Output "Usage:" 38 | Write-Output " ue-package.ps1 [-src:sourcefolder] [-out:folder] [-major|-minor|-patch|-hotfix] [-keepversion] [-force] [-variant=VariantName] [-test] [-dryrun]" 39 | Write-Output " " 40 | Write-Output " -src : Source folder (current folder if omitted), must contain packageconfig.json" 41 | Write-OUtput " -out : Overrides OutputDir in packageconfig.json" 42 | Write-Output " -major : Increment major version i.e. [x++].0.0.0" 43 | Write-Output " -minor : Increment minor version i.e. x.[x++].0.0" 44 | Write-Output " -patch : Increment patch version i.e. x.x.[x++].0 (default)" 45 | Write-Output " -hotfix : Increment hotfix version i.e. x.x.x.[x++]" 46 | Write-Output " -keepversion : Keep current version number, doesn't tag unless -forcetag" 47 | Write-Output " -nightly : Nightly build, doesn't tag, doesn't commit, re-uses same nightly folder, appends git rev version" 48 | Write-Output " -variants Name1,Name2,Name3" 49 | Write-Output " : Build only named variants instead of DefaultVariants from packageconfig.json" 50 | Write-Output " -test : Testing mode, separate builds, allow dirty working copy" 51 | Write-Output " -browse : After packaging, browse the output folder" 52 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 53 | Write-Output " -help : Print this help" 54 | Write-Output " " 55 | Write-Output "Environment Variables:" 56 | Write-Output " UEINSTALL : Use a specific Unreal install." 57 | Write-Output " : Default is to find one based on project version, under UEROOT" 58 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 59 | Write-Output " : Default C:\Program Files\Epic Games" 60 | Write-Output " " 61 | } 62 | 63 | if ($src.Length -eq 0) { 64 | $src = "." 65 | Write-Verbose "-src not specified, assuming current directory" 66 | } 67 | 68 | $ErrorActionPreference = "Stop" 69 | 70 | if ($help) { 71 | Write-Usage 72 | Exit 0 73 | } 74 | 75 | Write-Output "~-~-~ Unreal Packaging Helper Start ~-~-~" 76 | 77 | if ($test) { 78 | Write-Output "TEST MODE: No tagging, version bumping" 79 | } 80 | 81 | if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -gt 1) { 82 | Write-Output "ERROR: Can't set more than one of major/minor/patch/hotfix at the same time!" 83 | Print-Usage 84 | Exit 5 85 | } 86 | if (($major -or $minor -or $patch -or $hotfix) -and $keepversion) { 87 | Write-Output "ERROR: Can't set keepversion at the same time as major/minor/patch/hotfix!" 88 | Print-Usage 89 | Exit 5 90 | } 91 | 92 | # Detect Git 93 | if ($src -ne ".") { Push-Location $src } 94 | $isGit = Test-Path ".git" 95 | if ($src -ne ".") { Pop-Location } 96 | 97 | # Check working copy is clean (Git only) 98 | if (-not $test -and $isGit) { 99 | if ($src -ne ".") { Push-Location $src } 100 | 101 | if (Test-Path ".git") { 102 | git diff --no-patch --exit-code 103 | if ($LASTEXITCODE -ne 0) { 104 | Write-Output "Working copy is not clean (unstaged changes)" 105 | if ($dryrun) { 106 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 107 | } else { 108 | Exit $LASTEXITCODE 109 | } 110 | } 111 | git diff --no-patch --cached --exit-code 112 | if ($LASTEXITCODE -ne 0) { 113 | Write-Output "Working copy is not clean (staged changes)" 114 | if ($dryrun) { 115 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 116 | } else { 117 | Exit $LASTEXITCODE 118 | } 119 | } 120 | } 121 | if ($src -ne ".") { Pop-Location } 122 | } 123 | 124 | 125 | try { 126 | # Import config & project settings 127 | $config = Read-Package-Config -srcfolder:$src 128 | $projfile = Get-Uproject-Filename -srcfolder:$src -config:$config 129 | $proj = Read-Uproject $projfile 130 | $ueVersion = Get-UE-Version $proj 131 | $ueinstall = Get-UE-Install $ueVersion 132 | 133 | $chosenVariantNames = $config.DefaultVariants 134 | if ($variants) { 135 | $chosenVariantNames = $variants 136 | } 137 | # TODO support overriding default variants with args 138 | $chosenVariants = $config.Variants | Where-Object { $chosenVariantNames -contains $_.Name } 139 | 140 | if ($chosenVariants.Count -ne $chosenVariantNames.Count) { 141 | $unmatchedVariants = $chosenVariantNames | Where-Object { $chosenVariants.Name -notcontains $_ } 142 | Write-Warning "Unknown variant(s) ignored: $($unmatchedVariants -join ", ")" 143 | } 144 | 145 | $foundmaps = Find-Files -startDir:$(Join-Path $src "Content") -pattern:*.umap -includeByDefault:$config.CookAllMaps -includeBaseNames:$config.MapsIncluded -excludeBaseNames:$config.MapsExcluded 146 | 147 | $maps = $foundmaps.BaseNames 148 | 149 | $mapsdesc = $maps ? $maps -join ", " : "Default (Project Settings)" 150 | 151 | 152 | Write-Output "" 153 | Write-Output "Project File : $projfile" 154 | Write-Output "UE Version : $ueVersion" 155 | Write-Output "UE Install : $ueinstall" 156 | if ($out.Length -eq 0) { 157 | Write-Output "Output Folder : $($config.OutputDir)" 158 | } else { 159 | Write-Output "Output Folder : $out" 160 | } 161 | Write-Output "Zipped Folder : $($config.ZipDir)" 162 | Write-Output "" 163 | Write-Output "Chosen Variants : $chosenVariantNames" 164 | Write-Output "Maps to Cook : $mapsdesc" 165 | Write-Output "" 166 | 167 | if (-not $dryrun) 168 | { 169 | $editorprojname = [System.IO.Path]::GetFileNameWithoutExtension($projfile) 170 | Close-UE-Editor $editorprojname $dryrun 171 | } 172 | 173 | if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -eq 0) { 174 | $patch = $true 175 | } 176 | $versionNumber = $null 177 | if ($nightly) { 178 | $versionNumber = "nightly" 179 | 180 | if ($isGit) 181 | { 182 | # Add the git ref to the version number in the project ONLY (not our folder) 183 | $tempverobj = Get-ProjectVersionComponents $src 184 | $gitref = $(git rev-parse --short HEAD) 185 | $tempverobj.postfix = "-$gitref" 186 | Write-Output "Packaging nightly-$gitref" 187 | Write-ProjectVersionFromObject -srcfolder:$src -versionObj:$tempverobj -dryrun:$dryrun 188 | } 189 | 190 | } elseif ($keepversion) { 191 | $versionNumber = Get-Project-Version $src 192 | } else { 193 | # Bump up version, passthrough options 194 | try { 195 | $versionNumber = Increment-Project-Version -srcfolder:$src -major:$major -minor:$minor -patch:$patch -hotfix:$hotfix -dryrun:$dryrun 196 | if (-not $dryrun -and $isGit) { 197 | if ($src -ne ".") { Push-Location $src } 198 | 199 | $verIniFile = Get-Project-Version-Ini-Filename $src 200 | git add "$($verIniFile)" 201 | if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } 202 | git commit -m "Version bump to $versionNumber" 203 | if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } 204 | 205 | if ($src -ne ".") { Pop-Location } 206 | } 207 | } 208 | catch { 209 | Write-Output $_.Exception.Message 210 | Exit 6 211 | } 212 | } 213 | # Keep test builds separate 214 | if ($test) { 215 | $versionNumber = "$versionNumber-test" 216 | } 217 | Write-Output "Next version will be: $versionNumber" 218 | 219 | # For tagging release 220 | # We only need to grab the main version once 221 | if (-not $keepversion -and -not $nightly) { 222 | 223 | if (-not $test -and -not $dryrun -and $isGit) { 224 | if ($src -ne ".") { Push-Location $src } 225 | git tag -a $versionNumber -m "Automated release tag" 226 | if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } 227 | if ($src -ne ".") { Pop-Location } 228 | } 229 | } 230 | 231 | 232 | # We need to build the host Editor target explicitly first, which will be used 233 | # to run the "Cook" stage. If we don't do this, then any source plugins will 234 | # be missing in a clean checkout build and the cook stage will fail 235 | Write-Output "Building Editor (for Cooking)" 236 | $cmdargs = @() 237 | $cmdargs += "-src:$src" 238 | if ($dryrun) { 239 | $cmdargs += "-dryrun" 240 | } 241 | Invoke-Expression "&'$PSScriptRoot/ue-build.ps1' -mode:dev $cmdargs" 242 | 243 | $ueEditorCmd = Get-UEEditorCmd $ueVersion $ueinstall 244 | $runUAT = Join-Path $ueinstall "Engine/Build/BatchFiles/RunUAT$batchSuffix" 245 | 246 | 247 | foreach ($var in $chosenVariants) { 248 | 249 | if ($out.Length -gt 0) { 250 | $outDir = Join-Path $out "$($var.Name)-$($versionNumber)" 251 | } else { 252 | $outDir = Get-Package-Dir -config:$config -versionNumber:$versionNumber -variantName:$var.Name 253 | } 254 | 255 | # Delete previous 256 | Remove-Item -Path $outDir -Recurse -Force -ErrorAction SilentlyContinue 257 | 258 | $argList = [System.Collections.ArrayList]@() 259 | $argList.Add("-ScriptsForProject=`"$projfile`"") > $null 260 | $argList.Add("BuildCookRun") > $null 261 | $argList.Add("-nocompileeditor") > $null 262 | #$argList.Add("-installed") > $null # don't think we need this, seems to be detected 263 | $argList.Add("-nop4") > $null 264 | $argList.Add("-project=`"$projfile`"") > $null 265 | $argList.Add("-cook") > $null 266 | $argList.Add("-stage") > $null 267 | $argList.Add("-archive") > $null 268 | $argList.Add("-archivedirectory=`"$($outDir)`"") > $null 269 | $argList.Add("-package") > $null 270 | if ((Get-Is-UE5 $ueVersion)) { 271 | $argList.Add("-unrealexe=`"$ueEditorCmd`"") > $null 272 | } else { 273 | $argList.Add("-ue4exe=`"$ueEditorCmd`"") > $null 274 | } 275 | if ($config.UsePak) { 276 | $argList.Add("-pak") > $null 277 | } 278 | $argList.Add("-prereqs") > $null 279 | $argList.Add("-build") > $null 280 | $argList.Add("-target=$($config.Target)") > $null 281 | $argList.Add("-clientconfig=$($var.Configuration)") > $null 282 | $argList.Add("-targetplatform=$($var.Platform)") > $null 283 | $argList.Add("-utf8output") > $null 284 | if ($maps.Count) { 285 | $argList.Add("-Map=$($maps -join "+")") > $null 286 | } 287 | if ($var.Cultures) { 288 | $argList.Add("-cookcultures=$($var.Cultures -join "+")") > $null 289 | } 290 | $argList.Add($var.ExtraBuildArguments) > $null 291 | 292 | Write-Output "Building variant: $($var.Name)" 293 | 294 | if ($dryrun) { 295 | Write-Output "" 296 | Write-Output "Would have run:" 297 | Write-Output "> $runUAT $($argList -join " ")" 298 | Write-Output "" 299 | 300 | } else { 301 | $proc = Start-Process $runUAT $argList -Wait -PassThru -NoNewWindow 302 | if ($proc.ExitCode -ne 0) { 303 | throw "RunUAT failed!" 304 | } 305 | 306 | } 307 | 308 | if ($config.RenameExe.Length -gt 0) { 309 | if ($dryrun) { 310 | Write-Output "Would have renamed EXE from $($config.Target) to $($config.RenameExe)" 311 | } else { 312 | # Rename the executable 313 | $subdirs = @(Get-ChildItem $outdir) 314 | $subdirs | ForEach-Object { 315 | $renameExeSuffix = "" 316 | if ($var.Platform -like "Win*") { 317 | $renameExeSuffix = ".exe" 318 | } 319 | $exeSrcName = Join-Path $_.FullName "$($config.Target)$renameExeSuffix" 320 | $exeDestName = Join-Path $_.FullName "$($config.RenameExe)$renameExeSuffix" 321 | Move-Item $exeSrcName $exeDestName -Force 322 | } 323 | } 324 | 325 | } 326 | 327 | if ($var.Configuration -eq "Shipping") 328 | { 329 | # For shipping, move the PDBs aside but keep them for later use 330 | $outDirPDB = "$($outDir)-ShippingPDB" 331 | Remove-Item -Path $outDirPDB -Force -ErrorAction SilentlyContinue 332 | New-Item -ItemType Directory $outDirPDB -Force > $null 333 | 334 | $pdbs = @(Get-ChildItem -Path $outDir -Filter *.pdb -Recurse -ErrorAction SilentlyContinue -Force) 335 | # Need to be in dir to calculate relative 336 | Push-Location $outDir 337 | $pdbs | ForEach-Object { 338 | $pdbdir = Join-Path $outDirPDB $($_.DirectoryName | Resolve-Path -Relative) 339 | New-Item -ItemType Directory $pdbdir -Force > $null 340 | $pdbdest = Join-Path $outDirPDB $($_.FullName | Resolve-Path -Relative) 341 | Move-Item $_.FullName $pdbdest -Force 342 | } 343 | Pop-Location 344 | } 345 | 346 | 347 | if ($var.Zip) { 348 | if ($dryrun) { 349 | Write-Output "Would have compressed $outdir to $(Join-Path $config.ZipDir "$($config.Target)_$($versionNumber)_$($var.Name).zip")" 350 | } else { 351 | # We zip all subfolders of the out dir separately 352 | # Since there may be multiple build dirs in the case of server & client builds 353 | # E.g. WindowsNoEditor vs WindowsServer 354 | # BUT we omit the folder name in the zip if there's only one, for brevity 355 | $subdirs = @(Get-ChildItem $outdir) 356 | $multipleBuilds = ($subdirs.Count > 1) 357 | $subdirs | ForEach-Object { 358 | $zipsrc = "$($_.FullName)\*" # excludes folder name, compress contents 359 | $subdirSuffix = "" 360 | if ($multipleBuilds) { 361 | # Only include "WindowsNoEditor" etc part if there's a need to disambiguate 362 | $subdirSuffix = "_$($_.BaseName)" 363 | } 364 | $zipdst = Join-Path $config.ZipDir "$($config.Target)_$($versionNumber)_$($var.Name)$subdirSuffix.zip" 365 | 366 | New-Item -ItemType Directory -Path $config.ZipDir -Force > $null 367 | Write-Output "Compressing to $zipdst" 368 | Compress-Archive -Path $zipsrc -DestinationPath $zipdst 369 | } 370 | 371 | } 372 | } 373 | } 374 | 375 | if ($browse -and -not $dryrun) { 376 | Invoke-Item $(Join-Path $config.OutputDir $versionNumber) 377 | } 378 | 379 | } 380 | catch { 381 | Write-Output $_.Exception.Message 382 | Write-Output "~-~-~ Unreal Packaging Helper FAILED ~-~-~" 383 | Exit 9 384 | } 385 | 386 | # Revert any remaining temp changes 387 | git checkout . 388 | Write-Output "~-~-~ Unreal Packaging Helper Completed OK ~-~-~" 389 | -------------------------------------------------------------------------------- /ue-plugin-package.ps1: -------------------------------------------------------------------------------- 1 | # Plugin packaging helper 2 | # Bumps versions, zips for marketplace 3 | # Put pluginconfig.json in your project folder to configure 4 | # See pluginconfig_template.json 5 | [CmdletBinding()] # Fail on unknown args 6 | param ( 7 | [string]$src, 8 | [switch]$major = $false, 9 | [switch]$minor = $false, 10 | [switch]$patch = $false, 11 | [switch]$hotfix = $false, 12 | # Don't incrememnt version 13 | [switch]$keepversion = $false, 14 | # Never tag 15 | [switch]$notag = $false, 16 | # Testing mode; skips clean checks, tags 17 | [switch]$test = $false, 18 | # Browse the output directory in file explorer after packaging 19 | [switch]$browse = $false, 20 | # Dry-run; does nothing but report what *would* have happened 21 | [switch]$dryrun = $false, 22 | [switch]$help = $false 23 | ) 24 | 25 | . $PSScriptRoot\inc\platform.ps1 26 | . $PSScriptRoot\inc\pluginconfig.ps1 27 | . $PSScriptRoot\inc\pluginversion.ps1 28 | . $PSScriptRoot\inc\uproject.ps1 29 | . $PSScriptRoot\inc\uplugin.ps1 30 | . $PSScriptRoot\inc\filetools.ps1 31 | 32 | 33 | function Write-Usage { 34 | Write-Output "Steve's Unreal Plugin packaging tool" 35 | Write-Output "Usage:" 36 | Write-Output " ue-plugin-package.ps1 [-src:sourcefolder] [-major|-minor|-patch|-hotfix] [-keepversion] [-force] [-test] [-dryrun]" 37 | Write-Output " " 38 | Write-Output " -src : Source folder (current folder if omitted), must contain pluginconfig.json" 39 | Write-Output " -major : Increment major version i.e. [x++].0.0.0" 40 | Write-Output " -minor : Increment minor version i.e. x.[x++].0.0" 41 | Write-Output " -patch : Increment patch version i.e. x.x.[x++].0 (default)" 42 | Write-Output " -hotfix : Increment hotfix version i.e. x.x.x.[x++]" 43 | Write-Output " -keepversion : Keep current version number, doesn't tag unless -forcetag" 44 | Write-Output " -notag : Don't tag even if updating version" 45 | Write-Output " -test : Testing mode, separate builds, allow dirty working copy" 46 | Write-Output " -browse : After packaging, browse the output folder" 47 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 48 | Write-Output " -help : Print this help" 49 | Write-Output " " 50 | } 51 | 52 | if ($src.Length -eq 0) { 53 | $src = "." 54 | Write-Verbose "-src not specified, assuming current directory" 55 | } 56 | 57 | $ErrorActionPreference = "Stop" 58 | 59 | if ($help) { 60 | Write-Usage 61 | Exit 0 62 | } 63 | 64 | Write-Output "~-~-~ Unreal Plugin Package Start ~-~-~" 65 | 66 | if ($test) { 67 | Write-Output "TEST MODE: No tagging, version bumping" 68 | } 69 | 70 | if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -gt 1) { 71 | Write-Output "ERROR: Can't set more than one of major/minor/patch/hotfix at the same time!" 72 | Print-Usage 73 | Exit 5 74 | } 75 | if (($major -or $minor -or $patch -or $hotfix) -and $keepversion) { 76 | Write-Output "ERROR: Can't set keepversion at the same time as major/minor/patch/hotfix!" 77 | Print-Usage 78 | Exit 5 79 | } 80 | 81 | # Detect Git 82 | if ($src -ne ".") { Push-Location $src } 83 | $isGit = Test-Path ".git" 84 | if ($src -ne ".") { Pop-Location } 85 | 86 | # Check working copy is clean (Git only) 87 | if (-not $test -and $isGit) { 88 | if ($src -ne ".") { Push-Location $src } 89 | 90 | if (Test-Path ".git") { 91 | git diff --no-patch --exit-code 92 | if ($LASTEXITCODE -ne 0) { 93 | Write-Output "Working copy is not clean (unstaged changes)" 94 | if ($dryrun) { 95 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 96 | } else { 97 | Exit $LASTEXITCODE 98 | } 99 | } 100 | git diff --no-patch --cached --exit-code 101 | if ($LASTEXITCODE -ne 0) { 102 | Write-Output "Working copy is not clean (staged changes)" 103 | if ($dryrun) { 104 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 105 | } else { 106 | Exit $LASTEXITCODE 107 | } 108 | } 109 | } 110 | if ($src -ne ".") { Pop-Location } 111 | } 112 | 113 | 114 | try { 115 | # Import config & project settings 116 | $config = Read-Plugin-Config -srcfolder:$src 117 | # Need to explicitly set to UTF8, Out-File now converts to UTF16-LE?? 118 | $Utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False 119 | 120 | $pluginfile = Get-Uplugin-Filename -srcfolder:$src -config:$config 121 | if (-not $pluginfile) { 122 | throw "Not in a uplugin dir!" 123 | } 124 | $proj = Read-Uproject $pluginfile 125 | $pluginName = (Get-Item $pluginfile).Basename 126 | 127 | # Default to latest engine if not specified 128 | if (-not $config.EngineVersions -or $config.EngineVersions.Length -eq 0) { 129 | Write-Output "Warning: EngineVersions missing from pluginconfig.json, assuming latest only" 130 | $config.EngineVersions = [System.Collections.ArrayList]@() 131 | $config.EngineVersions.add($proj.EngineVersion) > $null 132 | } 133 | 134 | Write-Output "" 135 | Write-Output "Plugin File : $pluginfile" 136 | Write-Output "Output Folder : $($config.PackageDir)" 137 | Write-Output "Engine Versions : $($config.EngineVersions -join ", ")" 138 | Write-Output "" 139 | 140 | if (([bool]$major + [bool]$minor + [bool]$patch + [bool]$hotfix) -eq 0) { 141 | $patch = $true 142 | } 143 | $versionNumber = $proj.VersionName 144 | if (-not $keepversion) { 145 | # Bump up version, passthrough options 146 | try { 147 | $versionNumber = Get-NextPluginVersion -current:$versionNumber -major:$major -minor:$minor -patch:$patch -hotfix:$hotfix 148 | # Save incremented version back to uplugin object 149 | $proj.VersionName = $versionNumber 150 | if (-not $dryrun -and $isGit -and -not $notag) { 151 | # Save this now, we need to commit before tagging 152 | Write-Output "Incrementing version in .uproject" 153 | $newjson = ($proj | ConvertTo-Json -depth 100) 154 | [System.IO.File]::WriteAllLines($pluginfile, $newjson, $Utf8NoBomEncoding) 155 | 156 | git add . 157 | git commit -m "Version bump" 158 | } 159 | } 160 | catch { 161 | Write-Output $_.Exception.Message 162 | Exit 6 163 | } 164 | } 165 | Write-Output "Next version will be: $versionNumber" 166 | 167 | # For tagging release 168 | # We only need to grab the main version once 169 | if (-not ($keepversion -or $notag)) { 170 | 171 | if (-not $test -and -not $dryrun -and $isGit) { 172 | if ($src -ne ".") { Push-Location $src } 173 | git tag -a $versionNumber -m "Automated release tag" 174 | if ($LASTEXITCODE -ne 0) { Exit $LASTEXITCODE } 175 | if ($src -ne ".") { Pop-Location } 176 | } 177 | } 178 | 179 | $resetinstalled = $false 180 | if (-not $proj.Installed) { 181 | # Need to set the installed=true for marketplace 182 | $proj.Installed = $true 183 | $resetinstalled = $true 184 | } 185 | 186 | # Marketplace requires you to submit one package per EngineVersion for code plugins 187 | # Pretty dumb since the only diff is the EngineVersion in .uplugin but sure 188 | $oldEngineVer = $proj.EngineVersion 189 | foreach ($EngineVer in $config.EngineVersions) { 190 | Write-Output "Packaging for Engine Version $EngineVer" 191 | $proj.EngineVersion = $EngineVer 192 | 193 | $newjson = ($proj | ConvertTo-Json -depth 100) 194 | if (-not $dryrun) { 195 | Write-Output "Writing updates to .uproject" 196 | [System.IO.File]::WriteAllLines($pluginfile, $newjson, $Utf8NoBomEncoding) 197 | 198 | } 199 | 200 | # Zip parent of the uplugin folder 201 | $zipsrc = (Get-Item $pluginfile).Directory.FullName 202 | $zipdst = Join-Path $config.PackageDir "$($pluginName)_v$($versionNumber)_UE$($EngineVer).zip" 203 | $excludefilename = "packageexclusions.txt" 204 | $excludefile = Join-Path $zipsrc $excludefilename 205 | 206 | New-Item -ItemType Directory -Path $config.PackageDir -Force > $null 207 | Write-Output "Compressing to $zipdst" 208 | 209 | $argList = [System.Collections.ArrayList]@() 210 | $argList.Add("a") > $null 211 | $argList.Add($zipdst) > $null 212 | # Standard exclusions 213 | $argList.Add("-x!$pluginName\.git\") > $null 214 | $argList.Add("-x!$pluginName\.git*") > $null 215 | $argList.Add("-x!$pluginName\Binaries\") > $null 216 | $argList.Add("-x!$pluginName\Intermediate\") > $null 217 | $argList.Add("-x!$pluginName\Saved\") > $null 218 | $argList.Add("-x!$pluginName\pluginconfig.json") > $null 219 | 220 | if (Test-Path $excludefile) { 221 | $argList.Add("-x@`"$excludefile`"") > $null 222 | $argList.Add("-x!$pluginName\$excludefilename") > $null 223 | } 224 | 225 | $argList.Add($zipsrc) > $null 226 | 227 | if ($dryrun) { 228 | Write-Output "" 229 | Write-Output "Would have run:" 230 | Write-Output "> 7z.exe $($argList -join " ")" 231 | Write-Output "" 232 | 233 | } else { 234 | Remove-Item -Path $zipdst -Force -ErrorAction SilentlyContinue 235 | $proc = Start-Process "7z.exe" $argList -Wait -PassThru -NoNewWindow 236 | if ($proc.ExitCode -ne 0) { 237 | throw "7-Zip failed!" 238 | } 239 | 240 | } 241 | 242 | } 243 | 244 | 245 | # Reset the uplugin 246 | # Otherwise UE keeps prompting to update project files when using it as source 247 | if ($resetinstalled) { 248 | $proj.Installed = $false 249 | } 250 | $proj.EngineVersion = $oldEngineVer 251 | 252 | if (-not $dryrun) { 253 | $newjson = ($proj | ConvertTo-Json -depth 100) 254 | Write-Output "Resetting .uproject" 255 | [System.IO.File]::WriteAllLines($pluginfile, $newjson, $Utf8NoBomEncoding) 256 | } 257 | 258 | 259 | if ($browse -and -not $dryrun) { 260 | Invoke-Item $config.PackageDir 261 | } 262 | 263 | } 264 | catch { 265 | Write-Output $_.Exception.Message 266 | Write-Output "~-~-~ Unreal Plugin Package FAILED ~-~-~" 267 | Exit 9 268 | } 269 | 270 | Write-Output "~-~-~ Unreal Plugin Package Completed OK ~-~-~" 271 | -------------------------------------------------------------------------------- /ue-rebuild-lightmaps.ps1: -------------------------------------------------------------------------------- 1 | # Lightmap rebuild helper 2 | [CmdletBinding()] # Fail on unknown args 3 | param ( 4 | # Optional source folder, assumed current folder 5 | [string]$src, 6 | # quality level (Preview, Medium, High, Production), default = Production 7 | [string]$quality, 8 | # Explicit list of maps, if not supplied will use cooked maps in packageconfig.json 9 | [array]$maps, 10 | # Dry-run; does nothing but report what *would* have happened 11 | [switch]$dryrun = $false, 12 | [switch]$help = $false 13 | ) 14 | 15 | . $PSScriptRoot\inc\platform.ps1 16 | . $PSScriptRoot\inc\packageconfig.ps1 17 | . $PSScriptRoot\inc\projectversion.ps1 18 | . $PSScriptRoot\inc\uproject.ps1 19 | . $PSScriptRoot\inc\filetools.ps1 20 | 21 | # Include Git tools locking 22 | . $PSScriptRoot\GitScripts\inc\locking.ps1 23 | 24 | function Write-Usage { 25 | Write-Output "Steve's Unreal lightmap rebuilding tool" 26 | Write-Output "Usage:" 27 | Write-Output " ue-rebuild-lightmaps.ps1 [-src:sourcefolder] [-quality:(preview|medium|high|production)] [-maps Map1,Map2,Map3] [-dryrun]" 28 | Write-Output " " 29 | Write-Output " -src : Source folder (current folder if omitted)" 30 | Write-Output " -quality : Lightmap quality, preview/medium/high/production" 31 | Write-Output " : (Default: production)" 32 | Write-Output " -maps : List of maps to rebuild. If omitted, will derive which ones to" 33 | Write-Output " rebuild based on cooked maps in packageconfig.json" 34 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 35 | Write-Output " -help : Print this help" 36 | Write-Output " " 37 | Write-Output "Environment Variables:" 38 | Write-Output " UEINSTALL : Use a specific Unreal install." 39 | Write-Output " : Default is to find one based on project version, under UEROOT" 40 | Write-Output " UEROOT : Parent folder of all binary Unreal installs (detects version). " 41 | Write-Output " : Default C:\Program Files\Epic Games" 42 | Write-Output " " 43 | } 44 | 45 | if ($src.Length -eq 0) { 46 | $src = "." 47 | Write-Verbose "-src not specified, assuming current directory" 48 | } 49 | 50 | $ErrorActionPreference = "Stop" 51 | 52 | if ($help) { 53 | Write-Usage 54 | Exit 0 55 | } 56 | 57 | # Detect Git 58 | if ($src -ne ".") { Push-Location $src } 59 | $isGit = Test-Path ".git" 60 | if ($src -ne ".") { Pop-Location } 61 | 62 | Write-Output "~-~-~ Unreal Lightmap Rebuild Start ~-~-~" 63 | 64 | try { 65 | $config = Read-Package-Config -srcfolder:$src 66 | $projfile = Get-Uproject-Filename -srcfolder:$src -config:$config 67 | $proj = Read-Uproject $projfile 68 | $ueVersion = Get-UE-Version $proj 69 | $ueinstall = Get-UE-Install $ueVersion 70 | 71 | if ($maps) { 72 | # Explicit list of maps provided on command line 73 | $foundmaps = Find-File-Set -startDir:$(Join-Path $src "Content") -pattern:*.umap -includeByDefault:$false -includeBaseNames:$maps 74 | 75 | if ($mapsToRebuild.Count -ne $maps.Count) { 76 | Write-Warning "Ignoring missing map(s): $($maps | Where-Object { $_ -notin $mapsToRebuild })" 77 | } 78 | } else { 79 | # Derive maps from cook settings 80 | $foundmaps = Find-Files -startDir:$(Join-Path $src "Content") -pattern:*.umap -includeByDefault:$config.CookAllMaps -includeBaseNames:$config.MapsIncluded -excludeBaseNames:$config.MapsExcluded 81 | } 82 | 83 | if ($foundmaps.BaseNames.Count -eq 0) { 84 | throw "No maps found to rebuild" 85 | } 86 | 87 | if (-not $quality) { 88 | $quality = "Production" 89 | } 90 | if ($quality -notin @("Preview", "Medium", "High", "Production")) { 91 | throw "Invalid quality level: $quality" 92 | } 93 | 94 | Write-Output "" 95 | Write-Output "Project File : $projfile" 96 | Write-Output "UE Version : $ueVersion" 97 | Write-Output "UE Install : $ueinstall" 98 | Write-Output "" 99 | Write-Output "Maps : $($foundmaps.BaseNames)" 100 | Write-Output "Quality : $quality" 101 | Write-Output "" 102 | 103 | # lock map files if read-only 104 | if ($isGit -and -not $dryrun) { 105 | if ($src -ne ".") { Push-Location $src } 106 | 107 | foreach ($mapfullname in $foundmaps.FullNames) { 108 | $relativepath = Resolve-Path $mapfullname -Relative 109 | Lock-If-Required $relativepath 110 | Lock-If-Required $($relativepath -replace ".uasset","_BuiltData.uasset") 111 | } 112 | if ($src -ne ".") { Pop-Location } 113 | } 114 | 115 | $argList = [System.Collections.ArrayList]@() 116 | $argList.Add("`"$projfile`"") > $null 117 | $argList.Add("-run=ResavePackages") > $null 118 | $argList.Add("-buildtexturestreaming") > $null 119 | $argList.Add("-buildlighting") > $null 120 | $argList.Add("-buildreflectioncaptures") > $null 121 | $argList.Add("-MapsOnly") > $null 122 | $argList.Add("-ProjectOnly") > $null 123 | $argList.Add("-AllowCommandletRendering") > $null 124 | $argList.Add("-SkipSkinVerify") > $null 125 | $argList.Add("-Quality=$quality") > $null 126 | $argList.Add("-Map=$($foundmaps.BaseNames -join "+")") > $null 127 | 128 | $ueEditorCmd = Get-UEEditorCmd $ueVersion $ueinstall 129 | 130 | if ($dryrun) { 131 | Write-Output "Would have run:" 132 | Write-Output "> $ueEditorCmd $($argList -join " ")" 133 | 134 | } else { 135 | Write-Output "Starting lighting build; see Swarm Agent for full progress monitoring..." 136 | $proc = Start-Process $ueEditorCmd $argList -Wait -PassThru -NoNewWindow 137 | if ($proc.ExitCode -ne 0) { 138 | throw "Lightmap build failed!" 139 | } 140 | 141 | } 142 | 143 | } catch { 144 | Write-Output $_.Exception.Message 145 | Write-Output "~-~-~ Unreal Lightmap Rebuild FAILED ~-~-~" 146 | Exit 9 147 | 148 | } 149 | 150 | 151 | Write-Output "~-~-~ Unreal Lightmap Rebuild OK ~-~-~" 152 | Write-Output "Reminder: You may need to commit and unlock map files" 153 | -------------------------------------------------------------------------------- /ue-release.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | # Explicit version to release 4 | [string]$version, 5 | # Latest version option instead of explicit version 6 | [switch]$latest, 7 | # Variant name(s) to release; if not specified release all DefaultVariants with ReleaseTo options 8 | [array]$variants, 9 | # Source folder, current dir if omitted 10 | [string]$src, 11 | # Which service(s) to release on e.g. "steam": defaults to "ReleaseTo" services in packageconfig.json for variant 12 | [array]$services, 13 | # Dry-run; does nothing but report what *would* have happened 14 | [switch]$dryrun = $false, 15 | [switch]$help = $false 16 | ) 17 | 18 | . $PSScriptRoot\inc\platform.ps1 19 | . $PSScriptRoot\inc\packageconfig.ps1 20 | . $PSScriptRoot\inc\projectversion.ps1 21 | . $PSScriptRoot\inc\uproject.ps1 22 | . $PSScriptRoot\inc\filetools.ps1 23 | . $PSScriptRoot\inc\steam.ps1 24 | . $PSScriptRoot\inc\itch.ps1 25 | 26 | 27 | function Write-Usage { 28 | Write-Output "Steve's Unreal release tool" 29 | Write-Output "Usage:" 30 | Write-Output " ue-release.ps1 [-version:ver|-latest] -variant:var -services:steam,itch [-src:sourcefolder] [-dryrun]" 31 | Write-Output " " 32 | Write-Output " -version:ver : Version to release; must have been packaged already" 33 | Write-Output " -latest : Instead of an explicit version, release one identified in project settings" 34 | Write-Output " -variants:var1,var2 : Name of variants to release. Omit to use DefaultVariants" 35 | Write-Output " -services:s1,s2 : Name of services to release to. Can omit and rely on ReleaseTo" 36 | Write-Output " setting of variant in packageconfig.json " 37 | Write-Output " -src : Source folder (current folder if omitted), must contain packageconfig.json" 38 | Write-Output " -dryrun : Don't perform any actual actions, just report what would happen" 39 | Write-Output " -help : Print this help" 40 | } 41 | 42 | $ErrorActionPreference = "Stop" 43 | 44 | if ($help) { 45 | Write-Usage 46 | Exit 0 47 | } 48 | 49 | if ($src.Length -eq 0) { 50 | $src = "." 51 | Write-Verbose "-src not specified, assuming current directory" 52 | } 53 | 54 | if (-not $version -and -not $latest) { 55 | Write-Usage 56 | Write-Output "" 57 | Write-Output "ERROR: You must specify a version" 58 | Exit 1 59 | } 60 | 61 | if ($version -and $latest) { 62 | Write-Usage 63 | Write-Output "" 64 | Write-Output "ERROR: You cannot specify a -version and -latest at the same time" 65 | Exit 1 66 | } 67 | 68 | Write-Output "~-~-~ Unreal Release Helper Start ~-~-~" 69 | 70 | try { 71 | 72 | # Import config 73 | $config = Read-Package-Config -srcfolder:$src 74 | $projfile = Get-Uproject-Filename -srcfolder:$src -config:$config 75 | $proj = Read-Uproject $projfile 76 | $ueVersion = Get-UE-Version $proj 77 | 78 | if ($latest) { 79 | $version = Get-Project-Version $src 80 | } 81 | 82 | if ($variants) { 83 | $variantConfigs = $config.Variants | Where-Object { $_.Name -in $variants } 84 | if ($variantConfigs.Count -ne $variants.Count) { 85 | $unmatchedVariants = $variants | Where-Object { $_ -notin $variantConfigs.Name } 86 | Write-Warning "Variant(s) not found, ignoring: $($unmatchedVariants -join ", ")" 87 | } 88 | } else { 89 | # Use default variants 90 | $variantConfigs = $config.Variants | Where-Object { $_.Name -in $config.DefaultVariants } 91 | } 92 | 93 | $hasErrors = $false 94 | foreach ($variantConfig in $variantConfigs) { 95 | 96 | # Get source dir 97 | $sourcedir = Get-Package-Client-Dir -config:$config -versionNumber:$version -variantName:$variantConfig.Name -ueVersion:$ueVersion 98 | 99 | if (-not (Test-Path $sourcedir -PathType Container)) { 100 | Write-Error "Release folder $sourcedir does not exist, skipping" 101 | $hasErrors = $true 102 | continue 103 | } 104 | 105 | # Find service(s) 106 | if ($services) { 107 | # Release to a subset of allowed services 108 | $servicesFound = $services | Where-Object {$variantConfig.ReleaseTo -contains $_ } 109 | if ($servicesFound.Count -ne $services.Count) { 110 | $unmatchedServices = $services | Where-Object { $servicesFound -notcontains $_ } 111 | Write-Warning "Services(s) not supported by $($variantConfig.Name): $($unmatchedServices -join ", ")" 112 | } 113 | } else { 114 | $servicesFound = $variantConfig.ReleaseTo 115 | } 116 | 117 | if (-not $servicesFound) { 118 | Write-Verbose "Skipping $($variantConfig.Name), no matching release services" 119 | continue 120 | } 121 | 122 | Write-Output "" 123 | Write-Output "Variant : $($variantConfig.Name)" 124 | Write-Output "Version : $version" 125 | Write-Output "Source Folder : $sourcedir" 126 | Write-Output "Service(s) : $($servicesFound -join ", ")" 127 | Write-Output "" 128 | 129 | 130 | foreach ($service in $servicesFound) { 131 | if ($service -eq "steam") { 132 | Release-Steam -config:$config -variant:$variantConfig -sourcefolder:$sourcedir -version:$version -dryrun:$dryrun 133 | } elseif ($service -eq "itch") { 134 | Release-Itch -config:$config -variant:$variantConfig -sourcefolder:$sourcedir -version:$version -dryrun:$dryrun 135 | } else { 136 | throw "Unknown release service: $service" 137 | } 138 | } 139 | 140 | } 141 | 142 | if ($hasErrors) { 143 | throw "Errors occurred, see above" 144 | } 145 | 146 | } catch { 147 | Write-Output $_.Exception.Message 148 | Write-Output "~-~-~ Unreal Release Helper FAILED ~-~-~" 149 | Exit 9 150 | } 151 | 152 | 153 | Write-Output "~-~-~ Unreal Release Helper Completed OK ~-~-~" 154 | -------------------------------------------------------------------------------- /ue-updatelyra.ps1: -------------------------------------------------------------------------------- 1 | [CmdletBinding()] # Fail on unknown args 2 | param ( 3 | [string]$src, 4 | [string]$dest, 5 | [string]$mainbranch = "main", 6 | [string]$upstreambranch = "lyra-upstream", 7 | [string]$custombranch = "lyra-custom", 8 | [switch]$dryrun = $false, 9 | [switch]$help = $false 10 | ) 11 | 12 | function Print-Usage { 13 | Write-Output "Steve's Lyra Update Tool" 14 | Write-Output "Usage:" 15 | Write-Output " ue-updatelyra.ps1 [-src:]sourcefolder [-dest:destfolder] [Options]" 16 | Write-Output " " 17 | Write-Output " -src : Source Lyra folder" 18 | Write-Output " : Folder created by Create Project on Lyra in Marketplace" 19 | Write-Output " -dest : Destination folder (default: current directory)" 20 | Write-Output " : Must be root folder of your custom Lyra based project" 21 | Write-Output " -mainbranch : Name of main branch (default main)" 22 | Write-Output " -upstreambranch : Name of branch containing pristine upstream version of Lyra (default lyra-upstream)" 23 | Write-Output " : (MUST exist! We need to know where the pristine Lyra goes)" 24 | Write-Output " -custombranch : Name of branch with your custom changes to Lyra (default lyra-custom)" 25 | Write-Output " : (Will be created if missing)" 26 | Write-Output " -dryrun : Don't perform any actual actions, just report on what you would do" 27 | Write-Output " -help : Print this help" 28 | Write-Output " " 29 | 30 | } 31 | 32 | $ErrorActionPreference = "Stop" 33 | 34 | if ($help) { 35 | Print-Usage 36 | Exit 0 37 | } 38 | 39 | if ($src.Length -eq 0) { 40 | Write-Output "Error: Source directory argument is mandatory" 41 | Print-Usage 42 | Exit 3 43 | } 44 | 45 | if ($dest.Length -eq 0) { 46 | $dest = "." 47 | Write-Verbose "dest not specified, assuming current directory" 48 | } 49 | 50 | # Detect Git 51 | if ($dest -ne ".") { Push-Location $dest } 52 | $isGit = Test-Path ".git" 53 | if ($dest -ne ".") { Pop-Location } 54 | 55 | if (-not $isGit) 56 | { 57 | Write-Output "Destination '$dest' is not a Git repo, cannot continue!" 58 | Exit 3 59 | } 60 | # Check that source contains Lyra 61 | if (-not (Test-Path (Join-Path $src "LyraStarterGame.uproject"))) 62 | { 63 | Write-Output "Source folder '$src' does not contain LyraStarterGame.uproject" 64 | Exit 3 65 | } 66 | # Check that destination contains Lyra 67 | if (-not (Test-Path (Join-Path $dest "LyraStarterGame.uproject"))) 68 | { 69 | Write-Output "Destination folder '$dest' does not contain LyraStarterGame.uproject" 70 | Exit 3 71 | } 72 | # Check that source & destination are not the same (no standardise path in ps, Join-Path does it) 73 | if ((Join-Path (Resolve-Path $dest) "") -eq (Join-Path (Resolve-Path $src) "")) 74 | { 75 | Write-Output "Source and destination folder point to the same location, $src" 76 | Exit 3 77 | } 78 | 79 | # Check working copy is clean 80 | if ($dest -ne ".") { Push-Location $dest } 81 | 82 | if (Test-Path ".git") { 83 | git diff --no-patch --exit-code 84 | if ($LASTEXITCODE -ne 0) { 85 | Write-Output "Working copy is not clean (unstaged changes)" 86 | if ($dryrun) { 87 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 88 | } else { 89 | Exit $LASTEXITCODE 90 | } 91 | } 92 | git diff --no-patch --cached --exit-code 93 | if ($LASTEXITCODE -ne 0) { 94 | Write-Output "Working copy is not clean (staged changes)" 95 | if ($dryrun) { 96 | Write-Output "dryrun: Continuing but this will fail without -dryrun" 97 | } else { 98 | Exit $LASTEXITCODE 99 | } 100 | } 101 | } 102 | 103 | 104 | git rev-parse --verify -q $upstreambranch > $null 105 | if ($LASTEXITCODE -ne 0) 106 | { 107 | Write-Output "Missing Lyra upstream branch '$upstreambranch'" 108 | Exit 3 109 | } 110 | git rev-parse --verify -q $mainbranch > $null 111 | if ($LASTEXITCODE -ne 0) 112 | { 113 | Write-Output "Missing main branch '$mainbranch'" 114 | Exit 3 115 | } 116 | 117 | 118 | if ($dryrun) 119 | { 120 | Write-Output "Would have run:" 121 | } 122 | 123 | # Switch to lyra pristine branch 124 | if ($dryrun) 125 | { 126 | Write-Output " > git checkout $upstreambranch" 127 | } 128 | else 129 | { 130 | git checkout $upstreambranch 131 | } 132 | 133 | # Check that dest contains Lyra 134 | if (-not (Test-Path (Join-Path $dest "LyraStarterGame.uproject"))) 135 | { 136 | Write-Output "Destination folder '$dest' does not contain LyraStarterGame.uproject after checking out $upstreambranch" 137 | Exit 3 138 | } 139 | 140 | try { 141 | # Copy Lyra source 142 | # Use robocopy -mir so that we delete files that are missing in source 143 | 144 | $argList = [System.Collections.ArrayList]@() 145 | $argList.Add($src) > $null 146 | $argList.Add($dest) > $null 147 | # Mirror (deletes) 148 | $argList.Add("/mir") > $null 149 | # Allow resume 150 | $argList.Add("/z") > $null 151 | # Wait time for retry 152 | $argList.Add("/w:3") > $null 153 | # Exclude git files / dirs (do not delete because of mirroring) 154 | $argList.Add("/xf") > $null 155 | $argList.Add((Join-Path $dest ".gitignore")) > $null 156 | $argList.Add("/xf") > $null 157 | $argList.Add((Join-Path $dest ".gitattributes")) > $null 158 | $argList.Add("/xf") > $null 159 | $argList.Add((Join-Path $dest ".gitmodules")) > $null 160 | $argList.Add("/xd") > $null 161 | $argList.Add((Join-Path $dest ".git")) > $null 162 | # Exclude source Intermediate, Binaries, DDC 163 | $argList.Add("/xd") > $null 164 | $argList.Add((Join-Path $src "Binaries")) > $null 165 | $argList.Add("/xd") > $null 166 | $argList.Add((Join-Path $src "Intermediate")) > $null 167 | $argList.Add("/xd") > $null 168 | $argList.Add((Join-Path $src "DerivedDataCache")) > $null 169 | 170 | if ($dryrun) { 171 | Write-Output " > robocopy $($argList -join " ")" 172 | 173 | } else { 174 | $proc = Start-Process "robocopy" $argList -Wait -PassThru -NoNewWindow 175 | # Robocopy can return up to value 8 for success 176 | # See https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/robocopy 177 | if ($proc.ExitCode -gt 8) { 178 | throw "robocopy failed!" 179 | } 180 | } 181 | 182 | 183 | # Add & Commit changes, if any 184 | if (-not $dryrun) 185 | { 186 | git add . 187 | git diff --no-patch --cached --exit-code 188 | if ($LASTEXITCODE -eq 0) { 189 | Write-Output "No changes found to Lyra" 190 | git checkout $mainbranch 191 | Exit 0 192 | } 193 | git commit -m "Lyra update" 194 | } 195 | else 196 | { 197 | Write-Output " > git add . && git commit -m `"Lyra update`"" 198 | } 199 | 200 | 201 | if (-not $dryrun) 202 | { 203 | # Merge changes into custom 204 | git rev-parse --verify -q $custombranch > $null 205 | if ($LASTEXITCODE -ne 0) 206 | { 207 | Write-Output "Creating branch '$custombranch'" 208 | git checkout -b $custombranch 209 | } 210 | else 211 | { 212 | git checkout $custombranch 213 | git merge $upstreambranch 214 | if ($LASTEXITCODE -ne 0) 215 | { 216 | throw "Unable to merge $upstreambranch into $custombranch, resolve merge conflicts and finish merge into $mainbranch yourself " 217 | } 218 | } 219 | 220 | # merge changes into main branch 221 | git checkout $mainbranch 222 | git merge $custombranch 223 | if ($LASTEXITCODE -ne 0) 224 | { 225 | throw "Unable to merge $upstreambranch into $custombranch, resolve merge conflicts and finish merge into $mainbranch yourself " 226 | } 227 | 228 | 229 | } 230 | else 231 | { 232 | Write-Output " > git checkout $custombranch" 233 | Write-Output " > git merge $upstreambranch" 234 | Write-Output " > git checkout $mainbranch" 235 | Write-Output " > git merge $custombranch" 236 | 237 | } 238 | 239 | 240 | 241 | } 242 | catch { 243 | if ($dest -ne ".") { Pop-Location } 244 | Write-Output $_.Exception.Message 245 | Write-Output "~-~-~ Updating Lyra FAILED ~-~-~" 246 | Exit 9 247 | } 248 | 249 | if ($dest -ne ".") { Pop-Location } 250 | Write-Output "~-~-~ Lyra Update Completed OK ~-~-~" 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | --------------------------------------------------------------------------------