LDtk import support

This commit is contained in:
John McCardle 2026-02-07 11:30:32 -05:00
commit de7778b147
24 changed files with 26203 additions and 0 deletions

View file

@ -40,6 +40,8 @@
#include "tiled/PyTileSetFile.h" // Tiled tileset loading
#include "tiled/PyTileMapFile.h" // Tiled tilemap loading
#include "tiled/PyWangSet.h" // Wang auto-tile sets
#include "ldtk/PyLdtkProject.h" // LDtk project loading
#include "ldtk/PyAutoRuleSet.h" // LDtk auto-rule sets
#include "McRogueFaceVersion.h"
#include "GameEngine.h"
// ImGui is only available for SFML builds
@ -494,6 +496,10 @@ PyObject* PyInit_mcrfpy()
&mcrfpydef::PyTileMapFileType,
&mcrfpydef::PyWangSetType,
/*LDtk project loading*/
&mcrfpydef::PyLdtkProjectType,
&mcrfpydef::PyAutoRuleSetType,
nullptr};
// Types that are used internally but NOT exported to module namespace (#189)
@ -575,6 +581,12 @@ PyObject* PyInit_mcrfpy()
mcrfpydef::PyWangSetType.tp_methods = PyWangSet::methods;
mcrfpydef::PyWangSetType.tp_getset = PyWangSet::getsetters;
// LDtk types
mcrfpydef::PyLdtkProjectType.tp_methods = PyLdtkProject::methods;
mcrfpydef::PyLdtkProjectType.tp_getset = PyLdtkProject::getsetters;
mcrfpydef::PyAutoRuleSetType.tp_methods = PyAutoRuleSet::methods;
mcrfpydef::PyAutoRuleSetType.tp_getset = PyAutoRuleSet::getsetters;
// Set up weakref support for all types that need it
PyTimerType.tp_weaklistoffset = offsetof(PyTimerObject, weakreflist);
PyUIFrameType.tp_weaklistoffset = offsetof(PyUIFrameObject, weakreflist);

View file

@ -48,6 +48,30 @@ std::shared_ptr<PyTexture> PyTexture::from_rendered(sf::RenderTexture& render_te
return ptex;
}
// Factory method to create texture from an sf::Image (for LDtk flip-baked atlases)
std::shared_ptr<PyTexture> PyTexture::from_image(
const sf::Image& img, int sprite_w, int sprite_h,
const std::string& name)
{
struct MakeSharedEnabler : public PyTexture {
MakeSharedEnabler() : PyTexture() {}
};
auto ptex = std::make_shared<MakeSharedEnabler>();
ptex->texture.loadFromImage(img);
ptex->texture.setSmooth(false);
ptex->source = name;
ptex->sprite_width = sprite_w;
ptex->sprite_height = sprite_h;
auto size = ptex->texture.getSize();
ptex->sheet_width = (sprite_w > 0) ? (size.x / sprite_w) : 0;
ptex->sheet_height = (sprite_h > 0) ? (size.y / sprite_h) : 0;
return ptex;
}
sf::Sprite PyTexture::sprite(int index, sf::Vector2f pos, sf::Vector2f s)
{
// Protect against division by zero if texture failed to load

View file

@ -25,6 +25,11 @@ public:
// #144: Factory method to create texture from rendered content (snapshot)
static std::shared_ptr<PyTexture> from_rendered(sf::RenderTexture& render_tex);
// Factory method to create texture from an sf::Image (for LDtk flip-baked atlases)
static std::shared_ptr<PyTexture> from_image(
const sf::Image& img, int sprite_w, int sprite_h,
const std::string& name = "<generated>");
sf::Sprite sprite(int index, sf::Vector2f pos = sf::Vector2f(0, 0), sf::Vector2f s = sf::Vector2f(1.0, 1.0));
int getSpriteCount() const { return sheet_width * sheet_height; }

View file

@ -0,0 +1,187 @@
#include "AutoRuleResolve.h"
#include <functional>
namespace mcrf {
namespace ldtk {
// ============================================================
// Deterministic hash for pseudo-random decisions
// ============================================================
static uint32_t hashCell(uint32_t seed, int x, int y, int rule_uid) {
// Simple but deterministic hash combining seed, position, and rule
uint32_t h = seed;
h ^= static_cast<uint32_t>(x) * 374761393u;
h ^= static_cast<uint32_t>(y) * 668265263u;
h ^= static_cast<uint32_t>(rule_uid) * 2654435761u;
h = (h ^ (h >> 13)) * 1274126177u;
h = h ^ (h >> 16);
return h;
}
// ============================================================
// IntGrid access with out-of-bounds handling
// ============================================================
static inline int getIntGrid(const int* data, int w, int h, int x, int y, int oob_value) {
if (x < 0 || x >= w || y < 0 || y >= h) {
return (oob_value == -1) ? 0 : oob_value;
}
return data[y * w + x];
}
// ============================================================
// Pattern matching
// ============================================================
static bool matchPattern(const int* intgrid, int w, int h,
int cx, int cy,
const std::vector<int>& pattern, int size,
int oob_value)
{
int half = size / 2;
for (int py = 0; py < size; py++) {
for (int px = 0; px < size; px++) {
int pattern_val = pattern[py * size + px];
if (pattern_val == 0) continue; // Wildcard
int gx = cx + (px - half);
int gy = cy + (py - half);
int cell_val = getIntGrid(intgrid, w, h, gx, gy, oob_value);
if (pattern_val > 0) {
if (pattern_val >= 1000000) {
// LDtk IntGrid group reference: 1000001 = "any non-empty value"
// (group-based matching; for now treat as "any non-zero")
if (cell_val == 0) return false;
} else {
// Must match this exact value
if (cell_val != pattern_val) return false;
}
} else if (pattern_val < 0) {
if (pattern_val <= -1000000) {
// Negated group reference: -1000001 = "NOT any non-empty" = "must be empty"
if (cell_val != 0) return false;
} else {
// Must NOT be this value (negative = negation)
if (cell_val == -pattern_val) return false;
}
}
}
}
return true;
}
// ============================================================
// Flip pattern generation
// ============================================================
static std::vector<int> flipPatternX(const std::vector<int>& pattern, int size) {
std::vector<int> result(pattern.size());
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
result[y * size + (size - 1 - x)] = pattern[y * size + x];
}
}
return result;
}
static std::vector<int> flipPatternY(const std::vector<int>& pattern, int size) {
std::vector<int> result(pattern.size());
for (int y = 0; y < size; y++) {
for (int x = 0; x < size; x++) {
result[(size - 1 - y) * size + x] = pattern[y * size + x];
}
}
return result;
}
// ============================================================
// Resolution engine
// ============================================================
std::vector<AutoTileResult> resolveAutoRules(
const int* intgrid_data, int width, int height,
const AutoRuleSet& ruleset, uint32_t seed)
{
int total = width * height;
std::vector<AutoTileResult> result(total, {-1, 0});
for (const auto& group : ruleset.groups) {
if (!group.active) continue;
// Per-group break mask: once a cell is matched by a breakOnMatch rule,
// skip it for subsequent rules in this group
std::vector<bool> break_mask(total, false);
for (const auto& rule : group.rules) {
if (!rule.active) continue;
if (rule.tile_ids.empty()) continue;
if (rule.pattern.empty()) continue;
// Build flip variants: (pattern, flip_bits)
struct Variant {
std::vector<int> pattern;
int flip_bits;
};
std::vector<Variant> variants;
variants.push_back({rule.pattern, 0});
if (rule.flipX) {
std::vector<int> fx = flipPatternX(rule.pattern, rule.size);
variants.push_back({fx, 1});
}
if (rule.flipY) {
std::vector<int> fy = flipPatternY(rule.pattern, rule.size);
variants.push_back({fy, 2});
}
if (rule.flipX && rule.flipY) {
std::vector<int> fxy = flipPatternY(
flipPatternX(rule.pattern, rule.size), rule.size);
variants.push_back({fxy, 3});
}
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int idx = y * width + x;
if (break_mask[idx]) continue;
// Probability check (deterministic)
if (rule.chance < 1.0f) {
uint32_t h = hashCell(seed, x, y, rule.uid);
float roll = static_cast<float>(h & 0xFFFF) / 65535.0f;
if (roll >= rule.chance) continue;
}
// Try each variant (first match wins)
for (const auto& variant : variants) {
if (matchPattern(intgrid_data, width, height,
x, y, variant.pattern, rule.size,
rule.outOfBoundsValue))
{
// Pick tile (deterministic from seed)
int tile_idx = 0;
if (rule.tile_ids.size() > 1) {
uint32_t h = hashCell(seed, x, y, rule.uid + 1);
tile_idx = h % rule.tile_ids.size();
}
result[idx].tile_id = rule.tile_ids[tile_idx];
result[idx].flip = variant.flip_bits;
if (rule.breakOnMatch) {
break_mask[idx] = true;
}
break; // First matching variant wins
}
}
}
}
}
}
return result;
}
} // namespace ldtk
} // namespace mcrf

View file

@ -0,0 +1,17 @@
#pragma once
#include "LdtkTypes.h"
#include <vector>
#include <cstdint>
namespace mcrf {
namespace ldtk {
// Resolve auto-rules against IntGrid data.
// Returns a flat array of AutoTileResult (one per cell).
// tile_id = -1 means no rule matched that cell.
std::vector<AutoTileResult> resolveAutoRules(
const int* intgrid_data, int width, int height,
const AutoRuleSet& ruleset, uint32_t seed = 0);
} // namespace ldtk
} // namespace mcrf

455
src/ldtk/LdtkParse.cpp Normal file
View file

@ -0,0 +1,455 @@
#include "LdtkParse.h"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <filesystem>
namespace mcrf {
namespace ldtk {
// ============================================================
// Utility helpers (same pattern as TiledParse.cpp)
// ============================================================
// Null-safe string extraction from JSON.
// nlohmann::json::value() throws type_error if the field exists but is null.
static std::string jsonStr(const nlohmann::json& j, const char* key, const std::string& def = "") {
auto it = j.find(key);
if (it == j.end() || it->is_null()) return def;
return it->get<std::string>();
}
static int jsonInt(const nlohmann::json& j, const char* key, int def = 0) {
auto it = j.find(key);
if (it == j.end() || it->is_null()) return def;
return it->get<int>();
}
static float jsonFloat(const nlohmann::json& j, const char* key, float def = 0.0f) {
auto it = j.find(key);
if (it == j.end() || it->is_null()) return def;
return it->get<float>();
}
static bool jsonBool(const nlohmann::json& j, const char* key, bool def = false) {
auto it = j.find(key);
if (it == j.end() || it->is_null()) return def;
return it->get<bool>();
}
static std::string readFile(const std::string& path) {
std::ifstream f(path);
if (!f.is_open()) {
throw std::runtime_error("Cannot open file: " + path);
}
std::stringstream ss;
ss << f.rdbuf();
return ss.str();
}
static std::string parentDir(const std::string& path) {
std::filesystem::path p(path);
return p.parent_path().string();
}
static std::string resolvePath(const std::string& base_dir, const std::string& relative) {
std::filesystem::path p = std::filesystem::path(base_dir) / relative;
return p.lexically_normal().string();
}
// ============================================================
// Parse tileset definitions -> TileSetData
// ============================================================
static std::shared_ptr<tiled::TileSetData> parseTilesetDef(
const nlohmann::json& def, const std::string& base_dir)
{
auto ts = std::make_shared<tiled::TileSetData>();
ts->name = jsonStr(def, "identifier");
// Resolve image path (relPath can be null for embedded atlases)
std::string rel_path = jsonStr(def, "relPath");
if (!rel_path.empty()) {
ts->image_source = resolvePath(base_dir, rel_path);
}
ts->source_path = ts->image_source;
int grid_size = jsonInt(def, "tileGridSize");
ts->tile_width = grid_size;
ts->tile_height = grid_size;
ts->columns = jsonInt(def, "__cWid");
int rows = jsonInt(def, "__cHei");
ts->tile_count = ts->columns * rows;
ts->margin = jsonInt(def, "padding");
ts->spacing = jsonInt(def, "spacing");
ts->image_width = jsonInt(def, "pxWid");
ts->image_height = jsonInt(def, "pxHei");
// Per-tile custom data
if (def.contains("customData") && def["customData"].is_array()) {
for (const auto& cd : def["customData"]) {
int tile_id = jsonInt(cd, "tileId", -1);
std::string data = jsonStr(cd, "data");
if (tile_id >= 0 && !data.empty()) {
tiled::TileInfo ti;
ti.id = tile_id;
ti.properties["customData"] = tiled::PropertyValue(data);
ts->tile_info[tile_id] = std::move(ti);
}
}
}
// Enum tags as properties
if (def.contains("enumTags") && def["enumTags"].is_array()) {
for (const auto& et : def["enumTags"]) {
std::string enum_id = jsonStr(et, "enumValueId");
if (et.contains("tileIds") && et["tileIds"].is_array()) {
for (const auto& tid_json : et["tileIds"]) {
int tile_id = tid_json.get<int>();
auto& ti = ts->tile_info[tile_id];
ti.id = tile_id;
ti.properties["enum_tag"] = tiled::PropertyValue(enum_id);
}
}
}
}
return ts;
}
// ============================================================
// Convert LDtk tileRectsIds to flat tile index
// LDtk stores tile references as [x, y] pixel rectangles
// ============================================================
static int rectToTileId(const nlohmann::json& rect, int tile_size, int columns) {
if (!rect.is_array() || rect.size() < 2) return -1;
int px_x = rect[0].get<int>();
int px_y = rect[1].get<int>();
if (tile_size <= 0 || columns <= 0) return -1;
int col = px_x / tile_size;
int row = px_y / tile_size;
return row * columns + col;
}
// ============================================================
// Parse auto-rule definitions from layer definitions
// ============================================================
static AutoRule parseAutoRule(const nlohmann::json& rule_json,
int tileset_grid_size, int tileset_columns)
{
AutoRule rule;
rule.uid = jsonInt(rule_json, "uid");
rule.size = jsonInt(rule_json, "size", 3);
rule.active = jsonBool(rule_json, "active", true);
rule.chance = jsonFloat(rule_json, "chance", 1.0f);
rule.breakOnMatch = jsonBool(rule_json, "breakOnMatch", true);
rule.outOfBoundsValue = jsonInt(rule_json, "outOfBoundsValue", -1);
rule.flipX = jsonBool(rule_json, "flipX");
rule.flipY = jsonBool(rule_json, "flipY");
// Pivot offset
rule.pivotX = jsonFloat(rule_json, "pivotX") >= 0.5f ? 1 : 0;
rule.pivotY = jsonFloat(rule_json, "pivotY") >= 0.5f ? 1 : 0;
// Pattern: flat array of size*size integers
if (rule_json.contains("pattern") && rule_json["pattern"].is_array()) {
for (const auto& val : rule_json["pattern"]) {
rule.pattern.push_back(val.get<int>());
}
}
// Tile IDs from tileRectsIds
// Format varies by LDtk version:
// Newer: [[tile_id], [tile_id], ...] - each alternative is [flat_tile_id]
// Older: [[[px_x, px_y]], [[px_x, px_y]], ...] - each alternative is [[x,y] rect]
if (rule_json.contains("tileRectsIds") && rule_json["tileRectsIds"].is_array()) {
for (const auto& alt : rule_json["tileRectsIds"]) {
if (!alt.is_array() || alt.empty()) continue;
// Check the first element to determine format
const auto& first = alt[0];
if (first.is_number_integer()) {
// Flat tile ID format: [tile_id] or [tile_id, ...]
// Take first element as the tile ID for this alternative
rule.tile_ids.push_back(first.get<int>());
} else if (first.is_array()) {
// Pixel rect format: [[px_x, px_y], ...]
int tid = rectToTileId(first, tileset_grid_size, tileset_columns);
if (tid >= 0) {
rule.tile_ids.push_back(tid);
}
}
}
}
// Fallback: legacy tileIds field
if (rule.tile_ids.empty() && rule_json.contains("tileIds") && rule_json["tileIds"].is_array()) {
for (const auto& tid : rule_json["tileIds"]) {
rule.tile_ids.push_back(tid.get<int>());
}
}
return rule;
}
static AutoRuleSet parseAutoRuleSet(const nlohmann::json& layer_def,
const std::unordered_map<int, std::shared_ptr<tiled::TileSetData>>& tileset_by_uid)
{
AutoRuleSet rs;
rs.name = jsonStr(layer_def, "identifier");
rs.gridSize = jsonInt(layer_def, "gridSize");
rs.tilesetDefUid = jsonInt(layer_def, "tilesetDefUid", -1);
// Determine tileset dimensions for tile rect conversion
int ts_grid = 0;
int ts_columns = 0;
auto ts_it = tileset_by_uid.find(rs.tilesetDefUid);
if (ts_it != tileset_by_uid.end() && ts_it->second) {
ts_grid = ts_it->second->tile_width;
ts_columns = ts_it->second->columns;
}
// IntGrid values
if (layer_def.contains("intGridValues") && layer_def["intGridValues"].is_array()) {
for (const auto& igv : layer_def["intGridValues"]) {
IntGridValue v;
v.value = jsonInt(igv, "value");
v.name = jsonStr(igv, "identifier");
rs.intgrid_values.push_back(std::move(v));
}
}
// Auto-rule groups
if (layer_def.contains("autoRuleGroups") && layer_def["autoRuleGroups"].is_array()) {
for (const auto& group_json : layer_def["autoRuleGroups"]) {
AutoRuleGroup group;
group.name = jsonStr(group_json, "name");
group.active = jsonBool(group_json, "active", true);
if (group_json.contains("rules") && group_json["rules"].is_array()) {
for (const auto& rule_json : group_json["rules"]) {
group.rules.push_back(parseAutoRule(rule_json, ts_grid, ts_columns));
}
}
rs.groups.push_back(std::move(group));
}
}
return rs;
}
// ============================================================
// Parse pre-computed auto-layer tiles
// ============================================================
static std::vector<PrecomputedTile> parseAutoLayerTiles(
const nlohmann::json& tiles_json, int tile_size, int columns, int grid_size)
{
std::vector<PrecomputedTile> result;
if (!tiles_json.is_array()) return result;
for (const auto& t : tiles_json) {
PrecomputedTile pt;
// Tile ID from src rect [x, y]
if (t.contains("src") && t["src"].is_array() && t["src"].size() >= 2) {
int px_x = t["src"][0].get<int>();
int px_y = t["src"][1].get<int>();
if (tile_size > 0 && columns > 0) {
int col = px_x / tile_size;
int row = px_y / tile_size;
pt.tile_id = row * columns + col;
} else {
pt.tile_id = 0;
}
} else {
pt.tile_id = jsonInt(t, "t");
}
// Grid position from px array [x, y]
if (t.contains("px") && t["px"].is_array() && t["px"].size() >= 2) {
int px_x = t["px"][0].get<int>();
int px_y = t["px"][1].get<int>();
// Convert pixel position to grid cell
if (grid_size > 0) {
pt.grid_x = px_x / grid_size;
pt.grid_y = px_y / grid_size;
}
}
// Flip flags: f field (0=none, 1=flipX, 2=flipY, 3=both)
pt.flip = jsonInt(t, "f");
pt.alpha = jsonFloat(t, "a", 1.0f);
result.push_back(std::move(pt));
}
return result;
}
// ============================================================
// Parse grid tiles (manual placement)
// ============================================================
static std::vector<PrecomputedTile> parseGridTiles(
const nlohmann::json& tiles_json, int tile_size, int columns, int grid_size)
{
// Same format as auto-layer tiles
return parseAutoLayerTiles(tiles_json, tile_size, columns, grid_size);
}
// ============================================================
// Parse level layer instances
// ============================================================
static LevelLayerData parseLayerInstance(
const nlohmann::json& layer_json,
const std::unordered_map<int, std::shared_ptr<tiled::TileSetData>>& tileset_by_uid)
{
LevelLayerData layer;
layer.name = jsonStr(layer_json, "__identifier");
layer.type = jsonStr(layer_json, "__type");
layer.width = jsonInt(layer_json, "__cWid");
layer.height = jsonInt(layer_json, "__cHei");
layer.gridSize = jsonInt(layer_json, "__gridSize");
layer.tilesetDefUid = jsonInt(layer_json, "__tilesetDefUid", -1);
// Determine tileset parameters for tile rect conversion
int ts_grid = 0;
int ts_columns = 0;
auto ts_it = tileset_by_uid.find(layer.tilesetDefUid);
if (ts_it != tileset_by_uid.end() && ts_it->second) {
ts_grid = ts_it->second->tile_width;
ts_columns = ts_it->second->columns;
}
// IntGrid values (CSV format)
if (layer_json.contains("intGridCsv") && layer_json["intGridCsv"].is_array()) {
for (const auto& val : layer_json["intGridCsv"]) {
layer.intgrid.push_back(val.get<int>());
}
}
// Auto-layer tiles
if (layer_json.contains("autoLayerTiles") && layer_json["autoLayerTiles"].is_array()) {
layer.auto_tiles = parseAutoLayerTiles(
layer_json["autoLayerTiles"], ts_grid, ts_columns, layer.gridSize);
}
// Grid tiles (manual placement)
if (layer_json.contains("gridTiles") && layer_json["gridTiles"].is_array()) {
layer.grid_tiles = parseGridTiles(
layer_json["gridTiles"], ts_grid, ts_columns, layer.gridSize);
}
// Entity instances
if (layer_json.contains("entityInstances") && layer_json["entityInstances"].is_array()) {
layer.entities = layer_json["entityInstances"];
}
return layer;
}
// ============================================================
// Parse levels
// ============================================================
static LevelData parseLevel(
const nlohmann::json& level_json,
const std::unordered_map<int, std::shared_ptr<tiled::TileSetData>>& tileset_by_uid)
{
LevelData level;
level.name = jsonStr(level_json, "identifier");
level.width_px = jsonInt(level_json, "pxWid");
level.height_px = jsonInt(level_json, "pxHei");
level.worldX = jsonInt(level_json, "worldX");
level.worldY = jsonInt(level_json, "worldY");
// Layer instances
if (level_json.contains("layerInstances") && level_json["layerInstances"].is_array()) {
for (const auto& li : level_json["layerInstances"]) {
level.layers.push_back(parseLayerInstance(li, tileset_by_uid));
}
}
return level;
}
// ============================================================
// Public API: load LDtk project
// ============================================================
std::shared_ptr<LdtkProjectData> loadLdtkProject(const std::string& path) {
std::string abs_path = std::filesystem::absolute(path).string();
std::string text = readFile(abs_path);
nlohmann::json j = nlohmann::json::parse(text);
auto proj = std::make_shared<LdtkProjectData>();
proj->source_path = abs_path;
proj->json_version = jsonStr(j, "jsonVersion");
std::string base_dir = parentDir(abs_path);
// Build uid -> tileset map for cross-referencing
std::unordered_map<int, std::shared_ptr<tiled::TileSetData>> tileset_by_uid;
// Parse tileset definitions from defs.tilesets
if (j.contains("defs") && j["defs"].contains("tilesets") && j["defs"]["tilesets"].is_array()) {
for (const auto& ts_def : j["defs"]["tilesets"]) {
int uid = jsonInt(ts_def, "uid", -1);
auto ts = parseTilesetDef(ts_def, base_dir);
proj->tileset_uid_to_index[uid] = static_cast<int>(proj->tilesets.size());
proj->tilesets.push_back(ts);
tileset_by_uid[uid] = ts;
}
}
// Parse layer definitions for auto-rule sets
if (j.contains("defs") && j["defs"].contains("layers") && j["defs"]["layers"].is_array()) {
for (const auto& layer_def : j["defs"]["layers"]) {
std::string layer_type = jsonStr(layer_def, "type");
// Only IntGrid and AutoLayer types have auto-rules
if (layer_type == "IntGrid" || layer_type == "AutoLayer") {
// Only include if there are actual rules
bool has_rules = false;
if (layer_def.contains("autoRuleGroups") && layer_def["autoRuleGroups"].is_array()) {
for (const auto& grp : layer_def["autoRuleGroups"]) {
if (grp.contains("rules") && grp["rules"].is_array() && !grp["rules"].empty()) {
has_rules = true;
break;
}
}
}
bool has_intgrid = layer_def.contains("intGridValues") &&
layer_def["intGridValues"].is_array() &&
!layer_def["intGridValues"].empty();
if (has_rules || has_intgrid) {
int layer_uid = jsonInt(layer_def, "uid", -1);
AutoRuleSet rs = parseAutoRuleSet(layer_def, tileset_by_uid);
proj->ruleset_uid_to_index[layer_uid] = static_cast<int>(proj->rulesets.size());
proj->rulesets.push_back(std::move(rs));
}
}
}
}
// Parse enum definitions
if (j.contains("defs") && j["defs"].contains("enums") && j["defs"]["enums"].is_array()) {
proj->enums = j["defs"]["enums"];
}
// Parse levels
if (j.contains("levels") && j["levels"].is_array()) {
for (const auto& level_json : j["levels"]) {
proj->levels.push_back(parseLevel(level_json, tileset_by_uid));
}
}
return proj;
}
} // namespace ldtk
} // namespace mcrf

12
src/ldtk/LdtkParse.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include "LdtkTypes.h"
#include <Python.h>
namespace mcrf {
namespace ldtk {
// Load an LDtk project from a .ldtk JSON file
std::shared_ptr<LdtkProjectData> loadLdtkProject(const std::string& path);
} // namespace ldtk
} // namespace mcrf

119
src/ldtk/LdtkTypes.h Normal file
View file

@ -0,0 +1,119 @@
#pragma once
#include <string>
#include <vector>
#include <unordered_map>
#include <memory>
#include <cstdint>
#include <nlohmann/json.hpp>
#include "TiledTypes.h" // Reuse TileSetData
namespace mcrf {
namespace ldtk {
// ============================================================
// IntGrid terrain value definition
// ============================================================
struct IntGridValue {
int value; // 1-indexed (0 = empty)
std::string name; // e.g. "grass", "wall"
};
// ============================================================
// Auto-tile rule system
// ============================================================
// Single auto-tile rule
struct AutoRule {
int uid;
int size; // Pattern dimension: 1, 3, 5, or 7
std::vector<int> pattern; // size*size flat array
// 0 = wildcard (any value)
// +N = must match IntGrid value N
// -N = must NOT be IntGrid value N
std::vector<int> tile_ids; // Alternative tiles (random pick)
bool flipX = false;
bool flipY = false;
float chance = 1.0f; // 0.0-1.0 probability
bool breakOnMatch = true;
int outOfBoundsValue = -1; // -1 = treat as empty (0)
bool active = true;
int pivotX = 0, pivotY = 0; // Pivot offset within pattern
};
// Group of rules (evaluated together)
struct AutoRuleGroup {
std::string name;
bool active = true;
std::vector<AutoRule> rules;
};
// Resolution result for a single cell
struct AutoTileResult {
int tile_id;
int flip; // 0=none, 1=flipX, 2=flipY, 3=both
};
// Full rule set for one IntGrid/AutoLayer
struct AutoRuleSet {
std::string name;
int gridSize = 0;
int tilesetDefUid = -1;
std::vector<IntGridValue> intgrid_values;
std::vector<AutoRuleGroup> groups;
// Flip expansion mapping: (tile_id << 2) | flip_bits -> expanded_tile_id
std::unordered_map<uint32_t, int> flip_mapping;
int expanded_tile_count = 0; // Total tiles after flip expansion
};
// ============================================================
// Level and layer data
// ============================================================
// Pre-computed tile from LDtk editor
struct PrecomputedTile {
int tile_id;
int grid_x, grid_y; // Cell coordinates
int flip; // 0=none, 1=flipX, 2=flipY, 3=both
float alpha = 1.0f;
};
// Layer data within a level
struct LevelLayerData {
std::string name;
std::string type; // "IntGrid", "AutoLayer", "Tiles", "Entities"
int width = 0, height = 0; // In cells
int gridSize = 0; // Cell size in pixels
int tilesetDefUid = -1;
std::vector<int> intgrid; // Source IntGrid values
std::vector<PrecomputedTile> auto_tiles; // Pre-computed from editor
std::vector<PrecomputedTile> grid_tiles; // Manual tile placement
nlohmann::json entities; // Entity data as JSON
};
// Level
struct LevelData {
std::string name;
int width_px = 0, height_px = 0; // Pixel dimensions
int worldX = 0, worldY = 0;
std::vector<LevelLayerData> layers;
};
// ============================================================
// Top-level project
// ============================================================
struct LdtkProjectData {
std::string source_path;
std::string json_version;
std::vector<std::shared_ptr<tiled::TileSetData>> tilesets;
std::unordered_map<int, int> tileset_uid_to_index; // uid -> index into tilesets
std::vector<AutoRuleSet> rulesets;
std::unordered_map<int, int> ruleset_uid_to_index; // layer uid -> index into rulesets
std::vector<LevelData> levels;
nlohmann::json enums; // Enum definitions (lightweight JSON exposure)
};
} // namespace ldtk
} // namespace mcrf

307
src/ldtk/PyAutoRuleSet.cpp Normal file
View file

@ -0,0 +1,307 @@
#include "PyAutoRuleSet.h"
#include "AutoRuleResolve.h"
#include "McRFPy_Doc.h"
#include "PyDiscreteMap.h"
#include "GridLayers.h"
#include <cstring>
using namespace mcrf::ldtk;
// ============================================================
// Helper
// ============================================================
const AutoRuleSet& PyAutoRuleSet::getRuleSet(PyAutoRuleSetObject* self) {
return self->parent->rulesets[self->ruleset_index];
}
// ============================================================
// Factory
// ============================================================
PyObject* PyAutoRuleSet::create(std::shared_ptr<LdtkProjectData> parent, int index) {
auto* type = &mcrfpydef::PyAutoRuleSetType;
auto* self = (PyAutoRuleSetObject*)type->tp_alloc(type, 0);
if (!self) return NULL;
new (&self->parent) std::shared_ptr<LdtkProjectData>(parent);
self->ruleset_index = index;
return (PyObject*)self;
}
// ============================================================
// Type lifecycle
// ============================================================
void PyAutoRuleSet::dealloc(PyAutoRuleSetObject* self) {
self->parent.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyAutoRuleSet::repr(PyObject* obj) {
auto* self = (PyAutoRuleSetObject*)obj;
const auto& rs = getRuleSet(self);
int total_rules = 0;
for (const auto& g : rs.groups) total_rules += g.rules.size();
return PyUnicode_FromFormat("<AutoRuleSet '%s' values=%d rules=%d groups=%d>",
rs.name.c_str(), (int)rs.intgrid_values.size(),
total_rules, (int)rs.groups.size());
}
// ============================================================
// Properties
// ============================================================
PyObject* PyAutoRuleSet::get_name(PyAutoRuleSetObject* self, void*) {
return PyUnicode_FromString(getRuleSet(self).name.c_str());
}
PyObject* PyAutoRuleSet::get_grid_size(PyAutoRuleSetObject* self, void*) {
return PyLong_FromLong(getRuleSet(self).gridSize);
}
PyObject* PyAutoRuleSet::get_value_count(PyAutoRuleSetObject* self, void*) {
return PyLong_FromLong(getRuleSet(self).intgrid_values.size());
}
PyObject* PyAutoRuleSet::get_values(PyAutoRuleSetObject* self, void*) {
const auto& rs = getRuleSet(self);
PyObject* list = PyList_New(rs.intgrid_values.size());
if (!list) return NULL;
for (size_t i = 0; i < rs.intgrid_values.size(); i++) {
const auto& v = rs.intgrid_values[i];
PyObject* dict = Py_BuildValue("{s:i, s:s}",
"value", v.value,
"name", v.name.c_str());
if (!dict) {
Py_DECREF(list);
return NULL;
}
PyList_SET_ITEM(list, i, dict);
}
return list;
}
PyObject* PyAutoRuleSet::get_rule_count(PyAutoRuleSetObject* self, void*) {
const auto& rs = getRuleSet(self);
int total = 0;
for (const auto& g : rs.groups) total += g.rules.size();
return PyLong_FromLong(total);
}
PyObject* PyAutoRuleSet::get_group_count(PyAutoRuleSetObject* self, void*) {
return PyLong_FromLong(getRuleSet(self).groups.size());
}
// ============================================================
// Methods
// ============================================================
// Convert a name like "grass_terrain" or "Grass Terrain" to "GRASS_TERRAIN"
static std::string toUpperSnakeCase(const std::string& s) {
std::string result;
result.reserve(s.size());
for (size_t i = 0; i < s.size(); i++) {
char c = s[i];
if (c == ' ' || c == '-') {
result += '_';
} else {
result += static_cast<char>(toupper(static_cast<unsigned char>(c)));
}
}
return result;
}
PyObject* PyAutoRuleSet::terrain_enum(PyAutoRuleSetObject* self, PyObject*) {
const auto& rs = getRuleSet(self);
// Import IntEnum from enum module
PyObject* enum_module = PyImport_ImportModule("enum");
if (!enum_module) return NULL;
PyObject* int_enum = PyObject_GetAttrString(enum_module, "IntEnum");
Py_DECREF(enum_module);
if (!int_enum) return NULL;
// Build members dict: NONE=0, then each IntGrid value
PyObject* members = PyDict_New();
if (!members) { Py_DECREF(int_enum); return NULL; }
// NONE = 0 (empty terrain)
PyObject* zero = PyLong_FromLong(0);
PyDict_SetItemString(members, "NONE", zero);
Py_DECREF(zero);
for (const auto& v : rs.intgrid_values) {
std::string key = toUpperSnakeCase(v.name);
if (key.empty()) {
// Fallback name for unnamed values
key = "VALUE_" + std::to_string(v.value);
}
PyObject* val = PyLong_FromLong(v.value);
PyDict_SetItemString(members, key.c_str(), val);
Py_DECREF(val);
}
// Create enum class: IntEnum(rs.name, members)
PyObject* name = PyUnicode_FromString(rs.name.c_str());
PyObject* args = PyTuple_Pack(2, name, members);
Py_DECREF(name);
Py_DECREF(members);
PyObject* enum_class = PyObject_Call(int_enum, args, NULL);
Py_DECREF(args);
Py_DECREF(int_enum);
return enum_class;
}
PyObject* PyAutoRuleSet::resolve(PyAutoRuleSetObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"discrete_map", "seed", nullptr};
PyObject* dmap_obj;
unsigned int seed = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|I",
const_cast<char**>(keywords),
&dmap_obj, &seed))
return NULL;
// Validate DiscreteMap
const char* dmap_type_name = Py_TYPE(dmap_obj)->tp_name;
if (!dmap_type_name || strcmp(dmap_type_name, "mcrfpy.DiscreteMap") != 0) {
PyErr_SetString(PyExc_TypeError, "Expected a DiscreteMap object");
return NULL;
}
auto* dmap = (PyDiscreteMapObject*)dmap_obj;
const auto& rs = getRuleSet(self);
// Convert uint8 DiscreteMap to int array for resolution
int w = dmap->w;
int h = dmap->h;
std::vector<int> intgrid(w * h);
for (int i = 0; i < w * h; i++) {
intgrid[i] = static_cast<int>(dmap->values[i]);
}
std::vector<AutoTileResult> results = resolveAutoRules(intgrid.data(), w, h, rs, seed);
// Convert to Python list of tile IDs (last-wins for stacked, simple mode)
PyObject* list = PyList_New(results.size());
if (!list) return NULL;
for (size_t i = 0; i < results.size(); i++) {
PyList_SET_ITEM(list, i, PyLong_FromLong(results[i].tile_id));
}
return list;
}
PyObject* PyAutoRuleSet::apply(PyAutoRuleSetObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"discrete_map", "tile_layer", "seed", nullptr};
PyObject* dmap_obj;
PyObject* tlayer_obj;
unsigned int seed = 0;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "OO|I",
const_cast<char**>(keywords),
&dmap_obj, &tlayer_obj, &seed))
return NULL;
// Validate DiscreteMap
const char* dmap_tn = Py_TYPE(dmap_obj)->tp_name;
if (!dmap_tn || strcmp(dmap_tn, "mcrfpy.DiscreteMap") != 0) {
PyErr_SetString(PyExc_TypeError, "First argument must be a DiscreteMap");
return NULL;
}
// Validate TileLayer
const char* tl_tn = Py_TYPE(tlayer_obj)->tp_name;
if (!tl_tn || strcmp(tl_tn, "mcrfpy.TileLayer") != 0) {
PyErr_SetString(PyExc_TypeError, "Second argument must be a TileLayer");
return NULL;
}
auto* dmap = (PyDiscreteMapObject*)dmap_obj;
auto* tlayer = (PyTileLayerObject*)tlayer_obj;
const auto& rs = getRuleSet(self);
// Convert uint8 DiscreteMap to int array
int w = dmap->w;
int h = dmap->h;
std::vector<int> intgrid(w * h);
for (int i = 0; i < w * h; i++) {
intgrid[i] = static_cast<int>(dmap->values[i]);
}
std::vector<AutoTileResult> results = resolveAutoRules(intgrid.data(), w, h, rs, seed);
// Write into TileLayer, applying flip mapping if available
for (int y = 0; y < h && y < tlayer->data->grid_y; y++) {
for (int x = 0; x < w && x < tlayer->data->grid_x; x++) {
int idx = y * w + x;
int tid = results[idx].tile_id;
int flip = results[idx].flip;
if (tid >= 0) {
if (flip != 0 && !rs.flip_mapping.empty()) {
// Look up expanded tile ID for flipped variant
uint32_t key = (static_cast<uint32_t>(tid) << 2) | (flip & 3);
auto it = rs.flip_mapping.find(key);
if (it != rs.flip_mapping.end()) {
tid = it->second;
}
// If no mapping found, use original tile (no flip)
}
tlayer->data->at(x, y) = tid;
}
}
}
tlayer->data->markDirty();
Py_RETURN_NONE;
}
// ============================================================
// Method/GetSet tables
// ============================================================
PyMethodDef PyAutoRuleSet::methods[] = {
{"terrain_enum", (PyCFunction)PyAutoRuleSet::terrain_enum, METH_NOARGS,
MCRF_METHOD(AutoRuleSet, terrain_enum,
MCRF_SIG("()", "IntEnum"),
MCRF_DESC("Generate a Python IntEnum from this rule set's IntGrid values."),
MCRF_RETURNS("IntEnum class with NONE=0 and one member per IntGrid value (UPPER_SNAKE_CASE).")
)},
{"resolve", (PyCFunction)PyAutoRuleSet::resolve, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(AutoRuleSet, resolve,
MCRF_SIG("(discrete_map: DiscreteMap, seed: int = 0)", "list[int]"),
MCRF_DESC("Resolve IntGrid data to tile indices using LDtk auto-rules."),
MCRF_ARGS_START
MCRF_ARG("discrete_map", "A DiscreteMap with IntGrid values matching this rule set")
MCRF_ARG("seed", "Random seed for deterministic tile selection and probability (default: 0)")
MCRF_RETURNS("List of tile IDs (one per cell). -1 means no matching rule.")
)},
{"apply", (PyCFunction)PyAutoRuleSet::apply, METH_VARARGS | METH_KEYWORDS,
MCRF_METHOD(AutoRuleSet, apply,
MCRF_SIG("(discrete_map: DiscreteMap, tile_layer: TileLayer, seed: int = 0)", "None"),
MCRF_DESC("Resolve auto-rules and write tile indices directly into a TileLayer."),
MCRF_ARGS_START
MCRF_ARG("discrete_map", "A DiscreteMap with IntGrid values")
MCRF_ARG("tile_layer", "Target TileLayer to write resolved tiles into")
MCRF_ARG("seed", "Random seed for deterministic results (default: 0)")
)},
{NULL}
};
PyGetSetDef PyAutoRuleSet::getsetters[] = {
{"name", (getter)PyAutoRuleSet::get_name, NULL,
MCRF_PROPERTY(name, "Rule set name / layer identifier (str, read-only)."), NULL},
{"grid_size", (getter)PyAutoRuleSet::get_grid_size, NULL,
MCRF_PROPERTY(grid_size, "Cell size in pixels (int, read-only)."), NULL},
{"value_count", (getter)PyAutoRuleSet::get_value_count, NULL,
MCRF_PROPERTY(value_count, "Number of IntGrid terrain values (int, read-only)."), NULL},
{"values", (getter)PyAutoRuleSet::get_values, NULL,
MCRF_PROPERTY(values, "List of IntGrid value dicts with value and name (read-only)."), NULL},
{"rule_count", (getter)PyAutoRuleSet::get_rule_count, NULL,
MCRF_PROPERTY(rule_count, "Total number of rules across all groups (int, read-only)."), NULL},
{"group_count", (getter)PyAutoRuleSet::get_group_count, NULL,
MCRF_PROPERTY(group_count, "Number of rule groups (int, read-only)."), NULL},
{NULL}
};

75
src/ldtk/PyAutoRuleSet.h Normal file
View file

@ -0,0 +1,75 @@
#pragma once
#include "Python.h"
#include "LdtkTypes.h"
#include <memory>
// Python object structure
// Holds shared_ptr to parent project (keeps it alive) + index into rulesets
typedef struct PyAutoRuleSetObject {
PyObject_HEAD
std::shared_ptr<mcrf::ldtk::LdtkProjectData> parent;
int ruleset_index;
} PyAutoRuleSetObject;
// Python binding class
class PyAutoRuleSet {
public:
// Factory: create from parent project + index
static PyObject* create(std::shared_ptr<mcrf::ldtk::LdtkProjectData> parent, int index);
static void dealloc(PyAutoRuleSetObject* self);
static PyObject* repr(PyObject* obj);
// Read-only properties
static PyObject* get_name(PyAutoRuleSetObject* self, void* closure);
static PyObject* get_grid_size(PyAutoRuleSetObject* self, void* closure);
static PyObject* get_value_count(PyAutoRuleSetObject* self, void* closure);
static PyObject* get_values(PyAutoRuleSetObject* self, void* closure);
static PyObject* get_rule_count(PyAutoRuleSetObject* self, void* closure);
static PyObject* get_group_count(PyAutoRuleSetObject* self, void* closure);
// Methods
static PyObject* terrain_enum(PyAutoRuleSetObject* self, PyObject* args);
static PyObject* resolve(PyAutoRuleSetObject* self, PyObject* args, PyObject* kwds);
static PyObject* apply(PyAutoRuleSetObject* self, PyObject* args, PyObject* kwds);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
private:
static const mcrf::ldtk::AutoRuleSet& getRuleSet(PyAutoRuleSetObject* self);
};
// Type definition in mcrfpydef namespace
namespace mcrfpydef {
inline PyTypeObject PyAutoRuleSetType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.AutoRuleSet",
.tp_basicsize = sizeof(PyAutoRuleSetObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyAutoRuleSet::dealloc,
.tp_repr = PyAutoRuleSet::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"AutoRuleSet - LDtk auto-tile rule set for pattern-based terrain rendering.\n\n"
"AutoRuleSets are obtained from LdtkProject.ruleset().\n"
"They map IntGrid terrain values to sprite tiles using LDtk's\n"
"pattern-matching auto-rule system.\n\n"
"Properties:\n"
" name (str, read-only): Rule set name (layer identifier).\n"
" grid_size (int, read-only): Cell size in pixels.\n"
" value_count (int, read-only): Number of IntGrid values.\n"
" values (list, read-only): List of value dicts.\n"
" rule_count (int, read-only): Total rules across all groups.\n"
" group_count (int, read-only): Number of rule groups.\n\n"
"Example:\n"
" rs = project.ruleset('Walls')\n"
" Terrain = rs.terrain_enum()\n"
" rs.apply(discrete_map, tile_layer, seed=42)\n"
),
.tp_methods = nullptr, // Set before PyType_Ready
.tp_getset = nullptr, // Set before PyType_Ready
};
} // namespace mcrfpydef

315
src/ldtk/PyLdtkProject.cpp Normal file
View file

@ -0,0 +1,315 @@
#include "PyLdtkProject.h"
#include "LdtkParse.h"
#include "PyAutoRuleSet.h"
#include "PyTileSetFile.h" // For PyTileSetFileObject
#include "McRFPy_Doc.h"
#include "TiledParse.h" // For jsonToPython()
#include "PyTexture.h"
#include <cstring>
using namespace mcrf::ldtk;
// ============================================================
// Type lifecycle
// ============================================================
PyObject* PyLdtkProject::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) {
auto* self = (PyLdtkProjectObject*)type->tp_alloc(type, 0);
if (self) {
new (&self->data) std::shared_ptr<LdtkProjectData>();
}
return (PyObject*)self;
}
int PyLdtkProject::init(PyLdtkProjectObject* self, PyObject* args, PyObject* kwds) {
static const char* keywords[] = {"path", nullptr};
const char* path = nullptr;
if (!PyArg_ParseTupleAndKeywords(args, kwds, "s", const_cast<char**>(keywords), &path))
return -1;
try {
self->data = loadLdtkProject(path);
} catch (const std::exception& e) {
PyErr_Format(PyExc_IOError, "Failed to load LDtk project: %s", e.what());
return -1;
}
return 0;
}
void PyLdtkProject::dealloc(PyLdtkProjectObject* self) {
self->data.~shared_ptr();
Py_TYPE(self)->tp_free((PyObject*)self);
}
PyObject* PyLdtkProject::repr(PyObject* obj) {
auto* self = (PyLdtkProjectObject*)obj;
if (!self->data) {
return PyUnicode_FromString("<LdtkProject (uninitialized)>");
}
return PyUnicode_FromFormat("<LdtkProject v%s tilesets=%d rulesets=%d levels=%d>",
self->data->json_version.c_str(),
(int)self->data->tilesets.size(),
(int)self->data->rulesets.size(),
(int)self->data->levels.size());
}
// ============================================================
// Properties (all read-only)
// ============================================================
PyObject* PyLdtkProject::get_version(PyLdtkProjectObject* self, void*) {
return PyUnicode_FromString(self->data->json_version.c_str());
}
PyObject* PyLdtkProject::get_tileset_names(PyLdtkProjectObject* self, void*) {
PyObject* list = PyList_New(self->data->tilesets.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->tilesets.size(); i++) {
PyList_SET_ITEM(list, i, PyUnicode_FromString(self->data->tilesets[i]->name.c_str()));
}
return list;
}
PyObject* PyLdtkProject::get_ruleset_names(PyLdtkProjectObject* self, void*) {
PyObject* list = PyList_New(self->data->rulesets.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->rulesets.size(); i++) {
PyList_SET_ITEM(list, i, PyUnicode_FromString(self->data->rulesets[i].name.c_str()));
}
return list;
}
PyObject* PyLdtkProject::get_level_names(PyLdtkProjectObject* self, void*) {
PyObject* list = PyList_New(self->data->levels.size());
if (!list) return NULL;
for (size_t i = 0; i < self->data->levels.size(); i++) {
PyList_SET_ITEM(list, i, PyUnicode_FromString(self->data->levels[i].name.c_str()));
}
return list;
}
PyObject* PyLdtkProject::get_enums(PyLdtkProjectObject* self, void*) {
return mcrf::tiled::jsonToPython(self->data->enums);
}
// ============================================================
// Methods
// ============================================================
PyObject* PyLdtkProject::tileset(PyLdtkProjectObject* self, PyObject* args) {
const char* name = nullptr;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (size_t i = 0; i < self->data->tilesets.size(); i++) {
if (self->data->tilesets[i]->name == name) {
// Return a TileSetFile-compatible object
// We create a PyTileSetFileObject wrapping our existing TileSetData
PyObject* mcrfpy_module = PyImport_ImportModule("mcrfpy");
if (!mcrfpy_module) return NULL;
PyObject* tsf_type = PyObject_GetAttrString(mcrfpy_module, "TileSetFile");
Py_DECREF(mcrfpy_module);
if (!tsf_type) return NULL;
// Allocate without calling init (we set data directly)
PyObject* tsf_obj = ((PyTypeObject*)tsf_type)->tp_alloc((PyTypeObject*)tsf_type, 0);
Py_DECREF(tsf_type);
if (!tsf_obj) return NULL;
// Construct the shared_ptr in place using the proper type
auto* tsf = (PyTileSetFileObject*)tsf_obj;
new (&tsf->data) std::shared_ptr<mcrf::tiled::TileSetData>(self->data->tilesets[i]);
return tsf_obj;
}
}
PyErr_Format(PyExc_KeyError, "No tileset named '%s'", name);
return NULL;
}
PyObject* PyLdtkProject::ruleset(PyLdtkProjectObject* self, PyObject* args) {
const char* name = nullptr;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (size_t i = 0; i < self->data->rulesets.size(); i++) {
if (self->data->rulesets[i].name == name) {
return PyAutoRuleSet::create(self->data, static_cast<int>(i));
}
}
PyErr_Format(PyExc_KeyError, "No ruleset named '%s'", name);
return NULL;
}
PyObject* PyLdtkProject::level(PyLdtkProjectObject* self, PyObject* args) {
const char* name = nullptr;
if (!PyArg_ParseTuple(args, "s", &name))
return NULL;
for (size_t i = 0; i < self->data->levels.size(); i++) {
const auto& lvl = self->data->levels[i];
if (lvl.name == name) {
// Build Python dict representing the level
PyObject* dict = PyDict_New();
if (!dict) return NULL;
PyObject* py_name = PyUnicode_FromString(lvl.name.c_str());
PyDict_SetItemString(dict, "name", py_name);
Py_DECREF(py_name);
PyObject* py_w = PyLong_FromLong(lvl.width_px);
PyDict_SetItemString(dict, "width_px", py_w);
Py_DECREF(py_w);
PyObject* py_h = PyLong_FromLong(lvl.height_px);
PyDict_SetItemString(dict, "height_px", py_h);
Py_DECREF(py_h);
PyObject* py_wx = PyLong_FromLong(lvl.worldX);
PyDict_SetItemString(dict, "world_x", py_wx);
Py_DECREF(py_wx);
PyObject* py_wy = PyLong_FromLong(lvl.worldY);
PyDict_SetItemString(dict, "world_y", py_wy);
Py_DECREF(py_wy);
// Build layers list
PyObject* layers_list = PyList_New(lvl.layers.size());
if (!layers_list) { Py_DECREF(dict); return NULL; }
for (size_t j = 0; j < lvl.layers.size(); j++) {
const auto& layer = lvl.layers[j];
PyObject* layer_dict = PyDict_New();
if (!layer_dict) { Py_DECREF(layers_list); Py_DECREF(dict); return NULL; }
PyObject* ln = PyUnicode_FromString(layer.name.c_str());
PyDict_SetItemString(layer_dict, "name", ln);
Py_DECREF(ln);
PyObject* lt = PyUnicode_FromString(layer.type.c_str());
PyDict_SetItemString(layer_dict, "type", lt);
Py_DECREF(lt);
PyObject* lw = PyLong_FromLong(layer.width);
PyDict_SetItemString(layer_dict, "width", lw);
Py_DECREF(lw);
PyObject* lh = PyLong_FromLong(layer.height);
PyDict_SetItemString(layer_dict, "height", lh);
Py_DECREF(lh);
// IntGrid data
if (!layer.intgrid.empty()) {
PyObject* ig = PyList_New(layer.intgrid.size());
for (size_t k = 0; k < layer.intgrid.size(); k++) {
PyList_SET_ITEM(ig, k, PyLong_FromLong(layer.intgrid[k]));
}
PyDict_SetItemString(layer_dict, "intgrid", ig);
Py_DECREF(ig);
} else {
PyObject* ig = PyList_New(0);
PyDict_SetItemString(layer_dict, "intgrid", ig);
Py_DECREF(ig);
}
// Auto tiles
auto tile_to_dict = [](const PrecomputedTile& t) -> PyObject* {
return Py_BuildValue("{s:i, s:i, s:i, s:i, s:f}",
"tile_id", t.tile_id,
"x", t.grid_x,
"y", t.grid_y,
"flip", t.flip,
"alpha", (double)t.alpha);
};
PyObject* auto_list = PyList_New(layer.auto_tiles.size());
for (size_t k = 0; k < layer.auto_tiles.size(); k++) {
PyList_SET_ITEM(auto_list, k, tile_to_dict(layer.auto_tiles[k]));
}
PyDict_SetItemString(layer_dict, "auto_tiles", auto_list);
Py_DECREF(auto_list);
PyObject* grid_list = PyList_New(layer.grid_tiles.size());
for (size_t k = 0; k < layer.grid_tiles.size(); k++) {
PyList_SET_ITEM(grid_list, k, tile_to_dict(layer.grid_tiles[k]));
}
PyDict_SetItemString(layer_dict, "grid_tiles", grid_list);
Py_DECREF(grid_list);
// Entity instances
if (!layer.entities.is_null()) {
PyObject* ents = mcrf::tiled::jsonToPython(layer.entities);
PyDict_SetItemString(layer_dict, "entities", ents);
Py_DECREF(ents);
} else {
PyObject* ents = PyList_New(0);
PyDict_SetItemString(layer_dict, "entities", ents);
Py_DECREF(ents);
}
PyList_SET_ITEM(layers_list, j, layer_dict);
}
PyDict_SetItemString(dict, "layers", layers_list);
Py_DECREF(layers_list);
return dict;
}
}
PyErr_Format(PyExc_KeyError, "No level named '%s'", name);
return NULL;
}
// ============================================================
// Method/GetSet tables
// ============================================================
PyMethodDef PyLdtkProject::methods[] = {
{"tileset", (PyCFunction)PyLdtkProject::tileset, METH_VARARGS,
MCRF_METHOD(LdtkProject, tileset,
MCRF_SIG("(name: str)", "TileSetFile"),
MCRF_DESC("Get a tileset by name."),
MCRF_ARGS_START
MCRF_ARG("name", "Tileset identifier from the LDtk project")
MCRF_RETURNS("A TileSetFile object for texture creation and tile metadata.")
MCRF_RAISES("KeyError", "If no tileset with the given name exists")
)},
{"ruleset", (PyCFunction)PyLdtkProject::ruleset, METH_VARARGS,
MCRF_METHOD(LdtkProject, ruleset,
MCRF_SIG("(name: str)", "AutoRuleSet"),
MCRF_DESC("Get an auto-rule set by layer name."),
MCRF_ARGS_START
MCRF_ARG("name", "Layer identifier from the LDtk project")
MCRF_RETURNS("An AutoRuleSet for resolving IntGrid data to sprite tiles.")
MCRF_RAISES("KeyError", "If no ruleset with the given name exists")
)},
{"level", (PyCFunction)PyLdtkProject::level, METH_VARARGS,
MCRF_METHOD(LdtkProject, level,
MCRF_SIG("(name: str)", "dict"),
MCRF_DESC("Get level data by name."),
MCRF_ARGS_START
MCRF_ARG("name", "Level identifier from the LDtk project")
MCRF_RETURNS("Dict with name, dimensions, world position, and layer data.")
MCRF_RAISES("KeyError", "If no level with the given name exists")
)},
{NULL}
};
PyGetSetDef PyLdtkProject::getsetters[] = {
{"version", (getter)PyLdtkProject::get_version, NULL,
MCRF_PROPERTY(version, "LDtk JSON format version string (str, read-only)."), NULL},
{"tileset_names", (getter)PyLdtkProject::get_tileset_names, NULL,
MCRF_PROPERTY(tileset_names, "List of tileset identifier names (list[str], read-only)."), NULL},
{"ruleset_names", (getter)PyLdtkProject::get_ruleset_names, NULL,
MCRF_PROPERTY(ruleset_names, "List of rule set / layer names (list[str], read-only)."), NULL},
{"level_names", (getter)PyLdtkProject::get_level_names, NULL,
MCRF_PROPERTY(level_names, "List of level identifier names (list[str], read-only)."), NULL},
{"enums", (getter)PyLdtkProject::get_enums, NULL,
MCRF_PROPERTY(enums, "Enum definitions from the project as a list of dicts (read-only)."), NULL},
{NULL}
};

72
src/ldtk/PyLdtkProject.h Normal file
View file

@ -0,0 +1,72 @@
#pragma once
#include "Python.h"
#include "LdtkTypes.h"
#include <memory>
// Python object structure
typedef struct PyLdtkProjectObject {
PyObject_HEAD
std::shared_ptr<mcrf::ldtk::LdtkProjectData> data;
} PyLdtkProjectObject;
// Python binding class
class PyLdtkProject {
public:
static PyObject* pynew(PyTypeObject* type, PyObject* args, PyObject* kwds);
static int init(PyLdtkProjectObject* self, PyObject* args, PyObject* kwds);
static void dealloc(PyLdtkProjectObject* self);
static PyObject* repr(PyObject* obj);
// Read-only properties
static PyObject* get_version(PyLdtkProjectObject* self, void* closure);
static PyObject* get_tileset_names(PyLdtkProjectObject* self, void* closure);
static PyObject* get_ruleset_names(PyLdtkProjectObject* self, void* closure);
static PyObject* get_level_names(PyLdtkProjectObject* self, void* closure);
static PyObject* get_enums(PyLdtkProjectObject* self, void* closure);
// Methods
static PyObject* tileset(PyLdtkProjectObject* self, PyObject* args);
static PyObject* ruleset(PyLdtkProjectObject* self, PyObject* args);
static PyObject* level(PyLdtkProjectObject* self, PyObject* args);
static PyMethodDef methods[];
static PyGetSetDef getsetters[];
};
// Type definition in mcrfpydef namespace
namespace mcrfpydef {
inline PyTypeObject PyLdtkProjectType = {
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
.tp_name = "mcrfpy.LdtkProject",
.tp_basicsize = sizeof(PyLdtkProjectObject),
.tp_itemsize = 0,
.tp_dealloc = (destructor)PyLdtkProject::dealloc,
.tp_repr = PyLdtkProject::repr,
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = PyDoc_STR(
"LdtkProject(path: str)\n\n"
"Load an LDtk project file (.ldtk).\n\n"
"Parses the project and provides access to tilesets, auto-rule sets,\n"
"levels, and enum definitions.\n\n"
"Args:\n"
" path: Path to the .ldtk project file.\n\n"
"Properties:\n"
" version (str, read-only): LDtk JSON format version.\n"
" tileset_names (list[str], read-only): Names of all tilesets.\n"
" ruleset_names (list[str], read-only): Names of all rule sets.\n"
" level_names (list[str], read-only): Names of all levels.\n"
" enums (dict, read-only): Enum definitions from the project.\n\n"
"Example:\n"
" proj = mcrfpy.LdtkProject('dungeon.ldtk')\n"
" ts = proj.tileset('Dungeon_Tiles')\n"
" rs = proj.ruleset('Walls')\n"
" level = proj.level('Level_0')\n"
),
.tp_methods = nullptr, // Set before PyType_Ready
.tp_getset = nullptr, // Set before PyType_Ready
.tp_init = (initproc)PyLdtkProject::init,
.tp_new = PyLdtkProject::pynew,
};
} // namespace mcrfpydef