#include "Appearance.h" #include #include #include namespace bs { namespace { const char* kSlotNames[kSlotCount] = {"shoes", "bottom", "top", "hair", "hat"}; } const char* slotName(int slot) { return slot >= 0 && slot < kSlotCount ? kSlotNames[slot] : "?"; } int slotFromName(const std::string& s) { for (int i = 0; i < kSlotCount; ++i) if (s == kSlotNames[i]) return i; return -1; } uint32_t parseHexColor(const std::string& s, uint32_t fallback) { std::string h = s; if (!h.empty() && h[0] == '#') h.erase(0, 1); if (h.size() != 6) return fallback; char* end = nullptr; unsigned long v = std::strtoul(h.c_str(), &end, 16); return end && *end == '\0' ? uint32_t(v) : fallback; } std::string hexColor(uint32_t rgb) { char buf[8]; std::snprintf(buf, sizeof buf, "#%06x", rgb & 0xffffffu); return buf; } bool CharacterManifest::load(const std::string& path, std::string* error) { std::ifstream f(path); if (!f) { if (error) *error = "cannot open " + path; return false; } try { nlohmann::json j = nlohmann::json::parse(f); cell = j.value("cell", 64); framesPerDirection = j.value("framesPerDirection", 5); walkFrames = j.value("walkFrames", 4); shades = j.value("shades", 4); directions = j.value("directions", std::vector{}); for (auto& [k, v] : j.at("slots").items()) { int s = slotFromName(k); if (s >= 0) slotZ[s] = v.get(); } for (auto& c : j.at("skinTones")) skinTones.push_back(parseHexColor(c.get())); for (auto& c : j.value("palette", nlohmann::json::array())) palette.push_back(parseHexColor(c.get())); for (auto& [id, v] : j.at("items").items()) { ItemDef d; d.id = id; d.label = v.value("label", id); d.slot = slotFromName(v.at("slot").get()); d.color = parseHexColor(v.value("color", "#ffffff")); d.fromBody = v.value("fromBody", false); if (d.slot >= 0) items[id] = d; } for (auto& [id, v] : j.at("bodies").items()) { BodyDef b; b.id = id; b.label = v.value("label", id); b.anchorRow = v.value("anchorRow", cell - 1); b.items = v.value("items", std::vector{}); bodies[id] = b; } if (j.contains("npcs")) for (auto& [id, v] : j["npcs"].items()) npcs[id] = {v.value("sheet", ""), v.value("portrait", ""), v.value("anchorRow", cell - 1)}; } catch (const std::exception& e) { if (error) *error = path + ": " + e.what(); return false; } if (bodies.empty() || skinTones.empty()) { if (error) *error = path + ": no bodies or skin tones"; return false; } return true; } const ItemDef* CharacterManifest::item(const std::string& id) const { auto it = items.find(id); return it == items.end() ? nullptr : &it->second; } bool CharacterManifest::bodyHas(const std::string& body, const std::string& id) const { auto it = bodies.find(body); if (it == bodies.end()) return false; for (const std::string& s : it->second.items) if (s == id) return true; return false; } Appearance CharacterManifest::defaultLook(const std::string& body) const { Appearance a; a.body = bodies.count(body) ? body : bodies.begin()->first; a.skin = 1; auto pick = [&](int slot, const char* preferred) { if (bodyHas(a.body, preferred)) a.items[slot] = preferred; else for (const std::string& id : bodies.at(a.body).items) if (item(id)->slot == slot) { a.items[slot] = id; break; } }; pick(Top, "top_tee"); pick(Bottom, "bottom_shorts"); pick(Hair, "hair_short"); pick(Shoes, "shoes_sneakers"); for (int s = 0; s < kSlotCount; ++s) a.colors[s] = a.items[s].empty() ? 0xffffff : item(a.items[s])->color; return a; } bool CharacterManifest::sanitize(Appearance& a) const { bool ok = true; if (!bodies.count(a.body)) { a = defaultLook(); return false; } if (a.skin >= skinTones.size()) a.skin = 0, ok = false; for (int s = 0; s < kSlotCount; ++s) { const std::string& id = a.items[s]; if (id.empty()) continue; const ItemDef* d = item(id); if (!d || d->slot != s || !bodyHas(a.body, id)) a.items[s].clear(), ok = false; a.colors[s] &= 0xffffffu; } Appearance def = defaultLook(a.body); for (int s : {int(Top), int(Bottom)}) // always dressed if (a.items[s].empty()) a.items[s] = def.items[s], a.colors[s] = def.colors[s], ok = false; return ok; } } // namespace bs