Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .clangd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ensure clangd can find the compile_commands.json in the build dir.
CompileFlags:
CompilationDatabase: "build"
39 changes: 39 additions & 0 deletions customizer/INSTALL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Custom Model Installation

This file explains how to install a custom model into the randomizer. Support is
currently experimental and may softlock your game!

## Extracting Necessary Files

Download any custom model from Gamebanana and extract the zip file. You will
probably find a bunch of folders and from there we want to find some files :

- Either a `permanent_3d.pack` file or a `Link.szs` file (ideally the latter).

- Files with the following names, if present:
- `Jailnit.aaf`
- `voice_0.aw`

If you could find them within a folder with a name like `Raw Files` or
`Extra Files`, great! Otherwise, you may need to dig around folders with names
like `Cafe` or `Common`. The `Link.szs` file is essential; the others are
optional and the model creator might not have provided them.

If you have `permanent_3d.pack` and not `Link.szs`, we'll need just a few more
steps. You'll need to use
[Toolbox](https://github.com/KillzXGaming/Switch-Toolbox/releases/tag/Final) to
export a `Link.szs`. Simply open the `.pack` file in Toolbox, scroll to find
`Link.szs`, right-click and `Export Raw Data`.

## Installing on Console

Access your SD card (either directly or over FTP) and find the save data for the
randomizer app, usually `sd:/wiiu/apps/save/<8 hex digits> (TWWHD Randomizer)`.
Inside, find a folder called `custom_models`. Create a new directory with your
model's name and place `Link.szs`, along with the other, optional files.

You can now load the randomizer app on your Wii U and select the model from the
Color tab.

Randomize the game, and you'll find link has changed into someone else! Have
fun!
89 changes: 85 additions & 4 deletions customizer/model.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@
using eType = Utility::Endian::Type;

ModelError CustomModel::loadFromFolder() {

if (modelName == "") {
modelName = "Link";
auto model = modelName;
if (user_provided || model == "") {
model = "Link";
}

folder = Utility::get_data_path() / "customizer" / modelName;
folder = Utility::get_data_path() / "customizer" / model;

presets.clear();
heroOrdering.clear();
Expand Down Expand Up @@ -248,8 +248,89 @@ static const std::unordered_map<std::string, std::list<std::string>> casualTextu
{"Shoe Soles", {"linktexbci4"}},
};


void CustomModel::nextModel() {
// Each time the button is clicked, we load the directories in the
// models dir, sort it, and then binsearch the current model in it.
// We then go to the next one.
//
// This works fine even if binsearch fails: if a model was deleted, we
// just go to the next one alphabetically.
//
// This also works fine when customModel is "": this will give the first
// element of the list.

auto dir = Utility::get_models_dir();

std::vector<fspath> models;
for (auto entry : std::filesystem::directory_iterator(dir)) {
if (!entry.is_directory()) continue;
models.push_back(entry.path().filename());
}
std::sort(models.begin(), models.end());
// std::lower_bound returns the least element greater than or equal to
// current. Thus we need to do an extra == below to check if we found
// it and need to advance to the next slot or not.
auto found = std::lower_bound(models.begin(), models.end(), modelName);

if (found == models.end()) {
// Wrap back around.
modelName = "";
user_provided = false;
} else if (*found == modelName) {
// Advance to the next one, unless this is the last one.
auto idx = found - models.begin();
idx++;
if (idx < models.size()) {
modelName = models[idx];
user_provided = true;
} else {
modelName = "";
user_provided = false;
}
} else {
// Seems that the current value isn't actually a model.
// The model we found is the next one after it.
modelName = *found;
user_provided = true;
}
}

// IMPROVEMENT: Better generalize this in the future
ModelError CustomModel::applyModel() const {
if (!modelName.empty()) {
Utility::platformLog("Applying custom model " + modelName + "...");
auto model = Utility::get_models_dir() / modelName;

struct Resource {
std::string_view src, dst;
bool required = false;
};

static constexpr std::array<Resource, 3> resources = {
{.src = "Link.szs", .dst = "content/Common/Pack/permanent_3d.pack@SARC@Link.szs", .required = true},
// TODO: DungeonMapLink_00.szs, LinkPos_00.szs

// Sound clips.
{.src = "voice_0.aw", .dst = "content/Cafe/US/AudioRes/JAudioRes/Banks/voice_0.aw"},
{.src = "JaiInit.aaf", .dst = "content/Cafe/US/AudioRes/JAudioRes/JaiInit.aaf"},
};

for (auto rec : resources) {
auto src = model / rec.src;
if (!rec.required && !std::filesystem::exists(src)) continue;

Utility::platformLog("Patching " + std::string(rec.src) + "...");
if (!g_session.copyToGameFile(src, resource.)) {
Utility::platformLog("Couldn't patch " + rec.dst);
return ModelError::COULD_NOT_OPEN;
}
}

Utility::platformLog("Applied custom model " + modelName + "!");
return ModelError::NONE;
}

RandoSession::CacheEntry& link = g_session.openGameFile("content/Common/Pack/permanent_3d.pack@SARC@Link.szs@YAZ0@SARC@Link.bfres@BFRES");
link.addAction([&](RandoSession* session, FileType* data) -> int {
CAST_ENTRY_TO_FILETYPE(bfres, FileTypes::resFile, data)
Expand Down
6 changes: 4 additions & 2 deletions customizer/model.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class CustomModel {
ColorPreset colors;

public:
bool casual;
bool casual, user_provided = false;
std::string modelName;
// holds ordering of the customizable colors in the gui
std::list<std::string> heroOrdering;
Expand All @@ -61,8 +61,10 @@ class CustomModel {
void randomizeOrderly();
void randomizeChaotically();

void nextModel();

ModelError loadFromFolder();
ModelError applyModel() const;
ModelError applyModel() const;
};

std::string errorToName(const ModelError& err);
14 changes: 14 additions & 0 deletions gui/wiiu/OptionActions.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#include "OptionActions.hpp"
#include "utility/path.hpp"

#include <algorithm>

#include <filesystem>
#include <utility/platform.hpp>
#include <command/Log.hpp>
#include <seedgen/seed.hpp>
Expand Down Expand Up @@ -518,6 +520,18 @@ namespace OptionCB {
return "";
}

std::string toggleCustomModel() {
conf.settings.selectedModel.nextModel();
return customModel();
}

std::string customModel() {
if (conf.settings.selectedModel.modelName.empty()) {
return "Link";
}
return conf.settings.selectedModel.modelName;
}

std::string cyclePigColor() {
using enum PigColor;

Expand Down
2 changes: 2 additions & 0 deletions gui/wiiu/OptionActions.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ namespace OptionCB {
std::string isCasual();
std::string randomizeColorsOrderly();
std::string randomizeColorsChaotically();
std::string toggleCustomModel();
std::string customModel();

std::string cyclePigColor();

Expand Down
9 changes: 5 additions & 4 deletions gui/wiiu/Page.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1193,10 +1193,11 @@ void ColorPage::drawDRC() const {
ColorPage::PresetsSubpage::PresetsSubpage(ColorPage& parent_) :
parent(parent_)
{
toggles[0] = std::make_unique<ActionButton>("Casual Clothes", "Enable this if you want to wear your casual clothes instead of the Hero's Clothes.", &OptionCB::toggleCasualClothes, &OptionCB::isCasual);
toggles[1] = std::make_unique<ActionButton>("Randomize Colors Orderly", "", &OptionCB::randomizeColorsOrderly);
toggles[2] = std::make_unique<ActionButton>("Randomize Colors Chaotically", "", &OptionCB::randomizeColorsChaotically);
toggles[3] = std::make_unique<FunctionButton>("Select Colors Manually", "", std::bind(&ColorPage::setSubpage, &parent, Subpage::COLOR_PICKER));
toggles[0] = std::make_unique<ActionButton>("Link Model", "Select a model for Link. To install more, read https://github.com/mcy/TWWHD-Randomizer/tree/main/customizer/INSTALL.md", &OptionCB::toggleCustomModel, &OptionCB::customModel);
toggles[1] = std::make_unique<ActionButton>("Casual Clothes", "Enable this if you want to wear your casual clothes instead of the Hero's Clothes. Custom models will have their own variant of casual clothes", &OptionCB::toggleCasualClothes, &OptionCB::isCasual);
toggles[2] = std::make_unique<ActionButton>("Randomize Colors Orderly", "", &OptionCB::randomizeColorsOrderly);
toggles[3] = std::make_unique<ActionButton>("Randomize Colors Chaotically", "", &OptionCB::randomizeColorsChaotically);
toggles[4] = std::make_unique<FunctionButton>("Select Colors Manually", "", std::bind(&ColorPage::setSubpage, &parent, Subpage::COLOR_PICKER));
}

void ColorPage::PresetsSubpage::open() {
Expand Down
2 changes: 1 addition & 1 deletion gui/wiiu/Page.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ class ColorPage final : public EmptyPage {
size_t listScrollPos = 0;
size_t selectedListIdx = 0;

std::array<std::unique_ptr<BasicButton>, 4> toggles;
std::array<std::unique_ptr<BasicButton>, 5> toggles;

public:
PresetsSubpage(ColorPage& parent_);
Expand Down
1 change: 1 addition & 0 deletions options.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ void Settings::resetDefaultPreferences(const bool& paths) {
ui_display = UIDisplayPreference::On;

selectedModel.casual = false;
selectedModel.user_provided = false;
selectedModel.modelName = "Link";
selectedModel.resetColors();

Expand Down
1 change: 0 additions & 1 deletion randomizer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,6 @@ class Randomizer {
return 1;
}

//IMPROVEMENT: custom model things
if(const ModelError err = config.settings.selectedModel.applyModel(); err != ModelError::NONE) {
ErrorLog::getInstance().log("Failed to apply custom model, error " + errorToName(err));
return 1;
Expand Down
2 changes: 2 additions & 0 deletions seedgen/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ ConfigError Config::loadFromFile(const fspath& filePath, const fspath& preferenc
}

GET_FIELD(preferencesRoot, "custom_player_model", settings.selectedModel.modelName)
GET_FIELD(preferencesRoot, "model_user_provided", settings.selectedModel.user_provided)
if(settings.selectedModel.loadFromFolder() != ModelError::NONE) {
if(!ignoreErrors) LOG_ERR_AND_RETURN(ConfigError::MODEL_ERROR);
}
Expand Down Expand Up @@ -596,6 +597,7 @@ YAML::Node Config::preferencesToYaml() const {
SET_FIELD(preferencesRoot, "ui_display", UIDisplayPreferenceToName(settings.ui_display))

SET_FIELD(preferencesRoot, "player_in_casual_clothes", settings.selectedModel.casual)
SET_FIELD(preferencesRoot, "model_user_provided", settings.selectedModel.user_provided)
SET_FIELD(preferencesRoot, "custom_player_model", settings.selectedModel.modelName)
for (const auto& [texture, color] : settings.selectedModel.getSetColorsMap()) {
preferencesRoot["custom_colors"][texture] = color;
Expand Down
12 changes: 12 additions & 0 deletions utility/path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ namespace Utility {
return path;
}


fspath get_models_dir() {
const fspath path = get_app_save_path() / "custom_models/";

if (!std::filesystem::is_directory(path))
{
Utility::create_directories(path);
}

return path;
}

fspath get_temp_dir() {
// could get the OS-provided temp folder with Qt but it might be harder to find and debug should we use it for anything
const fspath path = get_app_save_path() / "temp/";
Expand Down
1 change: 1 addition & 0 deletions utility/path.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace Utility {
fspath get_data_path();
fspath get_app_save_path();
fspath get_logs_path();
fspath get_models_dir();
fspath get_temp_dir();

// On Windows, fspath.string() will throw an exception if the character requires some kind of Unicode/non-ANSI representation
Expand Down