diff options
Diffstat (limited to 'src/tiled')
| -rw-r--r-- | src/tiled/json.odin | 208 | ||||
| -rw-r--r-- | src/tiled/tiled.odin | 585 |
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 +} |
