Rewrite on simdjson with a flat src and smack-style build

This commit is contained in:
shweet committed 2026-11-12 21:43:49 -05:00
1 parent 2d11cddc5e
commit 1a0db1abbd
178 files changed
+5935 -44757

No files matched your search

+456
View File
@@ -0,0 +1,456 @@
#include "character.h"
#include "json.h"
#include <imgui.h>
constexpr float FONT_SIZE = 20;
constexpr const char* EFFECT_NAMES[] = {"animation", "big", "color", "delay", "oscillation", "scene",
"screenshake", "shake", "silent", "small", "sound"};
constexpr const char* USE_NAMES[] = {"", "edible", "tool"};
struct SoundSource
{
const char* key;
const char* section;
DataFile file;
};
constexpr SoundSource SOUND_SOURCES[] = {
#define X(id, file, section, key) {key, section, file},
SOUND_IDS(X)
#undef X
};
static int utf8_length_get(const std::string& text)
{
int length = 0;
for (unsigned char byte : text)
length += (byte & 0xC0) != 0x80;
return length;
}
Entry* Dialogue::entry_get(const std::string& label)
{
auto found = entries.find(label);
return found == entries.end() ? nullptr : &found->second;
}
Entry* Dialogue::pool_get(const std::string& pool)
{
auto found = pools.find(pool);
if (found == pools.end() || found->second.empty())
return nullptr;
return entry_get(found->second[SDL_rand((Sint32)found->second.size())]);
}
Entry* Dialogue::hook_get(const std::string& hook)
{
auto found = hooks.find(hook);
if (found == hooks.end())
return nullptr;
Entry* entry = pool_get(found->second);
return entry ? entry : entry_get(found->second);
}
bool Character::preview_load(const std::string& path, Renderer& renderer)
{
Json json;
if (!json.load(path + "character.json"))
return false;
object root = json.root;
folder = path;
key = path.substr(0, path.size() - 1);
key = key.substr(key.find_last_of('/') + 1);
name = json_string(root, "name", key);
description = json_string(root, "description");
credits = json_string(root, "credits");
color = json_color(root, "color", color);
weight = json_float(root, "weight");
anm2Path = json_string(root, "anm2");
portrait = root["portrait"].error() ? nullptr : renderer.texture_get(folder + json_string(root, "portrait"));
render = root["render"].error() ? nullptr : renderer.texture_get(folder + json_string(root, "render"));
for (element entry : json_array(root, "stages"))
{
object fields;
if (!entry.get(fields))
stages.push_back({json_string(fields, "animationSuffix"), json_string(fields, "dialoguePool"),
json_float(fields, "weight"), json_int(fields, "area", -1)});
}
return true;
}
Anm2* Character::anm2_get(const std::string& path, Renderer& renderer, Audio& audio)
{
if (path.empty())
return nullptr;
auto found = anm2s.find(path);
if (found != anm2s.end())
return found->second.animations.empty() ? nullptr : &found->second;
Anm2& loaded = anm2s[path];
return loaded.load(folder + path, renderer, audio) ? &loaded : nullptr;
}
// Sets an interact area's fields from source, keeping what it doesn't name (its type's settings).
static void interact_parse(object source, InteractArea& area, Character& character, Audio& audio)
{
area.animation = json_string(source, "animation", area.animation);
area.cursorActive = json_string(source, "cursorActive", area.cursorActive);
area.cursorHover = json_string(source, "cursorHover", area.cursorHover);
area.dialoguePool = json_string(source, "dialoguePool", area.dialoguePool);
area.dialoguePoolFull = json_string(source, "dialoguePoolFull", area.dialoguePoolFull);
area.glyph = json_string(source, "glyph", area.glyph);
area.idleOnEnd = json_string(source, "idleOnEnd", area.idleOnEnd);
area.digestionClick = json_float(source, "digestionClick", area.digestionClick);
area.digestionHover = json_float(source, "digestionHover", area.digestionHover);
area.scaleAmplitude = json_float(source, "scaleAmplitude", area.scaleAmplitude);
area.scaleCycles = json_float(source, "scaleCycles", area.scaleCycles);
area.scaleTime = json_float(source, "scaleTime", area.scaleTime);
area.dialogueCooldown = json_int(source, "dialogueCooldown", area.dialogueCooldown);
area.isHold = json_is(source, "isHold", area.isHold);
if (!source["layer"].error())
area.layer = character.anm2 ? character.anm2->layer_get(json_string(source, "layer")) : -1;
if (!source["null"].error())
area.null = character.anm2 ? character.anm2->null_get(json_string(source, "null")) : -1;
if (!source["particles"].error())
area.particles = json_strings(source, "particles");
if (!source["sounds"].error())
area.sounds = audio.sounds_get(character.folder, json_strings(source, "sounds"));
}
static void character_parse(object root, Character& character, Renderer& renderer, Audio& audio)
{
character.anm2 = character.anm2_get(character.anm2Path, renderer, audio);
character.weightMax = json_float(root, "weightMax", character.weight);
character.capacity = json_float(root, "capacity");
character.capacityMin = json_float(root, "capacityMin");
character.capacityMax = json_float(root, "capacityMax", character.capacity);
character.overstuffedMultiplier = json_float(root, "overstuffedMultiplier", 1);
character.overstuffedDigestBonus = json_float(root, "overstuffedDigestBonus");
character.caloriesToKilogram = json_float(root, "caloriesToKilogram");
character.digestionRate = json_float(root, "digestionRate") / TICK_RATE;
character.digestionRateMin = json_float(root, "digestionRateMin") / TICK_RATE;
character.digestionRateMax = json_float(root, "digestionRateMax", character.digestionRate * TICK_RATE) / TICK_RATE;
character.digestionTime = json_int(root, "digestionTime");
character.blinkChance = json_float(root, "blinkChance");
character.gurgleChance = json_float(root, "gurgleChance");
character.gurgleCapacityMultiplier = json_float(root, "gurgleCapacityMultiplier");
character.dialoguePool = json_string(root, "dialoguePool");
object alternate = json_object(root, "alternate");
if (!alternate["texture"].error())
character.alternateTexture = renderer.texture_get(character.folder + json_string(alternate, "texture"));
if (!alternate["sound"].error())
character.alternateSound = audio.sound_get(character.folder + json_string(alternate, "sound"));
character.alternateChance = json_float(alternate, "chance");
character.alternateSheet = json_int(alternate, "spritesheet");
object animations = json_object(root, "animations");
character.animationStart = json_string(animations, "start");
character.animationIdle = json_string(animations, "idle");
character.animationIdleFull = json_string(animations, "idleFull");
character.animationStageUp = json_string(animations, "stageUp");
Anm2* anm2 = character.anm2;
auto layer_get = [&](object source, const char* key) {
return anm2 ? anm2->layer_get(json_string(source, key)) : -1;
};
auto null_get = [&](object source, const char* key) {
return anm2 ? anm2->null_get(json_string(source, key)) : -1;
};
character.talkSource = layer_get(json_object(root, "talk"), "source");
character.talkDestination = layer_get(json_object(root, "talk"), "destination");
character.blinkSource = layer_get(json_object(root, "blink"), "source");
character.blinkDestination = layer_get(json_object(root, "blink"), "destination");
for (element entry : json_array(root, "eatAreas"))
{
object fields;
if (!entry.get(fields))
character.eatAreas.push_back({json_string(fields, "animation"),
anm2 ? anm2->event_get(json_string(fields, "event")) : -1,
null_get(fields, "null")});
}
for (element entry : json_array(root, "expandAreas"))
{
object fields;
if (!entry.get(fields))
character.expandAreas.push_back(
{json_float(fields, "scaleMax"), layer_get(fields, "layer"), null_get(fields, "null")});
}
std::vector<std::string> typeNames;
for (auto [name, value] : json_object(root, "interactTypes"))
{
object fields;
if (value.get(fields))
continue;
InteractArea& type = character.interactTypes.emplace_back();
interact_parse(fields, type, character, audio);
type.name = name;
type.type = (int)typeNames.size();
typeNames.emplace_back(name);
}
for (element entry : json_array(root, "interactAreas"))
{
object fields;
if (entry.get(fields))
continue;
auto type = std::find(typeNames.begin(), typeNames.end(), json_string(fields, "type"));
InteractArea area = type == typeNames.end() ? InteractArea{} : character.interactTypes[type - typeNames.begin()];
interact_parse(fields, area, character, audio);
if (area.null >= 0)
character.interactAreas.push_back(area);
}
object areas = json_object(root, "areas");
Anm2* areaAnm2 = character.anm2_get(json_string(areas, "anm2"), renderer, audio);
std::string boundsNull = json_string(areas, "boundsNull", "Bounds");
std::string spawnNull = json_string(areas, "spawnNull", boundsNull);
for (element entry : json_array(areas, "list"))
{
object fields;
if (entry.get(fields))
continue;
Area& area = character.areas.emplace_back();
area.anm2 = fields["anm2"].error() ? areaAnm2 : character.anm2_get(json_string(fields, "anm2"), renderer, audio);
area.gravity = json_float(fields, "gravity", area.gravity);
area.friction = json_float(fields, "friction", area.friction);
area.airResistance = json_float(fields, "airResistance", area.airResistance);
if (!area.anm2)
continue;
Actor actor(area.anm2);
area.bounds = actor.null_rect_get(area.anm2->null_get(boundsNull), -1, 0);
area.spawn = actor.null_rect_get(area.anm2->null_get(spawnNull), -1, 0);
area.spawn = is_rect_valid(area.spawn) ? area.spawn : area.bounds;
}
object cursor = json_object(root, "cursor");
character.cursor = character.anm2_get(json_string(cursor, "anm2"), renderer, audio);
object cursorAnimations = json_object(cursor, "animations");
character.cursorIdle = json_string(cursorAnimations, "idle");
character.cursorClick = json_string(cursorAnimations, "click");
character.cursorHover = json_string(cursorAnimations, "hover");
character.cursorGrab = json_string(cursorAnimations, "grab");
character.cursorPan = json_string(cursorAnimations, "pan");
character.cursorZoom = json_string(cursorAnimations, "zoom");
character.cursorDispose = json_string(cursorAnimations, "dispose");
character.grabParticles = json_strings(json_object(cursor, "particles"), "grab");
object menu = json_object(root, "menu");
ImFontAtlas* fonts = ImGui::GetIO().Fonts;
if (!menu["font"].error())
character.font = fonts->AddFontFromFileTTF((character.folder + json_string(menu, "font")).c_str(), FONT_SIZE);
if (!menu["iconsFont"].error())
character.iconsFont =
fonts->AddFontFromFileTTF((character.folder + json_string(menu, "iconsFont")).c_str(), FONT_SIZE);
character.rounding = json_float(menu, "rounding");
character.homeGlyph = json_string(menu, "homeGlyph");
for (auto [name, value] : json_object(root, "particles"))
{
object fields;
if (!value.get(fields))
character.particles[std::string(name)] = {
json_string(fields, "animation"), character.anm2_get(json_string(fields, "anm2"), renderer, audio)};
}
object strings = json_object(root, "strings");
for (int i = 0; i < STRING_COUNT; i++)
character.strings[i] = json_string(strings, STRING_NAMES[i], STRING_DEFAULTS[i]);
}
static void items_parse(object root, Character& character, Renderer& renderer, Audio& audio)
{
Anm2* baseAnm2 = character.anm2_get(json_string(root, "anm2"), renderer, audio);
character.itemSheet = json_int(root, "spritesheet");
character.spawnDelay = json_int(root, "spawnDelay", character.spawnDelay);
character.spawnDelayMultiplier = json_float(root, "spawnDelayMultiplier", character.spawnDelayMultiplier);
character.spawnLimit = json_int(root, "spawnLimit", character.spawnLimit);
object particles = json_object(root, "particles");
character.disposeParticles = json_strings(particles, "dispose");
character.summonParticles = json_strings(particles, "summon");
object categories = json_object(root, "categories");
std::vector<std::string> rarityNames;
for (auto [name, value] : json_object(root, "rarities"))
{
object fields;
if (value.get(fields))
continue;
std::string sound = json_string(fields, "sound");
character.rarities.push_back({std::string(name), sound.empty() ? nullptr : audio.sound_get(character.folder + sound),
json_float(fields, "weight")});
rarityNames.emplace_back(name);
}
for (element entry : json_array(root, "items"))
{
object fields;
if (entry.get(fields))
continue;
Item& item = character.items.emplace_back();
item.name = json_string(fields, "name", "???");
item.description = json_string(fields, "description", "???");
item.category = json_string(fields, "category");
item.flavor = json_string(fields, "flavor");
std::string use = json_string(categories, item.category.c_str());
for (int i = 0; i < (int)std::size(USE_NAMES); i++)
if (use == USE_NAMES[i])
item.use = (ItemUse)i;
auto rarity = std::find(rarityNames.begin(), rarityNames.end(), json_string(fields, "rarity"));
item.rarity = rarity == rarityNames.end() ? -1 : (int)(rarity - rarityNames.begin());
item.anm2 = fields["anm2"].error() ? baseAnm2 : character.anm2_get(json_string(fields, "anm2"), renderer, audio);
if (!fields["texture"].error())
item.texture = renderer.texture_get(character.folder + json_string(fields, "texture"));
item.color = json_color(fields, "color", CLEAR);
item.calories = json_float(fields, "calories");
item.capacityBonus = json_float(fields, "capacityBonus");
item.digestionBonus = json_float(fields, "digestionBonus") / TICK_RATE;
item.gravity = json_float(fields, "gravity");
item.isToggleSpritesheet = json_is(fields, "isToggleSpritesheet");
}
}
static void dialogue_parse(object root, Character& character, Audio& audio)
{
Dialogue& dialogue = character.dialogue;
dialogue.delay = std::max(1, json_int(root, "delay", dialogue.delay));
dialogue.blipDelay = json_int(root, "blipDelay");
dialogue.blip = audio.sounds_get(character.folder, json_strings(root, "blip"));
for (auto [letter, value] : json_object(root, "pauses"))
{
object fields;
if (!value.get(fields))
dialogue.pauses[std::string(letter)] = {json_int(fields, "delay"), json_is(fields, "isSilent")};
}
for (auto [hook, value] : json_object(root, "hooks"))
{
std::string_view target;
if (!value.get(target))
dialogue.hooks[std::string(hook)] = target;
}
for (auto [pool, value] : json_object(root, "pools"))
{
array list;
if (value.get(list))
continue;
std::vector<std::string>& labels = dialogue.pools[std::string(pool)];
for (element label : list)
{
std::string_view text;
if (!label.get(text))
labels.emplace_back(text);
}
}
for (auto [label, value] : json_object(root, "entries"))
{
object fields;
if (value.get(fields))
continue;
Entry& entry = dialogue.entries[std::string(label)];
entry.text = json_string(fields, "text");
entry.animation = json_string(fields, "animation");
entry.next = json_string(fields, "next");
entry.length = utf8_length_get(entry.text);
for (element choiceEntry : json_array(fields, "choices"))
{
object choice;
if (!choiceEntry.get(choice))
entry.choices.push_back({json_string(choice, "next"), json_string(choice, "text", "<choice>")});
}
for (element effectEntry : json_array(fields, "effects"))
{
object source;
if (effectEntry.get(source))
continue;
Effect& effect = entry.effects.emplace_back();
std::string type = json_string(source, "type");
for (int i = 0; i < EFFECT_COUNT; i++)
if (type == EFFECT_NAMES[i])
effect.type = (EffectType)i;
effect.start = json_int(source, "start");
effect.end = json_int(source, "end");
effect.delay = json_int(source, "delay");
effect.color = json_color(source, "color");
effect.animation = json_string(source, "animation");
if (!source["sound"].error())
effect.sound = audio.sound_get(character.folder + json_string(source, "sound"));
entry.isScene |= effect.type == EFFECT_SCENE;
}
entry.isTerminal = entry.next.empty() && entry.choices.empty();
}
}
static void sounds_parse(object root, DataFile file, Character& character, Audio& audio)
{
for (int i = 0; i < SOUND_COUNT; i++)
{
const SoundSource& source = SOUND_SOURCES[i];
object section = *source.section ? json_object(root, source.section) : root;
if (source.file == file)
character.sounds[i] = audio.sounds_get(character.folder, json_strings(json_object(section, "sounds"), source.key));
}
}
void Character::load(Renderer& renderer, Audio& audio)
{
if (isLoaded)
return;
isLoaded = true;
Json json;
if (json.load(folder + "character.json"))
{
character_parse(json.root, *this, renderer, audio);
sounds_parse(json.root, FILE_CHARACTER, *this, audio);
}
if (json.load(folder + "items.json"))
{
items_parse(json.root, *this, renderer, audio);
sounds_parse(json.root, FILE_ITEMS, *this, audio);
}
if (json.load(folder + "dialogue.json"))
dialogue_parse(json.root, *this, audio);
}
// An actor for an item, showing its texture in place of its anm2's item spritesheet.
Actor Character::item_actor_get(int item)
{
Actor actor(items[item].anm2);
actor.isStatic = true;
if (items[item].texture && itemSheet >= 0 && itemSheet < (int)actor.sheets.size())
actor.sheets[itemSheet] = items[item].texture;
return actor;
}
// The area a stage plays in: the last one a stage up to it names, or the first.
int Character::area_get(int stage)
{
if (areas.empty())
return -1;
int area = 0;
for (int i = 0; i < stage && i < (int)stages.size(); i++)
if (stages[i].area >= 0 && stages[i].area < (int)areas.size())
area = stages[i].area;
return area;
}
std::string Character::save_path_get()
{
return pref_path_get("saves/" + key + ".txt");
}
// Every folder in resources/characters holding a character.json, in name order, with its preview loaded.
std::vector<Character> characters_load(Renderer& renderer)
{
std::string folder = path_get("characters/");
int count = 0;
char** names = SDL_GlobDirectory(folder.c_str(), "*", 0, &count);
std::vector<std::string> sorted(names, names + (names ? count : 0));
SDL_free(names);
std::sort(sorted.begin(), sorted.end());
std::vector<Character> characters;
characters.reserve(sorted.size());
for (const std::string& name : sorted)
if (!characters.emplace_back().preview_load(folder + name + "/", renderer))
characters.pop_back();
return characters;
}