From 2838358993ade2ac1b770d675af9126749074e8f Mon Sep 17 00:00:00 2001 From: Xander Swan Date: Tue, 23 Dec 2025 20:58:32 -0500 Subject: Fix lotsa memory leaks mhm --- src/tiled/tiled.odin | 585 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 585 insertions(+) create mode 100644 src/tiled/tiled.odin (limited to 'src/tiled/tiled.odin') 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).. (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..