465 lines
13 KiB
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");
|
|
}
|