add tileset reading
This commit is contained in:
parent
1e072c5607
commit
62cbb9e380
76
assets/tiled/map.tmx
Normal file
76
assets/tiled/map.tmx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="0" nextlayerid="5" nextobjectid="1">
|
||||||
|
<tileset firstgid="1" source="tileset.tsx"/>
|
||||||
|
<layer id="2" name="Ground" width="30" height="20">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,90,90,90,90,90,90,90,90,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="1" name="Walls" width="30" height="20">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,98,101,98,98,98,98,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,0,0,98,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,0,0,0,0,0,0,98,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,0,0,0,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,0,0,98,0,0,0,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,98,98,98,98,98,101,101,101,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
<layer id="4" name="Pushable" width="30" height="20">
|
||||||
|
<data encoding="csv">
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,7,0,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,7,0,9,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||||
|
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||||
|
</data>
|
||||||
|
</layer>
|
||||||
|
</map>
|
||||||
14
assets/tiled/sokoban.tiled-project
Normal file
14
assets/tiled/sokoban.tiled-project
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"automappingRulesFile": "",
|
||||||
|
"commands": [
|
||||||
|
],
|
||||||
|
"compatibilityVersion": 1100,
|
||||||
|
"extensionsPath": "extensions",
|
||||||
|
"folders": [
|
||||||
|
"."
|
||||||
|
],
|
||||||
|
"properties": [
|
||||||
|
],
|
||||||
|
"propertyTypes": [
|
||||||
|
]
|
||||||
|
}
|
||||||
21
assets/tiled/sokoban.tiled-session
Normal file
21
assets/tiled/sokoban.tiled-session
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"activeFile": "",
|
||||||
|
"expandedProjectPaths": [
|
||||||
|
],
|
||||||
|
"fileStates": {
|
||||||
|
},
|
||||||
|
"last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
|
||||||
|
"map.lastUsedFormat": "tmx",
|
||||||
|
"map.tileHeight": 64,
|
||||||
|
"map.tileWidth": 64,
|
||||||
|
"openFiles": [
|
||||||
|
],
|
||||||
|
"project": "sokoban.tiled-project",
|
||||||
|
"recentFiles": [
|
||||||
|
],
|
||||||
|
"tileset.lastUsedFormat": "tsx",
|
||||||
|
"tileset.tileSize": {
|
||||||
|
"height": 64,
|
||||||
|
"width": 64
|
||||||
|
}
|
||||||
|
}
|
||||||
4
assets/tiled/tileset.tsx
Normal file
4
assets/tiled/tileset.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<tileset version="1.10" tiledversion="1.12.2" name="tileset" tilewidth="64" tileheight="64" tilecount="104" columns="13">
|
||||||
|
<image source="../sokoban/sokoban_tilesheet.png" width="832" height="512"/>
|
||||||
|
</tileset>
|
||||||
@ -8,7 +8,9 @@ pub fn build(b: *std.Build) !void {
|
|||||||
const assets_dir = "assets";
|
const assets_dir = "assets";
|
||||||
const asset_files = [_][]const u8{
|
const asset_files = [_][]const u8{
|
||||||
"sokoban/sokoban_tilesheet.png",
|
"sokoban/sokoban_tilesheet.png",
|
||||||
"roboto-font/Roboto-Regular.ttf"
|
"roboto-font/Roboto-Regular.ttf",
|
||||||
|
"tiled/map.tmx",
|
||||||
|
"tiled/tileset.tsx"
|
||||||
};
|
};
|
||||||
|
|
||||||
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
|
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
|
||||||
@ -42,6 +44,9 @@ pub fn build(b: *std.Build) !void {
|
|||||||
});
|
});
|
||||||
exe_mod.addImport("math", math_mod);
|
exe_mod.addImport("math", math_mod);
|
||||||
|
|
||||||
|
const dep_tiled = b.dependency("tiled", .{});
|
||||||
|
exe_mod.addImport("tiled", dep_tiled.module("tiled"));
|
||||||
|
|
||||||
const dep_sokol = b.dependency("sokol", .{
|
const dep_sokol = b.dependency("sokol", .{
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
|||||||
@ -11,6 +11,9 @@
|
|||||||
.stb = .{
|
.stb = .{
|
||||||
.path = "./libs/stb",
|
.path = "./libs/stb",
|
||||||
},
|
},
|
||||||
|
.tiled = .{
|
||||||
|
.path = "./libs/tiled",
|
||||||
|
},
|
||||||
.tracy = .{
|
.tracy = .{
|
||||||
.path = "./libs/tracy",
|
.path = "./libs/tracy",
|
||||||
},
|
},
|
||||||
|
|||||||
29
libs/tiled/build.zig
Normal file
29
libs/tiled/build.zig
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
pub fn build(b: *std.Build) !void {
|
||||||
|
const target = b.standardTargetOptions(.{});
|
||||||
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
|
const mod = b.addModule("tiled", .{
|
||||||
|
.target = target,
|
||||||
|
.optimize = optimize,
|
||||||
|
.root_source_file = b.path("src/root.zig")
|
||||||
|
});
|
||||||
|
|
||||||
|
const lib = b.addLibrary(.{
|
||||||
|
.name = "tiled",
|
||||||
|
.root_module = mod
|
||||||
|
});
|
||||||
|
b.installArtifact(lib);
|
||||||
|
|
||||||
|
{
|
||||||
|
const tests = b.addTest(.{
|
||||||
|
.root_module = mod
|
||||||
|
});
|
||||||
|
|
||||||
|
const run_tests = b.addRunArtifact(tests);
|
||||||
|
|
||||||
|
const test_step = b.step("test", "Run tests");
|
||||||
|
test_step.dependOn(&run_tests.step);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
libs/tiled/src/buffers.zig
Normal file
18
libs/tiled/src/buffers.zig
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Position = @import("./position.zig");
|
||||||
|
|
||||||
|
const Buffers = @This();
|
||||||
|
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
points: std.ArrayList(Position) = .empty,
|
||||||
|
|
||||||
|
pub fn init(gpa: std.mem.Allocator) Buffers {
|
||||||
|
return Buffers{
|
||||||
|
.allocator = gpa
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Buffers) void {
|
||||||
|
self.points.deinit(self.allocator);
|
||||||
|
}
|
||||||
52
libs/tiled/src/color.zig
Normal file
52
libs/tiled/src/color.zig
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Color = @This();
|
||||||
|
|
||||||
|
r: u8,
|
||||||
|
g: u8,
|
||||||
|
b: u8,
|
||||||
|
a: u8,
|
||||||
|
|
||||||
|
pub const black = Color{
|
||||||
|
.r = 0,
|
||||||
|
.g = 0,
|
||||||
|
.b = 0,
|
||||||
|
.a = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn parse(str: []const u8, hash_required: bool) !Color {
|
||||||
|
var color = Color{
|
||||||
|
.r = undefined,
|
||||||
|
.g = undefined,
|
||||||
|
.b = undefined,
|
||||||
|
.a = 255,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (str.len < 1) {
|
||||||
|
return error.InvalidColorFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const has_hash = str[0] == '#';
|
||||||
|
if (hash_required and !has_hash) {
|
||||||
|
return error.InvalidColorFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hex_str = if (has_hash) str[1..] else str;
|
||||||
|
|
||||||
|
if (hex_str.len == 6) {
|
||||||
|
color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||||
|
color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||||
|
color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||||
|
|
||||||
|
} else if (hex_str.len == 8) {
|
||||||
|
color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||||
|
color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||||
|
color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||||
|
color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return error.InvalidColorFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
return color;
|
||||||
|
}
|
||||||
15
libs/tiled/src/global_tile_id.zig
Normal file
15
libs/tiled/src/global_tile_id.zig
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
pub const Flag = enum(u32) {
|
||||||
|
flipped_horizontally = 1 << 31, // bit 32
|
||||||
|
flipped_vertically = 1 << 30, // bit 31
|
||||||
|
flipped_diagonally = 1 << 29, // bit 30
|
||||||
|
rotated_hexagonal_120 = 1 << 28, // bit 29
|
||||||
|
_,
|
||||||
|
|
||||||
|
pub const clear: u32 = ~(
|
||||||
|
@intFromEnum(Flag.flipped_horizontally) |
|
||||||
|
@intFromEnum(Flag.flipped_vertically) |
|
||||||
|
@intFromEnum(Flag.flipped_diagonally) |
|
||||||
|
@intFromEnum(Flag.rotated_hexagonal_120)
|
||||||
|
);
|
||||||
|
};
|
||||||
283
libs/tiled/src/layer.zig
Normal file
283
libs/tiled/src/layer.zig
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
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 TileVariant = struct {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
data: []u32,
|
||||||
|
|
||||||
|
fn initDataFromXml(
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer,
|
||||||
|
) ![]u32 {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const attrs = try iter.begin("data");
|
||||||
|
|
||||||
|
const encoding = attrs.get("encoding") orelse "csv";
|
||||||
|
// TODO: compression
|
||||||
|
|
||||||
|
var temp_tiles: std.ArrayList(u32) = .empty;
|
||||||
|
|
||||||
|
if (std.mem.eql(u8, encoding, "csv")) {
|
||||||
|
const text = try lexer.nextExpectText();
|
||||||
|
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 temp_tiles.append(scratch.allocator(), tile_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return error.UnknownEncodingType;
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.finish("data");
|
||||||
|
|
||||||
|
return try arena.dupe(u32, temp_tiles.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: TileVariant, x: usize, y: usize) ?u32 {
|
||||||
|
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
|
||||||
|
return self.data[y * self.width + x];
|
||||||
|
}
|
||||||
|
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 = &[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");
|
||||||
|
}
|
||||||
421
libs/tiled/src/object.zig
Normal file
421
libs/tiled/src/object.zig
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const xml = @import("./xml.zig");
|
||||||
|
const Position = @import("./position.zig");
|
||||||
|
const Color = @import("./color.zig");
|
||||||
|
|
||||||
|
const Object = @This();
|
||||||
|
|
||||||
|
pub const Shape = union (Type) {
|
||||||
|
pub const Type = enum {
|
||||||
|
rectangle,
|
||||||
|
point,
|
||||||
|
ellipse,
|
||||||
|
polygon,
|
||||||
|
tile,
|
||||||
|
// TODO: template
|
||||||
|
text
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Rectangle = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tile = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
gid: u32
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Ellipse = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Polygon = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
points: []const Position
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Text = struct {
|
||||||
|
pub const Font = struct {
|
||||||
|
family: []const u8,
|
||||||
|
pixel_size: f32,
|
||||||
|
bold: bool,
|
||||||
|
italic: bool,
|
||||||
|
underline: bool,
|
||||||
|
strikeout: bool,
|
||||||
|
kerning: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const HorizontalAlign = enum {
|
||||||
|
left,
|
||||||
|
center,
|
||||||
|
right,
|
||||||
|
justify,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
|
||||||
|
.{ "left", .left },
|
||||||
|
.{ "center", .center },
|
||||||
|
.{ "right", .right },
|
||||||
|
.{ "justify", .justify },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const VerticalAlign = enum {
|
||||||
|
top,
|
||||||
|
center,
|
||||||
|
bottom,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
|
||||||
|
.{ "top", .top },
|
||||||
|
.{ "center", .center },
|
||||||
|
.{ "bottom", .bottom },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
word_wrap: bool,
|
||||||
|
color: Color,
|
||||||
|
font: Font,
|
||||||
|
horizontal_align: HorizontalAlign,
|
||||||
|
vertical_align: VerticalAlign,
|
||||||
|
content: []const u8
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Point = struct {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
};
|
||||||
|
|
||||||
|
rectangle: Rectangle,
|
||||||
|
point: Point,
|
||||||
|
ellipse: Ellipse,
|
||||||
|
polygon: Polygon,
|
||||||
|
tile: Tile,
|
||||||
|
text: Text
|
||||||
|
};
|
||||||
|
|
||||||
|
id: u32,
|
||||||
|
name: []const u8,
|
||||||
|
class: []const u8,
|
||||||
|
rotation: f32, // TODO: maybe this field should be moved to Shape struct
|
||||||
|
visible: bool,
|
||||||
|
shape: Shape,
|
||||||
|
|
||||||
|
pub fn initFromXml(
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer,
|
||||||
|
) !Object {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const attrs = try iter.begin("object");
|
||||||
|
|
||||||
|
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, "type") orelse "";
|
||||||
|
const x = try attrs.getNumber(f32, "x") orelse 0;
|
||||||
|
const y = try attrs.getNumber(f32, "y") orelse 0;
|
||||||
|
const width = try attrs.getNumber(f32, "width") orelse 0;
|
||||||
|
const height = try attrs.getNumber(f32, "height") orelse 0;
|
||||||
|
const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
|
||||||
|
const visible = try attrs.getBool("visible", "1", "0") orelse true;
|
||||||
|
|
||||||
|
var shape: ?Shape = null;
|
||||||
|
|
||||||
|
while (try iter.next()) |node| {
|
||||||
|
if (shape == null) {
|
||||||
|
if (node.isTag("point")) {
|
||||||
|
shape = .{
|
||||||
|
.point = Shape.Point{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (node.isTag("ellipse")) {
|
||||||
|
shape = .{
|
||||||
|
.ellipse = Shape.Ellipse{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.width = width,
|
||||||
|
.height = height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} else if (node.isTag("text")) {
|
||||||
|
const HorizontalShape = Shape.Text.HorizontalAlign;
|
||||||
|
const VerticalAlign = Shape.Text.VerticalAlign;
|
||||||
|
|
||||||
|
const text_attrs = node.tag.attributes;
|
||||||
|
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
|
||||||
|
const color = try text_attrs.getColor("color", true) orelse Color.black;
|
||||||
|
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
|
||||||
|
const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
|
||||||
|
const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
|
||||||
|
const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
|
||||||
|
const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
|
||||||
|
const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
|
||||||
|
const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
|
||||||
|
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
|
||||||
|
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
|
||||||
|
|
||||||
|
_ = try lexer.nextExpectStartTag("text");
|
||||||
|
var content: []const u8 = "";
|
||||||
|
const content_value = try lexer.peek();
|
||||||
|
if (content_value != null and content_value.? == .text) {
|
||||||
|
content = try arena.dupe(u8, content_value.?.text);
|
||||||
|
}
|
||||||
|
try lexer.skipUntilMatchingEndTag("text");
|
||||||
|
|
||||||
|
shape = .{
|
||||||
|
.text = Shape.Text{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.word_wrap = word_wrap,
|
||||||
|
.color = color,
|
||||||
|
.horizontal_align = horizontal_align,
|
||||||
|
.vertical_align = vertical_align,
|
||||||
|
.content = try arena.dupe(u8, content),
|
||||||
|
.font = .{
|
||||||
|
.bold = bold,
|
||||||
|
.italic = italic,
|
||||||
|
.strikeout = strikeout,
|
||||||
|
.underline = underline,
|
||||||
|
.pixel_size = pixel_size,
|
||||||
|
.kerning = kerning,
|
||||||
|
.family = font_family,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
|
||||||
|
} else if (node.isTag("polygon")) {
|
||||||
|
const points_str = node.tag.attributes.get("points") orelse "";
|
||||||
|
|
||||||
|
var points: std.ArrayList(Position) = .empty;
|
||||||
|
var point_iter = std.mem.splitScalar(u8, points_str, ' ');
|
||||||
|
while (point_iter.next()) |point_str| {
|
||||||
|
const point = try Position.parseCommaDelimited(point_str);
|
||||||
|
try points.append(scratch.allocator(), point);
|
||||||
|
}
|
||||||
|
|
||||||
|
shape = .{
|
||||||
|
.polygon = Shape.Polygon{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.points = try arena.dupe(Position, points.items)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape == null) {
|
||||||
|
if (try attrs.getNumber(u32, "gid")) |gid| {
|
||||||
|
shape = .{
|
||||||
|
.tile = Shape.Tile{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.gid = gid
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
shape = .{
|
||||||
|
.rectangle = Shape.Rectangle{
|
||||||
|
.x = x,
|
||||||
|
.y = y,
|
||||||
|
.width = width,
|
||||||
|
.height = height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.finish("object");
|
||||||
|
|
||||||
|
return Object{
|
||||||
|
.id = id,
|
||||||
|
.name = name,
|
||||||
|
.class = class,
|
||||||
|
.rotation = rotation,
|
||||||
|
.visible = visible,
|
||||||
|
.shape = shape orelse return error.UnknownShapeType
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectParsedEquals(expected: Object, body: []const u8) !void {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: xml.Lexer.TestingContext = undefined;
|
||||||
|
ctx.init(allocator, body);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
var scratch = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer scratch.deinit();
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
|
||||||
|
try std.testing.expectEqualDeep(expected, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
test Object {
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 10,
|
||||||
|
.name = "rectangle",
|
||||||
|
.class = "object class",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.rectangle = .{
|
||||||
|
.x = 12.34,
|
||||||
|
.y = 56.78,
|
||||||
|
.width = 31.5,
|
||||||
|
.height = 20.25,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="10" name="rectangle" type="object class" x="12.34" y="56.78" width="31.5" height="20.25"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 3,
|
||||||
|
.name = "point",
|
||||||
|
.class = "foo",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.point = .{
|
||||||
|
.x = 77.125,
|
||||||
|
.y = 99.875
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
|
||||||
|
\\ <point/>
|
||||||
|
\\ </object>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 4,
|
||||||
|
.name = "ellipse",
|
||||||
|
.class = "",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.ellipse = .{
|
||||||
|
.x = 64.25,
|
||||||
|
.y = 108.25,
|
||||||
|
.width = 22.375,
|
||||||
|
.height = 15.375
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
|
||||||
|
\\ <ellipse/>
|
||||||
|
\\ </object>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 5,
|
||||||
|
.name = "",
|
||||||
|
.class = "",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.polygon = .{
|
||||||
|
.x = 40.125,
|
||||||
|
.y = 96.25,
|
||||||
|
.points = &[_]Position{
|
||||||
|
.{ .x = 0, .y = 0 },
|
||||||
|
.{ .x = 13.25, .y = -4.25 },
|
||||||
|
.{ .x = 10.125, .y = 18.625 },
|
||||||
|
.{ .x = 2.25, .y = 17.375 },
|
||||||
|
.{ .x = -0.125, .y = 25.75 },
|
||||||
|
.{ .x = -3.875, .y = 20.75 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="5" x="40.125" y="96.25">
|
||||||
|
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
|
||||||
|
\\ </object>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 2,
|
||||||
|
.name = "tile",
|
||||||
|
.class = "",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.tile = .{
|
||||||
|
.x = 60.125,
|
||||||
|
.y = 103.5,
|
||||||
|
.width = 8,
|
||||||
|
.height = 8,
|
||||||
|
.gid = 35
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Object{
|
||||||
|
.id = 6,
|
||||||
|
.name = "text",
|
||||||
|
.class = "",
|
||||||
|
.rotation = 0,
|
||||||
|
.visible = true,
|
||||||
|
.shape = .{
|
||||||
|
.text = .{
|
||||||
|
.x = 64.3906,
|
||||||
|
.y = 92.8594,
|
||||||
|
.width = 87.7188,
|
||||||
|
.height = 21.7813,
|
||||||
|
.content = "Hello World",
|
||||||
|
.word_wrap = true,
|
||||||
|
.color = .black,
|
||||||
|
.horizontal_align = .center,
|
||||||
|
.vertical_align = .top,
|
||||||
|
.font = .{
|
||||||
|
.family = "sans-serif",
|
||||||
|
.pixel_size = 16,
|
||||||
|
.bold = false,
|
||||||
|
.italic = false,
|
||||||
|
.underline = false,
|
||||||
|
.strikeout = false,
|
||||||
|
.kerning = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
|
||||||
|
\\ <text wrap="1" halign="center"> Hello World </text>
|
||||||
|
\\ </object>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
libs/tiled/src/position.zig
Normal file
20
libs/tiled/src/position.zig
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Position = @This();
|
||||||
|
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
|
||||||
|
pub fn parseCommaDelimited(str: []const u8) !Position {
|
||||||
|
const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma;
|
||||||
|
const x_str = str[0..comma_index];
|
||||||
|
const y_str = str[(comma_index+1)..];
|
||||||
|
|
||||||
|
const x = try std.fmt.parseFloat(f32, x_str);
|
||||||
|
const y = try std.fmt.parseFloat(f32, y_str);
|
||||||
|
|
||||||
|
return Position{
|
||||||
|
.x = x,
|
||||||
|
.y = y
|
||||||
|
};
|
||||||
|
}
|
||||||
153
libs/tiled/src/property.zig
Normal file
153
libs/tiled/src/property.zig
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const xml = @import("./xml.zig");
|
||||||
|
|
||||||
|
const Property = @This();
|
||||||
|
|
||||||
|
pub const Type = enum {
|
||||||
|
string,
|
||||||
|
int,
|
||||||
|
bool,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(Type) = .initComptime(.{
|
||||||
|
.{ "string", .string },
|
||||||
|
.{ "int", .int },
|
||||||
|
.{ "bool", .bool },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Value = union(Type) {
|
||||||
|
string: []const u8,
|
||||||
|
int: i32,
|
||||||
|
bool: bool
|
||||||
|
};
|
||||||
|
|
||||||
|
name: []const u8,
|
||||||
|
value: Value,
|
||||||
|
|
||||||
|
pub const List = struct {
|
||||||
|
items: []Property,
|
||||||
|
|
||||||
|
pub const empty = List{
|
||||||
|
.items = &[0]Property{}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn initFromXml(
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer
|
||||||
|
) !Property.List {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
_ = try iter.begin("properties");
|
||||||
|
|
||||||
|
var temp_properties: std.ArrayList(Property) = .empty;
|
||||||
|
|
||||||
|
while (try iter.next()) |node| {
|
||||||
|
if (node.isTag("property")) {
|
||||||
|
const property = try Property.initFromXml(arena, lexer);
|
||||||
|
try temp_properties.append(scratch.allocator(), property);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.skip();
|
||||||
|
}
|
||||||
|
try iter.finish("properties");
|
||||||
|
|
||||||
|
const properties = try arena.dupe(Property, temp_properties.items);
|
||||||
|
|
||||||
|
return List{
|
||||||
|
.items = properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: List, name: []const u8) ?Value {
|
||||||
|
for (self.items) |item| {
|
||||||
|
if (std.mem.eql(u8, item.name, name)) {
|
||||||
|
return item.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getString(self: List, name: []const u8) ?[]const u8 {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
return value.string;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(name: []const u8, value: Value) Property {
|
||||||
|
return Property{
|
||||||
|
.name = name,
|
||||||
|
.value = value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const attrs = try iter.begin("property");
|
||||||
|
|
||||||
|
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
|
||||||
|
const prop_type_str = attrs.get("type") orelse "string";
|
||||||
|
const value_str = attrs.get("value") orelse "";
|
||||||
|
|
||||||
|
const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType;
|
||||||
|
const value = switch(prop_type) {
|
||||||
|
.string => Value{
|
||||||
|
.string = try arena.dupe(u8, value_str)
|
||||||
|
},
|
||||||
|
.int => Value{
|
||||||
|
.int = try std.fmt.parseInt(i32, value_str, 10)
|
||||||
|
},
|
||||||
|
.bool => Value{
|
||||||
|
.bool = std.mem.eql(u8, value_str, "true")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try iter.finish("property");
|
||||||
|
|
||||||
|
return Property{
|
||||||
|
.name = name,
|
||||||
|
.value = value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expectParsedEquals(expected: Property, body: []const u8) !void {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: xml.Lexer.TestingContext = undefined;
|
||||||
|
ctx.init(allocator, body);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||||
|
defer arena.deinit();
|
||||||
|
|
||||||
|
const parsed = try initFromXml(arena.allocator(), &ctx.lexer);
|
||||||
|
try std.testing.expectEqualDeep(expected, parsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
test Property {
|
||||||
|
try expectParsedEquals(
|
||||||
|
Property.init("solid", .{ .string = "hello" }),
|
||||||
|
\\ <property name="solid" value="hello"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Property.init("solid", .{ .string = "hello" }),
|
||||||
|
\\ <property name="solid" type="string" value="hello"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Property.init("integer", .{ .int = 123 }),
|
||||||
|
\\ <property name="integer" type="int" value="123"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Property.init("boolean", .{ .bool = true }),
|
||||||
|
\\ <property name="boolean" type="bool" value="true"/>
|
||||||
|
);
|
||||||
|
|
||||||
|
try expectParsedEquals(
|
||||||
|
Property.init("boolean", .{ .bool = false }),
|
||||||
|
\\ <property name="boolean" type="bool" value="false"/>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
libs/tiled/src/root.zig
Normal file
16
libs/tiled/src/root.zig
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
// Warning:
|
||||||
|
// This library is not complete, it does not cover all features that the specification provides.
|
||||||
|
// But there are enough features implemented so that I could use this for my games.
|
||||||
|
|
||||||
|
// Map format specification:
|
||||||
|
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#
|
||||||
|
|
||||||
|
pub const xml = @import("./xml.zig");
|
||||||
|
pub const Tileset = @import("./tileset.zig");
|
||||||
|
pub const Tilemap = @import("./tilemap.zig");
|
||||||
|
|
||||||
|
test {
|
||||||
|
_ = std.testing.refAllDeclsRecursive(@This());
|
||||||
|
}
|
||||||
235
libs/tiled/src/tilemap.zig
Normal file
235
libs/tiled/src/tilemap.zig
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const xml = @import("./xml.zig");
|
||||||
|
const Io = std.Io;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Property = @import("./property.zig");
|
||||||
|
const Layer = @import("./layer.zig");
|
||||||
|
const Object = @import("./object.zig");
|
||||||
|
const Position = @import("./position.zig");
|
||||||
|
const Tileset = @import("./tileset.zig");
|
||||||
|
const GlobalTileId = @import("./global_tile_id.zig");
|
||||||
|
|
||||||
|
const Tilemap = @This();
|
||||||
|
|
||||||
|
pub const Orientation = enum {
|
||||||
|
orthogonal,
|
||||||
|
staggered,
|
||||||
|
hexagonal,
|
||||||
|
isometric,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(Orientation) = .initComptime(.{
|
||||||
|
.{ "orthogonal", .orthogonal },
|
||||||
|
.{ "staggered", .staggered },
|
||||||
|
.{ "hexagonal", .hexagonal },
|
||||||
|
.{ "isometric", .isometric }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const RenderOrder = enum {
|
||||||
|
right_down,
|
||||||
|
right_up,
|
||||||
|
left_down,
|
||||||
|
left_up,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(RenderOrder) = .initComptime(.{
|
||||||
|
.{ "right-down", .right_down },
|
||||||
|
.{ "right-up", .right_up },
|
||||||
|
.{ "left-down", .left_down },
|
||||||
|
.{ "left-up", .left_up },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StaggerAxis = enum {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
|
||||||
|
const map: std.StaticStringMap(StaggerAxis) = .initComptime(.{
|
||||||
|
.{ "x", .x },
|
||||||
|
.{ "y", .y }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TilesetReference = struct {
|
||||||
|
source: []const u8,
|
||||||
|
first_gid: u32,
|
||||||
|
|
||||||
|
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const attrs = try iter.begin("tileset");
|
||||||
|
|
||||||
|
const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid;
|
||||||
|
const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid;
|
||||||
|
|
||||||
|
try iter.finish("tileset");
|
||||||
|
|
||||||
|
return TilesetReference{
|
||||||
|
.source = source,
|
||||||
|
.first_gid = first_gid
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tile = struct {
|
||||||
|
tileset: *const Tileset,
|
||||||
|
id: u32,
|
||||||
|
|
||||||
|
pub fn getProperties(self: Tile) Property.List {
|
||||||
|
return self.tileset.getTileProperties(self.id) orelse .empty;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
version: []const u8,
|
||||||
|
tiled_version: ?[]const u8,
|
||||||
|
orientation: Orientation,
|
||||||
|
render_order: RenderOrder,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
tile_width: u32,
|
||||||
|
tile_height: u32,
|
||||||
|
infinite: bool,
|
||||||
|
|
||||||
|
stagger_axis: ?StaggerAxis,
|
||||||
|
next_layer_id: u32,
|
||||||
|
next_object_id: u32,
|
||||||
|
|
||||||
|
tilesets: []TilesetReference,
|
||||||
|
layers: []Layer,
|
||||||
|
|
||||||
|
pub fn initFromBuffer(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
xml_buffers: *xml.Lexer.Buffers,
|
||||||
|
buffer: []const u8
|
||||||
|
) !Tilemap {
|
||||||
|
var reader = Io.Reader.fixed(buffer);
|
||||||
|
return initFromReader(gpa, scratch, xml_buffers, &reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFromReader(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
xml_buffers: *xml.Lexer.Buffers,
|
||||||
|
reader: *Io.Reader,
|
||||||
|
) !Tilemap {
|
||||||
|
var lexer = xml.Lexer.init(reader, xml_buffers);
|
||||||
|
return initFromXml(gpa, scratch, &lexer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map specification:
|
||||||
|
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
|
||||||
|
pub fn initFromXml(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer,
|
||||||
|
) !Tilemap {
|
||||||
|
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
errdefer arena_allocator.deinit();
|
||||||
|
|
||||||
|
const arena = arena_allocator.allocator();
|
||||||
|
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const map_attrs = try iter.begin("map");
|
||||||
|
|
||||||
|
const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
|
||||||
|
const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
|
||||||
|
const orientation = try map_attrs.getEnum(Orientation, "orientation", Orientation.map) orelse return error.MissingOrientation;
|
||||||
|
const render_order = try map_attrs.getEnum(RenderOrder, "renderorder", RenderOrder.map) orelse return error.MissingRenderOrder;
|
||||||
|
// TODO: compressionlevel
|
||||||
|
const width = try map_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||||
|
const height = try map_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||||
|
const tile_width = try map_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTileWidth;
|
||||||
|
const tile_height = try map_attrs.getNumber(u32, "tileheight") orelse return error.MissingTileHeight;
|
||||||
|
// TODO: hexidelength
|
||||||
|
const infinite_int = try map_attrs.getNumber(u32, "infinite") orelse 0;
|
||||||
|
const infinite = infinite_int != 0;
|
||||||
|
const next_layer_id = try map_attrs.getNumber(u32, "nextlayerid") orelse return error.MissingLayerId;
|
||||||
|
const next_object_id = try map_attrs.getNumber(u32, "nextobjectid") orelse return error.MissingObjectId;
|
||||||
|
// TODO: parallaxoriginx
|
||||||
|
// TODO: parallaxoriginy
|
||||||
|
// TODO: backgroundcolor
|
||||||
|
|
||||||
|
var stagger_axis: ?StaggerAxis = null;
|
||||||
|
if (orientation == .hexagonal or orientation == .staggered) {
|
||||||
|
stagger_axis = try map_attrs.getEnum(StaggerAxis, "staggeraxis", StaggerAxis.map) orelse return error.MissingRenderOrder;
|
||||||
|
// TODO: staggerindex
|
||||||
|
}
|
||||||
|
|
||||||
|
var tileset_list: std.ArrayList(TilesetReference) = .empty;
|
||||||
|
var layer_list: std.ArrayList(Layer) = .empty;
|
||||||
|
|
||||||
|
while (try iter.next()) |node| {
|
||||||
|
if (node.isTag("tileset")) {
|
||||||
|
try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer));
|
||||||
|
continue;
|
||||||
|
} else if (Layer.isLayerNode(node)) {
|
||||||
|
const layer = try Layer.initFromXml(
|
||||||
|
arena,
|
||||||
|
scratch,
|
||||||
|
lexer,
|
||||||
|
);
|
||||||
|
try layer_list.append(scratch.allocator(), layer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.skip();
|
||||||
|
}
|
||||||
|
try iter.finish("map");
|
||||||
|
|
||||||
|
const tilesets = try arena.dupe(TilesetReference, tileset_list.items);
|
||||||
|
const layers = try arena.dupe(Layer, layer_list.items);
|
||||||
|
|
||||||
|
return Tilemap{
|
||||||
|
.arena = arena_allocator,
|
||||||
|
.version = version,
|
||||||
|
.tiled_version = tiled_version,
|
||||||
|
.orientation = orientation,
|
||||||
|
.render_order = render_order,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.tile_width = tile_width,
|
||||||
|
.tile_height = tile_height,
|
||||||
|
.infinite = infinite,
|
||||||
|
.stagger_axis = stagger_axis,
|
||||||
|
.next_object_id = next_object_id,
|
||||||
|
.next_layer_id = next_layer_id,
|
||||||
|
.tilesets = tilesets,
|
||||||
|
.layers = layers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
|
||||||
|
var result: ?TilesetReference = null;
|
||||||
|
for (self.tilesets) |tileset| {
|
||||||
|
if (gid < tileset.first_gid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (result != null and result.?.first_gid < tileset.first_gid) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result = tileset;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile {
|
||||||
|
assert(layer.variant == .tile);
|
||||||
|
const tile_variant = layer.variant.tile;
|
||||||
|
|
||||||
|
const gid = tile_variant.get(x, y) orelse return null;
|
||||||
|
const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null;
|
||||||
|
const tileset = tilesets.get(tileset_ref.source) orelse return null;
|
||||||
|
const id = gid - tileset_ref.first_gid;
|
||||||
|
|
||||||
|
return Tile{
|
||||||
|
.tileset = tileset,
|
||||||
|
.id = id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Tilemap) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
226
libs/tiled/src/tileset.zig
Normal file
226
libs/tiled/src/tileset.zig
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
const xml = @import("./xml.zig");
|
||||||
|
const Property = @import("./property.zig");
|
||||||
|
const Position = @import("./position.zig");
|
||||||
|
|
||||||
|
const Tileset = @This();
|
||||||
|
|
||||||
|
pub const Image = struct {
|
||||||
|
source: []const u8,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
|
||||||
|
pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const attrs = try iter.begin("image");
|
||||||
|
|
||||||
|
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
|
||||||
|
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||||
|
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||||
|
|
||||||
|
try iter.finish("image");
|
||||||
|
|
||||||
|
return Image{
|
||||||
|
.source = source,
|
||||||
|
.width = width,
|
||||||
|
.height = height
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tile = struct {
|
||||||
|
id: u32,
|
||||||
|
properties: Property.List,
|
||||||
|
|
||||||
|
pub fn initFromXml(
|
||||||
|
arena: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer,
|
||||||
|
) !Tile {
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const tile_attrs = try iter.begin("tile");
|
||||||
|
|
||||||
|
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||||
|
|
||||||
|
var properties: Property.List = .empty;
|
||||||
|
|
||||||
|
while (try iter.next()) |node| {
|
||||||
|
if (node.isTag("properties")) {
|
||||||
|
properties = try Property.List.initFromXml(arena, scratch, lexer);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.finish("tile");
|
||||||
|
|
||||||
|
return Tile{
|
||||||
|
.id = id,
|
||||||
|
.properties = properties
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const List = struct {
|
||||||
|
const Entry = struct {
|
||||||
|
name: []const u8,
|
||||||
|
tileset: Tileset
|
||||||
|
};
|
||||||
|
|
||||||
|
list: std.ArrayList(Entry),
|
||||||
|
|
||||||
|
pub const empty = List{
|
||||||
|
.list = .empty
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void {
|
||||||
|
if (self.get(name) != null) {
|
||||||
|
return error.DuplicateName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name_dupe = try gpa.dupe(u8, name);
|
||||||
|
errdefer gpa.free(name_dupe);
|
||||||
|
|
||||||
|
try self.list.append(gpa, .{
|
||||||
|
.name = name_dupe,
|
||||||
|
.tileset = tileset
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: *const List, name: []const u8) ?*const Tileset {
|
||||||
|
for (self.list.items) |*entry| {
|
||||||
|
if (std.mem.eql(u8, entry.name, name)) {
|
||||||
|
return &entry.tileset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *List, gpa: Allocator) void {
|
||||||
|
for (self.list.items) |entry| {
|
||||||
|
gpa.free(entry.name);
|
||||||
|
entry.tileset.deinit();
|
||||||
|
}
|
||||||
|
self.list.deinit(gpa);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
version: []const u8,
|
||||||
|
tiled_version: []const u8,
|
||||||
|
name: []const u8,
|
||||||
|
tile_width: u32,
|
||||||
|
tile_height: u32,
|
||||||
|
tile_count: u32,
|
||||||
|
columns: u32,
|
||||||
|
|
||||||
|
image: Image,
|
||||||
|
|
||||||
|
tiles: []Tile,
|
||||||
|
|
||||||
|
pub fn initFromBuffer(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
xml_buffers: *xml.Lexer.Buffers,
|
||||||
|
buffer: []const u8
|
||||||
|
) !Tileset {
|
||||||
|
var reader = Io.Reader.fixed(buffer);
|
||||||
|
return initFromReader(gpa, scratch, xml_buffers, &reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFromReader(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
xml_buffers: *xml.Lexer.Buffers,
|
||||||
|
reader: *Io.Reader,
|
||||||
|
) !Tileset {
|
||||||
|
var lexer = xml.Lexer.init(reader, xml_buffers);
|
||||||
|
return initFromXml(gpa, scratch, &lexer);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initFromXml(
|
||||||
|
gpa: std.mem.Allocator,
|
||||||
|
scratch: *std.heap.ArenaAllocator,
|
||||||
|
lexer: *xml.Lexer
|
||||||
|
) !Tileset {
|
||||||
|
var arena_state = std.heap.ArenaAllocator.init(gpa);
|
||||||
|
const arena = arena_state.allocator();
|
||||||
|
|
||||||
|
var iter = xml.TagParser.init(lexer);
|
||||||
|
const tileset_attrs = try iter.begin("tileset");
|
||||||
|
|
||||||
|
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag;
|
||||||
|
const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
|
||||||
|
const name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag;
|
||||||
|
const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag;
|
||||||
|
const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag;
|
||||||
|
const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag;
|
||||||
|
const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag;
|
||||||
|
|
||||||
|
var image: ?Image = null;
|
||||||
|
var tiles_list: std.ArrayList(Tile) = .empty;
|
||||||
|
|
||||||
|
while (try iter.next()) |node| {
|
||||||
|
if (node.isTag("image")) {
|
||||||
|
image = try Image.intFromXml(arena, lexer);
|
||||||
|
continue;
|
||||||
|
} else if (node.isTag("tile")) {
|
||||||
|
const tile = try Tile.initFromXml(arena, scratch, lexer);
|
||||||
|
try tiles_list.append(scratch.allocator(), tile);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try iter.skip();
|
||||||
|
}
|
||||||
|
try iter.finish("tileset");
|
||||||
|
|
||||||
|
const tiles = try arena.dupe(Tile, tiles_list.items);
|
||||||
|
|
||||||
|
return Tileset{
|
||||||
|
.arena = arena_state,
|
||||||
|
.version = version,
|
||||||
|
.tiled_version = tiled_version,
|
||||||
|
.name = name,
|
||||||
|
.tile_width = tile_width,
|
||||||
|
.tile_height = tile_height,
|
||||||
|
.tile_count = tile_count,
|
||||||
|
.columns = columns,
|
||||||
|
.image = image orelse return error.MissingImageTag,
|
||||||
|
.tiles = tiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
|
||||||
|
for (self.tiles) |tile| {
|
||||||
|
if (tile.id == id) {
|
||||||
|
return tile.properties;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
|
||||||
|
if (id >= self.tile_count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tileset_width = @divExact(self.image.width, self.tile_width);
|
||||||
|
|
||||||
|
const tile_x = @mod(id, tileset_width);
|
||||||
|
const tile_y = @divFloor(id, tileset_width);
|
||||||
|
|
||||||
|
return Position{
|
||||||
|
.x = @floatFromInt(tile_x * self.tile_width),
|
||||||
|
.y = @floatFromInt(tile_y * self.tile_height),
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *const Tileset) void {
|
||||||
|
self.arena.deinit();
|
||||||
|
}
|
||||||
687
libs/tiled/src/xml.zig
Normal file
687
libs/tiled/src/xml.zig
Normal file
@ -0,0 +1,687 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const Io = std.Io;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
|
||||||
|
const Color = @import("./color.zig");
|
||||||
|
|
||||||
|
pub const Attribute = struct {
|
||||||
|
name: []const u8,
|
||||||
|
value: []const u8,
|
||||||
|
|
||||||
|
pub const List = struct {
|
||||||
|
items: []const Attribute,
|
||||||
|
|
||||||
|
pub fn get(self: List, name: []const u8) ?[]const u8 {
|
||||||
|
for (self.items) |attr| {
|
||||||
|
if (std.mem.eql(u8, attr.name, name)) {
|
||||||
|
return attr.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
return try gpa.dupe(u8, value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getNumber(self: List, T: type, name: []const u8) !?T {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
if (@typeInfo(T) == .int) {
|
||||||
|
return try std.fmt.parseInt(T, value, 10);
|
||||||
|
} else if (@typeInfo(T) == .float) {
|
||||||
|
return try std.fmt.parseFloat(T, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
if (std.mem.eql(u8, value, true_value)) {
|
||||||
|
return true;
|
||||||
|
} else if (std.mem.eql(u8, value, false_value)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return error.InvalidBoolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
return map.get(value) orelse return error.InvalidEnumValue;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color {
|
||||||
|
if (self.get(name)) |value| {
|
||||||
|
return try Color.parse(value, hash_required);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
if (self.items.len > 0) {
|
||||||
|
try writer.writeAll("{ ");
|
||||||
|
for (self.items, 0..) |attribute, i| {
|
||||||
|
if (i > 0) {
|
||||||
|
try writer.writeAll(", ");
|
||||||
|
}
|
||||||
|
try writer.print("{f}", .{attribute});
|
||||||
|
}
|
||||||
|
try writer.writeAll(" }");
|
||||||
|
} else {
|
||||||
|
try writer.writeAll("{ }");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value });
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||||
|
if (data.len > 0) {
|
||||||
|
try writer.writeAll("{ ");
|
||||||
|
for (data, 0..) |attribute, i| {
|
||||||
|
if (i > 0) {
|
||||||
|
try writer.writeAll(", ");
|
||||||
|
}
|
||||||
|
try writer.print("{f}", .{attribute});
|
||||||
|
}
|
||||||
|
try writer.writeAll(" }");
|
||||||
|
} else {
|
||||||
|
try writer.writeAll("{ }");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) {
|
||||||
|
return .{ .data = data };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Tag = struct {
|
||||||
|
name: []const u8,
|
||||||
|
attributes: Attribute.List
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Lexer = struct {
|
||||||
|
pub const Buffers = struct {
|
||||||
|
scratch: std.heap.ArenaAllocator,
|
||||||
|
text: std.ArrayList(u8),
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator) Buffers {
|
||||||
|
return Buffers{
|
||||||
|
.scratch = std.heap.ArenaAllocator.init(allocator),
|
||||||
|
.text = .empty
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(self: *Buffers) void {
|
||||||
|
self.text.clearRetainingCapacity();
|
||||||
|
_ = self.scratch.reset(.retain_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Buffers) void {
|
||||||
|
const allocator = self.scratch.child_allocator;
|
||||||
|
self.scratch.deinit();
|
||||||
|
self.text.deinit(allocator);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const Token = union(enum) {
|
||||||
|
start_tag: Tag,
|
||||||
|
end_tag: []const u8,
|
||||||
|
text: []const u8,
|
||||||
|
|
||||||
|
pub fn isStartTag(self: Token, name: []const u8) bool {
|
||||||
|
if (self == .start_tag) {
|
||||||
|
return std.mem.eql(u8, self.start_tag.name, name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn isEndTag(self: Token, name: []const u8) bool {
|
||||||
|
if (self == .end_tag) {
|
||||||
|
return std.mem.eql(u8, self.end_tag, name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const StepResult = struct {
|
||||||
|
token: ?Token,
|
||||||
|
self_closing: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TestingContext = struct {
|
||||||
|
io_reader: Io.Reader,
|
||||||
|
buffers: Buffers,
|
||||||
|
lexer: Lexer,
|
||||||
|
|
||||||
|
pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void {
|
||||||
|
self.* = TestingContext{
|
||||||
|
.lexer = undefined,
|
||||||
|
.io_reader = Io.Reader.fixed(body),
|
||||||
|
.buffers = Buffers.init(allocator)
|
||||||
|
};
|
||||||
|
self.lexer = Lexer.init(&self.io_reader, &self.buffers);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *TestingContext) void {
|
||||||
|
self.buffers.deinit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
io_reader: *Io.Reader,
|
||||||
|
buffers: *Buffers,
|
||||||
|
|
||||||
|
peeked_value: ?Token,
|
||||||
|
cursor: usize,
|
||||||
|
queued_end_tag: ?[]const u8,
|
||||||
|
|
||||||
|
pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer {
|
||||||
|
buffers.clear();
|
||||||
|
|
||||||
|
return Lexer{
|
||||||
|
.io_reader = reader,
|
||||||
|
.buffers = buffers,
|
||||||
|
.cursor = 0,
|
||||||
|
.queued_end_tag = null,
|
||||||
|
.peeked_value = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn step(self: *Lexer) !StepResult {
|
||||||
|
_ = self.buffers.scratch.reset(.retain_capacity);
|
||||||
|
|
||||||
|
if (try self.peekByte() == '<') {
|
||||||
|
self.tossByte();
|
||||||
|
|
||||||
|
if (try self.peekByte() == '/') {
|
||||||
|
// End tag
|
||||||
|
self.tossByte();
|
||||||
|
|
||||||
|
const name = try self.parseName();
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
|
||||||
|
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
|
||||||
|
return error.InvalidEndTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = Token{ .end_tag = name };
|
||||||
|
return .{ .token = token };
|
||||||
|
|
||||||
|
} else if (try self.peekByte() == '?') {
|
||||||
|
// Prolog tag
|
||||||
|
self.tossByte();
|
||||||
|
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
|
||||||
|
return error.InvalidPrologTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = try self.parseAttributes();
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
|
||||||
|
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
|
||||||
|
return error.MissingPrologEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const version = attributes.get("version") orelse return error.InvalidProlog;
|
||||||
|
if (!std.mem.eql(u8, version, "1.0")) {
|
||||||
|
return error.InvalidPrologVersion;
|
||||||
|
}
|
||||||
|
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
|
||||||
|
if (!std.mem.eql(u8, encoding, "UTF-8")) {
|
||||||
|
return error.InvalidPrologEncoding;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{ .token = null };
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Start tag
|
||||||
|
const name = try self.parseName();
|
||||||
|
const attributes = try self.parseAttributes();
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
|
||||||
|
const token = Token{
|
||||||
|
.start_tag = .{
|
||||||
|
.name = name,
|
||||||
|
.attributes = attributes
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var self_closing = false;
|
||||||
|
if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
|
||||||
|
self.tossBytes(1);
|
||||||
|
|
||||||
|
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
|
||||||
|
self.tossBytes(2);
|
||||||
|
|
||||||
|
self_closing = true;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return error.UnfinishedStartTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return .{
|
||||||
|
.token = token,
|
||||||
|
.self_closing = self_closing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
|
||||||
|
const text_start = self.cursor;
|
||||||
|
while (try self.peekByte() != '<') {
|
||||||
|
self.tossByte();
|
||||||
|
}
|
||||||
|
var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
|
||||||
|
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
|
||||||
|
|
||||||
|
var token: ?Token = null;
|
||||||
|
if (text.len > 0) {
|
||||||
|
token = Token{ .text = text };
|
||||||
|
}
|
||||||
|
return .{ .token = token };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *Lexer) !?Token {
|
||||||
|
if (self.peeked_value) |value| {
|
||||||
|
self.peeked_value = null;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.queued_end_tag) |name| {
|
||||||
|
self.queued_end_tag = null;
|
||||||
|
return Token{
|
||||||
|
.end_tag = name
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (self.buffers.text.items.len == 0) {
|
||||||
|
self.readIntoTextBuffer() catch |e| switch (e) {
|
||||||
|
error.EndOfStream => break,
|
||||||
|
else => return e
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const saved_cursor = self.cursor;
|
||||||
|
const result = self.step() catch |e| switch(e) {
|
||||||
|
error.EndOfTextBuffer => {
|
||||||
|
self.cursor = saved_cursor;
|
||||||
|
|
||||||
|
const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len;
|
||||||
|
if (unused_capacity == 0 and self.cursor > 0) {
|
||||||
|
self.rebaseBuffer();
|
||||||
|
} else {
|
||||||
|
self.readIntoTextBuffer() catch |read_err| switch (read_err) {
|
||||||
|
error.EndOfStream => break,
|
||||||
|
else => return read_err
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => return e
|
||||||
|
};
|
||||||
|
|
||||||
|
if (result.token) |token| {
|
||||||
|
if (token == .start_tag and result.self_closing) {
|
||||||
|
self.queued_end_tag = token.start_tag.name;
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void {
|
||||||
|
const value = try self.next() orelse return error.MissingEndTag;
|
||||||
|
if (!value.isEndTag(name)) return error.MissingEndTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List {
|
||||||
|
const value = try self.next() orelse return error.MissingStartTag;
|
||||||
|
if (!value.isStartTag(name)) return error.MissingStartTag;
|
||||||
|
return value.start_tag.attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nextExpectText(self: *Lexer) ![]const u8 {
|
||||||
|
const value = try self.next() orelse return error.MissingTextTag;
|
||||||
|
if (value != .text) return error.MissingTextTag;
|
||||||
|
return value.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void {
|
||||||
|
var depth: usize = 0;
|
||||||
|
while (true) {
|
||||||
|
const value = try self.next() orelse return error.MissingEndTag;
|
||||||
|
|
||||||
|
if (depth == 0 and value == .end_tag) {
|
||||||
|
if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) {
|
||||||
|
return error.MismatchedEndTag;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value == .start_tag) {
|
||||||
|
depth += 1;
|
||||||
|
} else if (value == .end_tag) {
|
||||||
|
depth -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(self: *Lexer) !?Token {
|
||||||
|
if (try self.next()) |value| {
|
||||||
|
self.peeked_value = value;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn readIntoTextBuffer(self: *Lexer) !void {
|
||||||
|
const gpa = self.buffers.scratch.child_allocator;
|
||||||
|
const text = &self.buffers.text;
|
||||||
|
try text.ensureUnusedCapacity(gpa, 1);
|
||||||
|
|
||||||
|
var writer = Io.Writer.fixed(text.allocatedSlice());
|
||||||
|
writer.end = text.items.len;
|
||||||
|
|
||||||
|
_ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) {
|
||||||
|
error.WriteFailed => unreachable,
|
||||||
|
else => |ee| return ee
|
||||||
|
};
|
||||||
|
text.items.len = writer.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rebaseBuffer(self: *Lexer) void {
|
||||||
|
if (self.cursor == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = &self.buffers.text;
|
||||||
|
@memmove(
|
||||||
|
text.items[0..(text.items.len - self.cursor)],
|
||||||
|
text.items[self.cursor..]
|
||||||
|
);
|
||||||
|
|
||||||
|
text.items.len -= self.cursor;
|
||||||
|
self.cursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isNameStartChar(c: u8) bool {
|
||||||
|
return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn isNameChar(c: u8) bool {
|
||||||
|
return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hasBytes(self: *Lexer, n: usize) bool {
|
||||||
|
const text = self.buffers.text.items;
|
||||||
|
return self.cursor + n <= text.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||||
|
if (self.hasBytes(n)) {
|
||||||
|
const text = self.buffers.text.items;
|
||||||
|
return text[self.cursor..][0..n];
|
||||||
|
}
|
||||||
|
return error.EndOfTextBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tossBytes(self: *Lexer, n: usize) void {
|
||||||
|
assert(self.hasBytes(n));
|
||||||
|
self.cursor += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn takeBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||||
|
const result = try self.peekBytes(n);
|
||||||
|
self.tossBytes(n);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn peekByte(self: *Lexer) !u8 {
|
||||||
|
return (try self.peekBytes(1))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tossByte(self: *Lexer) void {
|
||||||
|
self.tossBytes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn takeByte(self: *Lexer) !u8 {
|
||||||
|
return (try self.takeBytes(1))[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseName(self: *Lexer) ![]const u8 {
|
||||||
|
const name_start = self.cursor;
|
||||||
|
|
||||||
|
if (isNameStartChar(try self.peekByte())) {
|
||||||
|
self.tossByte();
|
||||||
|
|
||||||
|
while (isNameChar(try self.peekByte())) {
|
||||||
|
self.tossByte();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.buffers.text.items[name_start..self.cursor];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn skipWhiteSpace(self: *Lexer) !void {
|
||||||
|
while (std.ascii.isWhitespace(try self.peekByte())) {
|
||||||
|
self.tossByte();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseAttributeValue(self: *Lexer) ![]const u8 {
|
||||||
|
const quote = try self.takeByte();
|
||||||
|
if (quote != '"' and quote != '\'') {
|
||||||
|
return error.InvalidAttributeValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value_start: usize = self.cursor;
|
||||||
|
var value_len: usize = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const c = try self.takeByte();
|
||||||
|
if (c == '<' or c == '&') {
|
||||||
|
return error.InvalidAttributeValue;
|
||||||
|
}
|
||||||
|
if (c == quote) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
value_len += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.buffers.text.items[value_start..][0..value_len];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parseAttributes(self: *Lexer) !Attribute.List {
|
||||||
|
const arena = self.buffers.scratch.allocator();
|
||||||
|
var attributes: std.ArrayList(Attribute) = .empty;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
const name = try self.parseName();
|
||||||
|
if (name.len == 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
if (try self.takeByte() != '=') {
|
||||||
|
return error.MissingAttributeEquals;
|
||||||
|
}
|
||||||
|
try self.skipWhiteSpace();
|
||||||
|
const value = try self.parseAttributeValue();
|
||||||
|
|
||||||
|
const list = Attribute.List{ .items = attributes.items };
|
||||||
|
if (list.get(name) != null) {
|
||||||
|
return error.DuplicateAttribute;
|
||||||
|
}
|
||||||
|
|
||||||
|
try attributes.append(arena, Attribute{
|
||||||
|
.name = name,
|
||||||
|
.value = value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Attribute.List{
|
||||||
|
.items = attributes.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test "self closing tag" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: TestingContext = undefined;
|
||||||
|
ctx.init(allocator,
|
||||||
|
\\ <hello />
|
||||||
|
);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tag" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: TestingContext = undefined;
|
||||||
|
ctx.init(allocator,
|
||||||
|
\\ <hello></hello>
|
||||||
|
);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "tag with prolog" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: TestingContext = undefined;
|
||||||
|
ctx.init(allocator,
|
||||||
|
\\ <?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
\\ <hello></hello>
|
||||||
|
);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "text content" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: TestingContext = undefined;
|
||||||
|
ctx.init(allocator,
|
||||||
|
\\ <hello> Hello World </hello>
|
||||||
|
);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||||
|
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
|
||||||
|
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||||
|
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||||
|
}
|
||||||
|
|
||||||
|
test "attributes" {
|
||||||
|
const allocator = std.testing.allocator;
|
||||||
|
var ctx: TestingContext = undefined;
|
||||||
|
ctx.init(allocator,
|
||||||
|
\\ <hello a='1' b='2'/>
|
||||||
|
);
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
const token = try ctx.lexer.next();
|
||||||
|
const attrs = token.?.start_tag.attributes;
|
||||||
|
try std.testing.expectEqualStrings("1", attrs.get("a").?);
|
||||||
|
try std.testing.expectEqualStrings("2", attrs.get("b").?);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: The API for this is easy to misuse.
|
||||||
|
// Design a better API for using Reader
|
||||||
|
// As a compromise `assert` was used to guard against some of the ways this can be misused
|
||||||
|
pub const TagParser = struct {
|
||||||
|
lexer: *Lexer,
|
||||||
|
|
||||||
|
begin_called: bool = false,
|
||||||
|
finish_called: bool = false,
|
||||||
|
|
||||||
|
pub const Node = union(enum) {
|
||||||
|
tag: Tag,
|
||||||
|
text: []const u8,
|
||||||
|
|
||||||
|
pub fn isTag(self: Node, name: []const u8) bool {
|
||||||
|
if (self == .tag) {
|
||||||
|
return std.mem.eql(u8, self.tag.name, name);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn init(lexer: *Lexer) TagParser {
|
||||||
|
return TagParser{
|
||||||
|
.lexer = lexer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
|
||||||
|
assert(!self.begin_called);
|
||||||
|
self.begin_called = true;
|
||||||
|
|
||||||
|
return try self.lexer.nextExpectStartTag(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(self: *TagParser, name: []const u8) !void {
|
||||||
|
assert(self.begin_called);
|
||||||
|
assert(!self.finish_called);
|
||||||
|
self.finish_called = true;
|
||||||
|
|
||||||
|
try self.lexer.skipUntilMatchingEndTag(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next(self: *TagParser) !?Node {
|
||||||
|
assert(self.begin_called);
|
||||||
|
assert(!self.finish_called);
|
||||||
|
|
||||||
|
const value = try self.lexer.peek() orelse return error.MissingEndTag;
|
||||||
|
if (value == .end_tag) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (value) {
|
||||||
|
.text => |text| Node{ .text = text },
|
||||||
|
.start_tag => |start_tag| Node{ .tag = start_tag },
|
||||||
|
.end_tag => unreachable,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip(self: *TagParser) !void {
|
||||||
|
assert(self.begin_called);
|
||||||
|
assert(!self.finish_called);
|
||||||
|
|
||||||
|
const value = try self.lexer.next() orelse return error.MissingNode;
|
||||||
|
if (value == .end_tag) {
|
||||||
|
return error.UnexpectedEndTag;
|
||||||
|
} else if (value == .start_tag) {
|
||||||
|
// TODO: Make this configurable
|
||||||
|
var name_buffer: [64]u8 = undefined;
|
||||||
|
var name: std.ArrayList(u8) = .initBuffer(&name_buffer);
|
||||||
|
try name.appendSliceBounded(value.start_tag.name);
|
||||||
|
|
||||||
|
try self.lexer.skipUntilMatchingEndTag(name.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
234
src/app.zig
234
src/app.zig
@ -1,12 +1,14 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const CLI = @import("./cli.zig");
|
const CLI = @import("./cli.zig");
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Math = @import("math");
|
const Math = @import("math");
|
||||||
const Vec2 = Math.Vec2;
|
const Vec2 = Math.Vec2;
|
||||||
|
|
||||||
const STBImage = @import("stb_image");
|
const STBImage = @import("stb_image");
|
||||||
const STBTrueType = @import("stb_truetype");
|
const STBTrueType = @import("stb_truetype");
|
||||||
|
const Tiled = @import("tiled");
|
||||||
|
|
||||||
const Platform = @import("./platform.zig");
|
const Platform = @import("./platform.zig");
|
||||||
const Gfx = Platform.Gfx;
|
const Gfx = Platform.Gfx;
|
||||||
@ -15,61 +17,162 @@ const Rect = Platform.Rect;
|
|||||||
|
|
||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
bg: Platform.Color,
|
const Direction = enum {
|
||||||
|
up,
|
||||||
|
down,
|
||||||
|
left,
|
||||||
|
right
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlayerSprites = struct {
|
||||||
|
up: [3]Gfx.Sprite.Id,
|
||||||
|
down: [3]Gfx.Sprite.Id,
|
||||||
|
left: [3]Gfx.Sprite.Id,
|
||||||
|
right: [3]Gfx.Sprite.Id,
|
||||||
|
|
||||||
|
pub fn getDirection(self: *PlayerSprites, dir: Direction) []Gfx.Sprite.Id {
|
||||||
|
return switch (dir) {
|
||||||
|
.up => &self.up,
|
||||||
|
.down => &self.down,
|
||||||
|
.left => &self.left,
|
||||||
|
.right => &self.right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
show_first_window: bool,
|
show_first_window: bool,
|
||||||
check: bool,
|
check: bool,
|
||||||
player_pos: Vec2,
|
player_pos: Vec2,
|
||||||
sprite_sprite: Gfx.Sprite.Id,
|
|
||||||
wall_sprite: Gfx.Sprite.Id,
|
wall_sprite: Gfx.Sprite.Id,
|
||||||
roboto_font: Gfx.Font.Id,
|
roboto_font: Gfx.Font.Id,
|
||||||
|
|
||||||
|
tilesheet: Tilesheet,
|
||||||
|
|
||||||
|
player_sprites: PlayerSprites,
|
||||||
|
player_direction: Direction,
|
||||||
|
player_frame_index: u32,
|
||||||
|
animation_timer: Platform.Nanoseconds,
|
||||||
|
|
||||||
|
const Tilesheet = struct {
|
||||||
|
image: Gfx.ImageData,
|
||||||
|
sprites: []Gfx.Sprite.Id,
|
||||||
|
tile_size: Vec2,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet {
|
||||||
|
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
||||||
|
errdefer gpa.free(sprites);
|
||||||
|
@memset(sprites, Gfx.getNilSprite());
|
||||||
|
|
||||||
|
return Tilesheet{
|
||||||
|
.sprites = sprites,
|
||||||
|
.image = image,
|
||||||
|
.width = width,
|
||||||
|
.height = height,
|
||||||
|
.tile_size = tile_size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: Tilesheet) void {
|
||||||
|
for (self.sprites) |sprite| {
|
||||||
|
Gfx.deinitSprite(sprite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id {
|
||||||
|
if (x >= self.width or y >= self.height) {
|
||||||
|
log.warn("Attempt to get tile which is out of bounds", .{});
|
||||||
|
return Gfx.getNilSprite();
|
||||||
|
}
|
||||||
|
|
||||||
|
const sprite_index = y * self.width + x;
|
||||||
|
if (self.sprites[sprite_index] == Gfx.getNilSprite()) {
|
||||||
|
const sprite = Gfx.initSprite(.{ .padding = 1 });
|
||||||
|
Gfx.setSprite(sprite, .{
|
||||||
|
.image = self.image,
|
||||||
|
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
|
||||||
|
});
|
||||||
|
self.sprites[sprite_index] = sprite;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.sprites[sprite_index];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||||
const sokoban_spritesheet = plt.assets.readFile("sokoban/sokoban_tilesheet.png");
|
|
||||||
|
|
||||||
// const font = try STBTrueType.Font.init(robot_font, STBTrueType.Font.getFontOffsetForIndex(robot_font, 0).?);
|
|
||||||
|
|
||||||
// const glyph = font.findGlyphIndex('A').?;
|
|
||||||
// const scale = font.scaleForPixelHeight(16);
|
|
||||||
// const box = font.getGlyphBitmapBox(glyph, scale, scale);
|
|
||||||
// const width = box.x1 - box.x0;
|
|
||||||
// const height = box.y1 - box.y0;
|
|
||||||
// font.makeGlyphBitmap
|
|
||||||
// std.debug.print("{}\n", .{box});
|
|
||||||
|
|
||||||
const robot_font = Gfx.initFont();
|
const robot_font = Gfx.initFont();
|
||||||
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
||||||
|
|
||||||
var image = try STBImage.load(sokoban_spritesheet);
|
var scratch = std.heap.ArenaAllocator.init(plt.gpa);
|
||||||
defer image.deinit();
|
defer scratch.deinit();
|
||||||
|
|
||||||
const spritesheet = Gfx.ImageData{
|
var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa);
|
||||||
.width = image.width,
|
defer buffers.deinit();
|
||||||
.height = image.height,
|
|
||||||
.pixels = .{ .rgba8 = image.rgba8_pixels }
|
|
||||||
};
|
|
||||||
|
|
||||||
const tile_size = Vec2.init(64, 64);
|
const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx"));
|
||||||
|
defer tilemap.deinit();
|
||||||
|
|
||||||
const player_sprite = Gfx.initSprite(.{});
|
var tilesets: Tiled.Tileset.List = .empty;
|
||||||
Gfx.setSprite(player_sprite, .{
|
defer tilesets.deinit(plt.gpa);
|
||||||
.image = spritesheet,
|
|
||||||
.rect = Rect.init(0, 4, 1, 1).multiply(tile_size)
|
|
||||||
});
|
|
||||||
|
|
||||||
const wall_sprite = Gfx.initSprite(.{});
|
try tilesets.add(plt.gpa, "tiled/tilemap.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx")));
|
||||||
Gfx.setSprite(wall_sprite, .{
|
|
||||||
.image = spritesheet,
|
for (tilemap.layers) |layer| {
|
||||||
.rect = Rect.init(1, 0, 1, 1).multiply(tile_size)
|
std.debug.print("{s}\n", .{layer.name});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const tilesheet_png = try STBImage.load(plt.assets.readFile("sokoban/sokoban_tilesheet.png"));
|
||||||
|
const tilesheet = try Tilesheet.init(
|
||||||
|
plt.arena,
|
||||||
|
Gfx.ImageData{
|
||||||
|
.width = tilesheet_png.width,
|
||||||
|
.height = tilesheet_png.height,
|
||||||
|
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
|
||||||
|
},
|
||||||
|
13,
|
||||||
|
8,
|
||||||
|
.init(64, 64)
|
||||||
|
);
|
||||||
|
|
||||||
|
const wall_sprite = tilesheet.get(1, 0);
|
||||||
|
|
||||||
|
var player_sprites: PlayerSprites = undefined;
|
||||||
|
inline for (.{
|
||||||
|
.{
|
||||||
|
.dir = .up,
|
||||||
|
.positions = [_]Vec2{ .init(3, 4), .init(4, 4), .init(5, 4) }
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.dir = .down,
|
||||||
|
.positions = [_]Vec2{ .init(0, 4), .init(1, 4), .init(2, 4) }
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.dir = .right,
|
||||||
|
.positions = [_]Vec2{ .init(0, 6), .init(1, 6), .init(2, 6) },
|
||||||
|
},
|
||||||
|
.{
|
||||||
|
.dir = .left,
|
||||||
|
.positions = [_]Vec2{ .init(3, 6), .init(4, 6), .init(5, 6) }
|
||||||
|
}
|
||||||
|
}) |entry| {
|
||||||
|
var sprite_ids = player_sprites.getDirection(entry.dir);
|
||||||
|
inline for (0.., entry.positions) |i, pos| {
|
||||||
|
sprite_ids[i] = tilesheet.get(@intFromFloat(pos.x), @intFromFloat(pos.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.* = App{
|
self.* = App{
|
||||||
.bg = .black,
|
|
||||||
.player_pos = .init(100, 100),
|
.player_pos = .init(100, 100),
|
||||||
.show_first_window = true,
|
.show_first_window = true,
|
||||||
.check = false,
|
.check = false,
|
||||||
.sprite_sprite = player_sprite,
|
.player_sprites = player_sprites,
|
||||||
.wall_sprite = wall_sprite,
|
.wall_sprite = wall_sprite,
|
||||||
.roboto_font = robot_font
|
.roboto_font = robot_font,
|
||||||
|
.player_direction = .left,
|
||||||
|
.player_frame_index = 0,
|
||||||
|
.animation_timer = 0,
|
||||||
|
.tilesheet = tilesheet
|
||||||
};
|
};
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@ -96,37 +199,44 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
|
|||||||
dir = dir.normalized();
|
dir = dir.normalized();
|
||||||
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
|
self.player_pos = self.player_pos.add(dir.multiplyScalar(50 * dt));
|
||||||
|
|
||||||
Gfx.setClearColor(self.bg);
|
var new_direction = self.player_direction;
|
||||||
Gfx.drawSprite(self.sprite_sprite, self.player_pos, .init(100, 100), .white);
|
if (dir.x < 0) {
|
||||||
Gfx.drawSprite(self.wall_sprite, self.player_pos.add(.init(0, 200)), .init(100, 100), .white);
|
new_direction = .left;
|
||||||
|
} else if (dir.x > 0) {
|
||||||
|
new_direction = .right;
|
||||||
|
} else if (dir.y > 0) {
|
||||||
|
new_direction = .down;
|
||||||
|
} else if (dir.y < 0) {
|
||||||
|
new_direction = .up;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.player_direction != new_direction) {
|
||||||
|
self.player_frame_index = 0;
|
||||||
|
self.player_direction = new_direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frames = self.player_sprites.getDirection(self.player_direction);
|
||||||
|
|
||||||
|
if (dir.x != 0 or dir.y != 0) {
|
||||||
|
self.animation_timer += plt.dt;
|
||||||
|
const animatin_time = std.time.ns_per_s / 5;
|
||||||
|
while (self.animation_timer >= animatin_time) {
|
||||||
|
self.player_frame_index = (self.player_frame_index + 1) % @as(u32, @intCast(frames.len));
|
||||||
|
self.animation_timer -= animatin_time;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.animation_timer = 0;
|
||||||
|
self.player_frame_index = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Gfx.setClearColor(.rgb(40, 40, 40));
|
||||||
|
Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white);
|
||||||
|
Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white);
|
||||||
Gfx.drawText(self.roboto_font, .{
|
Gfx.drawText(self.roboto_font, .{
|
||||||
.pos = .init(300, 100),
|
.pos = .init(300, 100),
|
||||||
.text = "Hello, World!",
|
.text = "Hello, World!",
|
||||||
.height = 32,
|
.height = 64,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ImGUI.beginWindow(.{
|
|
||||||
.name = "Hello",
|
|
||||||
.size = .init(400, 200),
|
|
||||||
.open = &self.show_first_window
|
|
||||||
})) {
|
|
||||||
defer ImGUI.endWindow();
|
|
||||||
|
|
||||||
ImGUI.text("Player pos: {}", .{self.player_pos});
|
|
||||||
if (ImGUI.button("foo")) {
|
|
||||||
std.debug.print("pressed\n", .{});
|
|
||||||
}
|
|
||||||
if (ImGUI.checkbox("Hello", &self.check)) {
|
|
||||||
std.debug.print("checkbox changed\n", .{});
|
|
||||||
}
|
|
||||||
ImGUI.separator();
|
|
||||||
if (ImGUI.colorEdit(.{
|
|
||||||
.label = "Color",
|
|
||||||
.value = &self.bg,
|
|
||||||
})) {
|
|
||||||
std.debug.print("color changed\n", .{});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
||||||
|
|||||||
@ -34,7 +34,10 @@ const State = struct {
|
|||||||
error_sprite: Sprite.Id,
|
error_sprite: Sprite.Id,
|
||||||
default_spritesheet: Spritesheet.Id,
|
default_spritesheet: Spritesheet.Id,
|
||||||
default_font_spritesheet: Spritesheet.Id,
|
default_font_spritesheet: Spritesheet.Id,
|
||||||
default_sampler: sg.Sampler,
|
|
||||||
|
linear_sampler: sg.Sampler,
|
||||||
|
nearest_sampler: sg.Sampler,
|
||||||
|
default_sampler: Sampler,
|
||||||
|
|
||||||
quads_buffer: [2048]Quad,
|
quads_buffer: [2048]Quad,
|
||||||
quads: std.ArrayList(Quad),
|
quads: std.ArrayList(Quad),
|
||||||
@ -71,13 +74,19 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
|
|||||||
const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend()));
|
const shader = sg.makeShader(shd.mainShaderDesc(sg.queryBackend()));
|
||||||
assert(shader.id != sg.invalid_id);
|
assert(shader.id != sg.invalid_id);
|
||||||
|
|
||||||
// TODO: Make sampler configurable.
|
const linear_sampler = sg.makeSampler(.{
|
||||||
const default_sampler = sg.makeSampler(.{
|
|
||||||
.min_filter = .LINEAR,
|
.min_filter = .LINEAR,
|
||||||
.mag_filter = .LINEAR,
|
.mag_filter = .LINEAR,
|
||||||
.label = "default-sampler"
|
.label = "linear-sampler"
|
||||||
});
|
});
|
||||||
assert(default_sampler.id != sg.invalid_id);
|
assert(linear_sampler.id != sg.invalid_id);
|
||||||
|
|
||||||
|
const nearest_sampler = sg.makeSampler(.{
|
||||||
|
.min_filter = .NEAREST,
|
||||||
|
.mag_filter = .NEAREST,
|
||||||
|
.label = "nearest-sampler"
|
||||||
|
});
|
||||||
|
assert(linear_sampler.id != sg.invalid_id);
|
||||||
|
|
||||||
const pipeline = sg.makePipeline(.{
|
const pipeline = sg.makePipeline(.{
|
||||||
.primitive_type = .TRIANGLES,
|
.primitive_type = .TRIANGLES,
|
||||||
@ -144,7 +153,6 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
|
|||||||
.shader = shader,
|
.shader = shader,
|
||||||
.pipeline = pipeline,
|
.pipeline = pipeline,
|
||||||
.bindings = bindings,
|
.bindings = bindings,
|
||||||
.default_sampler = default_sampler,
|
|
||||||
.quads_buffer = undefined,
|
.quads_buffer = undefined,
|
||||||
.quads = .initBuffer(&self.quads_buffer),
|
.quads = .initBuffer(&self.quads_buffer),
|
||||||
.clear_color = .black,
|
.clear_color = .black,
|
||||||
@ -153,6 +161,10 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
|
|||||||
.default_spritesheet = undefined,
|
.default_spritesheet = undefined,
|
||||||
.default_font_spritesheet = undefined,
|
.default_font_spritesheet = undefined,
|
||||||
|
|
||||||
|
.linear_sampler = linear_sampler,
|
||||||
|
.nearest_sampler = nearest_sampler,
|
||||||
|
.default_sampler = .linear,
|
||||||
|
|
||||||
.textures_buffer = undefined,
|
.textures_buffer = undefined,
|
||||||
.textures = .init(.initBuffer(&self.textures_buffer)),
|
.textures = .init(.initBuffer(&self.textures_buffer)),
|
||||||
.nil_texture = undefined,
|
.nil_texture = undefined,
|
||||||
@ -285,9 +297,6 @@ pub fn deinit() void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sg.destroyPipeline(self.pipeline);
|
|
||||||
sg.destroyShader(self.shader);
|
|
||||||
sg.destroySampler(self.default_sampler);
|
|
||||||
sg.shutdown();
|
sg.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,7 +345,7 @@ pub fn beginFrame() void {
|
|||||||
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
|
const default_spritesheet = self.spritesheets.getAssumeExists(self.default_spritesheet);
|
||||||
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
|
const spritesheet_texture = self.textures.getAssumeExists(default_spritesheet.texture);
|
||||||
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
|
self.bindings.views[shd.VIEW_tex] = spritesheet_texture.view;
|
||||||
self.bindings.samplers[shd.SMP_smp] = self.default_sampler;
|
self.bindings.samplers[shd.SMP_smp] = self.linear_sampler;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn showDebug() void {
|
pub fn showDebug() void {
|
||||||
@ -667,7 +676,18 @@ fn bindView(view: sg.View, opts: FlushOptions) void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setDefaultSampler(sampler: Sampler) void {
|
||||||
|
const self = &g_state;
|
||||||
|
self.default_sampler = sampler;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const Sampler = enum {
|
||||||
|
linear,
|
||||||
|
nearest,
|
||||||
|
};
|
||||||
|
|
||||||
const DrawOptions = struct {
|
const DrawOptions = struct {
|
||||||
|
sampler: ?Sampler = null,
|
||||||
texture: Texture.Id,
|
texture: Texture.Id,
|
||||||
quad: Quad
|
quad: Quad
|
||||||
};
|
};
|
||||||
@ -704,6 +724,16 @@ pub fn draw(opts: DrawOptions) void {
|
|||||||
|
|
||||||
bindView(view, .{});
|
bindView(view, .{});
|
||||||
|
|
||||||
|
const sampler = switch (opts.sampler orelse self.default_sampler) {
|
||||||
|
.linear => self.linear_sampler,
|
||||||
|
.nearest => self.nearest_sampler,
|
||||||
|
};
|
||||||
|
const bound_sampler = &self.bindings.samplers[shd.SMP_smp];
|
||||||
|
if (bound_sampler.id != sampler.id) {
|
||||||
|
flush(.{});
|
||||||
|
bound_sampler.* = sampler;
|
||||||
|
}
|
||||||
|
|
||||||
if (self.quads.items.len == self.quads.capacity) {
|
if (self.quads.items.len == self.quads.capacity) {
|
||||||
flush(.{});
|
flush(.{});
|
||||||
}
|
}
|
||||||
@ -716,6 +746,12 @@ const SpriteOptions = struct {
|
|||||||
padding: u32 = 0,
|
padding: u32 = 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: I don't like this getter, maybe nil_sprite should be a public global variable.
|
||||||
|
pub fn getNilSprite() Sprite.Id {
|
||||||
|
const self = &g_state;
|
||||||
|
return self.nil_sprite;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn initSprite(opts: SpriteOptions) Sprite.Id {
|
pub fn initSprite(opts: SpriteOptions) Sprite.Id {
|
||||||
const self = &g_state;
|
const self = &g_state;
|
||||||
const id = self.sprites.insertBounded() catch {
|
const id = self.sprites.insertBounded() catch {
|
||||||
@ -1112,8 +1148,8 @@ pub fn deinitFont(id: Font.Id) void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.fonts.get(id)) {
|
if (self.fonts.get(id)) |font| {
|
||||||
// TODO: deinit sprites
|
font.cache.deinit();
|
||||||
self.fonts.removeAssumeExists(id);
|
self.fonts.removeAssumeExists(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1142,6 +1178,8 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void {
|
|||||||
// TODO: deinit previous glyphs
|
// TODO: deinit previous glyphs
|
||||||
|
|
||||||
font.stb = stb_font;
|
font.stb = stb_font;
|
||||||
|
|
||||||
|
font.cache.deinit();
|
||||||
font.cache = .init();
|
font.cache = .init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1508,6 +1546,17 @@ pub const Font = struct {
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn deinit(self: *GlyphCache) void {
|
||||||
|
for (self.glyphs_hashmap) |glyph_id| {
|
||||||
|
if (glyph_id == GlyphSlot.nil_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const slot = self.glyphs[glyph_id];
|
||||||
|
slot.glyph.deinit();
|
||||||
|
}
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph {
|
fn get(self: *GlyphCache, id: GlyphSlot.Id) *Glyph {
|
||||||
return &self.glyphs[id].glyph;
|
return &self.glyphs[id].glyph;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,12 @@ const std = @import("std");
|
|||||||
const Platform = @import("platform.zig");
|
const Platform = @import("platform.zig");
|
||||||
const App = @import("app.zig");
|
const App = @import("app.zig");
|
||||||
|
|
||||||
|
pub const std_options: std.Options = .{
|
||||||
|
.log_scope_levels = &.{
|
||||||
|
.{ .scope = .graphics, .level = .info }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
pub fn main(init: std.process.Init) void {
|
pub fn main(init: std.process.Init) void {
|
||||||
Platform.run(App, .{
|
Platform.run(App, .{
|
||||||
.init = init,
|
.init = init,
|
||||||
|
|||||||
@ -34,6 +34,7 @@ fn PlatformType(App: type) type {
|
|||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
|
arena: *std.heap.ArenaAllocator,
|
||||||
io: Io,
|
io: Io,
|
||||||
cli_args: []const [:0]const u8,
|
cli_args: []const [:0]const u8,
|
||||||
|
|
||||||
@ -71,6 +72,7 @@ fn PlatformType(App: type) type {
|
|||||||
if (@hasDecl(App, "init")) {
|
if (@hasDecl(App, "init")) {
|
||||||
const plt = Init{
|
const plt = Init{
|
||||||
.gpa = self.gpa,
|
.gpa = self.gpa,
|
||||||
|
.arena = self.arena.allocator(),
|
||||||
.io = self.io,
|
.io = self.io,
|
||||||
.assets = &self.assets
|
.assets = &self.assets
|
||||||
};
|
};
|
||||||
@ -112,9 +114,10 @@ fn PlatformType(App: type) type {
|
|||||||
|
|
||||||
if (@hasDecl(App, "frame")) {
|
if (@hasDecl(App, "frame")) {
|
||||||
const plt = Frame{
|
const plt = Frame{
|
||||||
.t = t,
|
.t = t.nanoseconds,
|
||||||
.dt = dt,
|
.dt = dt.nanoseconds,
|
||||||
.gpa = self.gpa,
|
.gpa = self.gpa,
|
||||||
|
.arena = self.arena.allocator(),
|
||||||
.io = self.io,
|
.io = self.io,
|
||||||
.input = &self.input,
|
.input = &self.input,
|
||||||
.input_events = self.events.items,
|
.input_events = self.events.items,
|
||||||
@ -494,6 +497,7 @@ pub const Assets = struct {
|
|||||||
|
|
||||||
pub const Init = struct {
|
pub const Init = struct {
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
|
arena: Allocator,
|
||||||
io: Io,
|
io: Io,
|
||||||
assets: *Assets
|
assets: *Assets
|
||||||
};
|
};
|
||||||
@ -503,12 +507,15 @@ pub const Deinit = struct {
|
|||||||
io: Io,
|
io: Io,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Frame = struct {
|
pub const Nanoseconds = i96;
|
||||||
t: Io.Duration,
|
|
||||||
dt: Io.Duration,
|
pub const Frame = struct {
|
||||||
|
t: Nanoseconds,
|
||||||
|
dt: Nanoseconds,
|
||||||
|
|
||||||
frame: Allocator,
|
|
||||||
gpa: Allocator,
|
gpa: Allocator,
|
||||||
|
arena: Allocator,
|
||||||
|
frame: Allocator,
|
||||||
io: Io,
|
io: Io,
|
||||||
assets: *Assets,
|
assets: *Assets,
|
||||||
input: *Input,
|
input: *Input,
|
||||||
@ -519,11 +526,11 @@ pub const Frame = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deltaTime(self: Frame) f32 {
|
pub fn deltaTime(self: Frame) f32 {
|
||||||
return nanosecondsToSeconds(self.dt.nanoseconds);
|
return nanosecondsToSeconds(self.dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn time(self: Frame) f32 {
|
pub fn time(self: Frame) f32 {
|
||||||
return nanosecondsToSeconds(self.t.nanoseconds);
|
return nanosecondsToSeconds(self.t);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -545,6 +552,7 @@ pub fn run(App: type, opts: RunOptions) void {
|
|||||||
const plt = arena.create(PlatformApp) catch @panic("OOM");
|
const plt = arena.create(PlatformApp) catch @panic("OOM");
|
||||||
plt.* = PlatformApp{
|
plt.* = PlatformApp{
|
||||||
.gpa = gpa,
|
.gpa = gpa,
|
||||||
|
.arena = opts.init.arena,
|
||||||
.io = io,
|
.io = io,
|
||||||
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"),
|
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"),
|
||||||
.app = undefined,
|
.app = undefined,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user