From d92e2b004e0650856aa6942e3bc3cf087d1a65d5 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 23 Sep 2025 01:54:37 +0200 Subject: [PATCH] add initial implementation of LDtk importer with data models and scene builder --- addons/csharp_ldtk_importer/LdtkImporter.cs | 25 +- .../LdtkResourceImporter.cs | 95 ++ .../LdtkResourceImporter.cs.uid | 1 + .../csharp_ldtk_importer/LdtkSceneBuilder.cs | 152 ++ .../LdtkSceneBuilder.cs.uid | 1 + .../csharp_ldtk_importer/Models/LdtkData.cs | 30 + .../Models/LdtkData.cs.uid | 1 + .../Models/LdtkEntityInstance.cs | 12 + .../Models/LdtkEntityInstance.cs.uid | 1 + .../Models/LdtkLayerInstance.cs | 27 + .../Models/LdtkLayerInstance.cs.uid | 1 + .../csharp_ldtk_importer/Models/LdtkLevel.cs | 12 + .../Models/LdtkLevel.cs.uid | 1 + .../Models/LdtkTileInstance.cs | 18 + .../Models/LdtkTileInstance.cs.uid | 1 + addons/csharp_ldtk_importer/plugin.cfg | 2 +- assets/PS_Tileset_10_nes.png | Bin 0 -> 5990 bytes assets/PS_Tileset_10_nes.png.import | 40 + assets/test.ldtk | 1267 +++++++++++++++++ assets/test.ldtk.import | 16 + project.godot | 7 +- scenes/world.tscn | 3 + 22 files changed, 1710 insertions(+), 3 deletions(-) create mode 100644 addons/csharp_ldtk_importer/LdtkResourceImporter.cs create mode 100644 addons/csharp_ldtk_importer/LdtkResourceImporter.cs.uid create mode 100644 addons/csharp_ldtk_importer/LdtkSceneBuilder.cs create mode 100644 addons/csharp_ldtk_importer/LdtkSceneBuilder.cs.uid create mode 100644 addons/csharp_ldtk_importer/Models/LdtkData.cs create mode 100644 addons/csharp_ldtk_importer/Models/LdtkData.cs.uid create mode 100644 addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs create mode 100644 addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs.uid create mode 100644 addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs create mode 100644 addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs.uid create mode 100644 addons/csharp_ldtk_importer/Models/LdtkLevel.cs create mode 100644 addons/csharp_ldtk_importer/Models/LdtkLevel.cs.uid create mode 100644 addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs create mode 100644 addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs.uid create mode 100644 assets/PS_Tileset_10_nes.png create mode 100644 assets/PS_Tileset_10_nes.png.import create mode 100644 assets/test.ldtk create mode 100644 assets/test.ldtk.import create mode 100644 scenes/world.tscn diff --git a/addons/csharp_ldtk_importer/LdtkImporter.cs b/addons/csharp_ldtk_importer/LdtkImporter.cs index 9659ffd..d20a2bd 100644 --- a/addons/csharp_ldtk_importer/LdtkImporter.cs +++ b/addons/csharp_ldtk_importer/LdtkImporter.cs @@ -1,6 +1,29 @@ using Godot; using System; -public partial class LdtkImporter : Node +namespace CSharpLdtkImporter; + + +[Tool] +public partial class LdtkImporter : EditorPlugin { + private LdtkResourceImporter _importer; + + public override void _EnterTree() + { + GD.Print("LdtkImporter: Plugin has been enabled."); + + _importer = new LdtkResourceImporter(); + AddImportPlugin(_importer); + + GD.Print("LdtkImporter: Import plugin registered."); + } + + public override void _ExitTree() + { + RemoveImportPlugin(_importer); + _importer = null; + GD.Print("LdtkImporter: Import plugin unregistered."); + GD.Print("LdtkImporter: Plugin has been disabled."); + } } diff --git a/addons/csharp_ldtk_importer/LdtkResourceImporter.cs b/addons/csharp_ldtk_importer/LdtkResourceImporter.cs new file mode 100644 index 0000000..1b6ca02 --- /dev/null +++ b/addons/csharp_ldtk_importer/LdtkResourceImporter.cs @@ -0,0 +1,95 @@ +using System.Text.Json; +using CSharpLdtkImporter.Models; +using Godot; +using Godot.Collections; + +namespace CSharpLdtkImporter; + +[Tool] +public partial class LdtkResourceImporter : EditorImportPlugin +{ + public override string _GetImporterName() => "ldtk.importer"; + public override string _GetVisibleName() => "LDtk Level Importer"; + public override string[] _GetRecognizedExtensions() => new string[] { "ldtk" }; + public override string _GetResourceType() => "PackedScene"; + public override string _GetSaveExtension() => "tscn"; + public override int _GetPresetCount() => 0; + public override float _GetPriority() => 1.0f; + + public override Error _Import(string sourceFile, string savePath, Godot.Collections.Dictionary options, Godot.Collections.Array platformVariants, Godot.Collections.Array genFiles) + { + // 1. Read the .ldtk file content + using var file = FileAccess.Open(sourceFile, FileAccess.ModeFlags.Read); + if (file == null) + { + GD.PushError($"Failed to open LDTK file: {sourceFile}"); + return Error.CantOpen; + } + string jsonContent = file.GetAsText(); + + // 2. Deserialize the JSON into our C# data models + LdtkData ldtkData; + try + { + ldtkData = JsonSerializer.Deserialize(jsonContent); + } + catch (JsonException e) + { + GD.PushError($"Failed to parse LDTK JSON: {e.Message}"); + return Error.ParseError; + } + + if (ldtkData == null) + { + GD.PushError("Parsed LDTK data is null."); + return Error.Failed; + } + + // 3. Use the Scene Builder to generate the Godot scene + var builder = new LdtkSceneBuilder(ldtkData, sourceFile); + var rootNode = builder.BuildLdtkProjectRoot(); + + var scene = new PackedScene(); + scene.Pack(rootNode); + + var newSceneNodeCount = scene.GetState().GetNodeCount(); + + GD.Print($"New scene node count: {newSceneNodeCount}, expected: {rootNode.GetChildCount() + 1} (including root)"); + + // 4. Save the generated scene + var destinationPath = $"{savePath}.{_GetSaveExtension()}"; + var error = ResourceSaver.Save(scene, destinationPath); + if (error != Error.Ok) + { + GD.PushError($"Failed to save generated scene to {destinationPath}"); + } + + return error; + } + + public override Array _GetImportOptions(string path, int presetIndex) + { + var options = new Array() + { + new() + { + { "name", "import_entities" }, + { "default_value", true }, + { "usage", (int)(PropertyUsageFlags.Default | PropertyUsageFlags.UpdateAllIfModified) } + }, + new() + { + { "name", "import_tilemaps" }, + { "default_value", true }, + { "usage", (int)(PropertyUsageFlags.Default | PropertyUsageFlags.UpdateAllIfModified) } + } + }; + + return options; + } + + public override bool _GetOptionVisibility(string path, StringName optionName, Dictionary options) + { + return true; + } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/LdtkResourceImporter.cs.uid b/addons/csharp_ldtk_importer/LdtkResourceImporter.cs.uid new file mode 100644 index 0000000..7b52488 --- /dev/null +++ b/addons/csharp_ldtk_importer/LdtkResourceImporter.cs.uid @@ -0,0 +1 @@ +uid://dbybnkjcgt6tn diff --git a/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs b/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs new file mode 100644 index 0000000..8f802b6 --- /dev/null +++ b/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs @@ -0,0 +1,152 @@ +using System.Linq; +using CSharpLdtkImporter.Models; +using Godot; + +namespace CSharpLdtkImporter; + +public class LdtkSceneBuilder +{ + private readonly LdtkData _ldtkData; + private readonly string _basePath; + private readonly Godot.Collections.Dictionary _tileSetCache = new(); + + public LdtkSceneBuilder(LdtkData ldtkData, string sourceFile) + { + _ldtkData = ldtkData; + _basePath = sourceFile.GetBaseDir(); + } + + public Node2D BuildLdtkProjectRoot() + { + var root = new Node2D { Name = "LDTKProject" }; + + // Step 1: Build the entire node hierarchy in memory. + foreach (var level in _ldtkData.Levels) + { + var levelNode = BuildLevel(level); + root.AddChild(levelNode); + } + + // Step 2: After the tree is built, set the owner for all descendants. + // This is the crucial step that fixes the "Invalid owner" error. + SetOwnerRecursive(root, root); + + return root; + } + + // A helper function to recursively set the owner on all children. + private void SetOwnerRecursive(Node node, Node owner) + { + foreach (var child in node.GetChildren()) + { + child.Owner = owner; + SetOwnerRecursive(child, owner); + } + } + + private Node2D BuildLevel(LdtkLevel level) + { + var levelRoot = new Node2D { Name = level.Identifier }; + + foreach (var layer in level.LayerInstances.Reverse()) + { + var layerNode = layer.Type switch + { + "Tiles" => BuildTileMapLayer(layer), + "AutoLayer" => BuildTileMapLayer(layer), + "Entities" => BuildEntityLayer(layer), + _ => new Node2D { Name = $"{layer.Identifier}_Unsupported" } + }; + + levelRoot.AddChild(layerNode); + } + + return levelRoot; + } + + private TileMapLayer BuildTileMapLayer(LdtkLayerInstance layer) + { + var tileMapLayer = new TileMapLayer { Name = layer.Identifier }; + if (!layer.TilesetDefUid.HasValue) return tileMapLayer; + + var tileSet = GetOrCreateTileSet(layer.TilesetDefUid.Value); + if (tileSet == null) return tileMapLayer; + tileMapLayer.TileSet = tileSet; + + var allTiles = layer.GridTiles.Concat(layer.AutoLayerTiles); + if (tileMapLayer.TileSet.GetSource(0) is not TileSetAtlasSource atlasSource) return tileMapLayer; + + int atlasWidthInTiles = atlasSource.GetAtlasGridSize().X; + if (atlasWidthInTiles == 0) return tileMapLayer; + + foreach (var tile in allTiles) + { + var gridCoords = new Vector2I(tile.Px[0] / layer.GridSize, tile.Px[1] / layer.GridSize); + var atlasCoords = new Vector2I(tile.TileId % atlasWidthInTiles, tile.TileId / atlasWidthInTiles); + long alternativeId = 0; + if ((tile.FlipBits & 1) == 1) alternativeId |= TileSetAtlasSource.TransformFlipH; + if ((tile.FlipBits & 2) == 2) alternativeId |= TileSetAtlasSource.TransformFlipV; + tileMapLayer.SetCell(gridCoords, 0, atlasCoords, (int)alternativeId); + } + return tileMapLayer; + } + + private Node2D BuildEntityLayer(LdtkLayerInstance layer) + { + var entityLayerRoot = new Node2D { Name = layer.Identifier }; + foreach (var entity in layer.EntityInstances) + { + var marker = new Marker2D + { + Name = entity.Identifier, + Position = new Vector2(entity.Px[0], entity.Px[1]) + }; + entityLayerRoot.AddChild(marker); + } + return entityLayerRoot; + } + + private TileSet GetOrCreateTileSet(int tilesetDefUid) + { + if (_tileSetCache.TryGetValue(tilesetDefUid, out var cachedTileSet)) return cachedTileSet; + + var tilesetDef = _ldtkData.Defs.Tilesets.FirstOrDefault(t => t.Uid == tilesetDefUid); + if (tilesetDef?.RelPath == null) return null; + + var texturePath = _basePath.PathJoin(tilesetDef.RelPath); + var texture = ResourceLoader.Load(texturePath); + + if (texture == null) + { + GD.PushError($"LDTK Importer: Could not load texture at path: {texturePath}"); + return null; + } + + var newTileSet = new TileSet + { + TileShape = TileSet.TileShapeEnum.Square, + TileLayout = TileSet.TileLayoutEnum.Stacked, + TileSize = new Vector2I(tilesetDef.TileGridSize, tilesetDef.TileGridSize) + }; + + var atlasSource = new TileSetAtlasSource + { + Texture = texture, + TextureRegionSize = new Vector2I(tilesetDef.TileGridSize, tilesetDef.TileGridSize) + }; + + newTileSet.AddSource(atlasSource); + + var (widthInTiles, heightInTiles) = (texture.GetWidth() / tilesetDef.TileGridSize, texture.GetHeight() / tilesetDef.TileGridSize); + for (int x = 0; x < widthInTiles; x++) + { + for (int y = 0; y < heightInTiles; y++) + { + atlasSource.CreateTile(new Vector2I(x, y)); + } + } + + _tileSetCache[tilesetDefUid] = newTileSet; + return newTileSet; + } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs.uid b/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs.uid new file mode 100644 index 0000000..5e275b0 --- /dev/null +++ b/addons/csharp_ldtk_importer/LdtkSceneBuilder.cs.uid @@ -0,0 +1 @@ +uid://nf7qcxuuaajw diff --git a/addons/csharp_ldtk_importer/Models/LdtkData.cs b/addons/csharp_ldtk_importer/Models/LdtkData.cs new file mode 100644 index 0000000..659cac7 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkData.cs @@ -0,0 +1,30 @@ +using System.Text.Json.Serialization; + +namespace CSharpLdtkImporter.Models; + +public class LdtkData +{ + [JsonPropertyName("levels")] + public LdtkLevel[] Levels { get; set; } + + [JsonPropertyName("defs")] + public LdtkDefinitions Defs { get; set; } +} + +public class LdtkDefinitions +{ + [JsonPropertyName("tilesets")] + public LdtkTilesetDef[] Tilesets { get; set; } +} + +public class LdtkTilesetDef +{ + [JsonPropertyName("uid")] + public int Uid { get; set; } + + [JsonPropertyName("relPath")] + public string RelPath { get; set; } + + [JsonPropertyName("tileGridSize")] + public int TileGridSize { get; set; } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/Models/LdtkData.cs.uid b/addons/csharp_ldtk_importer/Models/LdtkData.cs.uid new file mode 100644 index 0000000..cfae81b --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkData.cs.uid @@ -0,0 +1 @@ +uid://8f7a424k36sb diff --git a/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs b/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs new file mode 100644 index 0000000..299324b --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CSharpLdtkImporter.Models; + +public class LdtkEntityInstance +{ + [JsonPropertyName("__identifier")] + public string Identifier { get; set; } + + [JsonPropertyName("px")] + public int[] Px { get; set; } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs.uid b/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs.uid new file mode 100644 index 0000000..9a22c98 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkEntityInstance.cs.uid @@ -0,0 +1 @@ +uid://ck2tr052fmrx2 diff --git a/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs b/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs new file mode 100644 index 0000000..6aaf2f7 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace CSharpLdtkImporter.Models; + +public class LdtkLayerInstance +{ + [JsonPropertyName("__identifier")] + public string Identifier { get; set; } + + [JsonPropertyName("__type")] + public string Type { get; set; } + + [JsonPropertyName("__gridSize")] + public int GridSize { get; set; } + + [JsonPropertyName("__tilesetDefUid")] + public int? TilesetDefUid { get; set; } // Nullable for entity layers + + [JsonPropertyName("gridTiles")] + public LdtkTileInstance[] GridTiles { get; set; } + + [JsonPropertyName("autoLayerTiles")] + public LdtkTileInstance[] AutoLayerTiles { get; set; } + + [JsonPropertyName("entityInstances")] + public LdtkEntityInstance[] EntityInstances { get; set; } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs.uid b/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs.uid new file mode 100644 index 0000000..7a2200e --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkLayerInstance.cs.uid @@ -0,0 +1 @@ +uid://crre3v7sfnqq6 diff --git a/addons/csharp_ldtk_importer/Models/LdtkLevel.cs b/addons/csharp_ldtk_importer/Models/LdtkLevel.cs new file mode 100644 index 0000000..24e90db --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkLevel.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace CSharpLdtkImporter.Models; + +public class LdtkLevel +{ + [JsonPropertyName("identifier")] + public string Identifier { get; set; } + + [JsonPropertyName("layerInstances")] + public LdtkLayerInstance[] LayerInstances { get; set; } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/Models/LdtkLevel.cs.uid b/addons/csharp_ldtk_importer/Models/LdtkLevel.cs.uid new file mode 100644 index 0000000..681ff46 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkLevel.cs.uid @@ -0,0 +1 @@ +uid://c5kc2tyb2kf1e diff --git a/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs b/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs new file mode 100644 index 0000000..bf90569 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs @@ -0,0 +1,18 @@ +using System.Text.Json.Serialization; + +namespace CSharpLdtkImporter.Models; + +public class LdtkTileInstance +{ + // Pixel coordinates [x,y] + [JsonPropertyName("px")] + public int[] Px { get; set; } + + // Tile ID in the tileset + [JsonPropertyName("t")] + public int TileId { get; set; } + + // Flip bits (0=none, 1=X, 2=Y, 3=X&Y) + [JsonPropertyName("f")] + public int FlipBits { get; set; } +} \ No newline at end of file diff --git a/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs.uid b/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs.uid new file mode 100644 index 0000000..799f339 --- /dev/null +++ b/addons/csharp_ldtk_importer/Models/LdtkTileInstance.cs.uid @@ -0,0 +1 @@ +uid://dal7woqq2nlrd diff --git a/addons/csharp_ldtk_importer/plugin.cfg b/addons/csharp_ldtk_importer/plugin.cfg index bfb67f7..eb15671 100644 --- a/addons/csharp_ldtk_importer/plugin.cfg +++ b/addons/csharp_ldtk_importer/plugin.cfg @@ -4,4 +4,4 @@ name="C# LDTK Importer" description="Imports LDtk level files as Godot scenes." author="Gabriel Kaszewski" version="1.0" -script="LdtkImporter.cs" \ No newline at end of file +script="LdtkImporter.cs" diff --git a/assets/PS_Tileset_10_nes.png b/assets/PS_Tileset_10_nes.png new file mode 100644 index 0000000000000000000000000000000000000000..4b7589153dec431a871c512f1275f9aab52a1baa GIT binary patch literal 5990 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 Lq*