Add complexionFormId to appearance payload
Introduce complexionFormId (TXST) into RemoteAppearance and the appearance protocol so complexion textures are captured and serialized. Updates: header/state, Networking JSON parse/serialize, proxy actor comparisons, main capture logic, protocol docs, fake client/player samples, changelog and dev-log. Capture reads PlayerCharacter::complexion and serializes it, but applying to proxies is intentionally a no-op because TESNPC proxies lack a complexion field (documented in comments/dev-log). Files: plugin/include/F4TRemotePlayerState.h, plugin/src/*, protocol/packets.md, server/fake_client.py, server/fake_player.py, changelog.md, docs/dev-log.md.
This commit is contained in:
@@ -487,9 +487,10 @@ namespace
|
||||
appearance.version = static_cast<std::uint32_t>(*version);
|
||||
}
|
||||
}
|
||||
appearance.raceFormId = ReadOptionalHexFormId(*appearanceObject, "raceFormId");
|
||||
appearance.hairColorFormId = ReadOptionalHexFormId(*appearanceObject, "hairColorFormId");
|
||||
appearance.facialHairColorFormId = ReadOptionalHexFormId(*appearanceObject, "facialHairColorFormId");
|
||||
appearance.raceFormId = ReadOptionalHexFormId(*appearanceObject, "raceFormId");
|
||||
appearance.hairColorFormId = ReadOptionalHexFormId(*appearanceObject, "hairColorFormId");
|
||||
appearance.facialHairColorFormId = ReadOptionalHexFormId(*appearanceObject, "facialHairColorFormId");
|
||||
appearance.complexionFormId = ReadOptionalHexFormId(*appearanceObject, "complexionFormId");
|
||||
|
||||
if (const auto height = ReadDouble(*appearanceObject, "height")) {
|
||||
if (std::isfinite(*height) && *height > 0.0 && *height < 10.0) {
|
||||
@@ -1247,7 +1248,8 @@ namespace F4T::Networking
|
||||
{ "a", a_appearance->bodyTintColor.a }
|
||||
} },
|
||||
{ "hairColorFormId", a_appearance->hairColorFormId == 0 ? std::string{} : std::format("{:08X}", a_appearance->hairColorFormId) },
|
||||
{ "facialHairColorFormId", a_appearance->facialHairColorFormId == 0 ? std::string{} : std::format("{:08X}", a_appearance->facialHairColorFormId) }
|
||||
{ "facialHairColorFormId", a_appearance->facialHairColorFormId == 0 ? std::string{} : std::format("{:08X}", a_appearance->facialHairColorFormId) },
|
||||
{ "complexionFormId", a_appearance->complexionFormId == 0 ? std::string{} : std::format("{:08X}", a_appearance->complexionFormId) }
|
||||
};
|
||||
|
||||
auto headParts = nlohmann::json::array();
|
||||
|
||||
@@ -2050,9 +2050,10 @@ namespace
|
||||
a_left.bodyTintColor.g != a_right.bodyTintColor.g ||
|
||||
a_left.bodyTintColor.b != a_right.bodyTintColor.b ||
|
||||
a_left.bodyTintColor.a != a_right.bodyTintColor.a ||
|
||||
a_left.hairColorFormId != a_right.hairColorFormId ||
|
||||
a_left.facialHairColorFormId != a_right.facialHairColorFormId ||
|
||||
a_left.headPartFormIds != a_right.headPartFormIds ||
|
||||
a_left.hairColorFormId != a_right.hairColorFormId ||
|
||||
a_left.facialHairColorFormId != a_right.facialHairColorFormId ||
|
||||
a_left.complexionFormId != a_right.complexionFormId ||
|
||||
a_left.headPartFormIds != a_right.headPartFormIds ||
|
||||
a_left.morphRegionSliders != a_right.morphRegionSliders ||
|
||||
a_left.tints != a_right.tints ||
|
||||
a_left.morphSliders.size() != a_right.morphSliders.size() ||
|
||||
@@ -2460,6 +2461,10 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
// Note: complexion (hands/arms texture) cannot be applied to proxy NPCs because
|
||||
// TESNPC does not have a complexion field. This is a known limitation; the
|
||||
// complexionFormId is captured and serialized but not applied to proxies.
|
||||
|
||||
std::size_t appliedHeadPartCount = 0;
|
||||
if (!appearance.headPartFormIds.empty()) {
|
||||
// Resolve the sender's head parts to local forms first; skip any that the
|
||||
|
||||
+18
-10
@@ -251,14 +251,21 @@ namespace
|
||||
appearance.bodyTintColor.b = static_cast<std::uint8_t>(npc->bodyTintColorB);
|
||||
appearance.bodyTintColor.a = static_cast<std::uint8_t>(npc->bodyTintColorA);
|
||||
|
||||
if (npc->headRelatedData) {
|
||||
if (npc->headRelatedData->hairColor) {
|
||||
appearance.hairColorFormId = npc->headRelatedData->hairColor->GetFormID();
|
||||
}
|
||||
if (npc->headRelatedData->facialHairColor) {
|
||||
appearance.facialHairColorFormId = npc->headRelatedData->facialHairColor->GetFormID();
|
||||
}
|
||||
if (npc->headRelatedData) {
|
||||
if (npc->headRelatedData->hairColor) {
|
||||
appearance.hairColorFormId = npc->headRelatedData->hairColor->GetFormID();
|
||||
}
|
||||
if (npc->headRelatedData->facialHairColor) {
|
||||
appearance.facialHairColorFormId = npc->headRelatedData->facialHairColor->GetFormID();
|
||||
}
|
||||
}
|
||||
|
||||
// Complexion (hands/arms texture) is only on PlayerCharacter, not TESNPC.
|
||||
// Capture from the player directly; apply is skipped because proxy NPCs don't
|
||||
// have a complexion field. This is a known limitation.
|
||||
if (a_player.complexion) {
|
||||
appearance.complexionFormId = a_player.complexion->GetFormID();
|
||||
}
|
||||
|
||||
for (const auto* headPart : npc->GetHeadParts()) {
|
||||
if (headPart) {
|
||||
@@ -354,9 +361,10 @@ namespace
|
||||
a_left.bodyTintColor.g != a_right.bodyTintColor.g ||
|
||||
a_left.bodyTintColor.b != a_right.bodyTintColor.b ||
|
||||
a_left.bodyTintColor.a != a_right.bodyTintColor.a ||
|
||||
a_left.hairColorFormId != a_right.hairColorFormId ||
|
||||
a_left.facialHairColorFormId != a_right.facialHairColorFormId ||
|
||||
a_left.headPartFormIds != a_right.headPartFormIds ||
|
||||
a_left.hairColorFormId != a_right.hairColorFormId ||
|
||||
a_left.facialHairColorFormId != a_right.facialHairColorFormId ||
|
||||
a_left.complexionFormId != a_right.complexionFormId ||
|
||||
a_left.headPartFormIds != a_right.headPartFormIds ||
|
||||
a_left.morphRegionSliders != a_right.morphRegionSliders ||
|
||||
a_left.tints != a_right.tints ||
|
||||
a_left.morphSliders.size() != a_right.morphSliders.size() ||
|
||||
|
||||
Reference in New Issue
Block a user