diff --git a/CMakeLists.txt b/CMakeLists.txt index 693fe94a..0bdcc1a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -468,6 +468,10 @@ if(DINGOSDK_BUILD_PLUGIN_TESTS AND WIN32) add_executable(dingosdk_board_material_tests Extension/Customization/Test/board_material_tests.cpp) target_include_directories(dingosdk_board_material_tests PRIVATE "${PROJECT_SOURCE_DIR}") add_test(NAME board_material COMMAND dingosdk_board_material_tests) + # What a catalog category is worn as (Extension/Customization/outfit_kind.h, reskate.outfit.worn). + add_executable(dingosdk_outfit_kind_tests Extension/Customization/Test/outfit_kind_tests.cpp) + target_include_directories(dingosdk_outfit_kind_tests PRIVATE "${PROJECT_SOURCE_DIR}") + add_test(NAME outfit_kind COMMAND dingosdk_outfit_kind_tests) endif() get_property(project_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS) foreach(target IN LISTS project_targets) diff --git a/Extension/Customization/Test/outfit_kind_tests.cpp b/Extension/Customization/Test/outfit_kind_tests.cpp new file mode 100644 index 00000000..b7c2d07e --- /dev/null +++ b/Extension/Customization/Test/outfit_kind_tests.cpp @@ -0,0 +1,49 @@ +// What a catalog category is worn as (Extension/Customization/outfit_kind.h): clothes by their slot, +// full-body costumes too, and the body's own parts left out. +#include "Extension/Customization/outfit_kind.h" +#include +#include +#include + +namespace { +using namespace dingosdk; + +void check(bool ok, const std::string& message) { + if (ok) return; + std::cerr << "FAILED: " << message << '\n'; + std::exit(1); +} +void worn_as(const char* category, const char* label) { + const auto kind = outfit_kind::of(category); + check(kind && kind->label == label, std::string(category) + " is worn as " + label); +} +void not_worn(const char* category) { check(!outfit_kind::of(category), std::string(category) + " is not worn"); } +} // namespace + +int main() { + // Clothes and accessories, by the slot they are worn in. + worn_as("tops", "Top"); + worn_as("headgear", "Headwear"); + worn_as("eyewear", "Eyewear"); + worn_as("bottoms", "Bottoms"); + worn_as("shoes", "Shoes"); + // A full-body costume (a mod's costume is one, Own_... in cust_fullBodyCostume): worn, though its + // category starts like the body's own parts and names the body. + worn_as("cust_fullBodyCostume", "Costume"); + // The body's own parts, and what is not clothing. + not_worn("custhead"); + not_worn("custbodies"); + not_worn("custnoses"); + not_worn("hair"); + not_worn("tattoos"); + not_worn("gestures"); + not_worn(""); + // A category this does not know: worn, its name made readable, after what it knows. + const auto unknown = outfit_kind::of("patches_x"); + check(unknown && unknown->label == "Patches x" && unknown->order == 20, "an unknown category is its name made readable"); + // From head to foot. + check(outfit_kind::of("headgear")->order < outfit_kind::of("cust_fullBodyCostume")->order && + outfit_kind::of("cust_fullBodyCostume")->order < outfit_kind::of("shoes")->order, + "a costume sits between the head and the feet"); + std::cout << "outfit_kind tests passed\n"; +} diff --git a/Extension/Customization/outfit_kind.h b/Extension/Customization/outfit_kind.h new file mode 100644 index 00000000..84853aa9 --- /dev/null +++ b/Extension/Customization/outfit_kind.h @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +// The kind of item a catalog category is, for what the skater wears (worn_items.cpp, reskate.outfit.worn): +// clothes and accessories by the slot they are worn in, from head to foot; nothing for the body's own +// parts, hair, tattoos, gestures or the board. Pure, so it is tested (Test/outfit_kind_tests.cpp). +namespace dingosdk::outfit_kind { +struct Kind { + std::string label; // "Headwear", "Top", ... "Shoes"; a category this does not know, made readable + int order; // from head to foot +}; +namespace detail { +struct Word { const char* word; const char* label; int order; }; +constexpr Word words[] = { + {"head", "Headwear", 0}, {"hat", "Headwear", 0}, {"cap", "Headwear", 0}, {"beanie", "Headwear", 0}, {"helmet", "Headwear", 0}, + {"eye", "Eyewear", 1}, {"glass", "Eyewear", 1}, {"mask", "Face", 1}, + {"neck", "Neckwear", 2}, {"jewel", "Jewellery", 2}, {"earring", "Earrings", 2}, + {"costume", "Costume", 3}, {"outfit", "Outfit", 3}, + {"outer", "Outerwear", 4}, {"jacket", "Outerwear", 4}, {"top", "Top", 5}, {"shirt", "Top", 5}, {"hood", "Top", 5}, + {"glove", "Gloves", 6}, {"wrist", "Wrist", 6}, {"watch", "Wrist", 6}, {"bag", "Bag", 7}, {"backpack", "Bag", 7}, + {"bottom", "Bottoms", 8}, {"pant", "Bottoms", 8}, {"short", "Bottoms", 8}, {"leg", "Bottoms", 8}, + {"sock", "Socks", 9}, {"foot", "Shoes", 10}, {"shoe", "Shoes", 10}}; +constexpr const char* not_outfit[] = {"skin", "face_", "body", "hair", "beard", "brow", "makeup", "eyecolo", "eye_colo", + "tattoo", "gesture", "emote", "voice", "bkobject", "board", "deck", "truck", "wheel", "grip", + "locomotion", "lipcolo", "mole", "stubble", "freckle", "scar", "nose", "mouth", "teeth"}; +} // namespace detail + +inline std::optional of(std::string_view category) { + std::string folded(category); + for (auto& c : folded) c = static_cast(std::tolower(static_cast(c))); + const auto has = [&](const char* word) { return folded.find(word) != std::string::npos; }; + if (folded.empty()) return std::nullopt; + // A full-body costume is worn over the whole skater, and its category is the body's kind of name + // ("cust_fullBodyCostume"): taken before the body's own parts are left out. + if (!has("costume")) { + // The body's own parts are "cust..." (custhead, custeye, custnoses, custbodies, custmoles...). + if (folded.starts_with("cust") || std::any_of(std::begin(detail::not_outfit), std::end(detail::not_outfit), has)) + return std::nullopt; + } + const auto word = std::find_if(std::begin(detail::words), std::end(detail::words), [&](const detail::Word& w) { return has(w.word); }); + if (word != std::end(detail::words)) return Kind{word->label, word->order}; + // A category this does not know: its name made readable. + std::string label(category); + std::replace(label.begin(), label.end(), '_', ' '); + label[0] = static_cast(std::toupper(static_cast(label[0]))); + return Kind{label, 20}; +} +} // namespace dingosdk::outfit_kind diff --git a/Extension/Customization/worn_items.cpp b/Extension/Customization/worn_items.cpp index 4b45229a..b22eb41a 100644 --- a/Extension/Customization/worn_items.cpp +++ b/Extension/Customization/worn_items.cpp @@ -1,5 +1,6 @@ #include "worn_items.h" #include "item_browser.h" +#include "outfit_kind.h" #include "local_customization.h" #include "local_customization_runtime.h" #include "Engine/Core/Platform/memory.h" @@ -9,8 +10,6 @@ #include #include #include -#include -#include // What the skater wears, read from the game's own presets: ported from Fit Check (pull request 326, // turnaround.cpp), for plugins (reskate.outfit.worn, board, title). @@ -76,28 +75,8 @@ std::vector worn_board() { // What the skater wears now: every item of the selected preset // but the board's, labelled by its catalog category (the game's names for its grids: "tops", -// "headgear"...), clothes and accessories only (not the body, hair, tattoos or gestures), in the -// order they are worn from head to foot. Game thread. -namespace { -struct OutfitKind { const char* word; const char* label; int order; }; -constexpr OutfitKind outfit_kinds[] = { - {"head", "Headwear", 0}, {"hat", "Headwear", 0}, {"cap", "Headwear", 0}, {"beanie", "Headwear", 0}, {"helmet", "Headwear", 0}, - {"eye", "Eyewear", 1}, {"glass", "Eyewear", 1}, {"mask", "Face", 1}, - {"neck", "Neckwear", 2}, {"jewel", "Jewellery", 2}, {"earring", "Earrings", 2}, - {"costume", "Costume", 3}, {"outfit", "Outfit", 3}, - {"outer", "Outerwear", 4}, {"jacket", "Outerwear", 4}, {"top", "Top", 5}, {"shirt", "Top", 5}, {"hood", "Top", 5}, - {"glove", "Gloves", 6}, {"wrist", "Wrist", 6}, {"watch", "Wrist", 6}, {"bag", "Bag", 7}, {"backpack", "Bag", 7}, - {"bottom", "Bottoms", 8}, {"pant", "Bottoms", 8}, {"short", "Bottoms", 8}, {"leg", "Bottoms", 8}, - {"sock", "Socks", 9}, {"foot", "Shoes", 10}, {"shoe", "Shoes", 10}}; -constexpr const char* not_outfit[] = {"skin", "face_", "body", "hair", "beard", "brow", "makeup", "eyecolo", "eye_colo", - "tattoo", "gesture", "emote", "voice", "bkobject", "board", "deck", "truck", "wheel", "grip", - "locomotion", "lipcolo", "mole", "stubble", "freckle", "scar", "nose", "mouth", "teeth"}; -std::string folded(std::string_view text) { - std::string out(text); - for (auto& c : out) c = static_cast(std::tolower(static_cast(c))); - return out; -} -} // namespace +// "headgear"...), clothes, accessories and full-body costumes only (not the body, hair, tattoos or +// gestures; outfit_kind.h), in the order they are worn from head to foot. Game thread. std::vector worn_outfit() { std::vector parts; const auto base = reinterpret_cast(GetModuleHandleW(nullptr)); @@ -119,22 +98,9 @@ std::vector worn_outfit() { for (const auto& item : recipe.items) { if (item.asset.empty()) continue; const auto entry = known.find(item.asset); - const auto category = folded(entry != known.end() ? entry->second.category : std::string()); - const auto has = [&](const char* word) { return category.find(word) != std::string::npos; }; - // The body's own parts are "cust..." (custhead, custeye, custnoses, custbodies, custmoles...). - if (category.empty() || category.starts_with("cust") || std::any_of(std::begin(not_outfit), std::end(not_outfit), has)) - continue; - const auto kind = std::find_if(std::begin(outfit_kinds), std::end(outfit_kinds), [&](const OutfitKind& k) { return has(k.word); }); - std::string label; - int order = 20; - if (kind != std::end(outfit_kinds)) label = kind->label, order = kind->order; - else { - // A category this does not know: its name made readable. - label = entry->second.category; - std::replace(label.begin(), label.end(), '_', ' '); - if (!label.empty()) label[0] = static_cast(std::toupper(static_cast(label[0]))); - } - found.push_back({order, {label, item.asset, item_title(item.asset)}}); + const auto kind = outfit_kind::of(entry != known.end() ? entry->second.category : std::string()); + if (!kind) continue; + found.push_back({kind->order, {kind->label, item.asset, item_title(item.asset)}}); } } std::stable_sort(found.begin(), found.end(), [](const auto& a, const auto& b) { return a.first < b.first; });