aboutsummaryrefslogtreecommitdiff
path: root/src/tiled
diff options
context:
space:
mode:
authorXander Swan <no email>2025-12-23 20:58:32 -0500
committerXander Swan <no email>2025-12-23 20:58:32 -0500
commit2838358993ade2ac1b770d675af9126749074e8f (patch)
treeb423ac832dd86cec1863c725fd2957eddd83c28f /src/tiled
parentce4d64bd41937d7dff18ca607122188dc338d696 (diff)
Fix lotsa memory leaks mhm
Diffstat (limited to 'src/tiled')
-rw-r--r--src/tiled/json.odin208
-rw-r--r--src/tiled/tiled.odin585
2 files changed, 793 insertions, 0 deletions
diff --git a/src/tiled/json.odin b/src/tiled/json.odin
new file mode 100644
index 0000000..874430e
--- /dev/null
+++ b/src/tiled/json.odin
@@ -0,0 +1,208 @@
+package tiled
+
+Json_Map :: struct {
+ backgroundcolor: string,
+ class: string,
+ compressionlevel: i32,
+ width: i32,
+ height: i32,
+ infinite: bool,
+ layers: []Json_Layer,
+ nextlayerid: i32,
+ nextobjectid: i32,
+ orientation: string,
+ parallaxoriginx: f64,
+ parallaxoriginy: f64,
+ properties: []Json_Property,
+ renderorder: string,
+ staggeraxis: string,
+ staggerindex: string,
+ tilewidth: i32,
+ tileheight: i32,
+ tilesets: []Json_Tile_Set,
+}
+
+Json_Layer :: struct {
+ class: string,
+ compression: string,
+ data: union {
+ []i32,
+ string,
+ },
+ draworder: string,
+ encoding: string,
+ id: i32,
+ image: string,
+ image_width: i32,
+ image_height: i32,
+ layers: []Json_Layer,
+ // locked: bool,
+ name: string,
+ objects: []Json_Object,
+ offsetx: f64,
+ offsety: f64,
+ opacity: f32,
+ parallaxx: f64,
+ parallaxy: f64,
+ properties: []Json_Property,
+ repeatx: bool,
+ repeaty: bool,
+ tintcolor: string,
+ transparentcolor: string,
+ type: string,
+ visible: bool,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+}
+
+Json_Object :: struct {
+ ellipse: bool,
+ gid: i32,
+ width: f64,
+ height: f64,
+ id: i32,
+ name: string,
+ point: bool,
+ polygon: []Json_Point,
+ polyline: []Json_Point,
+ properties: []Json_Property,
+ rotation: f64,
+ template: string,
+ text: Maybe(struct {
+ fontfamily: string,
+ color: string,
+ text: string,
+ halign: string,
+ valign: string,
+ pixelsize: i32,
+ wrap: bool,
+ underline: bool,
+ bold: bool,
+ italic: bool,
+ kerning: bool,
+ strikeout: bool,
+ }),
+ type: string,
+ visible: bool,
+ x: f64,
+ y: f64,
+}
+
+Json_Tile_Set :: struct {
+ background_color: string,
+ class: string,
+ rows: i32,
+ columns: i32,
+ fillmode: string,
+ firstgid: i32,
+ grid: Maybe(Json_Grid),
+ image: string,
+ imagewidth: i32,
+ imageheight: i32,
+ margin: i32,
+ name: string,
+ objectalignment: string,
+ properties: []Json_Property,
+ source: string,
+ spacing: i32,
+ terrains: []Json_Terrain,
+ tilecount: i32,
+ tilewidth: i32,
+ tileheight: i32,
+ tileoffset: Json_Tile_Offset,
+ tilerendersize: string,
+ tiles: []Json_Tile,
+ transformations: Json_Transformations,
+ transparentcolor: string,
+ type: string,
+ wangsets: []Json_Wang_Set,
+}
+
+Json_Grid :: struct {
+ width: i32,
+ height: i32,
+ orientation: string,
+}
+
+Json_Tile_Offset :: struct {
+ x: i32,
+ y: i32,
+}
+
+Json_Transformations :: struct {
+ hflip: bool,
+ vflip: bool,
+ rotate: bool,
+ preferuntransformed: bool,
+}
+
+Json_Tile :: struct {
+ animation: []Json_Frame,
+ id: i32,
+ image: string,
+ imagewidth: i32,
+ imageheight: i32,
+ x: i32,
+ y: i32,
+ width: i32,
+ height: i32,
+ objectgroup: Json_Layer,
+ probability: f64,
+ properties: []Json_Property,
+ terrain: []i32,
+ type: string,
+}
+
+Json_Frame :: struct {
+ duration: i32,
+ tileid: i32,
+}
+
+Json_Terrain :: struct {
+ name: string,
+ properties: []Json_Property,
+ tile: i32,
+}
+
+Json_Wang_Set :: struct {
+ class: string,
+ colors: []Json_Wang_Color,
+ name: string,
+ properties: []Json_Property,
+ tile: i32,
+ type: string,
+ wangtiles: []Json_Wang_Tile,
+}
+
+Json_Wang_Color :: struct {
+ class: string,
+ color: string,
+ name: string,
+ probability: f64,
+ properties: []Json_Property,
+ tile: i32,
+}
+
+Json_Wang_Tile :: struct {
+ tileid: i32,
+ wangid: []u8,
+}
+
+Json_Object_Template :: struct {
+ tileset: Maybe(Json_Tile_Set),
+ object: Json_Object,
+}
+
+Json_Property :: struct {
+ name: string,
+ type: string,
+ propertytype: string,
+ value: any,
+}
+
+Json_Point :: struct {
+ x: f64,
+ y: f64,
+}
diff --git a/src/tiled/tiled.odin b/src/tiled/tiled.odin
new file mode 100644
index 0000000..e1627a5
--- /dev/null
+++ b/src/tiled/tiled.odin
@@ -0,0 +1,585 @@
+package tiled
+
+import os "core:os/os2"
+import mem "core:mem"
+import strings "core:strings"
+import "core:path/filepath"
+import "core:log"
+import "core:encoding/json"
+
+import rl "vendor:raylib"
+
+Color :: [4]f32
+
+Error :: enum {
+ NONE = 0,
+ COULD_NOT_LOAD,
+ NON_ORTHOGONAL_NOT_SUPPORTED,
+ INFINITE_NOT_SUPPORTED,
+ ELLIPSE_OBJ_NOT_SUPPORTED,
+ POLYGON_OBJ_NOT_SUPPORTED,
+ POLYLINE_OBJ_NOT_SUPPORTED,
+ TEXT_OBJ_NOT_SUPPORTED,
+}
+
+Axis :: enum {
+ X,
+ Y,
+}
+
+Properties :: map[string]any
+
+Map :: struct {
+ // Currently, only orthogonal maps are allowed. Any non-orthogonal maps
+ // throw an error.
+ // Infinite maps will also throw an error for now.
+
+ background_color: Color,
+ class: string,
+
+ width: i32,
+ height: i32,
+
+ layers: []Layer,
+
+ next_layer_id: i32,
+ next_object_id: i32,
+
+ parallax_origin_x: f64,
+ parallax_origin_y: f64,
+
+ properties: Properties,
+
+ // stagger_axis: Axis,
+ // stagger_offset: i32, // To replace stagger index.
+
+ tile_width: i32,
+ tile_height: i32,
+
+ tile_sets: []Tile_Set,
+
+ tiles: [dynamic]Tile,
+}
+
+Layer :: struct {
+ // Layers that aren't visible are entirely ignored.
+ name: string,
+ properties: Properties,
+
+ parallax_x: f64,
+ parallax_y: f64,
+
+ tint: Color,
+ id: i32,
+
+ layer: union #no_nil {
+ Tile_Layer,
+ Image_Layer,
+ Object_Layer,
+ }
+}
+
+Tile_Layer :: struct {
+ // Data will automatically become uncompressed.
+ width: i32,
+ height: i32,
+ data: []i32,
+}
+
+Image_Layer :: struct {
+ texture: rl.Texture2D,
+ transparent_color: Color,
+ repeat_x: bool,
+ repeat_y: bool,
+}
+
+Object_Layer :: []Object
+
+Object_Type :: enum {
+ POINT,
+ TILE,
+ RECT,
+}
+
+Object :: struct {
+ // For now, anything that *isn't* one of these is ignored and
+ // a warning is thrown:
+ // - Point
+ // - Tile
+ // - Rectangle
+ //
+ // Objects that aren't visible will be ignored entirely.
+
+ type: Object_Type,
+ name: string,
+ class: string,
+ properties: Properties,
+ position: [2]f32,
+ size: [2]f32,
+ tile_id: i32,
+}
+
+Tile_Set :: struct {
+ // For now, any autotiling information is ignored.
+
+ properties: Properties,
+
+ rows: i32,
+ columns: i32,
+
+ first_gid: i32,
+
+ texture: rl.Texture2D,
+ margin: i32,
+ spacing: i32,
+
+ tile_count: i32,
+ tile_width: i32,
+ tile_height: i32,
+ tile_offset: [2]i32, // in pixels
+
+ tiles: []Tile,
+}
+
+Tile :: struct {
+ // For now, animations are ignored. Support in the future is somewhat likely.
+ // And similar to above, autotiling information is ignored.
+
+ is_image_collection: bool,
+
+ tile_set: i32, // index for the tile set in Map
+
+ id: i32,
+ gid: i32,
+
+ texture: Maybe(rl.Texture2D), // If the tile is part of an image collection.
+
+ // Where on the texture is this tile
+ src_start: [2]i32,
+ src_size: [2]i32,
+
+ object_group: Layer, // Object layer for collisions.
+
+ properties: Properties,
+}
+
+load_map :: proc(path: string) -> (Map, Error) {
+ defer free_all(context.temp_allocator)
+
+ jmap_text, read_err := os.read_entire_file(path, context.temp_allocator)
+ if read_err != nil {
+ log.errorf("Failed to read file %v (%v)", path, read_err)
+ return {}, .COULD_NOT_LOAD
+ }
+
+ jmap: Json_Map
+ unmarshal_err := json.unmarshal(
+ jmap_text,
+ &jmap,
+ allocator = context.temp_allocator,
+ )
+ if unmarshal_err != nil {
+ log.errorf("Failed to unmarshal file %v (%v)", path, unmarshal_err)
+ return {}, .COULD_NOT_LOAD
+ }
+
+ log.info(jmap)
+
+ tmap: Map
+ err := convert_json_map(jmap, &tmap, path)
+ if err != .NONE {
+ return {}, err
+ }
+ return tmap, .NONE
+}
+
+delete_map :: proc(tmap: Map) {
+ delete(tmap.class)
+ delete(tmap.properties)
+
+ for layer in tmap.layers {
+ delete_layer(layer)
+ }
+ delete(tmap.layers)
+
+ for tile_set in tmap.tile_sets {
+ delete(tile_set.properties)
+ delete(tile_set.tiles) // all tiles are deleted later
+ rl.UnloadTexture(tile_set.texture)
+ }
+ delete(tmap.tile_sets)
+
+ for tile in tmap.tiles {
+ delete(tile.properties)
+
+ if tile.texture != nil {
+ rl.UnloadTexture(tile.texture.(rl.Texture2D))
+ }
+
+ delete_layer(tile.object_group)
+ }
+ delete(tmap.tiles)
+}
+
+@(private)
+delete_layer :: proc(layer: Layer) {
+ delete(layer.name)
+ delete(layer.properties)
+
+ switch l in layer.layer {
+ case Tile_Layer:
+ delete(l.data)
+ case Image_Layer:
+ rl.UnloadTexture(l.texture)
+ case Object_Layer:
+ for obj in l {
+ delete(obj.name)
+ delete(obj.class)
+ delete(obj.properties)
+ }
+ delete(l)
+ }
+}
+
+@(private)
+convert_json_color :: proc(jcolor: string) -> Color {
+ return Color{1, 1, 1, 1}
+}
+
+@(private)
+convert_json_properties :: proc(jprops: []Json_Property) -> Properties {
+ props := make(Properties)
+
+ for jprop in jprops {
+ props[jprop.name] = jprop.value
+ }
+
+ return props
+}
+
+@(private)
+convert_json_map :: proc(jmap: Json_Map, tmap: ^Map, path: string) -> Error {
+ // ensure the map is orthogonal
+ if strings.compare(jmap.orientation, "orthogonal") != 0 {
+ return .NON_ORTHOGONAL_NOT_SUPPORTED
+ }
+
+ // ensure the map is not infinite
+ if jmap.infinite {
+ return .INFINITE_NOT_SUPPORTED
+ }
+
+ tmap.background_color = convert_json_color(jmap.backgroundcolor)
+ tmap.class = strings.clone(jmap.class)
+
+ tmap.width = jmap.width
+ tmap.height = jmap.height
+
+ tmap.tile_width = jmap.tilewidth
+ tmap.tile_height = jmap.tileheight
+
+ tmap.next_layer_id = jmap.nextlayerid
+ tmap.next_object_id = jmap.nextobjectid
+
+ tmap.parallax_origin_x = jmap.parallaxoriginx
+ tmap.parallax_origin_y = jmap.parallaxoriginy
+
+ tmap.properties = convert_json_properties(jmap.properties)
+
+ // TODO: flatten groups
+ layer_count := len(jmap.layers)
+ for jlayer in jmap.layers {
+ layer_count += len(jlayer.layers) // take into consideration groups
+ }
+
+ tmap.layers = make([]Layer, layer_count)
+ current_index := 0
+
+ for jlayer in jmap.layers {
+ if !jlayer.visible {
+ continue // ignore hidden layers
+ }
+
+ // FIXME: Some duplicated code down here
+ if strings.compare(jlayer.type, "group") == 0 {
+ for jlayer2 in jlayer.layers {
+ if !jlayer2.visible {
+ continue // ignore hidden layers
+ }
+
+ layer, err := convert_json_layer(jlayer2)
+ if err != .NONE {
+ return err
+ }
+ tmap.layers[current_index] = layer
+ current_index += 1
+ }
+ continue
+ }
+
+ layer, err := convert_json_layer(jlayer)
+ if err != .NONE {
+ return err
+ }
+ tmap.layers[current_index] = layer
+ current_index += 1
+ }
+
+ tmap.tile_sets = make([]Tile_Set, len(jmap.tilesets))
+
+ res_dir := filepath.dir(path, allocator = context.temp_allocator)
+
+ for jtile_set, i in jmap.tilesets {
+ tile_set, err := convert_json_tile_set(tmap, res_dir, jtile_set)
+ if err != .NONE {
+ return err
+ }
+ tmap.tile_sets[i] = tile_set
+
+ for j in int(tile_set.first_gid)..<len(tmap.tiles) {
+ tile_set.tiles[j].tile_set = i32(i)
+ }
+ }
+
+ return .NONE
+}
+
+draw_map :: proc(tmap: Map) {
+ for layer in tmap.layers {
+ tile_layer, ok := layer.layer.(Tile_Layer)
+ if !ok {
+ continue
+ }
+
+ x: i32 = 0
+ y: i32 = 0
+
+ for cell in tile_layer.data {
+ if cell != 0 {
+ tile := tmap.tiles[cell - 1]
+ tile_set := tmap.tile_sets[tile.tile_set]
+
+ rl.DrawTexturePro(
+ tile_set.texture,
+ rl.Rectangle{
+ x = f32(tile.src_start.x),
+ y = f32(tile.src_start.y),
+ width = f32(tile.src_size.x),
+ height = f32(tile.src_size.y),
+ },
+ rl.Rectangle{
+ x = f32(x),
+ y = f32(y),
+ width = f32(tile.src_size.x),
+ height = f32(tile.src_size.y),
+ },
+ {0, 0},
+ 0,
+ rl.WHITE,
+ )
+ }
+
+ x += tmap.tile_width
+ if x > (tmap.width - 1) * tmap.tile_width {
+ x = 0
+ y += tmap.tile_height
+ }
+ }
+ }
+}
+
+@(private)
+load_image :: proc(filename: string, res_dir: string) -> rl.Texture2D {
+ path := strings.concatenate({res_dir, "/", filename})
+ defer delete(path)
+
+ byte_path := make([]u8, len(path) + 1, context.temp_allocator)
+ copy(byte_path, path)
+ byte_path[len(path)] = 0
+ return rl.LoadTexture(transmute(cstring)&byte_path[0])
+}
+
+@(private)
+convert_json_layer :: proc(jlayer: Json_Layer) -> (Layer, Error) {
+ // TODO: uncompress tile data
+
+ layer: Layer
+
+ layer.name = strings.clone(jlayer.name)
+
+ layer.parallax_x = jlayer.parallaxx
+ layer.parallax_y = jlayer.parallaxy
+
+ layer.id = jlayer.id
+
+ layer.tint = convert_json_color(jlayer.tintcolor)
+
+ layer.properties = convert_json_properties(jlayer.properties)
+
+ switch jlayer.type {
+ case "tilelayer":
+ tile_layer := Tile_Layer{
+ width = jlayer.width,
+ height = jlayer.height,
+ }
+
+ data := jlayer.data.([]i32)
+ tile_layer.data = make([]i32, len(data))
+ copy(tile_layer.data, data)
+
+ layer.layer = tile_layer
+
+ case "objectgroup":
+ obj_layer := make(Object_Layer, len(jlayer.objects))
+
+ for jobj, i in jlayer.objects {
+ if !jobj.visible {
+ continue // ignore hidden objects
+ }
+
+ obj, err := convert_json_object(jobj)
+ if err != .NONE {
+ return {}, err
+ }
+ obj_layer[i] = obj
+ }
+
+ layer.layer = obj_layer
+
+ case "imagelayer":
+ image_layer := Image_Layer{
+ texture = load_image(jlayer.image, "res"),
+ transparent_color = convert_json_color(jlayer.transparentcolor),
+ repeat_x = jlayer.repeatx,
+ repeat_y = jlayer.repeaty,
+ }
+
+ layer.layer = image_layer
+
+ case "group":
+ // groups are handled elsewhere
+ }
+
+ return layer, .NONE
+}
+
+@(private)
+convert_json_object :: proc(jobj: Json_Object) -> (Object, Error) {
+ obj: Object
+
+ if jobj.ellipse {
+ return {}, .ELLIPSE_OBJ_NOT_SUPPORTED
+ }
+ if jobj.polygon != nil {
+ return {}, .POLYGON_OBJ_NOT_SUPPORTED
+ }
+ if jobj.polyline != nil {
+ return {}, .POLYLINE_OBJ_NOT_SUPPORTED
+ }
+ if jobj.text != nil {
+ return {}, .TEXT_OBJ_NOT_SUPPORTED
+ }
+
+ obj.position = {
+ f32(jobj.x),
+ f32(jobj.y),
+ }
+ obj.size = {
+ f32(jobj.width),
+ f32(jobj.height),
+ }
+ obj.tile_id = jobj.gid
+
+ obj.name = strings.clone(jobj.name)
+ obj.class = strings.clone(jobj.type)
+ obj.properties = convert_json_properties(jobj.properties)
+
+ return obj, .NONE
+}
+
+@(private)
+convert_json_tile_set :: proc(
+ tmap: ^Map,
+ res_dir: string,
+ jtile_set: Json_Tile_Set,
+) -> (Tile_Set, Error) {
+ if len(jtile_set.source) > 0 {
+ // Tileset links somewhere else
+ path := strings.concatenate({res_dir, "/", jtile_set.source})
+ defer delete(path)
+
+ data, err := os.read_entire_file(path, allocator = context.temp_allocator)
+ if err != nil {
+ log.errorf("Could not load external tile set '%v'", path)
+ return {}, .COULD_NOT_LOAD
+ }
+
+ new_jtile_set: Json_Tile_Set
+ marshal_err := json.unmarshal(
+ data,
+ &new_jtile_set,
+ allocator = context.temp_allocator,
+ )
+ if marshal_err != nil {
+ log.errorf("Could not unmarshal external tile set '%v'", path)
+ return {}, .COULD_NOT_LOAD
+ }
+
+ return convert_json_tile_set(tmap, res_dir, new_jtile_set)
+ }
+
+ tile_set: Tile_Set
+
+ tile_set.rows = jtile_set.rows
+ tile_set.columns = jtile_set.columns
+
+ tile_set.first_gid = jtile_set.firstgid
+
+ tile_set.texture = load_image(jtile_set.image, res_dir)
+ tile_set.margin = jtile_set.margin
+ tile_set.spacing = jtile_set.spacing
+
+ tile_set.tile_count = jtile_set.tilecount
+ tile_set.tile_width = jtile_set.tilewidth
+ tile_set.tile_height = jtile_set.tileheight
+ tile_set.tile_offset = {
+ jtile_set.tileoffset.x,
+ jtile_set.tileoffset.y,
+ }
+
+ tile_set.properties = convert_json_properties(jtile_set.properties)
+
+ tile_set.tiles = make([]Tile, jtile_set.tilecount)
+
+ // Ignore margin and spacing for now
+ // TODO: account for margin and spacing
+ tile_x := tile_set.texture.width / tile_set.tile_width
+ tile_y := tile_set.texture.height / tile_set.tile_height
+
+ current_index := 0
+ for y in 0..<tile_y {
+ for x in 0..<tile_x {
+ tile := &tile_set.tiles[current_index]
+
+ tile.id = i32(current_index)
+
+ tile.src_start = {
+ x * tile_set.tile_width,
+ y * tile_set.tile_height,
+ }
+ tile.src_size = {
+ tile_set.tile_width,
+ tile_set.tile_height,
+ }
+
+ tile.gid = i32(len(tmap.tiles))
+ append(&tmap.tiles, tile^)
+
+ current_index += 1
+ }
+ }
+
+ // TODO: here, go through jtile_set.tiles and load in more tile data
+
+ return tile_set, .NONE
+}