Skip to content

Commit

Permalink
Deployment rework
Browse files Browse the repository at this point in the history
  • Loading branch information
GingerAvalanche committed Sep 14, 2024
1 parent 67da084 commit f3f821d
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 66 deletions.
96 changes: 64 additions & 32 deletions book/src/setup/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ generally more useful to use automatic deployment.

## Deployment Locations

Where should you deploy your mods? It depends mostly on where you play your
game.
Where should you deploy your mods and what layout should you use? It depends
mostly on where you play your game.

### Cemu

Cemu users will generally want to deploy their mods as a graphic pack. In that
case you will need to set your deployment location somewhere inside Cemu's
`graphicPacks` folder. A customary option is a new folder named
`BreathOfTheWild_UKMM`. So, for example, the full path might be something like:
case, the best idea is to set your deployment location to Cemu's `graphicPacks`
folder and turn on the With Name option for Deploy Layout. So, for example, the
full path might be something like: `C:\Cemu\graphicPacks\`. For that example,
and with the With Name option, UKMM will actually deploy to
`C:\Cemu\graphicPacks\BreathOfTheWild_UKMM`.

**Additional note for Cemu users**: You almost certainly want the "Deploy
Expand All @@ -32,37 +33,41 @@ rules.txt" option selected for Cemu integration.
Wii U users have a few options, but the most widely used and supported method to
load mods is via [SDCafiine for the Wii U Plugin
System](https://zeldamods.org/wiki/Help:Using_mods#Setting_up_WUPS_SDCafiine).
In that case you would generally want your mods to end up on your SD card under
something like `/sdcafiine/<title ID>/ukmm`. If you use UKMM while your SD card
is not in, however, you might want to set a temporary directory for deploying
mods, or you can merge without the SD card but wait and deploy when the SD card
is mountained.
In that case, you would generally want your mods to end up on your SD card under
something like `/sdcafiine/<title ID>/ukmm`. To achieve that, you could set the
folder directly and choose the Without Name option for Deploy Layout.

You could also set the deployment location to `/sdcafiine/<title ID>` and choose
the With Name option, and UKMM will add the final folder on its own.

If you use UKMM while your SD card is not in, however, you might want to set a
temporary directory for deploying mods, or you can merge without the SD card but
wait and deploy when the SD card is mounted.

### Switch

With the Switch, you generally want your mods to end up on your SD card under
`/atmosphere/contents`. If you use UKMM while your SD card is not in, however,
you might want to set a temporary directory for deploying mods, or you can merge
without the SD card but wait and deploy when the SD card is mountained.
`/atmosphere/contents`, and you will always want to use Without Name for the
Deploy Layout. If you use UKMM while your SD card is not in, however, you might
want to set a temporary directory for deploying mods, or you can merge without
the SD card but wait and deploy when the SD card is mounted.

### Yuzu or Ryujinx

Yuzu and Ryujinx both allow you to install mods in two different locations, one
specific to their own files and the other for emulating Atmosphere's LayeredFS
setup on SD card. *You must use the LayeredFS arrangement.*
setup on SD card. You may use either arrangement, but you *must* choose the
correct Deploy Layout, or the emulator will not read the merged mod correctly.

So, for example, the Yuzu user storage folder is
`C:\Users\[USER]\AppData\Roaming\yuzu` on Windows or `~/.local/share/yuzu` on
Linux. In this case, you want your deployment folder at
`[USER-FOLDER]/sdmc/atmosphere/contents`.
So, for example, if you want to use Yuzu with the Atmosphere implementation, then
the Yuzu user storage folder is `C:\Users\[USER]\AppData\Roaming\yuzu` on Windows
or `~/.local/share/yuzu` on Linux. In this case, you want your deployment folder at
`[USER-FOLDER]/sdmc/atmosphere/contents` and you want your Deploy Layout set to
Without Name.

> **Note on Switch-based deployment**:
> When using Switch, Yuzu, or Ryujinx, you will need to generally use the
> `contents` folder as the actual deployment folder, and the two title ID
> folders for BOTW and its DLC will be used to store mod files. If you mod other
> games besides BOTW, note that they will also have their mods in a title ID
> folder in the same `contents` folder, and some operations could affect them.
> Be aware of this particularly when using the symlink method discussed below.
If you want to use Yuzu's specific mod loader implementation, then that will read
from `[YUZU-DIRECTORY]/load`, so you will set that as your deployment location and
set your Deploy Layout to With Name.

## Deployment Methods

Expand Down Expand Up @@ -119,12 +124,6 @@ networked drives are not supported. If that fails, it will try to use a regular
directory symbolic link. These have fewer restrictions, but usually (for some
dumb reason) require administrator permissions to create.[^1]

> **Note for Switch/Yuzu/Ryujinx:** Since the deployment folder will need to be
> set the `atmosphere/contents` root, and it will deploy the two title ID folders
> for BOTW and its DLC inside, you may run into issues if you also have mods for
> other games (other title IDs). They might be erased or end up inside UKMM's
> storage folder, depening on your precise process and settings.
So, in sum:

**Advantages**
Expand All @@ -135,10 +134,43 @@ So, in sum:
- Windows support is complicated
- No chance to change your mind before deploying mods after applying load order
changes
- Does not mesh well for Switch users modding other games

**Best for**: Linux systems, or advanced users on Windows

## Deployment Layouts

### Without Name

UKMM will not add any folders called `BreathOfTheWild_UKMM` without you telling it.
On WiiU, this means that content files will be deployed to `[Output Folder]/content`
and dlc files will be deployed to `[Output Folder]/aoc`. On Switch, this means that
content files will be deployed to `[Output Folder]/01007EF00011E000/romfs` and dlc
files will be deployed to `[Output Folder]/01007EF00011F001/romfs`.

This is useful for if you're following an old setup tutorial that tells you to put a
specific folder for your mod manager in the output path, if you're on a Switch
console, or if you're on a Switch emulator and using the atmosphere mod directory for
it.

This is how BCML and previous beta builds of UKMM always handled deployment. If
you are upgrading from BCML or an old build of UKMM and your paths already work
for you, then you can leave this as your deployment layout and it will just work.

### With Name

UKMM will add folders called `BreathOfTheWild_UKMM` to the appropriate places when
deploying. On WiiU, this means that content files will be deployed to
`[Output Folder]/BreathOfTheWild_UKMM/content` and dlc fils will be deployed to
`[Output Folder]/BreathOfTheWild_UKMM/aoc`. On Switch, this means that content files
will be deployed to `[Output Folder]/01007EF00011E000/BreathOfTheWild_UKMM/romfs`
and dlc files will be deployed to
`[Output Folder]/01007EF00011F001/BreathOfTheWild_UKMM/romfs`.

This is useful if you just want to point UKMM at your Cemu graphic pack folder or
WiiU SD Card and call it a day, or if you're using a Switch emulator and deploying
to the regular mods directory so that you can activate/deactivate mods in the
in-emulator menu.

---

[^1]: Starting back in Windows 10, build 14972, it has been possible to create
Expand Down
81 changes: 50 additions & 31 deletions crates/uk-manager/src/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,37 +234,57 @@ impl Manager {
})
.context("No deployment config for current platform")?;
log::debug!("Deployment config:\n{:#?}", &config);

// Determine src and dest folders
let (content, aoc) = uk_content::platform_prefixes(settings.current_mode.into());
let src_content = settings.merged_dir().join(content);
let src_aoc = settings.merged_dir().join(aoc);
let (dest_content, dest_aoc) = config.final_output_paths(settings.current_mode.into());
// Remove old behavior
if is_symlink(&config.output) {
log::info!("Removing old symlink deployment behavior");
util::remove_symlink(&config.output)
.context("Failed to remove old deployment behavior symlink")?;
}

if config.method == DeployMethod::Symlink {
log::info!("Deploy method is symlink, checking for symlink");
if !is_symlink(&config.output) {
if config.output.exists() {
log::warn!("Removing old stuff from deploy folder");
util::remove_dir_all(&config.output)

for (src, dest, type_) in [
(src_content, dest_content.clone(), "content"),
(src_aoc, dest_aoc, "aoc")
] {
log::info!("Generating {} links", type_);
let parent = dest.parent().context("Dest has no parent?")?;
if src.exists() && !parent.exists() {
fs::create_dir_all(parent)
.context("Failed to create parents for dest folder")?;
}
if dest.exists() && !is_symlink(&dest) {
log::warn!("Removing old stuff from {} deploy folder", type_);
util::remove_dir_all(&dest)
.context("Failed to remove old deployment folder")?;
}
log::info!("Creating new symlink");
create_symlink(&config.output, &settings.merged_dir())
.context("Failed to symlink deployment folder")?;
} else if !is_symlink_to(&config.output, &settings.merged_dir()) {
log::info!("Refreshing symlink to correct profile");
util::remove_symlink(&config.output)?;
create_symlink(&config.output, &settings.merged_dir())?;
} else {
log::info!("Symlink exists, no deployment needed")
if src.exists() && !dest.exists() {
log::info!("Creating new symlink for {} folder", type_);
create_symlink(&dest, &src)
.context("Failed to deploy symlink")?;
} else if !src.exists() && dest.exists() {
log::info!("No {} files, removing link", type_);
util::remove_symlink(&dest)
.context("Failed to remove symlink to non-existent folder")?;
} else if src.exists() && dest.exists() &&
!is_symlink_to(&dest, &src) {
log::info!("Refreshing {} link to correct profile", type_);
util::remove_symlink(&dest)
.context("Failed to remove symlink to incorrect profile")?;
create_symlink(&dest, &src)
.context("Failed to create symlink to correct profile")?;
} else {
log::info!("Symlink exists, no deployment needed")
}
}
} else {
if is_symlink(&config.output) {
util::remove_symlink(&config.output)?;
/*
anyhow_ext::bail!(
"Deployment folder is currently a symlink or junction, but the current \
deployment method is not symlinking. Please manually remove the existing \
link at {} to prevent unexpected results.",
config.output.display()
);
*/
}
let (content, aoc) = uk_content::platform_prefixes(settings.current_mode.into());
let deletes = self.pending_delete.read();
log::debug!("Deployed files to delete:\n{:#?}", &deletes);
let syncs = self.pending_files.read();
Expand All @@ -274,17 +294,16 @@ impl Manager {
DeployMethod::HardLink => "hard links",
DeployMethod::Symlink => unsafe { std::hint::unreachable_unchecked() },
});
log::info!("Deploy layout: {}", config.layout.name());

let filter_xbootup = |file: &&String| -> bool {
!file.starts_with("Pack/Bootup_") || **file == lang.bootup_path()
};

for (dir, dels, syncs) in [
(content, &deletes.content_files, &syncs.content_files),
(aoc, &deletes.aoc_files, &syncs.aoc_files),
for (source, dest, dels, syncs) in [
(src_content, dest_content.clone(), &deletes.content_files, &syncs.content_files),
(src_aoc, dest_aoc, &deletes.aoc_files, &syncs.aoc_files),
] {
let dest = config.output.join(dir);
let source = settings.merged_dir().join(dir);
dels.par_iter()
.filter(filter_xbootup)
.try_for_each(|f| -> Result<()> {
Expand Down Expand Up @@ -334,7 +353,7 @@ impl Manager {
}
log::info!("Deployment complete");
}
let rules_path = config.output.join("rules.txt");
let rules_path = dest_content.parent().unwrap().join("rules.txt");
if settings.current_mode == Platform::WiiU
&& settings
.platform_config()
Expand Down
62 changes: 61 additions & 1 deletion crates/uk-manager/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DefaultOnError};
use smartstring::alias::String;
use uk_content::constants::Language;
use uk_content::{constants::Language, prelude::Endian};
use uk_reader::ResourceReader;

#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
Expand Down Expand Up @@ -105,6 +105,48 @@ pub struct DeployConfig {
pub cemu_rules: bool,
#[serde(default)]
pub executable: Option<std::string::String>,
#[serde(default)]
pub layout: DeployLayout,
}

impl DeployConfig {
pub fn final_output_paths(&self, endian: Endian) -> (PathBuf, PathBuf) {
match endian {
Endian::Little => {
match self.layout {
DeployLayout::WithoutName => (
self.output.join("01007EF00011E000").join("romfs"),
self.output.join("01007EF00011F001").join("romfs"),
),
DeployLayout::WithName => (
self.output
.join("01007EF00011E000")
.join("BreathOfTheWild_UKMM")
.join("romfs"),
self.output
.join("01007EF00011F001")
.join("BreathOfTheWild_UKMM")
.join("romfs"),
),
}
}
Endian::Big => {
match self.layout {
DeployLayout::WithoutName => (
self.output.join("content"),
self.output.join("aoc").join("0010"),
),
DeployLayout::WithName => (
self.output.join("BreathOfTheWild_UKMM").join("content"),
self.output
.join("BreathOfTheWild_UKMM")
.join("aoc")
.join("0010"),
),
}
}
}
}
}

impl Default for DeployConfig {
Expand All @@ -115,6 +157,7 @@ impl Default for DeployConfig {
auto: false,
cemu_rules: false,
executable: None,
layout: DeployLayout::WithoutName,
}
}
}
Expand All @@ -137,6 +180,23 @@ impl DeployMethod {
}
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum DeployLayout {
#[default]
WithoutName,
WithName
}

impl DeployLayout {
#[inline(always)]
pub fn name(&self) -> &str {
match self {
DeployLayout::WithoutName => "SD Card",
DeployLayout::WithName => "Emulator",
}
}
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PlatformSettings {
pub language: Language,
Expand Down
27 changes: 27 additions & 0 deletions src/gui/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,33 @@ fn render_deploy_config(config: &mut DeployConfig, platform: Platform, ui: &mut
.changed();
},
);
render_setting(
"Deploy Layout",
"There are two methods of deployment layout: without a folder named for UKMM, \
and with a folder named for UKMM. If you select With Name, UKMM will add a \
BreathOfTheWild_UKMM folder to the end of your Output Folder path, where appropriate. \
If you don't know what to choose for this: On WiiU, choose With Name. On Switch consoles or \
when your output folder is an atmosphere folder, choose Without Name. On Switch emulators \
where your output folder is NOT an atmosphere folder, choose With Name. For more on this, \
consult the docs.",
ui,
|ui| {
changed |= ui
.radio_value(
&mut config.layout,
uk_manager::settings::DeployLayout::WithoutName,
"Without Name",
)
.changed();
changed |= ui
.radio_value(
&mut config.layout,
uk_manager::settings::DeployLayout::WithName,
"With Name",
)
.changed();
}
);
render_setting(
"Auto Deploy",
"Whether to automatically deploy changes to the mod configuration every time they are \
Expand Down
Loading

0 comments on commit f3f821d

Please sign in to comment.