Forgotten Elixir

An enchanting engine where magic and technology intertwine

Changing yaml parsing library

created: 2025-03-09 17:08:31 UTC
last updated: 2025-03-09 17:09:53 UTC

Initially I had planned to use libcyaml since that is what I was using in my initial prototype written in C, prior to the current conversion to C++. Migrating the code over, I ended up running into issues with C++ playing nicely with it. Instead of fixing those issues, since I am now using C++, I decided to take a look at the other options. Right now I am currently playing around with using a library called yaml-cpp. So far, it is pretty simple to get going with.

#include "Loader.h"
#include <yaml-cpp/yaml.h>

#include <iostream>

Loader::Loader(std::string path) : path(path) {}

Loader::~Loader() {}

void Loader::Load() {
  YAML::Node config = YAML::LoadFile(path);

  float playerX = config["player"]["x"].as<float>();
  float playerY = config["player"]["y"].as<float>();
  std::cout << "Player X from config: " << playerX << std::endl;
  std::cout << "Player Y from config: " << playerY << std::endl;

  if (config["wall_textures"]) {
    if (config["wall_textures"].IsSequence()) {
      std::vector<std::string> wallTextures =
          config["wall_textures"].as<std::vector<std::string>>();
      for (auto texture : wallTextures) {
        std::cout << "Wall Texture: " << texture << std::endl;
      }
    }
  }

  if (config["sprite_textures"]) {
    if (config["sprite_textures"].IsSequence()) {
      std::vector<std::string> spriteTextures =
          config["sprite_textures"].as<std::vector<std::string>>();
      for (auto texture : spriteTextures) {
        std::cout << "Sprite Texture: " << texture << std::endl;
      }
    }
  }

  int mapRows = config["map"]["rows"].as<int>();
  int mapColumns = config["map"]["columns"].as<int>();
  std::string mapPath = config["map"]["path"].as<std::string>();
  std::cout << "Map Rows: " << mapRows << std::endl;
  std::cout << "Map Columns: " << mapColumns << std::endl;
  std::cout << "Map File: " << mapPath << std::endl;
}

That is what the loader currently looks like. Not a whole lot and will definitely be built up over time, but working with the library is a bit simpler than libcyaml. Libcyaml requires setting up a lot up front. However, that set up is beneficial since it does give fine grain control over deserializing the data in a yaml file into structs. But with yaml-cpp, you can just index into it like a map and define the type it should deserialize into. 

Below is a code snippet from the C version.

#include "level.h"
#include <stdio.h>

level_t *level = NULL;

static const cyaml_config_t config = {
    .log_fn = cyaml_log,
    .mem_fn = cyaml_mem,
    .log_level = CYAML_LOG_WARNING,
};

static const cyaml_schema_value_t string_ptr_schema = {
    CYAML_VALUE_STRING(CYAML_FLAG_POINTER, char, 0, CYAML_UNLIMITED),
};

static const cyaml_schema_field_t sprite_field_schema[] = {
    CYAML_FIELD_FLOAT("x", CYAML_FLAG_DEFAULT, sprite_t, x),
    CYAML_FIELD_FLOAT("y", CYAML_FLAG_DEFAULT, sprite_t, y),
    CYAML_FIELD_INT("texture", CYAML_FLAG_DEFAULT, sprite_t, texture),
    CYAML_FIELD_END,
};

static const cyaml_schema_value_t sprite_schema = {
    CYAML_VALUE_MAPPING(CYAML_FLAG_DEFAULT, sprite_t, sprite_field_schema),
};

static const cyaml_schema_field_t player_field_schema[] = {
    CYAML_FIELD_FLOAT("x", CYAML_FLAG_DEFAULT, player_t, x),
    CYAML_FIELD_FLOAT("y", CYAML_FLAG_DEFAULT, player_t, y),
    CYAML_FIELD_END,
};

static const cyaml_schema_field_t map_field_schema[] = {
    CYAML_FIELD_INT("rows", CYAML_FLAG_DEFAULT, map_t, rows),
    CYAML_FIELD_INT("columns", CYAML_FLAG_DEFAULT, map_t, cols),
    CYAML_FIELD_STRING_PTR("file", CYAML_FLAG_POINTER, map_t, map_file, 0, CYAML_UNLIMITED),
  CYAML_FIELD_END,
};

static const cyaml_schema_field_t level_fields_schema[] = {
    CYAML_FIELD_MAPPING_PTR("player", CYAML_FLAG_POINTER, level_t, p,
                            player_field_schema),
    CYAML_FIELD_MAPPING_PTR("map", CYAML_FLAG_POINTER, level_t, map, map_field_schema),
    CYAML_FIELD_SEQUENCE_COUNT("textures", CYAML_FLAG_POINTER, level_t,
                               textures, n_textures, &string_ptr_schema, 0,
                               CYAML_UNLIMITED),
    CYAML_FIELD_SEQUENCE_COUNT("sprites", CYAML_FLAG_POINTER | CYAML_FLAG_OPTIONAL, level_t, sprites,
                               n_sprites, &sprite_schema, 0, CYAML_UNLIMITED),
    CYAML_FIELD_END,
};

static const cyaml_schema_value_t level_schema = {
    CYAML_VALUE_MAPPING(CYAML_FLAG_POINTER, level_t, level_fields_schema),
};

void level_load(char *path) {
  cyaml_err_t err;

  if (level != NULL) {
    level_free();
  }
  
  err = cyaml_load_file(path, &config, &level_schema, (void **)&level, NULL);
  if (err != CYAML_OK) {
    fprintf(stderr, "Error: %s\n", cyaml_strerror(err));
    return;
  }
}

void level_free() {
  cyaml_free(&config, &level_schema, level, 0);
}

I am doing some changes to the structure of the yaml files. Right now, primarily splitting wall textures from sprite textures.

player:
  x: 110
  y: 110
map:
  rows: 13
  columns: 20
  path: ./assets/map/test
wall_textures:
  - "./assets/textures/redbrick.png"
  - "./assets/textures/graystone.png"
sprite_textures:
  - "./assets/textures/armor.png"
  - "./assets/textures/barrel.png"