game-2026-01-18/libs/tiled/src/layer.zig

465 lines
13 KiB
Zig

const std = @import("std");
const Property = @import("./property.zig");
const xml = @import("./xml.zig");
const Color = @import("./color.zig");
const Object = @import("./object.zig");
const Position = @import("./position.zig");
const Layer = @This();
pub const Bounds = struct {
left: i32,
right: i32,
top: i32,
bottom: i32,
pub const zero = Bounds{
.left = 0,
.right = 0,
.top = 0,
.bottom = 0,
};
pub fn initFromRect(x: i32, y: i32, width: i32, height: i32) Bounds {
return Bounds{
.left = x,
.right = x + width,
.top = y,
.bottom = y + height,
};
}
pub fn getWidth(self: Bounds) u32 {
return @intCast(self.right - self.left + 1);
}
pub fn getHeight(self: Bounds) u32 {
return @intCast(self.bottom - self.top + 1);
}
pub fn combine(lhs: Bounds, rhs: Bounds) Bounds {
return Bounds{
.left = @min(lhs.left, rhs.left),
.right = @max(lhs.right, rhs.right),
.top = @min(lhs.top, rhs.top),
.bottom = @max(lhs.bottom, rhs.bottom)
};
}
};
pub const TileVariant = struct {
pub const Chunk = struct {
x: i32,
y: i32,
width: u32,
height: u32,
data: []u32,
pub fn getBounds(self: Chunk) Bounds {
return Bounds.initFromRect(self.x, self.y, @intCast(self.width), @intCast(self.height));
}
};
pub const Data = union(enum) {
fixed: []u32,
chunks: []Chunk
};
const Encoding = enum {
csv,
const map: std.StaticStringMap(Encoding) = .initComptime(.{
.{ "csv", .csv },
});
};
width: u32,
height: u32,
data: Data,
fn initChunkDataFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
encoding: Encoding
) !Chunk {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("chunk");
const x = try attrs.getNumber(i32, "x") orelse return error.MissingAttribute;
const y = try attrs.getNumber(i32, "y") orelse return error.MissingAttribute;
const width = try attrs.getNumber(u32, "width") orelse return error.MissingAttribute;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingAttribute;
var temp_tiles: std.ArrayList(u32) = .empty;
while (try iter.next()) |node| {
if (node == .text) {
const encoded_tiles = try lexer.nextExpectText();
const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles);
try temp_tiles.appendSlice(scratch.allocator(), tiles.items);
continue;
}
try iter.skip();
}
try iter.finish("chunk");
return Chunk{
.x = x,
.y = y,
.width = width,
.height = height,
.data = try arena.dupe(u32, temp_tiles.items)
};
}
fn initDataFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Data {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("data");
const encoding = try attrs.getEnum(Encoding, "encoding", Encoding.map) orelse .csv;
// TODO: compression
var temp_chunks: std.ArrayList(Chunk) = .empty;
var temp_tiles: std.ArrayList(u32) = .empty;
while (try iter.next()) |node| {
if (node == .text) {
const encoded_tiles = try lexer.nextExpectText();
const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles);
try temp_tiles.appendSlice(scratch.allocator(), tiles.items);
} else if (node.isTag("chunk")) {
const chunk = try initChunkDataFromXml(arena, scratch, lexer, encoding);
try temp_chunks.append(scratch.allocator(), chunk);
continue;
}
try iter.skip();
}
try iter.finish("data");
if (temp_chunks.items.len > 0) {
return .{
.chunks = try arena.dupe(Chunk, temp_chunks.items)
};
} else {
return .{
.fixed = try arena.dupe(u32, temp_tiles.items)
};
}
}
fn parseEncoding(
encoding: Encoding,
allocator: std.mem.Allocator,
text: []const u8
) !std.ArrayList(u32) {
return switch (encoding) {
.csv => try parseCSVEncoding(allocator, text)
};
}
fn parseCSVEncoding(
allocator: std.mem.Allocator,
text: []const u8
) !std.ArrayList(u32) {
var result: std.ArrayList(u32) = .empty;
var split_iter = std.mem.splitScalar(u8, text, ',');
while (split_iter.next()) |raw_tile_id| {
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace);
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
try result.append(allocator, tile_id);
}
return result;
}
pub fn getBounds(self: TileVariant) Bounds {
if (self.data == .fixed) {
return Bounds.initFromRect(0, 0, @intCast(self.width), @intCast(self.height));
} else if (self.data == .chunks) {
const chunks = self.data.chunks;
var result: Bounds = .zero;
if (chunks.len > 0) {
result = chunks[0].getBounds();
for (chunks[1..]) |chunk| {
result = result.combine(chunk.getBounds());
}
}
return result;
} else {
unreachable;
}
}
pub fn get(self: TileVariant, x: i32, y: i32) ?u32 {
if (self.data == .fixed) {
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
const x_u32: u32 = @intCast(x);
const y_u32: u32 = @intCast(y);
return self.data.fixed[y_u32 * self.width + x_u32];
}
} else if (self.data == .chunks) {
for (self.data.chunks) |chunk| {
const ox = x - chunk.x;
const oy = y - chunk.y;
if ((0 <= ox and ox < chunk.width) and (0 <= oy and oy < chunk.height)) {
const ox_u32: u32 = @intCast(ox);
const oy_u32: u32 = @intCast(oy);
return chunk.data[oy_u32 * chunk.width + ox_u32];
}
}
} else {
unreachable;
}
return null;
}
};
pub const ImageVariant = struct {
pub const Image = struct {
// TODO: format
source: []const u8,
transparent_color: ?Color,
width: ?u32,
height: ?u32,
fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("image");
// TODO: format
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
const width = try attrs.getNumber(u32, "width") orelse null;
const height = try attrs.getNumber(u32, "height") orelse null;
const transparent_color = try attrs.getColor("trans", false);
try iter.finish("image");
return Image{
.source = source,
.transparent_color = transparent_color,
.width = width,
.height = height
};
}
};
repeat_x: bool,
repeat_y: bool,
image: ?Image
};
pub const ObjectVariant = struct {
pub const DrawOrder = enum {
top_down,
index,
const map: std.StaticStringMap(DrawOrder) = .initComptime(.{
.{ "topdown", .top_down },
.{ "index", .index },
});
};
color: ?Color,
draw_order: DrawOrder,
items: []Object
};
pub const GroupVariant = struct {
layers: []Layer
};
pub const Type = enum {
tile,
object,
image,
group,
const name_map: std.StaticStringMap(Type) = .initComptime(.{
.{ "layer", .tile },
.{ "objectgroup", .object },
.{ "imagelayer", .image },
.{ "group", .group }
});
fn toXmlName(self: Type) []const u8 {
return switch (self) {
.tile => "layer",
.object => "objectgroup",
.image => "imagelayer",
.group => "group",
};
}
};
pub const Variant = union(Type) {
tile: TileVariant,
object: ObjectVariant,
image: ImageVariant,
group: GroupVariant
};
id: u32,
name: []const u8,
class: []const u8,
opacity: f32,
visible: bool,
tint_color: ?Color,
offset_x: f32,
offset_y: f32,
parallax_x: f32,
parallax_y: f32,
properties: Property.List,
variant: Variant,
pub fn initFromXml(
arena: std.mem.Allocator,
scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) !Layer {
const value = try lexer.peek() orelse return error.MissingStartTag;
if (value != .start_tag) return error.MissingStartTag;
var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType;
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin(layer_type.toXmlName());
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse "";
const class = try attrs.getDupe(arena, "class") orelse "";
const opacity = try attrs.getNumber(f32, "opacity") orelse 1;
const visible = try attrs.getBool("visible", "1", "0") orelse true;
const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0;
const offset_y = try attrs.getNumber(f32, "offsety") orelse 0;
const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1;
const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1;
const tint_color = try attrs.getColor("tintcolor", true);
var variant: Variant = undefined;
switch (layer_type) {
.tile => {
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
variant = .{
.tile = TileVariant{
.width = width,
.height = height,
.data = .{ .fixed = &[0]u32{} }
}
};
},
.image => {
const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false;
const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false;
variant = .{
.image = ImageVariant{
.repeat_x = repeat_x,
.repeat_y = repeat_y,
.image = null
}
};
},
.object => {
const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down;
const color = try attrs.getColor("color", true);
variant = .{
.object = ObjectVariant{
.color = color,
.draw_order = draw_order,
.items = &.{}
}
};
},
.group => {
variant = .{
.group = .{
.layers = &.{}
}
};
},
}
var properties: Property.List = .empty;
var objects: std.ArrayList(Object) = .empty;
var layers: std.ArrayList(Layer) = .empty;
while (try iter.next()) |node| {
if (node.isTag("properties")) {
properties = try Property.List.initFromXml(arena, scratch, lexer);
continue;
}
if (variant == .tile and node.isTag("data")) {
variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer);
continue;
}
if (variant == .image and node.isTag("image")) {
variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer);
continue;
}
if (variant == .object and node.isTag("object")) {
const object = try Object.initFromXml(arena, scratch, lexer);
try objects.append(scratch.allocator(), object);
continue;
}
if (variant == .group and isLayerNode(node)) {
const layer = try initFromXml(arena, scratch, lexer);
try layers.append(scratch.allocator(), layer);
continue;
}
try iter.skip();
}
try iter.finish(layer_type.toXmlName());
if (variant == .object) {
variant.object.items = try arena.dupe(Object, objects.items);
}
if (variant == .group) {
variant.group.layers = try arena.dupe(Layer, layers.items);
}
return Layer{
.id = id,
.name = name,
.class = class,
.opacity = opacity,
.visible = visible,
.tint_color = tint_color,
.offset_x = offset_x,
.offset_y = offset_y,
.parallax_x = parallax_x,
.parallax_y = parallax_y,
.properties = properties,
.variant = variant,
};
}
pub fn isLayerNode(node: xml.TagParser.Node) bool {
return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group");
}