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..