diff --git a/code/client/CMakeLists.txt b/code/client/CMakeLists.txt index 0841613..0f09e42 100644 --- a/code/client/CMakeLists.txt +++ b/code/client/CMakeLists.txt @@ -23,6 +23,7 @@ set(HOGWARTSMP_CLIENT_FILES src/core/ue4_impl.cpp src/core/ue4_reflection.cpp src/core/playground.cpp + src/core/appearance_dump.cpp src/core/student_proxy.cpp src/core/ui/chat.cpp diff --git a/code/client/src/core/appearance_dump.cpp b/code/client/src/core/appearance_dump.cpp new file mode 100644 index 0000000..e924619 --- /dev/null +++ b/code/client/src/core/appearance_dump.cpp @@ -0,0 +1,404 @@ +// AppearanceDump — in-game appearance harvester (Playground → "Dump Nearby +// NPC Appearances", logs to the AppearanceDump channel). Scans live NPC +// students and logs everything the student proxy dressing flow needs: +// +// - per-actor SkeletalMeshComponent mesh paths + material paths (runtime +// MIDs resolved to their loadable on-disk parent MI) +// - full MID parameter arrays (scalar/vector/texture) for one robe per +// gender — the source of the kit_params_*_uniNN.h headers +// - per-robe house color signatures (Color_Tint_*/UVBias + textures), +// labeled by CharacterName — the source of kit_params_houses.h +// - loaded head/arms skeletal mesh assets and T_Patch crest textures +// - the CustomizableCharacterComponent property list (field hunting for +// future appearance work: gear slots, Outfits/CharacterItems maps) +// +// See APPEARANCE_SYNC_RESEARCH.md for the findings this produced. + +#include + +#include "appearance_dump.h" + +#include "application.h" +#include "ue4_natives.h" +#include "ue4_reflection.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace { + using HogwartsMP::Core::gGlobals; + using namespace HogwartsMP::Core::UE4; + + std::atomic g_dumpPending{false}; + + // ── MID parameter-array dump ───────────────────────────────────────────── + // Walks UMaterialInstance's ScalarParameterValues / VectorParameterValues / + // TextureParameterValues TArrays raw. UE4.27 shipping layouts (FName = 8): + // FMaterialParameterInfo { FName Name; uint8 Association; int32 Index; } = 16 + // FScalarParameterValue { Info; float Value; FGuid; } = 36, stride 36 + // FVectorParameterValue { Info; FLinearColor Value; FGuid; } = 48, stride 48 + // FTextureParameterValue { Info; UTexture *Value; FGuid; } = 40, stride 40 + // This is the same data the kit_params headers were harvested from — + // needed per-gender and per-house. + template + void DumpMidParams(TLogger log, UObjectBase *mid, const char *label) { + auto *cls = mid->GetClass(); + auto arrOf = [&](const char *propName) -> std::pair { + auto *prop = FindPropertyInChain(cls, propName); + if (!prop) { + return {nullptr, 0}; + } + auto *base = reinterpret_cast(mid) + prop->GetOffset_ForInternal(); + return {*reinterpret_cast(base), *reinterpret_cast(base + 8)}; + }; + + log->info("--- MID params [{}] parent={} ---", label, + AssetPath(ReadObjectProperty(mid, "Parent"))); + + if (auto [data, num] = arrOf("ScalarParameterValues"); data) { + for (int k = 0; k < num && k < 100; ++k) { + auto *e = data + k * 36; + log->info(" scalar {} = {}", narrow(*reinterpret_cast(e)), + *reinterpret_cast(e + 20)); + } + } + if (auto [data, num] = arrOf("VectorParameterValues"); data) { + for (int k = 0; k < num && k < 100; ++k) { + auto *e = data + k * 48; + auto *v = reinterpret_cast(e + 16); + log->info(" vector {} = ({}, {}, {}, {})", narrow(*reinterpret_cast(e)), + v[0], v[1], v[2], v[3]); + } + } + if (auto [data, num] = arrOf("TextureParameterValues"); data) { + for (int k = 0; k < num && k < 100; ++k) { + auto *e = data + k * 40; + auto *tex = *reinterpret_cast(e + 16); + log->info(" texture {} = {}", narrow(*reinterpret_cast(e)), + tex ? AssetPath(tex) : "(null)"); + } + } + } + + void DumpNearbyNow() { + auto log = Framework::Logging::GetLogger("AppearanceDump"); + auto *arr = gGlobals.objectArray; + if (!arr) { + log->error("No object array"); + return; + } + + // ── Collect: SkeletalMeshComponents owned by CustomizableCharacterComponent ── + // UE4 outer chain for dressed NPC students: + // SMC → outer: CustomizableCharacterComponent (CCC) → outer: pawn actor + // + // Confirmed by diagnostic: 57 SMCs have CCC as outer. All others (Actor, + // Package, ABP_*, BTS_*, etc.) are unrelated and are skipped here. + + struct MeshEntry { + std::string smcName; + std::string meshPath; + std::vector matPaths; + }; + struct ActorEntry { + std::string actorClass; + std::string actorName; + std::string cccClass; // exact class name of the CCC + std::string cccType; // CCC "Type" FName + std::string charName; // CCC "CharacterName" FName — encodes house+gender + int gender{-1}; + std::vector meshes; + }; + + // Keyed by actor UObjectBase* so multiple SMCs on the same actor merge. + std::unordered_map actorMap; + + // First robe ("Upper") MID seen per gender — full param harvest targets. + UObjectBase *upperMidM = nullptr; + UObjectBase *upperMidM03 = nullptr; // male StuUni03 robe specifically + UObjectBase *upperMidF = nullptr; + // Every robe MID with its owner, for the per-house color signature table. + struct UpperRef { + UObjectBase *mid; + std::string actorName; + std::string charName; + int gender; + }; + std::vector upperMids; + // Any CCC instance — used to enumerate its property list below. + UObjectBase *anyCcc = nullptr; + + const int total = arr->GetObjectArrayNum(); + for (int i = 0; i < total; ++i) { + auto *item = arr->IndexToObject(i); + if (!item || !item->Object) { + continue; + } + auto *obj = item->Object; + + if (narrow(obj->GetClass()->GetFName()) != "SkeletalMeshComponent") { + continue; + } + + auto *ccc = obj->GetOuter(); + if (!ccc) { + continue; + } + // Only student/NPC dressed characters — skip raw actor-owned SMCs (our own proxies etc.) + if (narrow(ccc->GetClass()->GetFName()) != "CustomizableCharacterComponent") { + continue; + } + + auto *actor = ccc->GetOuter(); + if (!actor) { + continue; + } + + // First time seeing this actor: record it + if (actorMap.find(actor) == actorMap.end()) { + ActorEntry e; + e.actorClass = narrow(actor->GetClass()->GetFName()); + e.actorName = narrow(actor->GetFName()); + e.cccClass = narrow(ccc->GetClass()->GetFName()); + // The CCC has no house property — house is encoded in + // CharacterName (e.g. "T4StudentGryffindorMale3"). + e.gender = ReadByteProperty(ccc, "Gender"); + e.cccType = ReadNameProperty(ccc, "Type"); + e.charName = ReadNameProperty(ccc, "CharacterName"); + actorMap.emplace(actor, std::move(e)); + } + if (!anyCcc) { + anyCcc = ccc; + } + + // Read the SkeletalMesh asset path + MeshEntry me; + me.smcName = narrow(obj->GetFName()); + if (auto *meshObj = ReadObjectProperty(obj, "SkeletalMesh")) { + me.meshPath = AssetPath(meshObj); + } + else { + me.meshPath = "(none)"; + } + + // Read material slots: call GetNumMaterials then GetMaterial(slot). + // Runtime MIDs aren't loadable by path, so when the slot holds a + // MaterialInstanceDynamic, follow its Parent chain to the on-disk + // MI asset — that is the path SpawnStudent can pass to + // StaticLoadObject. + struct { int32_t ReturnValue{}; } numMatsP; + CallUFunction(obj, "GetNumMaterials", &numMatsP); + for (int slot = 0; slot < numMatsP.ReturnValue && slot < 8; ++slot) { + struct { + int32_t ElementIndex{}; + UObjectBase *ReturnValue{}; + } getMatP{slot, nullptr}; + CallUFunction(obj, "GetMaterial", &getMatP); + auto *mat = getMatP.ReturnValue; + if (!mat) { + continue; + } + if (narrow(mat->GetClass()->GetFName()) == "MaterialInstanceDynamic") { + // Remember the first robe MID per gender for the full + // parameter harvest below, and every robe MID for the + // house color signature table. + if (me.smcName == "Upper" && slot == 0) { + const int g = actorMap[actor].gender; + if (g == 0 && !upperMidM) { + upperMidM = mat; + } + else if (g == 1 && !upperMidF) { + upperMidF = mat; + } + // Male StuUni03 robe specifically — base param set for + // switching the male proxy off StuUni01 (whose slot + // layout doesn't match the house overlay table). + if (g == 0 && !upperMidM03) { + if (auto *parent = ReadObjectProperty(mat, "Parent")) { + if (AssetPath(parent).find("StuUni03") != std::string::npos) { + upperMidM03 = mat; + } + } + } + if (upperMids.size() < 40) { + upperMids.push_back({mat, actorMap[actor].actorName, + actorMap[actor].charName, g}); + } + } + if (auto *parent = ReadObjectProperty(mat, "Parent")) { + me.matPaths.push_back("parent=" + AssetPath(parent)); + continue; + } + } + me.matPaths.push_back(AssetPath(mat)); + } + + actorMap[actor].meshes.push_back(std::move(me)); + } + + log->info("=== AppearanceDump: {} NPC actors with CCC-owned SMCs ===", + static_cast(actorMap.size())); + + // Print high-quality student actors first (BP_Student_C, BP_Tier3_Character_C) + // with no cap — needed to harvest female mesh paths. Then up to 6 misc actors. + auto printActor = [&](int idx, const ActorEntry &e) { + log->info("Actor[{}] class={} name={} type={} char={} gender={}", + idx, e.actorClass, e.actorName, e.cccType, e.charName, e.gender); + for (auto &me : e.meshes) { + log->info(" SMC={} mesh={}", me.smcName, me.meshPath); + for (size_t m = 0; m < me.matPaths.size(); ++m) { + log->info(" mat[{}]={}", static_cast(m), me.matPaths[m]); + } + } + }; + + int idx = 0; + for (auto &[actorPtr, e] : actorMap) { + if (e.actorClass == "BP_Student_C" || e.actorClass == "BP_Tier3_Character_C") { + printActor(idx++, e); + } + } + int misc = 0; + for (auto &[actorPtr, e] : actorMap) { + if (e.actorClass == "BP_Student_C" || e.actorClass == "BP_Tier3_Character_C") { + continue; + } + if (misc >= 6) { + log->info(" ... ({} more misc actors omitted)", + static_cast(actorMap.size()) - idx - 6); + break; + } + printActor(idx++, e); + ++misc; + } + + // ── Loaded head / arms mesh assets ─────────────────────────────────── + // The student SMC scan never shows head/skin components (heads live on + // a different component on NPCs), so list SkeletalMesh ASSETS in memory + // instead — heads for the proxy skin, arms/hands because the female + // outfit + head meshes have no hands. + log->info("--- Loaded SkeletalMesh assets (heads / arms / hands) ---"); + int headCount = 0, armsCount = 0; + for (int i = 0; i < total; ++i) { + auto *item = arr->IndexToObject(i); + if (!item || !item->Object) { + continue; + } + auto *obj = item->Object; + const auto clsName = narrow(obj->GetClass()->GetFName()); + // Crest patch texture variants (house + possibly generic/phoenix). + if (clsName == "Texture2D") { + const auto tpath = AssetPath(obj); + if (tpath.find("T_Patch") != std::string::npos) { + log->info(" patch={}", tpath); + } + continue; + } + if (clsName != "SkeletalMesh") { + continue; + } + const auto path = AssetPath(obj); + if (path.find("/Heads/") != std::string::npos) { + if (headCount < 40) { + log->info(" head[{}]={}", headCount, path); + } + ++headCount; + } + else if (path.find("Arms") != std::string::npos || path.find("Hand") != std::string::npos) { + if (armsCount < 40) { + log->info(" arms[{}]={}", armsCount, path); + } + ++armsCount; + } + } + + // ── Full robe MID parameter harvest (one male, one female) ─────────── + // This is the source data for the kit_params_*_uniNN.h headers. + if (upperMidM) { + DumpMidParams(log, upperMidM, "male Upper"); + } + if (upperMidM03 && upperMidM03 != upperMidM) { + DumpMidParams(log, upperMidM03, "male Upper StuUni03"); + } + if (upperMidF) { + DumpMidParams(log, upperMidF, "female Upper"); + } + + // ── House color signature table ────────────────────────────────────── + // One line per robe MID, only the house-distinguishing params + // (Color_Tint_* and UVBias). Standing near students of different houses + // and diffing these lines gives the per-house color/crest overlay. + log->info("--- Robe color signatures ({} MIDs) ---", static_cast(upperMids.size())); + for (auto &ur : upperMids) { + std::string sig; + auto *prop = FindPropertyInChain(ur.mid->GetClass(), "VectorParameterValues"); + if (!prop) { + continue; + } + auto *base = reinterpret_cast(ur.mid) + prop->GetOffset_ForInternal(); + auto *data = *reinterpret_cast(base); + const int num = *reinterpret_cast(base + 8); + for (int k = 0; k < num && k < 100; ++k) { + auto *e = data + k * 48; + const auto name = narrow(*reinterpret_cast(e)); + if (name.rfind("Color_Tint", 0) != 0 && name.rfind("UVBias", 0) != 0) { + continue; + } + auto *v = reinterpret_cast(e + 16); + char buf[96]; + snprintf(buf, sizeof(buf), " %s=(%.3f,%.3f,%.3f)", name.c_str(), v[0], v[1], v[2]); + sig += buf; + } + // Texture params too — the house crest is texture-selected (the + // tint-only overlay left males with the donor's Hufflepuff crest). + if (auto *tprop = FindPropertyInChain(ur.mid->GetClass(), "TextureParameterValues")) { + auto *tbase = reinterpret_cast(ur.mid) + tprop->GetOffset_ForInternal(); + auto *tdata = *reinterpret_cast(tbase); + const int tnum = *reinterpret_cast(tbase + 8); + for (int k = 0; k < tnum && k < 60; ++k) { + auto *e = tdata + k * 40; + auto *tex = *reinterpret_cast(e + 16); + sig += " T:" + narrow(*reinterpret_cast(e)) + "=" + (tex ? AssetPath(tex) : "(null)"); + } + } + log->info(" sig actor={} char={} gender={}{}", ur.actorName, ur.charName, ur.gender, sig); + } + + // ── CCC property list ──────────────────────────────────────────────── + // Inventory of CustomizableCharacterComponent's reflection surface — + // useful when hunting fields for future appearance work (gear slots, + // Outfits/CharacterItems maps, CCD data assets). The component has no + // house property; house comes from CharacterName. + if (anyCcc) { + log->info("--- CustomizableCharacterComponent properties ---"); + int propCount = 0; + for (UStruct *s = anyCcc->GetClass(); s && propCount < 150; s = s->GetSuperStruct()) { + for (FField *f = s->ChildProperties; f && propCount < 150; f = f->Next) { + log->info(" prop {}::{} ({})", narrow(s->GetFName()), + narrow(f->GetFName()), narrow(f->GetClass()->GetFName())); + ++propCount; + } + } + } + log->info("=== End AppearanceDump ==="); + } +} // namespace + +namespace HogwartsMP::Core::AppearanceDump { + void RequestDump() { + g_dumpPending = true; + } + + void ProcessPending() { + if (g_dumpPending.exchange(false)) { + DumpNearbyNow(); + } + } +} // namespace HogwartsMP::Core::AppearanceDump diff --git a/code/client/src/core/appearance_dump.h b/code/client/src/core/appearance_dump.h new file mode 100644 index 0000000..d53872c --- /dev/null +++ b/code/client/src/core/appearance_dump.h @@ -0,0 +1,11 @@ +#pragma once + +namespace HogwartsMP::Core::AppearanceDump { + // Thread-safe request setter; the actual dump happens on the next engine + // tick via ProcessPending (game thread required: GObjects walk and + // ProcessEvent are not safe off-thread). + void RequestDump(); + + // Game-thread pump — Playground_Tick calls this once per engine tick. + void ProcessPending(); +} // namespace HogwartsMP::Core::AppearanceDump diff --git a/code/client/src/core/kit_params.h b/code/client/src/core/kit_params.h index 3b0bd6b..39c40eb 100644 --- a/code/client/src/core/kit_params.h +++ b/code/client/src/core/kit_params.h @@ -1,7 +1,29 @@ -#pragma once +#pragma once // AUTO-GENERATED from a live BP_Student_C MID dump (2026-06-10). // Same parameter set applies to all 7 outfit material slots (verified: two // independent in-game harvests were byte-identical). +// +// ── Material zones ────────────────────────────────────────────────────────── +// Param names like "Color_Tint_High[7]" are flat FNames; the [n] suffix is the +// artists' convention for a ZONE of the outfit's swatch-kit master material. +// A region-ID mask texture maps each texel to a zone; zone n then gets its own +// Swatch_{Diffuse,MRAB,Normal}[n] fabric textures, Color_Tint_{High,Low}[n] +// tints, UVBias/UvRotationScale[n] mapping, and Fuzz/Overlay_Opacity/ +// Base_SROD_Modifier[n] surface tweaks. +// +// Zone numbering is PER-MATERIAL — each robe material has its own layout, so +// house overlays only apply to the robe they were harvested from. Per-material +// zone maps live at the top of each kit_params_*_uniNN.h header ("StuUniNN" is +// the game's uniform-style asset variant, not our versioning). Adding another +// uniform style later (StuUni01/02/04…) means harvesting a new header with the +// same AppearanceDump pipeline. +// +// The legacy arrays below belong to the male StuUni01 robe +// (MI_HUM_M_CMBH_Robed_StuUni01) — no longer worn by the proxy (it switched to +// StuUni03 so the house overlays line up). Known zones: 12 = visible skin +// (T_Skin_Arms*); the rest is unmapped. The house tables in +// kit_params_houses.h DO NOT apply to this layout. +// ──────────────────────────────────────────────────────────────────────────── #include diff --git a/code/client/src/core/kit_params_female_uni03.h b/code/client/src/core/kit_params_female_uni03.h new file mode 100644 index 0000000..45a3c9c --- /dev/null +++ b/code/client/src/core/kit_params_female_uni03.h @@ -0,0 +1,173 @@ +#pragma once +// AUTO-GENERATED from the in-game AppearanceDump MID harvest (2026-06-12). +// Female StuUni03 robe MID parameters, read from a live female +// BP_Tier3_Character_C Upper MID (parent MI_HUM_F_CMBH_StuUni03_Robed). +// "StuUni03" is the game's uniform-style asset variant (StuUni01/03/… are +// different designs), not our versioning. +// +// Zone map (female StuUni03; empirical — see kit_params.h for what zones are): +// zone 4 = house crest patch (Swatch_* = T_Patch_{G,S,R,H}_*) +// zone 3 = primary house color (classic house colors in Color_Tint_Low[3]) +// zone 11 = secondary house color +// zone 12 = visible skin (T_Skin_Arms* — varies per INDIVIDUAL, not house) +// remaining zones unidentified +// +// NOTE: the female zone layout differs from the male StuUni03 layout — house +// overlays are per-gender (HouseParamsF/HouseTexturesF in kit_params_houses.h). + +#include "kit_params.h" + +namespace HogwartsMP::KitParams +{ + inline constexpr auto FemaleUni03Scalars = std::to_array({ + {L"Fuzz[14]", 0.0f}, + {L"Overlay_Opacity[7]", 0.0f}, + {L"Normal_Intensity[2]", 0.0f}, + {L"Overlay_Opacity[1]", 0.0f}, + {L"Fuzz[6]", 0.0f}, + {L"Normal_Intensity[6]", 0.0f}, + {L"Fuzz[9]", 0.0f}, + {L"Fuzz[5]", 0.0f}, + {L"Fuzz[8]", 0.0f}, + {L"Overlay_Opacity[15]", 0.0f}, + {L"Fuzz[10]", 0.0f}, + {L"Fuzz[11]", 0.0f}, + {L"Fuzz[12]", 0.0f}, + {L"Fuzz[4]", 0.0f}, + {L"Normal_Intensity[4]", 0.0f}, + {L"Normal_Intensity[1]", 0.0f}, + {L"Overlay_Opacity[11]", 0.0f}, + {L"Overlay_Opacity[2]", 0.0f}, + {L"Normal_Intensity[12]", 0.0f}, + {L"Overlay_Opacity[10]", 0.0f}, + {L"Overlay_Opacity[13]", 0.0f}, + {L"Fuzz[13]", 0.0f}, + }); + + inline constexpr auto FemaleUni03Vectors = std::to_array({ + {L"Base_SROD_Modifier[1]", 1.0f, 0.5f, 1.0f, 0.19f}, + {L"Color_Tint_High[1]", 0.947917f, 0.947917f, 0.947917f, 1.0f}, + {L"Color_Tint_Low[1]", 0.921875f, 0.921875f, 0.921875f, 1.0f}, + {L"MRAB_Modifier[1]", 1.0f, 1.0f, 1.0f, 0.14f}, + {L"Scatter_Color[1]", 0.064803f, 0.07036f, 0.082283f, 1.0f}, + {L"Scatter_Color[14]", 0.609375f, 0.609375f, 0.609375f, 1.0f}, + {L"UvRotationScale[1]", 0.934579f, -0.0f, 0.0f, 0.934579f}, + {L"Base_SROD_Modifier[7]", 1.0f, 0.5f, 1.0f, 0.12f}, + {L"Color_Tint_High[7]", 0.401978f, 0.467784f, 0.55834f, 1.0f}, + {L"Color_Tint_Low[7]", 0.514918f, 0.55834f, 0.651406f, 1.0f}, + {L"MRAB_Modifier[7]", 1.0f, 1.0f, 1.0f, 0.14f}, + {L"Color_Tint_Low[2]", 0.515408f, 0.553514f, 0.651042f, 1.0f}, + {L"Color_Tint_High[14]", 0.130208f, 0.114756f, 0.114756f, 1.0f}, + {L"UvRotationScale[7]", 12.5f, -0.0f, 0.0f, 12.5f}, + {L"Scatter_Color[7]", 0.064803f, 0.07036f, 0.082283f, 1.0f}, + {L"Scatter_Color[2]", 0.064542f, 0.069887f, 0.083333f, 1.0f}, + {L"Color_Tint_High[2]", 0.400584f, 0.466517f, 0.557292f, 1.0f}, + {L"UvRotationScale[2]", 13.333333f, -0.0f, 0.0f, 13.333333f}, + {L"Color_Tint_Low[14]", 0.270833f, 0.223226f, 0.207356f, 1.0f}, + {L"Color_Tint_High[4]", 0.445201f, 0.445201f, 0.445201f, 1.0f}, + {L"Color_Tint_Low[4]", 0.296138f, 0.327778f, 0.768151f, 1.0f}, + {L"Base_SROD_Modifier[4]", 1.0f, 0.5f, 1.0f, 0.24f}, + {L"Scatter_Color[4]", 0.033087f, 0.038267f, 0.046875f, 1.0f}, + {L"Base_SROD_Modifier[15]", 1.0f, 0.5f, 0.5f, 0.5f}, + {L"UvRotationScale[4]", 25.0f, -0.0f, 0.0f, 25.0f}, + {L"Color_Tint_High[5]", 0.427083f, 0.389754f, 0.295541f, 1.0f}, + {L"Color_Tint_Low[5]", 0.432292f, 0.394752f, 0.30001f, 1.0f}, + {L"Base_SROD_Modifier[6]", 1.0f, 0.5f, 1.0f, 0.25f}, + {L"Color_Tint_High[6]", 0.201556f, 0.201556f, 0.201556f, 1.0f}, + {L"Color_Tint_Low[6]", 0.165132f, 0.174647f, 0.201556f, 1.0f}, + {L"UvRotationScale[6]", 10.0f, -0.0f, 0.0f, 10.0f}, + {L"Color_Tint_High[15]", 0.250158f, 0.258183f, 0.291771f, 1.0f}, + {L"UVBias[6]", 0.54f, 0.64f, 0.0f, 1.0f}, + {L"Scatter_Color[15]", 0.024158f, 0.033105f, 0.040915f, 1.0f}, + {L"Color_Tint_Low[8]", 0.250158f, 0.215861f, 0.171441f, 1.0f}, + {L"Color_Tint_High[8]", 0.201556f, 0.194618f, 0.177888f, 1.0f}, + {L"Scatter_Color[8]", 0.14996f, 0.14996f, 0.14996f, 1.0f}, + {L"Color_Tint_Low[9]", 0.364583f, 0.353889f, 0.343696f, 1.0f}, + {L"Color_Tint_High[10]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"Scatter_Color[9]", 0.14996f, 0.14996f, 0.14996f, 1.0f}, + {L"Color_Tint_Low[10]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"Color_Tint_High[9]", 0.296875f, 0.295042f, 0.292236f, 1.0f}, + {L"Scatter_Color[10]", 0.024382f, 0.033458f, 0.041667f, 1.0f}, + {L"Color_Tint_Low[11]", 0.252301f, 0.333644f, 0.421875f, 1.0f}, + {L"Scatter_Color[13]", 0.0625f, 0.0625f, 0.0625f, 1.0f}, + {L"Scatter_Color[11]", 0.137052f, 0.181239f, 0.229167f, 1.0f}, + {L"Color_Tint_High[13]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"Color_Tint_High[11]", 0.319264f, 0.415742f, 0.526042f, 1.0f}, + {L"Color_Tint_Low[15]", 0.250158f, 0.258183f, 0.291771f, 1.0f}, + {L"Scatter_Color[12]", 0.0f, 0.0f, 0.0f, 1.0f}, + {L"Color_Tint_Low[3]", 0.341914f, 0.42869f, 0.527115f, 1.0f}, + {L"Color_Tint_High[3]", 0.75f, 0.563927f, 0.368145f, 1.0f}, + {L"Scatter_Color[3]", 0.084217f, 0.097124f, 0.119792f, 1.0f}, + {L"UvRotationScale[3]", 14.142136f, 14.142136f, -14.142136f, 14.142136f}, + {L"Base_SROD_Modifier[3]", 1.0f, 0.0f, 1.0f, 0.06f}, + {L"Scatter_Color[5]", 0.296875f, 0.296875f, 0.296875f, 1.0f}, + {L"Base_SROD_Modifier[8]", 1.0f, 0.5f, 0.5f, 0.0f}, + {L"UvRotationScale[10]", 8.333333f, -0.0f, 0.0f, 8.333333f}, + {L"UvRotationScale[9]", 7.407407f, -0.0f, 0.0f, 7.407407f}, + {L"Base_SROD_Modifier[10]", 0.5f, 0.5f, 1.0f, 0.25f}, + {L"UvRotationScale[11]", 3.746066f, 9.271839f, -9.271839f, 3.746066f}, + {L"Base_SROD_Modifier[5]", 1.0f, 0.0f, 1.0f, 0.0f}, + {L"UvRotationScale[12]", 5.0f, -0.0f, 0.0f, 5.0f}, + {L"MRAB_Modifier[2]", 1.0f, 1.0f, 1.0f, 0.14f}, + {L"UVBias[4]", 0.47f, 0.484f, 0.0f, 1.0f}, + {L"Base_SROD_Modifier[2]", 1.0f, 0.5f, 1.0f, 0.0f}, + {L"UvRotationScale[13]", 20.0f, -0.0f, 0.0f, 20.0f}, + {L"Scatter_Color[6]", 0.088656f, 0.107023f, 0.14996f, 1.0f}, + {L"UVBias[12]", 0.5015f, 0.5f, 0.0f, 1.0f}, + {L"MRAB_Modifier[5]", 1.0f, 1.25f, 1.0f, 1.0f}, + {L"Base_SROD_Modifier[13]", 0.5f, 0.5f, 0.5f, 0.25f}, + {L"Color_Tint_Low[13]", 0.15f, 0.15f, 0.15f, 1.0f}, + {L"MRAB_Modifier[13]", 0.75f, 1.0f, 1.0f, 1.0f}, + {L"MRAB_Modifier[11]", 1.0f, 1.3f, 1.0f, 1.0f}, + {L"Base_SROD_Modifier[11]", 0.2f, 0.5f, 1.0f, 0.25f}, + }); + + inline constexpr auto FemaleUni03Textures = std::to_array({ + {L"Swatch_Diffuse[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_D.T_B_Cotton_Dirty_00_D"}, + {L"Swatch_MRAB[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_MRAB.T_B_Cotton_Dirty_00_MRAB"}, + {L"Swatch_Normal[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_N.T_B_Cotton_Dirty_00_N"}, + {L"Swatch_Diffuse[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_D.T_B_Cotton_Plaid_00_D"}, + {L"Swatch_MRAB[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_MRAB.T_B_Cotton_Plaid_00_MRAB"}, + {L"Swatch_Normal[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_N.T_B_Cotton_Plaid_00_N"}, + {L"Swatch_Diffuse[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_D.T_B_Cotton_Plaid_00_D"}, + {L"Swatch_MRAB[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_MRAB.T_B_Cotton_Plaid_00_MRAB"}, + {L"Swatch_Normal[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_N.T_B_Cotton_Plaid_00_N"}, + {L"Swatch_Diffuse[4]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_R_D.T_Patch_R_D"}, + {L"Swatch_MRAB[4]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_R_MRAB.T_Patch_R_MRAB"}, + {L"Swatch_Normal[4]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_R_N.T_Patch_R_N"}, + {L"Swatch_Diffuse[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_D.T_M_Aluminum_Anodized_00_D"}, + {L"Swatch_MRAB[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_MRAB.T_M_Aluminum_Anodized_00_MRAB"}, + {L"Swatch_Normal[6]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_N.T_TileableStandard_Cotton_N"}, + {L"Swatch_Diffuse[6]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_MRAB[6]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_MRAB.T_TileableStandard_Cotton_MRAB"}, + {L"Swatch_Normal[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_N.T_M_Aluminum_Anodized_00_N"}, + {L"Swatch_MRAB[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_MRAB.T_TileableStandard_Cotton_MRAB"}, + {L"Swatch_Diffuse[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_D.T_L_Leather_Smooth_00_D"}, + {L"Swatch_Normal[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Rough_03_N.T_L_Leather_Rough_03_N"}, + {L"Swatch_MRAB[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Rough_00_MRAB.T_L_Leather_Rough_00_MRAB"}, + {L"Swatch_Diffuse[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_D.T_B_Cotton_Striped_05_D"}, + {L"Swatch_Normal[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_N.T_TileableStandard_Cotton_N"}, + {L"Swatch_MRAB[11]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Silk_MRAB.T_TileableStandard_Silk_MRAB"}, + {L"Swatch_Diffuse[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_Normal[11]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Silk_N.T_TileableStandard_Silk_N"}, + {L"Swatch_MRAB[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_MRAB.T_B_Cotton_Striped_05_MRAB"}, + {L"Swatch_Diffuse[11]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_Normal[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_N.T_B_Cotton_Striped_05_N"}, + {L"Swatch_Diffuse[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsIvory_D.T_Skin_ArmsIvory_D"}, + {L"Swatch_Normal[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsEbony_N.T_Skin_ArmsEbony_N"}, + {L"Swatch_Normal[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_N.T_S_Silk_Striped_00_N"}, + {L"Swatch_Diffuse[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_D.T_S_Silk_Striped_00_D"}, + {L"Swatch_MRAB[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_MRAB.T_S_Silk_Striped_00_MRAB"}, + {L"Swatch_MRAB[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsEbony_MRAB.T_Skin_ArmsEbony_MRAB"}, + {L"Swatch_Diffuse[15]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_D.T_B_Cotton_Striped_05_D"}, + {L"Swatch_MRAB[15]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_MRAB.T_B_Cotton_Striped_05_MRAB"}, + {L"Swatch_Normal[15]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_N.T_B_Cotton_Striped_05_N"}, + {L"Swatch_MRAB[14]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Rough_03_MRAB.T_L_Leather_Rough_03_MRAB"}, + {L"Swatch_Diffuse[14]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_D.T_L_Leather_Smooth_00_D"}, + {L"Swatch_Normal[14]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_N.T_L_Leather_Smooth_00_N"}, + {L"Swatch_Diffuse[13]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_MRAB[13]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Velvet_MRAB.T_TileableStandard_Velvet_MRAB"}, + {L"Swatch_Normal[13]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandardVelvet_N.T_TileableStandardVelvet_N"}, + }); +} // namespace HogwartsMP::KitParams + diff --git a/code/client/src/core/kit_params_houses.h b/code/client/src/core/kit_params_houses.h new file mode 100644 index 0000000..e0b61e1 --- /dev/null +++ b/code/client/src/core/kit_params_houses.h @@ -0,0 +1,83 @@ +#pragma once +// AUTO-GENERATED from the in-game AppearanceDump house signature harvest +// (2026-06-12, T3/T4 students of all four houses, both genders). +// House-distinguishing robe MID vector params: constant within a house, +// different across houses. STUUNI03-SPECIFIC — zone indices belong to the +// male/female StuUni03 robe layouts (see kit_params_{male,female}_uni03.h); +// they do not transfer to other uniform styles (StuUni01 etc.). +// House order: 0=Gryffindor, 1=Slytherin, 2=Ravenclaw, 3=Hufflepuff. +// Caveat: a couple of vector entries may be sampling artifacts (per-individual +// variety that happened to correlate with house in the harvest, e.g. skin-tone +// swatches) — harmless, but suspect these first if a house look seems off. + +#include "kit_params.h" + +namespace HogwartsMP::KitParams +{ + struct HouseParam { const wchar_t* name; float v[4][4]; }; // [house][rgba] + struct HouseTexture { const wchar_t* name; const wchar_t* path[4]; }; // [house] + + // Hand-added from the texture signature diff (2026-06-12): the house crest + // patch is the T_Patch_{G,S,R,H} texture set — slot 6 on the male StuUni03 + // robe, slot 4 on the female. Same house order as HouseParam. +#define HMP_PATCH(suffix) \ + {L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_G_" #suffix ".T_Patch_G_" #suffix, \ + L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_S_" #suffix ".T_Patch_S_" #suffix, \ + L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_R_" #suffix ".T_Patch_R_" #suffix, \ + L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_H_" #suffix ".T_Patch_H_" #suffix} + + inline constexpr auto HouseTexturesM = std::to_array({ + {L"Swatch_Diffuse[6]", HMP_PATCH(D)}, + {L"Swatch_MRAB[6]", HMP_PATCH(MRAB)}, + {L"Swatch_Normal[6]", HMP_PATCH(N)}, + }); + + inline constexpr auto HouseTexturesF = std::to_array({ + {L"Swatch_Diffuse[4]", HMP_PATCH(D)}, + {L"Swatch_MRAB[4]", HMP_PATCH(MRAB)}, + {L"Swatch_Normal[4]", HMP_PATCH(N)}, + }); +#undef HMP_PATCH + + inline constexpr auto HouseParamsM = std::to_array({ + {L"Color_Tint_High[1]", {{0.604f, 0.434f, 0.392f, 1.0f}, {0.445f, 0.468f, 0.423f, 1.0f}, {0.402f, 0.468f, 0.558f, 1.0f}, {0.539f, 0.491f, 0.418f, 1.0f}}}, + {L"Color_Tint_High[10]", {{0.730f, 0.686f, 0.610f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}}}, + {L"Color_Tint_High[11]", {{0.432f, 0.360f, 0.338f, 1.0f}, {0.223f, 0.205f, 0.184f, 1.0f}, {0.223f, 0.205f, 0.184f, 1.0f}, {0.223f, 0.205f, 0.184f, 1.0f}}}, + {L"Color_Tint_High[2]", {{0.584f, 0.485f, 0.445f, 1.0f}, {0.485f, 0.533f, 0.515f, 1.0f}, {0.527f, 0.565f, 0.644f, 1.0f}, {0.584f, 0.558f, 0.491f, 1.0f}}}, + {L"Color_Tint_High[4]", {{0.558f, 0.141f, 0.141f, 1.0f}, {0.319f, 0.402f, 0.371f, 1.0f}, {0.319f, 0.418f, 0.527f, 1.0f}, {0.617f, 0.497f, 0.266f, 1.0f}}}, + {L"Color_Tint_High[7]", {{0.730f, 0.651f, 0.468f, 1.0f}, {0.597f, 0.597f, 0.597f, 1.0f}, {0.753f, 0.565f, 0.366f, 1.0f}, {0.250f, 0.250f, 0.250f, 1.0f}}}, + {L"Color_Tint_High[8]", {{0.604f, 0.434f, 0.392f, 1.0f}, {0.445f, 0.468f, 0.423f, 1.0f}, {0.402f, 0.468f, 0.558f, 1.0f}, {0.539f, 0.491f, 0.418f, 1.0f}}}, + {L"Color_Tint_Low[1]", {{0.672f, 0.533f, 0.451f, 1.0f}, {0.584f, 0.584f, 0.533f, 1.0f}, {0.515f, 0.558f, 0.651f, 1.0f}, {0.708f, 0.638f, 0.558f, 1.0f}}}, + {L"Color_Tint_Low[10]", {{0.729f, 0.636f, 0.608f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}, {0.133f, 0.130f, 0.117f, 1.0f}}}, + {L"Color_Tint_Low[11]", {{0.146f, 0.146f, 0.146f, 1.0f}, {0.223f, 0.209f, 0.181f, 1.0f}, {0.223f, 0.209f, 0.181f, 1.0f}, {0.223f, 0.209f, 0.181f, 1.0f}}}, + {L"Color_Tint_Low[2]", {{0.584f, 0.485f, 0.445f, 1.0f}, {0.485f, 0.533f, 0.515f, 1.0f}, {0.527f, 0.565f, 0.644f, 1.0f}, {0.584f, 0.558f, 0.491f, 1.0f}}}, + {L"Color_Tint_Low[4]", {{0.631f, 0.347f, 0.347f, 1.0f}, {0.306f, 0.402f, 0.365f, 1.0f}, {0.254f, 0.332f, 0.423f, 1.0f}, {0.558f, 0.521f, 0.337f, 1.0f}}}, + {L"Color_Tint_Low[6]", {{0.771f, 0.297f, 0.304f, 1.0f}, {0.296f, 0.328f, 0.771f, 1.0f}, {0.296f, 0.328f, 0.771f, 1.0f}, {0.296f, 0.328f, 0.771f, 1.0f}}}, + {L"Color_Tint_Low[7]", {{0.703f, 0.314f, 0.290f, 1.0f}, {0.194f, 0.323f, 0.205f, 1.0f}, {0.342f, 0.429f, 0.527f, 1.0f}, {0.831f, 0.708f, 0.397f, 1.0f}}}, + {L"Color_Tint_Low[8]", {{0.672f, 0.533f, 0.451f, 1.0f}, {0.584f, 0.584f, 0.533f, 1.0f}, {0.515f, 0.558f, 0.651f, 1.0f}, {0.708f, 0.638f, 0.558f, 1.0f}}}, + }); + + inline constexpr auto HouseParamsF = std::to_array({ + {L"Color_Tint_High[1]", {{0.927f, 0.927f, 0.927f, 1.0f}, {0.927f, 0.927f, 0.927f, 1.0f}, {0.948f, 0.948f, 0.948f, 1.0f}, {0.930f, 0.930f, 0.930f, 1.0f}}}, + {L"Color_Tint_High[11]", {{0.558f, 0.141f, 0.141f, 1.0f}, {0.319f, 0.402f, 0.371f, 1.0f}, {0.319f, 0.416f, 0.526f, 1.0f}, {0.620f, 0.496f, 0.265f, 1.0f}}}, + {L"Color_Tint_High[2]", {{0.604f, 0.434f, 0.392f, 1.0f}, {0.442f, 0.469f, 0.425f, 1.0f}, {0.401f, 0.467f, 0.557f, 1.0f}, {0.539f, 0.491f, 0.418f, 1.0f}}}, + {L"Color_Tint_High[3]", {{0.730f, 0.651f, 0.468f, 1.0f}, {0.597f, 0.597f, 0.597f, 1.0f}, {0.750f, 0.564f, 0.368f, 1.0f}, {0.250f, 0.250f, 0.250f, 1.0f}}}, + {L"Color_Tint_High[4]", {{0.516f, 0.516f, 0.516f, 1.0f}, {0.443f, 0.443f, 0.443f, 1.0f}, {0.445f, 0.445f, 0.445f, 1.0f}, {0.443f, 0.443f, 0.443f, 1.0f}}}, + {L"Color_Tint_High[6]", {{0.200f, 0.200f, 0.200f, 1.0f}, {0.200f, 0.200f, 0.200f, 1.0f}, {0.202f, 0.202f, 0.202f, 1.0f}, {0.443f, 0.443f, 0.443f, 1.0f}}}, + {L"Color_Tint_High[7]", {{0.600f, 0.600f, 0.600f, 1.0f}, {0.445f, 0.468f, 0.423f, 1.0f}, {0.402f, 0.468f, 0.558f, 1.0f}, {0.539f, 0.491f, 0.418f, 1.0f}}}, + {L"Color_Tint_High[8]", {{0.130f, 0.115f, 0.115f, 1.0f}, {0.275f, 0.254f, 0.231f, 1.0f}, {0.202f, 0.195f, 0.178f, 1.0f}, {0.275f, 0.254f, 0.231f, 1.0f}}}, + {L"Color_Tint_High[9]", {{0.130f, 0.115f, 0.115f, 1.0f}, {0.503f, 0.462f, 0.413f, 1.0f}, {0.297f, 0.295f, 0.292f, 1.0f}, {0.202f, 0.181f, 0.150f, 1.0f}}}, + {L"Color_Tint_Low[1]", {{0.938f, 0.938f, 0.938f, 1.0f}, {0.938f, 0.938f, 0.938f, 1.0f}, {0.922f, 0.922f, 0.922f, 1.0f}, {0.939f, 0.939f, 0.939f, 1.0f}}}, + {L"Color_Tint_Low[11]", {{0.631f, 0.347f, 0.347f, 1.0f}, {0.308f, 0.402f, 0.351f, 1.0f}, {0.252f, 0.334f, 0.422f, 1.0f}, {0.557f, 0.522f, 0.339f, 1.0f}}}, + {L"Color_Tint_Low[14]", {{0.271f, 0.223f, 0.207f, 1.0f}, {0.271f, 0.223f, 0.207f, 1.0f}, {0.271f, 0.223f, 0.207f, 1.0f}, {1.000f, 0.033f, 0.000f, 1.0f}}}, + {L"Color_Tint_Low[2]", {{0.672f, 0.533f, 0.451f, 1.0f}, {0.584f, 0.584f, 0.533f, 1.0f}, {0.515f, 0.554f, 0.651f, 1.0f}, {0.708f, 0.638f, 0.558f, 1.0f}}}, + {L"Color_Tint_Low[3]", {{0.599f, 0.268f, 0.247f, 1.0f}, {0.175f, 0.292f, 0.185f, 1.0f}, {0.342f, 0.429f, 0.527f, 1.0f}, {0.833f, 0.706f, 0.399f, 1.0f}}}, + {L"Color_Tint_Low[4]", {{0.738f, 0.423f, 0.328f, 1.0f}, {0.771f, 0.297f, 0.304f, 1.0f}, {0.296f, 0.328f, 0.768f, 1.0f}, {0.771f, 0.297f, 0.304f, 1.0f}}}, + {L"Color_Tint_Low[6]", {{0.200f, 0.200f, 0.200f, 1.0f}, {0.200f, 0.200f, 0.200f, 1.0f}, {0.165f, 0.175f, 0.202f, 1.0f}, {0.771f, 0.297f, 0.304f, 1.0f}}}, + {L"Color_Tint_Low[7]", {{0.600f, 0.600f, 0.600f, 1.0f}, {0.584f, 0.584f, 0.533f, 1.0f}, {0.515f, 0.558f, 0.651f, 1.0f}, {0.708f, 0.638f, 0.558f, 1.0f}}}, + {L"Color_Tint_Low[8]", {{0.271f, 0.172f, 0.140f, 1.0f}, {0.250f, 0.216f, 0.171f, 1.0f}, {0.250f, 0.216f, 0.171f, 1.0f}, {0.181f, 0.156f, 0.125f, 1.0f}}}, + {L"Color_Tint_Low[9]", {{0.271f, 0.223f, 0.207f, 1.0f}, {0.223f, 0.209f, 0.178f, 1.0f}, {0.365f, 0.354f, 0.344f, 1.0f}, {0.242f, 0.198f, 0.156f, 1.0f}}}, + {L"UVBias[4]", {{0.470f, 0.485f, 0.000f, 1.0f}, {0.470f, 0.482f, 0.000f, 1.0f}, {0.470f, 0.484f, 0.000f, 1.0f}, {0.470f, 0.485f, 0.000f, 1.0f}}}, + }); + +} // namespace HogwartsMP::KitParams diff --git a/code/client/src/core/kit_params_male_uni03.h b/code/client/src/core/kit_params_male_uni03.h new file mode 100644 index 0000000..42e1dcd --- /dev/null +++ b/code/client/src/core/kit_params_male_uni03.h @@ -0,0 +1,158 @@ +#pragma once +// AUTO-GENERATED from the in-game AppearanceDump MID harvest (2026-06-12). +// Male StuUni03 robe MID parameters (parent MI_HUM_M_CMB_StuUni03_Robed), +// read from a live male T3 student. "StuUni03" is the game's uniform-style +// asset variant (StuUni01/03/… are different designs), not our versioning. +// Replaces the StuUni01 set for the male proxy so the StuUni03-derived house +// overlay slots line up. +// +// Zone map (male StuUni03; empirical — see kit_params.h for what zones are): +// zone 6 = house crest patch (Swatch_* = T_Patch_{G,S,R,H}_*) +// zone 7 = robe trim/lining — primary house color (Color_Tint_Low[7]: +// Gry scarlet / Huf gold / Rav blue / Sly green) +// zone 13 = visible skin (arms/hands; T_Skin_Arms* — varies per INDIVIDUAL, +// not per house) +// zones 1,2,4,8,10,11 = secondary fabric zones with house-correlated tints +// remaining zones unidentified + +#include "kit_params.h" + +namespace HogwartsMP::KitParams +{ + inline constexpr auto MaleUni03Scalars = std::to_array({ + {L"Overlay_Opacity[1]", 0.0f}, + {L"Overlay_Opacity[8]", 0.0f}, + {L"Normal_Intensity[3]", 0.0f}, + {L"Fuzz[13]", 0.0f}, + {L"Overlay_Opacity[2]", 0.0f}, + {L"Normal_Intensity[6]", 0.0f}, + {L"Fuzz[5]", 0.0f}, + {L"Fuzz[12]", 0.0f}, + {L"Overlay_Opacity[12]", 0.0f}, + {L"Overlay_Opacity[4]", 0.0f}, + {L"Fuzz[6]", 0.0f}, + {L"Fuzz[10]", 0.0f}, + {L"Normal_Intensity[13]", 0.0f}, + {L"Overlay_Opacity[9]", 0.0f}, + {L"Fuzz[9]", 0.0f}, + }); + + inline constexpr auto MaleUni03Vectors = std::to_array({ + {L"Base_SROD_Modifier[1]", 1.0f, 0.5f, 1.0f, 0.0f}, + {L"Color_Tint_High[1]", 0.539479f, 0.491021f, 0.417885f, 1.0f}, + {L"Color_Tint_Low[1]", 0.708376f, 0.637597f, 0.55834f, 1.0f}, + {L"MRAB_Modifier[1]", 1.0f, 1.0f, 1.0f, 0.14f}, + {L"Scatter_Color[1]", 0.046665f, 0.045186f, 0.040915f, 1.0f}, + {L"UvRotationScale[1]", 12.5f, -0.0f, 0.0f, 12.5f}, + {L"Base_SROD_Modifier[8]", 1.0f, 0.5f, 1.0f, 0.0f}, + {L"Color_Tint_High[8]", 0.539479f, 0.491021f, 0.417885f, 1.0f}, + {L"UvRotationScale[8]", 12.5f, -0.0f, 0.0f, 12.5f}, + {L"MRAB_Modifier[8]", 1.0f, 1.0f, 1.0f, 0.14f}, + {L"Scatter_Color[8]", 0.046665f, 0.045186f, 0.040915f, 1.0f}, + {L"Color_Tint_Low[8]", 0.708376f, 0.637597f, 0.55834f, 1.0f}, + {L"Color_Tint_High[5]", 0.427083f, 0.389754f, 0.295541f, 1.0f}, + {L"Color_Tint_Low[3]", 0.921875f, 0.921875f, 0.921875f, 1.0f}, + {L"UvRotationScale[3]", 12.5f, -0.0f, 0.0f, 12.5f}, + {L"Color_Tint_High[3]", 0.947917f, 0.947917f, 0.947917f, 1.0f}, + {L"Color_Tint_Low[5]", 0.432292f, 0.394752f, 0.30001f, 1.0f}, + {L"Base_SROD_Modifier[2]", 1.0f, 0.0f, 1.0f, 0.0f}, + {L"Color_Tint_High[2]", 0.584078f, 0.55834f, 0.491021f, 1.0f}, + {L"Color_Tint_Low[2]", 0.584078f, 0.55834f, 0.491021f, 1.0f}, + {L"MRAB_Modifier[2]", 0.5f, 1.0f, 1.0f, 1.0f}, + {L"Scatter_Color[2]", 0.584078f, 0.55834f, 0.491021f, 1.0f}, + {L"Base_SROD_Modifier[7]", 1.0f, 0.0f, 1.0f, 0.06f}, + {L"Color_Tint_High[7]", 0.25015828f, 0.25015828f, 0.25015828f, 1.0f}, + {L"Color_Tint_Low[7]", 0.8307699f, 0.70837575f, 0.39675522f, 1.0f}, + {L"Scatter_Color[7]", 0.02121901f, 0.01850022f, 0.014443844f, 1.0f}, + {L"UvRotationScale[7]", -5.4463906f, 8.386705f, -8.386705f, -5.4463906f}, + {L"Color_Tint_High[11]", 0.223228f, 0.205079f, 0.184475f, 1.0f}, + {L"Color_Tint_Low[11]", 0.223228f, 0.208637f, 0.181164f, 1.0f}, + {L"Scatter_Color[13]", 0.0f, 0.0f, 0.0f, 1.0f}, + {L"Color_Tint_Low[10]", 0.132868f, 0.130136f, 0.116971f, 1.0f}, + {L"Scatter_Color[11]", 0.14996f, 0.14996f, 0.14996f, 1.0f}, + {L"Color_Tint_High[10]", 0.132868f, 0.130136f, 0.116971f, 1.0f}, + {L"Base_SROD_Modifier[6]", 1.0f, 0.5f, 1.0f, 0.25f}, + {L"Color_Tint_High[6]", 0.442708f, 0.442708f, 0.442708f, 1.0f}, + {L"Color_Tint_Low[6]", 0.296444f, 0.327741f, 0.770741f, 1.0f}, + {L"Scatter_Color[6]", 0.036458332f, 0.036458332f, 0.036458332f, 1.0f}, + {L"Color_Tint_Low[12]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"UvRotationScale[6]", 25.974026f, -0.0f, 0.0f, 25.974026f}, + {L"Color_Tint_High[12]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"UVBias[6]", 0.71f, 0.3f, 0.0f, 1.0f}, + {L"Scatter_Color[12]", 0.024382f, 0.033458f, 0.041667f, 1.0f}, + {L"Base_SROD_Modifier[4]", 0.2f, 0.5f, 1.0f, 0.25f}, + {L"Color_Tint_High[4]", 0.6172066f, 0.49693298f, 0.2663556f, 1.0f}, + {L"Color_Tint_Low[4]", 0.5583404f, 0.52099556f, 0.33716363f, 1.0f}, + {L"Scatter_Color[4]", 0.309469f, 0.258183f, 0.0865f, 1.0f}, + {L"UvRotationScale[4]", 3.746066f, 9.271839f, -9.271839f, 3.746066f}, + {L"Color_Tint_Low[9]", 0.15f, 0.15f, 0.15f, 1.0f}, + {L"Color_Tint_High[9]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"Scatter_Color[9]", 0.0625f, 0.0625f, 0.0625f, 1.0f}, + {L"UvRotationScale[10]", 5.714286f, -0.0f, 0.0f, 5.714286f}, + {L"Base_SROD_Modifier[10]", 1.0f, 0.5f, 1.0f, 0.25f}, + {L"Scatter_Color[10]", 0.401978f, 0.386429f, 0.371238f, 1.0f}, + {L"Base_SROD_Modifier[14]", 1.0f, 0.5f, 1.0f, 0.25f}, + {L"Color_Tint_Low[14]", 0.166086f, 0.175972f, 0.2f, 1.0f}, + {L"Color_Tint_High[14]", 0.2f, 0.2f, 0.2f, 1.0f}, + {L"UvRotationScale[11]", 5.882353f, -0.0f, 0.0f, 5.882353f}, + {L"MRAB_Modifier[14]", 1.0f, 1.0f, 1.0f, 0.5f}, + {L"Scatter_Color[14]", 0.087971f, 0.107702f, 0.15f, 1.0f}, + {L"Base_SROD_Modifier[5]", 1.0f, 0.0f, 1.0f, 0.0f}, + {L"UvRotationScale[13]", 5.0f, -0.0f, 0.0f, 5.0f}, + {L"UVBias[13]", 0.5015f, 0.5f, 0.0f, 1.0f}, + {L"Base_SROD_Modifier[12]", 0.5f, 0.5f, 1.0f, 0.25f}, + {L"UvRotationScale[12]", 8.333333f, -0.0f, 0.0f, 8.333333f}, + {L"MRAB_Modifier[4]", 1.0f, 1.25f, 1.0f, 1.0f}, + {L"Base_SROD_Modifier[9]", 0.5f, 0.5f, 0.5f, 0.25f}, + {L"UvRotationScale[9]", 20.0f, -0.0f, 0.0f, 20.0f}, + {L"MRAB_Modifier[9]", 0.75f, 1.0f, 1.0f, 1.0f}, + {L"UVBias[7]", 0.13f, 0.0f, 0.0f, 1.0f}, + {L"MRAB_Modifier[5]", 1.0f, 1.25f, 1.0f, 1.0f}, + {L"Scatter_Color[5]", 0.296875f, 0.296875f, 0.296875f, 1.0f}, + }); + + inline constexpr auto MaleUni03Textures = std::to_array({ + {L"Swatch_Diffuse[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_D.T_B_Cotton_Plaid_00_D"}, + {L"Swatch_MRAB[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_MRAB.T_B_Cotton_Plaid_00_MRAB"}, + {L"Swatch_Normal[1]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_N.T_B_Cotton_Plaid_00_N"}, + {L"Swatch_Diffuse[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_D.T_B_Cotton_Dirty_00_D"}, + {L"Swatch_MRAB[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_MRAB.T_B_Cotton_Dirty_00_MRAB"}, + {L"Swatch_Normal[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_N.T_B_Cotton_Plaid_00_N"}, + {L"Swatch_Diffuse[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_D.T_B_Cotton_Plaid_00_D"}, + {L"Swatch_MRAB[8]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Plaid_00_MRAB.T_B_Cotton_Plaid_00_MRAB"}, + {L"Swatch_Normal[3]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Dirty_00_N.T_B_Cotton_Dirty_00_N"}, + {L"Swatch_Diffuse[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_D.T_M_Aluminum_Anodized_00_D"}, + {L"Swatch_MRAB[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_MRAB.T_M_Aluminum_Anodized_00_MRAB"}, + {L"Swatch_Normal[5]", L"/Game/RiggedObjects/Characters/TileableTextures/T_M_Aluminum_Anodized_00_N.T_M_Aluminum_Anodized_00_N"}, + {L"Swatch_Diffuse[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_D.T_S_Silk_Striped_00_D"}, + {L"Swatch_MRAB[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_MRAB.T_S_Silk_Striped_00_MRAB"}, + {L"Swatch_Normal[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_N.T_S_Silk_Striped_00_N"}, + {L"Swatch_Diffuse[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_D.T_S_Silk_Striped_00_D"}, + {L"Swatch_MRAB[7]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_MRAB.T_S_Silk_Striped_00_MRAB"}, + {L"Swatch_Normal[2]", L"/Game/RiggedObjects/Characters/TileableTextures/T_S_Silk_Striped_00_N.T_S_Silk_Striped_00_N"}, + {L"Swatch_Diffuse[11]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Rough_03_D.T_L_Leather_Rough_03_D"}, + {L"Swatch_MRAB[11]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_MRAB.T_L_Leather_Smooth_00_MRAB"}, + {L"Swatch_Normal[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_N.T_L_Leather_Smooth_00_N"}, + {L"Swatch_Diffuse[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Rough_03_D.T_L_Leather_Rough_03_D"}, + {L"Swatch_MRAB[10]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_MRAB.T_L_Leather_Smooth_00_MRAB"}, + {L"Swatch_Normal[11]", L"/Game/RiggedObjects/Characters/TileableTextures/T_L_Leather_Smooth_00_N.T_L_Leather_Smooth_00_N"}, + {L"Swatch_Diffuse[13]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsIvory_D.T_Skin_ArmsIvory_D"}, + {L"Swatch_Normal[13]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsEbony_N.T_Skin_ArmsEbony_N"}, + {L"Swatch_MRAB[13]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Skin_ArmsEbony_MRAB.T_Skin_ArmsEbony_MRAB"}, + {L"Swatch_Diffuse[6]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_H_D.T_Patch_H_D"}, + {L"Swatch_MRAB[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_MRAB.T_B_Cotton_Striped_05_MRAB"}, + {L"Swatch_Normal[6]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_H_N.T_Patch_H_N"}, + {L"Swatch_Diffuse[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_D.T_B_Cotton_Striped_05_D"}, + {L"Swatch_MRAB[6]", L"/Game/RiggedObjects/Characters/TileableTextures/T_Patch_H_MRAB.T_Patch_H_MRAB"}, + {L"Swatch_Normal[12]", L"/Game/RiggedObjects/Characters/TileableTextures/T_B_Cotton_Striped_05_N.T_B_Cotton_Striped_05_N"}, + {L"Swatch_Diffuse[4]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_MRAB[4]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Silk_MRAB.T_TileableStandard_Silk_MRAB"}, + {L"Swatch_Normal[4]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Silk_N.T_TileableStandard_Silk_N"}, + {L"Swatch_Diffuse[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_MRAB[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Velvet_MRAB.T_TileableStandard_Velvet_MRAB"}, + {L"Swatch_Normal[14]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_N.T_TileableStandard_Cotton_N"}, + {L"Swatch_Diffuse[14]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_D.T_TileableStandard_D"}, + {L"Swatch_MRAB[14]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandard_Cotton_MRAB.T_TileableStandard_Cotton_MRAB"}, + {L"Swatch_Normal[9]", L"/Game/RiggedObjects/Characters/TileableTextures/Standard/T_TileableStandardVelvet_N.T_TileableStandardVelvet_N"}, + }); +} // namespace HogwartsMP::KitParams diff --git a/code/client/src/core/playground.cpp b/code/client/src/core/playground.cpp index a9f8979..0aa7ae5 100644 --- a/code/client/src/core/playground.cpp +++ b/code/client/src/core/playground.cpp @@ -1,5 +1,6 @@ #include "playground.h" +#include "appearance_dump.h" #include "application.h" #include "aob_scan.h" #include "student_proxy.h" @@ -111,6 +112,7 @@ void Playground_Tick() { // here, not in the (render-thread) ImGui callback. The buttons below only // set thread-safe request flags. HogwartsMP::Core::StudentProxy::ProcessPending(); + HogwartsMP::Core::AppearanceDump::ProcessPending(); static AActor *lastActor = nullptr; @@ -195,6 +197,14 @@ void Playground_Tick() { HogwartsMP::Core::StudentProxy::RequestDespawnAll(); } + ImGui::Separator(); + ImGui::TextDisabled("Appearance harvest"); + if (ImGui::Button("Dump Nearby NPC Appearances")) { + HogwartsMP::Core::AppearanceDump::RequestDump(); + } + ImGui::SameLine(); + ImGui::TextDisabled("(logs to AppearanceDump channel)"); + ImGui::End(); }); } diff --git a/code/client/src/core/student_proxy.cpp b/code/client/src/core/student_proxy.cpp index b956655..6cdb980 100644 --- a/code/client/src/core/student_proxy.cpp +++ b/code/client/src/core/student_proxy.cpp @@ -10,11 +10,13 @@ // Spawns via native UWorld::SpawnActor — the FVector+FRotator overload // (the engine/console/UE4SS one; the FTransform overload the mod used to // hook was the wrong function on this build). See game_layout.h. -// 2. Dress CharacterMesh0 with the student CMBH outfit mesh + the ~150 -// harvested swatch MID parameters (kit_params.h) — without them the -// outfit renders as a white placeholder. +// 2. Dress CharacterMesh0 with the student outfit mesh + the harvested +// swatch MID parameters (kit_params_*_uni03.h) — without them the +// outfit renders as a white placeholder. House identity is a per-gender +// tint + crest-texture overlay on top (kit_params_houses.h). // 3. Add a second SkeletalMeshComponent for the head+hands skin, attach it -// to CharacterMesh0 (single actor — no z-fighting). +// to CharacterMesh0 (single actor — no z-fighting). Females get a third +// component for hair (their head mesh has none baked in). // 4. Animation: the game's AnimationArchitect middleware starves regular // AnimBPs on raw-spawned actors (they instantiate but output ref pose), // so play a raw idle AnimSequence on the skin component and master-pose @@ -36,6 +38,9 @@ #include "application.h" #include "kit_params.h" +#include "kit_params_female_uni03.h" +#include "kit_params_houses.h" +#include "kit_params_male_uni03.h" #include "ue4_natives.h" #include "ue4_reflection.h" @@ -65,33 +70,70 @@ namespace { return Framework::Logging::GetLogger("StudentProxy"); } - // ── Student kit asset paths (harvested from live NPCs; loadable by path - // from any save, no NPCs required in memory) ────────────────────────── - const wchar_t *OUTFIT_MESH_PATH = - L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni01/SK_HUM_M_CMBH_StuUni01_Robed_Master_ClothJoint.SK_HUM_M_CMBH_StuUni01_Robed_Master_ClothJoint"; - constexpr std::array OUTFIT_MATS{ - L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni01/Materials/MI_HUM_M_CMBH_Robed_StuUni01.MI_HUM_M_CMBH_Robed_StuUni01", - L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni01/Materials/MI_HUM_M_CMBH_Robed_StuUni01.MI_HUM_M_CMBH_Robed_StuUni01", - L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni01/Materials/MI_HUM_M_CMBH_Robed_StuUni01.MI_HUM_M_CMBH_Robed_StuUni01", - L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni01/Materials/MI_HUM_M_CMBH_Robed_StuUni01.MI_HUM_M_CMBH_Robed_StuUni01", + // ── Student kit asset paths (harvested from live NPCs via AppearanceDump; + // loadable by path from any save, no NPCs required in memory) ───────── + // + // Both genders wear the StuUni03 robe family (what T3 ambient students + // wear) so the per-gender house overlays in kit_params_houses.h land on + // the right material zones. + + // ── Male ───────────────────────────────────────────────────────────────── + const wchar_t *OUTFIT_MESH_PATH_M = + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni03/SK_HUM_M_CMB_StuUni03_Dynamics_Master.SK_HUM_M_CMB_StuUni03_Dynamics_Master"; + constexpr std::array OUTFIT_MATS_M{ + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni03/Materials/MI_HUM_M_CMB_StuUni03_Robed.MI_HUM_M_CMB_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni03/Materials/MI_HUM_M_CMB_StuUni03_Robed.MI_HUM_M_CMB_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni03/Materials/MI_HUM_M_CMB_StuUni03_Robed.MI_HUM_M_CMB_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_M/StuUni03/Materials/MI_HUM_M_CMB_StuUni03_Robed.MI_HUM_M_CMB_StuUni03_Robed", L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", }; - static_assert(OUTFIT_MATS.size() == 7); // one per outfit material slot + static_assert(OUTFIT_MATS_M.size() == 7); // one per outfit material slot // Sebastian Sallow head+hands ("Sebastien" typo is in the game assets). - const wchar_t *SKIN_MESH_PATH = + const wchar_t *SKIN_MESH_PATH_M = L"/Game/RiggedObjects/Characters/Human/Heads/NPC_YM_SebastianSallow/NPC_YM_SebastienSallow_Master.NPC_YM_SebastienSallow_Master"; - constexpr std::array SKIN_MATS{ + constexpr std::array SKIN_MATS_M{ L"/Game/RiggedObjects/Characters/Human/Heads/Materials/MI_HUM_N_Heads_Eyelash.MI_HUM_N_Heads_Eyelash", L"/Game/RiggedObjects/Characters/Human/Heads/Materials/MI_HUM_N_Heads_Teeth.MI_HUM_N_Heads_Teeth", L"/Game/RiggedObjects/Characters/Human/Heads/Young_M/Materials/MI_Young_M_Head.MI_Young_M_Head", L"/Game/RiggedObjects/Characters/Human/Heads/Materials/MI_HUM_N_Heads_Eye.MI_HUM_N_Heads_Eye", L"/Game/RiggedObjects/Characters/Human/Heads/Materials/MI_HUM_N_Heads_EyeOcclusion.MI_HUM_N_Heads_EyeOcclusion", }; - static_assert(SKIN_MATS.size() == 5); // one per skin material slot + static_assert(SKIN_MATS_M.size() == 5); // one per skin material slot + + // ── Female ─────────────────────────────────────────────────────────────── + // Mesh + material harvested from live female BP_Tier3_Character_C robes + // (material via the runtime MID's Parent chain — naming is inconsistent + // between genders in the game's own assets). + const wchar_t *OUTFIT_MESH_PATH_F = + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_F/StuUni03/SK_HUM_F_CMB_StuUni03_Robed_Dynamics_Master.SK_HUM_F_CMB_StuUni03_Robed_Dynamics_Master"; + constexpr std::array OUTFIT_MATS_F{ + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_F/StuUni03/Materials/MI_HUM_F_CMBH_StuUni03_Robed.MI_HUM_F_CMBH_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_F/StuUni03/Materials/MI_HUM_F_CMBH_StuUni03_Robed.MI_HUM_F_CMBH_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_F/StuUni03/Materials/MI_HUM_F_CMBH_StuUni03_Robed.MI_HUM_F_CMBH_StuUni03_Robed", + L"/Game/RiggedObjects/Characters/Human/Clothing/Combined_F/StuUni03/Materials/MI_HUM_F_CMBH_StuUni03_Robed.MI_HUM_F_CMBH_StuUni03_Robed", + L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", + L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", + L"/Game/RiggedObjects/MasterMaterials/Misc/MI_ClothSim.MI_ClothSim", + }; + static_assert(OUTFIT_MATS_F.size() == 7); + // Generic young-female NPC head (incl. hands), confirmed loaded in the + // AppearanceDump head scan. Slot layout differs from the male head — her + // mesh's default materials are kept (the male override made the face + // translucent and hid the hands), so no SKIN_MATS_F set exists. + const wchar_t *SKIN_MESH_PATH_F = + L"/Game/RiggedObjects/Characters/Human/Heads/NPC_Young_F/SK_NPC_Young_F_Head_Master.SK_NPC_Young_F_Head_Master"; + // The female head has no baked hair (Sebastian's male mesh does) — add a + // separate hair component. Mesh + material harvested from live female NPCs. + const wchar_t *HAIR_MESH_PATH_F = + L"/Game/RiggedObjects/Characters/Human/Hair/Hair_F/ChunkHair01/SK_HUM_F_Hair_ChunkHair01_Bang0Bun1_Master.SK_HUM_F_Hair_ChunkHair01_Bang0Bun1_Master"; + constexpr std::array HAIR_MATS_F{ + L"/Game/RiggedObjects/Characters/Human/Hair/Hair_F/ChunkHair01/Materials/MI_HUM_F_Hair_ChunkHair01_Bang0Bun1.MI_HUM_F_Hair_ChunkHair01_Bang0Bun1", + }; + // Raw idle sequences (engine single-node playback bypasses the - // AnimationArchitect middleware). Male first, female fallback. + // AnimationArchitect middleware). Index 0=male, 1=female. constexpr std::array IDLE_SEQ_PATHS{ L"/Game/Animation/Human/Student/Student_M/StuM_BM_Idle_Loop_Stand_anm.StuM_BM_Idle_Loop_Stand_anm", L"/Game/Animation/Human/Student/Student_F/StuF_BM_Idle_Loop_Stand_anm.StuF_BM_Idle_Loop_Stand_anm", @@ -259,9 +301,55 @@ namespace { Log()->info("MID params applied on {}/{} slots", applied, matPaths.size()); } + // Apply per-house tint overrides + crest patch textures on top of the base + // kit params. Works on the MIDs ApplyMidParams already installed (fetched + // back via GetMaterial). House order matches kit_params_houses.h: + // 0=Gry, 1=Sly, 2=Rav, 3=Huf. + void ApplyHouseOverlay(void *comp, int numSlots, + std::span params, + std::span htex, + int house, UClass *texCls) { + house = std::clamp(house, 0, 3); + + // Crest patch textures (T_Patch_{G,S,R,H}) — same set for every slot. + std::vector> texParams; + texParams.reserve(htex.size()); + for (const auto &t : htex) { + if (auto *tex = LoadObjectByPath(texCls, t.path[house])) { + texParams.emplace_back(MakeFName(t.name), tex); + } + } + + for (int i = 0; i < numSlots; ++i) { + struct { + int32_t ElementIndex; + UObjectBase *ReturnValue; + } gm{i, nullptr}; + CallUFunction(comp, "GetMaterial", &gm); + auto *mid = gm.ReturnValue; + if (!mid || narrow(mid->GetClass()->GetFName()) != "MaterialInstanceDynamic") { + continue; + } + for (const auto &p : params) { + struct { + FName Name; + FLinearColorF Value; + } vp{MakeFName(p.name), {p.v[house][0], p.v[house][1], p.v[house][2], p.v[house][3]}}; + CallUFunction(mid, "SetVectorParameterValue", &vp); + } + for (const auto &[texName, tex] : texParams) { + struct { + FName Name; + UObjectBase *Value; + } tp{texName, tex}; + CallUFunction(mid, "SetTextureParameterValue", &tp); + } + } + } + // ── The spawn itself ───────────────────────────────────────────────────── - AActor *SpawnStudent(const FVector &pos, float yawDeg) { + AActor *SpawnStudent(const FVector &pos, float yawDeg, bool female = false, int house = 0) { auto *cls = reinterpret_cast(find_uobject("Class /Script/Phoenix.Biped_Character")); if (!cls) { Log()->error("Biped_Character class not found"); @@ -339,20 +427,53 @@ namespace { using namespace HogwartsMP::KitParams; - auto *outfitMesh = LoadObjectByPath(skelMeshCls, OUTFIT_MESH_PATH); + const auto *outfitMeshPath = female ? OUTFIT_MESH_PATH_F : OUTFIT_MESH_PATH_M; + std::span outfitMats = female ? OUTFIT_MATS_F : OUTFIT_MATS_M; + + auto *outfitMesh = LoadObjectByPath(skelMeshCls, outfitMeshPath); + if (!outfitMesh && female) { + // Fall back to the male outfit so the proxy is still visible + // rather than invisible. + Log()->warn("Female outfit mesh not found — falling back to male"); + female = false; + outfitMeshPath = OUTFIT_MESH_PATH_M; + outfitMats = OUTFIT_MATS_M; + outfitMesh = LoadObjectByPath(skelMeshCls, outfitMeshPath); + } if (!outfitMesh) { return finalize(actor); } - DressComponent(meshComp, outfitMesh, OUTFIT_MATS, matCls); - ApplyMidParams(meshComp, OUTFIT_MATS, Scalars, Vectors, Textures, matCls, texCls); - - // The outfit is a *_ClothJoint mesh — keep cloth sim off; the robe is + DressComponent(meshComp, outfitMesh, outfitMats, matCls); + if (female) { + ApplyMidParams(meshComp, outfitMats, + FemaleUni03Scalars, FemaleUni03Vectors, FemaleUni03Textures, + matCls, texCls); + } + else { + ApplyMidParams(meshComp, outfitMats, + MaleUni03Scalars, MaleUni03Vectors, MaleUni03Textures, + matCls, texCls); + } + // House tint + crest overlay on top of the base params (zone layouts + // are per-gender, see kit_params_houses.h). + ApplyHouseOverlay(meshComp, static_cast(outfitMats.size()), + female ? std::span{HouseParamsF} : std::span{HouseParamsM}, + female ? std::span{HouseTexturesF} : std::span{HouseTexturesM}, + house, texCls); + + // The outfit is a cloth-sim mesh — keep cloth sim off; the robe is // master-posed to the skin below instead of simulating. SetBoolProperty(meshComp, "bDisableClothSimulation", true); // Skin (head+hands) on ONE added component, attached to CharacterMesh0. + const auto *skinMeshPath = female ? SKIN_MESH_PATH_F : SKIN_MESH_PATH_M; + UObjectBase *skinComp = nullptr; - auto *skinMesh = LoadObjectByPath(skelMeshCls, SKIN_MESH_PATH); + auto *skinMesh = LoadObjectByPath(skelMeshCls, skinMeshPath); + if (!skinMesh && female) { + Log()->warn("Female skin mesh not found — falling back to male head"); + skinMesh = LoadObjectByPath(skelMeshCls, SKIN_MESH_PATH_M); + } if (smcCls && skinMesh) { // AddComponentByClass(UClass*, bool bManualAttachment, FTransform, bool bDeferredFinish) // UE4.27 FTransform in a ProcessEvent param block: vectorized floats, @@ -382,25 +503,71 @@ namespace { attach.Parent = meshComp; CallUFunction(skinComp, "K2_AttachToComponent", &attach); - DressComponent(skinComp, skinMesh, SKIN_MATS, matCls); - // Skin materials are parameterized like the outfit's — the - // *_WithHands_* textures in this set are what fixes the hands. - ApplyMidParams(skinComp, SKIN_MATS, SkinScalars, SkinVectors, SkinTextures, matCls, texCls); + if (female) { + // Female head: keep the mesh's default materials (single + // slot, MI_NPC_Young_F) — the male override set made her + // face translucent and hid the hands. + DressComponent(skinComp, skinMesh, {}, matCls); + } + else { + DressComponent(skinComp, skinMesh, SKIN_MATS_M, matCls); + // Skin materials are parameterized like the outfit's — the + // *_WithHands_* textures in this set are what fixes the hands. + ApplyMidParams(skinComp, SKIN_MATS_M, SkinScalars, SkinVectors, SkinTextures, matCls, texCls); + } } else { Log()->warn("AddComponentByClass returned null — no skin component"); } } + // Hair for females — separate component master-posed to the head. + if (female && skinComp) { + if (auto *hairMesh = LoadObjectByPath(skelMeshCls, HAIR_MESH_PATH_F)) { + struct alignas(16) { + UClass *Class{}; + bool bManualAttachment{}; + uint8_t pad[7]{}; + float Transform[12]{0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 1, 0}; + bool bDeferredFinish{}; + uint8_t pad2[7]{}; + UObjectBase *ReturnValue{}; + } addHair{}; + addHair.Class = smcCls; + CallUFunction(actor, "AddComponentByClass", &addHair); + if (auto *hairComp = addHair.ReturnValue) { + struct { + UObjectBase *Parent{}; + FName SocketName{}; + uint8_t LocationRule{2}; + uint8_t RotationRule{2}; + uint8_t ScaleRule{2}; + bool bWeldSimulatedBodies{}; + bool ReturnValue{}; + } attachHair{}; + attachHair.Parent = skinComp; + CallUFunction(hairComp, "K2_AttachToComponent", &attachHair); + + DressComponent(hairComp, hairMesh, HAIR_MATS_F, matCls); + struct { + UObjectBase *NewMasterBoneComponent; + bool bForceUpdate; + } hairPose{skinComp, true}; + CallUFunction(hairComp, "SetMasterPoseComponent", &hairPose); + } + else { + Log()->warn("AddComponentByClass returned null — no hair component"); + } + } + } + // Idle animation: raw sequence playback on the skin comp (bypasses the - // starved AnimBPs), outfit master-posed to it. + // starved AnimBPs), outfit master-posed to it. Try the gender-matched + // idle first; fall through to the other if it fails. auto *seqCls = reinterpret_cast(find_uobject("Class /Script/Engine.AnimSequence")); - UObjectBase *idle = nullptr; - for (const auto *path : IDLE_SEQ_PATHS) { - idle = LoadObjectByPath(seqCls, path); - if (idle) { - break; - } + UObjectBase *idle = LoadObjectByPath(seqCls, IDLE_SEQ_PATHS[female ? 1 : 0]); + if (!idle) { + idle = LoadObjectByPath(seqCls, IDLE_SEQ_PATHS[female ? 0 : 1]); } if (idle && skinComp) { struct { @@ -450,8 +617,11 @@ namespace { const float offsetDeg = (static_cast(i) - static_cast(count - 1) * 0.5f) * kSpacingDeg; const float rad = (rot.Yaw + offsetDeg) * kPi / 180.f; const FVector pos{loc.X + std::cos(rad) * kDistance, loc.Y + std::sin(rad) * kDistance, loc.Z}; - // +180 so the students face back toward the player. - if (auto *actor = SpawnStudent(pos, rot.Yaw + offsetDeg + 180.f)) { + // Alternate gender and cycle houses (spawn 8 to see all four houses + // in both genders); +180 so the students face back toward the player. + const bool female = (i % 2 == 1); + const int house = (i / 2) % 4; + if (auto *actor = SpawnStudent(pos, rot.Yaw + offsetDeg + 180.f, female, house)) { auto *obj = reinterpret_cast(actor); const auto index = static_cast(obj->GetUniqueID()); int32_t serial = 0; diff --git a/code/client/src/core/ue4_reflection.cpp b/code/client/src/core/ue4_reflection.cpp index 0352173..8893640 100644 --- a/code/client/src/core/ue4_reflection.cpp +++ b/code/client/src/core/ue4_reflection.cpp @@ -141,4 +141,73 @@ namespace HogwartsMP::Core::UE4 { *reinterpret_cast(reinterpret_cast(obj) + prop->GetOffset_ForInternal()) = value; return true; } + + int ReadByteProperty(void *obj, const char *name) { + auto *prop = FindPropertyInChain(static_cast(obj)->GetClass(), name); + if (!prop) { + return -1; + } + const auto type = narrow(prop->GetClass()->GetFName()); + if (type != "ByteProperty" && type != "EnumProperty") { + return -1; + } + return static_cast(*reinterpret_cast( + reinterpret_cast(obj) + prop->GetOffset_ForInternal())); + } + + std::string ReadNameProperty(void *obj, const char *name) { + auto *prop = FindPropertyInChain(static_cast(obj)->GetClass(), name); + if (!prop || narrow(prop->GetClass()->GetFName()) != "NameProperty") { + return {}; + } + auto *fname = reinterpret_cast( + reinterpret_cast(obj) + prop->GetOffset_ForInternal()); + return narrow(*fname); + } + + std::string ReadStringProperty(void *obj, const char *name) { + auto *prop = FindPropertyInChain(static_cast(obj)->GetClass(), name); + if (!prop || narrow(prop->GetClass()->GetFName()) != "StrProperty") { + return {}; + } + auto *fstr = reinterpret_cast( + reinterpret_cast(obj) + prop->GetOffset_ForInternal()); + return narrow(*fstr); + } + + UObjectBase *ReadObjectProperty(void *obj, const char *name) { + auto *prop = FindPropertyInChain(static_cast(obj)->GetClass(), name); + if (!prop) { + return nullptr; + } + // Only dereference if the property really stores a UObject* — otherwise + // we'd reinterpret arbitrary bytes (a struct/array/etc.) as a pointer. + // TObjectPtr (ObjectPtrProperty) is layout-compatible with T* here. + const auto type = narrow(prop->GetClass()->GetFName()); + if (type != "ObjectProperty" && type != "ObjectPtrProperty") { + return nullptr; + } + return *reinterpret_cast( + reinterpret_cast(obj) + prop->GetOffset_ForInternal()); + } + + std::string AssetPath(UObjectBase *obj) { + if (!obj) { + return "(null)"; + } + std::string name = narrow(obj->GetFName()); + for (auto *outer = obj->GetOuter(); outer; outer = outer->GetOuter()) { + name = narrow(outer->GetFName()) + "." + name; + } + return name; + } + + bool IsSubclassOf(UClass *cls, const char *baseName) { + for (UStruct *s = cls; s; s = s->GetSuperStruct()) { + if (narrow(s->GetFName()) == baseName) { + return true; + } + } + return false; + } } // namespace HogwartsMP::Core::UE4 diff --git a/code/client/src/core/ue4_reflection.h b/code/client/src/core/ue4_reflection.h index 79da4e6..ede3153 100644 --- a/code/client/src/core/ue4_reflection.h +++ b/code/client/src/core/ue4_reflection.h @@ -7,6 +7,8 @@ #include "UObject/Class.h" #include "UObject/UnrealType.h" +#include + namespace HogwartsMP::Core::UE4 { // Equivalent of UE4SS's GetFunctionByNameInChain: walk the class hierarchy // looking for a UFunction by short name. Avoids hardcoding declaring @@ -35,4 +37,24 @@ namespace HogwartsMP::Core::UE4 { bool SetBoolProperty(void *obj, const char *name, bool value); // Unchecked: assumes the named property really is a float. bool SetFloatProperty(void *obj, const char *name, float value); + + // Typed property reads (used by the AppearanceDump harvester). + // Read a byte/uint8 (or enum) property by name; returns -1 if not found. + int ReadByteProperty(void *obj, const char *name); + // Read an FName property by name; returns empty string if not found. + std::string ReadNameProperty(void *obj, const char *name); + // Read an FString property by name; returns empty string if not found. + std::string ReadStringProperty(void *obj, const char *name); + // Read a raw UObject* property (ObjectProperty / ObjectPtrProperty — + // TObjectPtr is layout-compatible with T* in UE4.27). Returns nullptr + // if the property is not found or the stored pointer is null. + UObjectBase *ReadObjectProperty(void *obj, const char *name); + + // Full UE4 asset path for an object: outer chain joined onto the package + // path, e.g. "/Game/RiggedObjects/.../SK_Foo.SK_Foo" — the form + // LoadObjectByPath expects. + std::string AssetPath(UObjectBase *obj); + + // True if cls or any superclass has the given short name. + bool IsSubclassOf(UClass *cls, const char *baseName); } // namespace HogwartsMP::Core::UE4