- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass / water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon - Client: title / join / character creator / play screens, prediction + reconciliation, interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection) - Shared deterministic zone generator, movement, protocol, roster, appearance - Paper-doll character art pipeline (PixelLab base walk + per-item animation edits) - Brandling roster from the company list (logo colors, jev types), Blender safari props - Headless test bot
140 lines
4.7 KiB
C++
140 lines
4.7 KiB
C++
#include "Appearance.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <cstdio>
|
|
#include <fstream>
|
|
|
|
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<std::string>{});
|
|
for (auto& [k, v] : j.at("slots").items()) {
|
|
int s = slotFromName(k);
|
|
if (s >= 0) slotZ[s] = v.get<int>();
|
|
}
|
|
for (auto& c : j.at("skinTones")) skinTones.push_back(parseHexColor(c.get<std::string>()));
|
|
for (auto& c : j.value("palette", nlohmann::json::array())) palette.push_back(parseHexColor(c.get<std::string>()));
|
|
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<std::string>());
|
|
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<std::string>{});
|
|
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
|