Compare commits

..

No commits in common. "7632329c92e2263bdc155c1783990d5aad130185" and "0ac0cd0c3755dae3461e6792df5d8c744c608ced" have entirely different histories.

20 changed files with 974 additions and 1407 deletions

BIN
assets/sokoban/Preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

View File

@ -1,104 +1,81 @@
<?xml version="1.0" encoding="UTF-8"?> <?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="1" nextlayerid="8" nextobjectid="19"> <map version="1.10" tiledversion="1.12.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="64" tileheight="64" infinite="0" nextlayerid="6" nextobjectid="4">
<tileset firstgid="1" source="tileset.tsx"/> <tileset firstgid="1" source="tileset.tsx"/>
<layer id="2" name="Ground" width="30" height="20"> <layer id="2" name="Ground" width="30" height="20">
<data encoding="csv"> <data encoding="csv">
<chunk x="0" y="0" width="16" height="16"> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,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,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,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,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,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,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,
</chunk> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
<chunk x="16" y="0" width="16" height="16"> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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>
0,0,0,0,0,0,0,0,0,0,0,0,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,0,0,0,0,0,
0,0,0,0,90,90,90,90,90,90,90,0,0,0,0,0,
0,0,0,0,90,90,90,90,90,90,90,0,0,0,0,0,
0,0,0,0,90,90,90,90,90,90,90,0,0,0,0,0,
0,0,0,0,90,90,90,90,90,90,90,0,0,0,0,0,
0,0,0,0,90,90,90,90,90,90,90,0,0,0,0,0,
0,0,0,0,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
</chunk>
</data>
</layer> </layer>
<layer id="1" name="Foreground" width="30" height="20"> <layer id="1" name="Walls" width="30" height="20">
<data encoding="csv"> <data encoding="csv">
<chunk x="0" y="0" width="16" height="16"> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,98,101,98,98,98,98,98,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,98,0,0,0,98,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,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,7,0,0,26,0,98,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,0,0,0,0,101,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,0,0,98,0,0,0,101,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,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,
</chunk> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
<chunk x="16" y="0" width="16" height="16"> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, </layer>
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, <layer id="4" name="Boxes" width="30" height="20">
0,0,0,0,98,98,98,98,98,98,98,0,0,0,0,0, <data encoding="csv">
0,0,0,0,98,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,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,98,0,7,0,26,0,98,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,98,0,7,0,26,0,98,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,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,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,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,98,98,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,0,0,0,0,0,0,0,0,0,0,0,0,0,
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,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,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,
</chunk> 0,0,0,0,0,0,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> 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,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>
<objectgroup id="5" name="Markers"> <objectgroup id="5" name="Markers">
<object id="1" name="spawn" x="484" y="555"> <object id="1" name="spawn" x="479" y="550">
<point/> <point/>
</object> </object>
<object id="17" name="spawn" x="1381.33" y="497.333">
<point/>
</object>
</objectgroup>
<objectgroup id="7" name="Level bounds">
<object id="15" x="324" y="214.667" width="633.333" height="566.667">
<properties>
<property name="level" type="int" value="1"/>
</properties>
</object>
<object id="18" x="1196.67" y="206" width="633.333" height="566.667">
<properties>
<property name="level" type="int" value="2"/>
</properties>
</object>
</objectgroup> </objectgroup>
</map> </map>

View File

@ -13,8 +13,7 @@
} }
}, },
"tileset.tsx": { "tileset.tsx": {
"scaleInDock": 0.5, "scaleInDock": 1
"scaleInEditor": 1
} }
}, },
"last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban", "last.imagePath": "/home/rokas/code/games/sokol-template-v2/assets/sokoban",
@ -25,9 +24,7 @@
"map.tmx" "map.tmx"
], ],
"project": "sokoban.tiled-project", "project": "sokoban.tiled-project",
"property.type": "int",
"recentFiles": [ "recentFiles": [
"tileset.tsx",
"map.tmx" "map.tmx"
], ],
"tileset.lastUsedFormat": "tsx", "tileset.lastUsedFormat": "tsx",

View File

@ -1,74 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <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"/> <image source="../sokoban/sokoban_tilesheet.png" width="832" height="512"/>
<tile id="6">
<properties>
<property name="type" value="box"/>
</properties>
</tile>
<tile id="7">
<properties>
<property name="type" value="box"/>
</properties>
</tile>
<tile id="8">
<properties>
<property name="type" value="box"/>
</properties>
</tile>
<tile id="9">
<properties>
<property name="type" value="box"/>
</properties>
</tile>
<tile id="10">
<properties>
<property name="type" value="box"/>
</properties>
</tile>
<tile id="25">
<properties>
<property name="type" value="goal_marker"/>
</properties>
</tile>
<tile id="38">
<properties>
<property name="type" value="goal_marker"/>
</properties>
</tile>
<tile id="51">
<properties>
<property name="type" value="goal_marker"/>
</properties>
</tile>
<tile id="64">
<properties>
<property name="type" value="goal_marker"/>
</properties>
</tile>
<tile id="77">
<properties>
<property name="type" value="goal_marker"/>
</properties>
</tile>
<tile id="97">
<properties>
<property name="type" value="wall"/>
</properties>
</tile>
<tile id="98">
<properties>
<property name="type" value="wall"/>
</properties>
</tile>
<tile id="99">
<properties>
<property name="type" value="wall"/>
</properties>
</tile>
<tile id="100">
<properties>
<property name="type" value="wall"/>
</properties>
</tile>
</tileset> </tileset>

View File

@ -1,13 +1,10 @@
const std = @import("std"); const std = @import("std");
const Build = std.Build;
const sokol = @import("sokol"); const sokol = @import("sokol");
pub fn build(b: *std.Build) !void { pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{}); const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
const name = "sokol_template_v2";
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",
@ -37,39 +34,29 @@ pub fn build(b: *std.Build) !void {
.optimize = optimize, .optimize = optimize,
}); });
const dep_tiled = b.dependency("tiled", .{});
exe_mod.addImport("tiled", dep_tiled.module("tiled"));
const dep_stb = b.dependency("stb", .{});
exe_mod.addImport("stb_image", dep_stb.module("stb_image"));
exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype"));
var dep_sokol: *Build.Dependency = undefined;
const platform_mod = b.createModule(.{
.root_source_file = b.path("src/platform/root.zig")
});
exe_mod.addImport("platform", platform_mod);
{
const build_options = b.addOptions(); const build_options = b.addOptions();
build_options.addOption(bool, "has_imgui", has_imgui); build_options.addOption(bool, "has_imgui", has_imgui);
build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null); build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null);
platform_mod.addOptions("build_options", build_options); exe_mod.addOptions("build_options", build_options);
platform_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files)); exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files));
const math_mod = b.createModule(.{ const math_mod = b.createModule(.{
.root_source_file = b.path("src/platform/math.zig") .root_source_file = b.path("src/platform/math.zig")
}); });
platform_mod.addImport("math", math_mod); exe_mod.addImport("math", math_mod);
dep_sokol = b.dependency("sokol", .{ const dep_tiled = b.dependency("tiled", .{});
exe_mod.addImport("tiled", dep_tiled.module("tiled"));
const dep_sokol = b.dependency("sokol", .{
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
.with_sokol_imgui = has_imgui .with_sokol_imgui = has_imgui
}); });
const mod_sokol = dep_sokol.module("sokol"); const mod_sokol = dep_sokol.module("sokol");
platform_mod.linkLibrary(dep_sokol.artifact("sokol_clib")); exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
platform_mod.addImport("sokol", mod_sokol); exe_mod.addImport("sokol", mod_sokol);
const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); const dep_shdc = dep_sokol.builder.dependency("shdc", .{});
@ -89,7 +76,7 @@ pub fn build(b: *std.Build) !void {
.slang = slang .slang = slang
}); });
mod_shader.addImport("math", math_mod); mod_shader.addImport("math", math_mod);
platform_mod.addImport("shader", mod_shader); exe_mod.addImport("shader", mod_shader);
if (has_imgui) { if (has_imgui) {
if (b.lazyDependency("cimgui", .{ if (b.lazyDependency("cimgui", .{
@ -98,13 +85,16 @@ pub fn build(b: *std.Build) !void {
})) |dep_cimgui| { })) |dep_cimgui| {
const cimgui = b.lazyImport(@This(), "cimgui").?; const cimgui = b.lazyImport(@This(), "cimgui").?;
const cimgui_conf = cimgui.getConfig(true); const cimgui_conf = cimgui.getConfig(true);
platform_mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name)); exe_mod.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path(cimgui_conf.include_dir)); dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
} }
} }
platform_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis")); const dep_stb = b.dependency("stb", .{});
platform_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack")); exe_mod.addImport("stb_image", dep_stb.module("stb_image"));
exe_mod.addImport("stb_vorbis", dep_stb.module("stb_vorbis"));
exe_mod.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack"));
exe_mod.addImport("stb_truetype", dep_stb.module("stb_truetype"));
const dep_tracy = b.dependency("tracy", .{ const dep_tracy = b.dependency("tracy", .{
.target = target, .target = target,
@ -113,31 +103,32 @@ pub fn build(b: *std.Build) !void {
.tracy_only_localhost = true .tracy_only_localhost = true
}); });
if (has_tracy) { if (has_tracy) {
platform_mod.linkLibrary(dep_tracy.artifact("tracy")); exe_mod.linkLibrary(dep_tracy.artifact("tracy"));
}
platform_mod.addImport("tracy", dep_tracy.module("tracy"));
} }
exe_mod.addImport("tracy", dep_tracy.module("tracy"));
var run_cmd_step: *std.Build.Step = undefined; var run_cmd_step: *std.Build.Step = undefined;
if (is_wasm) { if (is_wasm) {
@panic("TODO: Test wasm support on 0.17"); if (true) {
@panic("TODO: test in 0.17");
}
// const build_step = buildWasm(b, .{ const build_step = buildWasm(b, .{
// .name = "index", .name = "index",
// .root_module = exe_mod, .root_module = exe_mod,
// .dep_sokol = dep_sokol, .dep_sokol = dep_sokol,
// }); });
// b.getInstallStep().dependOn(build_step); b.getInstallStep().dependOn(build_step);
//
// const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{}); const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
// const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk }); const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
// emrun_step.step.dependOn(build_step); emrun_step.step.dependOn(build_step);
// run_cmd_step = &emrun_step.step; run_cmd_step = &emrun_step.step;
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload // TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
} else { } else {
const exe = buildNative(b, .{ const exe = buildNative(b, .{
.name = name, .name = "sokol_template_v2",
.root_module = exe_mod .root_module = exe_mod
}); });
b.installArtifact(exe); b.installArtifact(exe);

View File

@ -2,15 +2,11 @@
.name = .sokol_template_v2, .name = .sokol_template_v2,
.version = "0.0.0", .version = "0.0.0",
.fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications. .fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications.
.minimum_zig_version = "0.16.0",
.dependencies = .{ .dependencies = .{
.sokol = .{ .sokol = .{
.url = "git+https://github.com/floooh/sokol-zig.git#ff40f04d4f51c73b2bf460d87fd2d7aa0dedc22f", .url = "git+https://github.com/floooh/sokol-zig.git#ba97ac4e82e36a0f6c62f7fad5dc9c2d82beadd8",
.hash = "sokol-0.1.0-pb1HKygbQQCINV-qSe4SFm92QWaT_95VpWTJkwYqzOrX", .hash = "sokol-0.1.0-pb1HK4prNwDGx2RJFyv7VtYJubDahi19XKioPzPeMCxv",
},
.cimgui = .{
.url = "git+https://github.com/floooh/dcimgui.git#e2f0e0d93adec02743c55940be23ffe286e857f7",
.hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV",
.lazy = true
}, },
.stb = .{ .stb = .{
.path = "./libs/stb", .path = "./libs/stb",
@ -21,6 +17,11 @@
.tracy = .{ .tracy = .{
.path = "./libs/tracy", .path = "./libs/tracy",
}, },
.cimgui = .{
.url = "git+https://github.com/floooh/dcimgui.git#3c2b41a9333b1195e2c2f8ce6a2d3e50660a73bf",
.hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV",
.lazy = true
},
}, },
.paths = .{ .paths = .{
"build.zig", "build.zig",

View File

@ -1,5 +1,4 @@
const std = @import("std"); const std = @import("std");
const assert = std.debug.assert;
const Property = @import("./property.zig"); const Property = @import("./property.zig");
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
@ -10,106 +9,43 @@ const Position = @import("./position.zig");
const Layer = @This(); const Layer = @This();
pub const TileVariant = struct { pub const TileVariant = struct {
width: u32,
height: u32,
chunks: []Chunk,
const Chunk = struct {
x: i32,
y: i32,
width: u32, width: u32,
height: u32, height: u32,
data: []u32, data: []u32,
};
fn parseCSVData(arena: std.mem.Allocator, csv: []const u8) ![]u32 { fn initDataFromXml(
const cell_count = std.mem.countScalar(u8, csv, ',')+1; arena: std.mem.Allocator,
var cells: std.ArrayList(u32) = try .initCapacity(arena, cell_count); scratch: *std.heap.ArenaAllocator,
lexer: *xml.Lexer,
) ![]u32 {
var iter = xml.TagParser.init(lexer);
const attrs = try iter.begin("data");
var split_iter = std.mem.splitScalar(u8, csv, ','); 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| { while (split_iter.next()) |raw_tile_id| {
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace); 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); const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
cells.appendAssumeCapacity(tile_id); try temp_tiles.append(scratch.allocator(), tile_id);
} }
return cells.items;
}
fn initFromXml(
arena: std.mem.Allocator,
doc: xml.Document,
element_id: xml.Element.Id,
) !TileVariant {
const tag = try doc.getTagExpect(element_id, "layer");
const attrs = tag.variant.tag.attributes;
const layer_width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const layer_height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
const data_id = tag.tree.first_child;
var data_element = try doc.getTagExpect(data_id, "data");
const encoding = data_element.variant.tag.attributes.get("encoding") orelse "csv";
// TODO: compression
var chunks: std.ArrayList(Chunk) = .empty;
if (std.mem.eql(u8, encoding, "csv")) {
try chunks.ensureTotalCapacityPrecise(arena, doc.countChildren(data_id));
var iter = doc.iterChildren(data_id);
while (iter.next()) |child_id| {
const child = doc.get(child_id).?;
if (child.variant == .text) {
const data = try parseCSVData(arena, child.variant.text);
assert(data.len == layer_width * layer_height);
chunks.appendAssumeCapacity(Chunk{
.data = data,
.x = 0,
.y = 0,
.width = layer_width,
.height = layer_height,
});
} else if (child.variant == .tag and std.mem.eql(u8, child.variant.tag.name, "chunk")) {
const chunk_attrs = child.variant.tag.attributes;
const x = try chunk_attrs.getNumber(i32, "x") orelse return error.MissingWidth;
const y = try chunk_attrs.getNumber(i32, "y") orelse return error.MissingWidth;
const width = try chunk_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try chunk_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
const csv_text = doc.get(child.tree.first_child).?.variant.text; // TODO: Add checks
const data = try parseCSVData(arena, csv_text);
assert(data.len == width * height);
chunks.appendAssumeCapacity(Chunk{
.data = data,
.x = x,
.y = y,
.width = width,
.height = height,
});
}
}
} else { } else {
return error.UnknownEncodingType; return error.UnknownEncodingType;
} }
return TileVariant{ try iter.finish("data");
.width = layer_width,
.height = layer_height, return try arena.dupe(u32, temp_tiles.items);
.chunks = chunks.items
};
} }
pub fn get(self: TileVariant, x: i32, y: i32) ?u32 { pub fn get(self: TileVariant, x: usize, y: usize) ?u32 {
for (self.chunks) |chunk| { if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
const ox = x - chunk.x; return self.data[y * self.width + x];
const oy = y - chunk.y;
if ((0 <= ox and ox < chunk.width) and (0 <= oy and oy < chunk.height)) {
return chunk.data[@as(u32, @intCast(oy)) * chunk.width + @as(u32, @intCast(ox))];
}
} }
return null; return null;
} }
@ -123,9 +59,9 @@ pub const ImageVariant = struct {
width: ?u32, width: ?u32,
height: ?u32, height: ?u32,
fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image { fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
const tag = try doc.getTagExpect(element_id, "image"); var iter = xml.TagParser.init(lexer);
const attrs = tag.variant.tag.attributes; const attrs = try iter.begin("image");
// TODO: format // TODO: format
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource; const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
@ -133,6 +69,8 @@ pub const ImageVariant = struct {
const height = try attrs.getNumber(u32, "height") orelse null; const height = try attrs.getNumber(u32, "height") orelse null;
const transparent_color = try attrs.getColor("trans", false); const transparent_color = try attrs.getColor("trans", false);
try iter.finish("image");
return Image{ return Image{
.source = source, .source = source,
.transparent_color = transparent_color, .transparent_color = transparent_color,
@ -188,6 +126,15 @@ pub const Type = enum {
.{ "imagelayer", .image }, .{ "imagelayer", .image },
.{ "group", .group } .{ "group", .group }
}); });
fn toXmlName(self: Type) []const u8 {
return switch (self) {
.tile => "layer",
.object => "objectgroup",
.image => "imagelayer",
.group => "group",
};
}
}; };
pub const Variant = union(Type) { pub const Variant = union(Type) {
@ -212,13 +159,16 @@ variant: Variant,
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer,
) !Layer { ) !Layer {
const tag = doc.getTag(element_id) orelse return error.TagNotFound; const value = try lexer.peek() orelse return error.MissingStartTag;
if (value != .start_tag) return error.MissingStartTag;
const layer_type = Type.name_map.get(tag.variant.tag.name) orelse return error.UnknownLayerType; var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType;
const attrs = tag.variant.tag.attributes;
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 id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse ""; const name = try attrs.getDupe(arena, "name") orelse "";
@ -234,12 +184,14 @@ pub fn initFromXml(
var variant: Variant = undefined; var variant: Variant = undefined;
switch (layer_type) { switch (layer_type) {
.tile => { .tile => {
variant = Variant{ const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
.tile = try TileVariant.initFromXml( const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
arena, variant = .{
doc, .tile = TileVariant{
element_id .width = width,
) .height = height,
.data = &[0]u32{}
}
}; };
}, },
.image => { .image => {
@ -278,41 +230,45 @@ pub fn initFromXml(
var objects: std.ArrayList(Object) = .empty; var objects: std.ArrayList(Object) = .empty;
var layers: std.ArrayList(Layer) = .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) { if (variant == .object) {
try objects.ensureTotalCapacityPrecise(arena, doc.countChildren(element_id)); variant.object.items = try arena.dupe(Object, objects.items);
} }
if (variant == .group) { if (variant == .group) {
try layers.ensureTotalCapacityPrecise(arena, doc.countChildren(element_id)); variant.group.layers = try arena.dupe(Layer, layers.items);
}
var iter = doc.iterChildren(element_id);
while (iter.next()) |child_id| {
const child = doc.getTag(child_id) orelse continue;
const child_name = child.variant.tag.name;
if (std.mem.eql(u8, child_name, "properties")) {
properties = try Property.List.initFromXml(arena, doc, child_id);
} else if (variant == .image and std.mem.eql(u8, child_name, "image")) {
variant.image.image = try ImageVariant.Image.initFromXml(arena, doc, child_id);
} else if (variant == .object and std.mem.eql(u8, child_name, "object")) {
const object = try Object.initFromXml(arena, doc, child_id);
objects.appendAssumeCapacity(object);
} else if (variant == .group and Layer.Type.name_map.has(child_name)) {
const layer = try initFromXml(arena, doc, child_id);
layers.appendAssumeCapacity(layer);
}
}
if (variant == .object) {
variant.object.items = objects.items;
}
if (variant == .group) {
variant.group.layers = layers.items;
} }
return Layer{ return Layer{
@ -330,3 +286,7 @@ pub fn initFromXml(
.variant = variant, .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");
}

View File

@ -3,7 +3,6 @@ const std = @import("std");
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
const Position = @import("./position.zig"); const Position = @import("./position.zig");
const Color = @import("./color.zig"); const Color = @import("./color.zig");
const Property = @import("./property.zig");
const Object = @This(); const Object = @This();
@ -114,15 +113,14 @@ class: []const u8,
rotation: f32, // TODO: maybe this field should be moved to Shape struct rotation: f32, // TODO: maybe this field should be moved to Shape struct
visible: bool, visible: bool,
shape: Shape, shape: Shape,
properties: Property.List,
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer,
) !Object { ) !Object {
const tag = try doc.getTagExpect(element_id, "object"); var iter = xml.TagParser.init(lexer);
const attrs = tag.variant.tag.attributes; const attrs = try iter.begin("object");
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
const name = try attrs.getDupe(arena, "name") orelse ""; const name = try attrs.getDupe(arena, "name") orelse "";
@ -136,14 +134,9 @@ pub fn initFromXml(
var shape: ?Shape = null; var shape: ?Shape = null;
var iter = doc.iterChildren(element_id); while (try iter.next()) |node| {
while (iter.next()) |child_id| {
const child = doc.getTag(child_id) orelse continue;
const child_name = child.variant.tag.name;
const child_attrs = child.variant.tag.attributes;
if (shape == null) { if (shape == null) {
if (std.mem.eql(u8, child_name, "point")) { if (node.isTag("point")) {
shape = .{ shape = .{
.point = Shape.Point{ .point = Shape.Point{
.x = x, .x = x,
@ -151,7 +144,7 @@ pub fn initFromXml(
} }
}; };
} else if (std.mem.eql(u8, child_name, "ellipse")) { } else if (node.isTag("ellipse")) {
shape = .{ shape = .{
.ellipse = Shape.Ellipse{ .ellipse = Shape.Ellipse{
.x = x, .x = x,
@ -161,11 +154,11 @@ pub fn initFromXml(
} }
}; };
} else if (std.mem.eql(u8, child_name, "text")) { } else if (node.isTag("text")) {
const HorizontalShape = Shape.Text.HorizontalAlign; const HorizontalShape = Shape.Text.HorizontalAlign;
const VerticalAlign = Shape.Text.VerticalAlign; const VerticalAlign = Shape.Text.VerticalAlign;
const text_attrs = child_attrs; const text_attrs = node.tag.attributes;
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false; const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
const color = try text_attrs.getColor("color", true) orelse Color.black; 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 horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
@ -178,13 +171,13 @@ pub fn initFromXml(
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16; const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif"; const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
_ = try lexer.nextExpectStartTag("text");
var content: []const u8 = ""; var content: []const u8 = "";
const text_element = try doc.getTagExpect(child.tree.first_child, "text"); const content_value = try lexer.peek();
if (doc.get(text_element.tree.first_child)) |content_element| { if (content_value != null and content_value.? == .text) {
if (content_element.variant == .text) { content = try arena.dupe(u8, content_value.?.text);
content = try arena.dupe(u8, content_element.variant.text);
}
} }
try lexer.skipUntilMatchingEndTag("text");
shape = .{ shape = .{
.text = Shape.Text{ .text = Shape.Text{
@ -210,16 +203,14 @@ pub fn initFromXml(
}; };
continue; continue;
} else if (std.mem.eql(u8, child_name, "polygon")) { } else if (node.isTag("polygon")) {
const points_str = child_attrs.get("points") orelse ""; const points_str = node.tag.attributes.get("points") orelse "";
var points: std.ArrayList(Position) = .empty; var points: std.ArrayList(Position) = .empty;
try points.ensureTotalCapacityPrecise(arena, std.mem.countScalar(u8, points_str, ' ')+1);
var point_iter = std.mem.splitScalar(u8, points_str, ' '); var point_iter = std.mem.splitScalar(u8, points_str, ' ');
while (point_iter.next()) |point_str| { while (point_iter.next()) |point_str| {
const point = try Position.parseCommaDelimited(point_str); const point = try Position.parseCommaDelimited(point_str);
points.appendAssumeCapacity(point); try points.append(scratch.allocator(), point);
} }
shape = .{ shape = .{
@ -232,6 +223,7 @@ pub fn initFromXml(
} }
} }
try iter.skip();
} }
if (shape == null) { if (shape == null) {
@ -257,7 +249,7 @@ pub fn initFromXml(
} }
} }
const properties = try Property.List.initFromXmlContainer(arena, doc, element_id); try iter.finish("object");
return Object{ return Object{
.id = id, .id = id,
@ -265,8 +257,7 @@ pub fn initFromXml(
.class = class, .class = class,
.rotation = rotation, .rotation = rotation,
.visible = visible, .visible = visible,
.shape = shape orelse return error.UnknownShapeType, .shape = shape orelse return error.UnknownShapeType
.properties = properties
}; };
} }

View File

@ -33,48 +33,32 @@ pub const List = struct {
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer
) !Property.List { ) !Property.List {
_ = try doc.getTagExpect(element_id, "properties"); var iter = xml.TagParser.init(lexer);
_ = try iter.begin("properties");
var properties: std.ArrayList(Property) = .empty; var temp_properties: std.ArrayList(Property) = .empty;
try properties.ensureTotalCapacityPrecise(arena, doc.countChildren(element_id));
var iter = doc.iterChildren(element_id); while (try iter.next()) |node| {
while (iter.next()) |child_id| { if (node.isTag("property")) {
const child = doc.getTag(child_id) orelse continue; const property = try Property.initFromXml(arena, lexer);
const child_name = child.variant.tag.name; try temp_properties.append(scratch.allocator(), property);
continue;
if (std.mem.eql(u8, child_name, "property")) {
const property = try Property.initFromXml(arena, doc, child_id);
properties.appendAssumeCapacity(property);
} }
try iter.skip();
} }
try iter.finish("properties");
const properties = try arena.dupe(Property, temp_properties.items);
return List{ return List{
.items = properties.items .items = properties
}; };
} }
pub fn initFromXmlContainer(
arena: std.mem.Allocator,
doc: xml.Document,
element_id: xml.Element.Id
) !Property.List {
var iter = doc.iterChildren(element_id);
while (iter.next()) |child_id| {
const child = doc.getTag(child_id) orelse continue;
const child_name = child.variant.tag.name;
if (std.mem.eql(u8, child_name, "properties")) {
return try Property.List.initFromXml(arena, doc, child_id);
}
}
return .empty;
}
pub fn get(self: List, name: []const u8) ?Value { pub fn get(self: List, name: []const u8) ?Value {
for (self.items) |item| { for (self.items) |item| {
if (std.mem.eql(u8, item.name, name)) { if (std.mem.eql(u8, item.name, name)) {
@ -86,19 +70,8 @@ pub const List = struct {
pub fn getString(self: List, name: []const u8) ?[]const u8 { pub fn getString(self: List, name: []const u8) ?[]const u8 {
if (self.get(name)) |value| { if (self.get(name)) |value| {
if (value == .string) {
return value.string; return value.string;
} }
}
return null;
}
pub fn getInt(self: List, name: []const u8) ?i32 {
if (self.get(name)) |value| {
if (value == .int) {
return value.int;
}
}
return null; return null;
} }
}; };
@ -110,9 +83,9 @@ pub fn init(name: []const u8, value: Value) Property {
}; };
} }
pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Property { pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
const tag = try doc.getTagExpect(element_id, "property"); var iter = xml.TagParser.init(lexer);
const attrs = tag.variant.tag.attributes; const attrs = try iter.begin("property");
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName; const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
const prop_type_str = attrs.get("type") orelse "string"; const prop_type_str = attrs.get("type") orelse "string";
@ -131,6 +104,8 @@ pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.
} }
}; };
try iter.finish("property");
return Property{ return Property{
.name = name, .name = name,
.value = value .value = value

View File

@ -12,5 +12,5 @@ pub const Tileset = @import("./tileset.zig");
pub const Tilemap = @import("./tilemap.zig"); pub const Tilemap = @import("./tilemap.zig");
test { test {
_ = std.testing.refAllDecls(@This()); _ = std.testing.refAllDeclsRecursive(@This());
} }

View File

@ -1,5 +1,4 @@
const std = @import("std"); const std = @import("std");
const log = std.log;
const xml = @import("./xml.zig"); const xml = @import("./xml.zig");
const Io = std.Io; const Io = std.Io;
const assert = std.debug.assert; const assert = std.debug.assert;
@ -55,13 +54,15 @@ pub const TilesetReference = struct {
source: []const u8, source: []const u8,
first_gid: u32, first_gid: u32,
pub fn initFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !TilesetReference { pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference {
const tag = try doc.getTagExpect(element_id, "tileset"); var iter = xml.TagParser.init(lexer);
const attrs = tag.variant.tag.attributes; const attrs = try iter.begin("tileset");
const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid; const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid;
const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid; const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid;
try iter.finish("tileset");
return TilesetReference{ return TilesetReference{
.source = source, .source = source,
.first_gid = first_gid .first_gid = first_gid
@ -105,20 +106,40 @@ next_object_id: u32,
tilesets: []TilesetReference, tilesets: []TilesetReference,
layers: []Layer, 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: // Map specification:
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map // https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
pub fn initFromXml( pub fn initFromXml(
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer,
) !Tilemap { ) !Tilemap {
var arena_allocator = std.heap.ArenaAllocator.init(gpa); var arena_allocator = std.heap.ArenaAllocator.init(gpa);
errdefer arena_allocator.deinit(); errdefer arena_allocator.deinit();
const arena = arena_allocator.allocator(); const arena = arena_allocator.allocator();
const tag = try doc.getTagExpect(element_id, "map"); var iter = xml.TagParser.init(lexer);
const map_attrs = tag.variant.tag.attributes; const map_attrs = try iter.begin("map");
const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion; const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
const tiled_version = try map_attrs.getDupe(arena, "tiledversion"); const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
@ -144,43 +165,26 @@ pub fn initFromXml(
// TODO: staggerindex // TODO: staggerindex
} }
var layer_count: usize = 0;
var tileset_count: usize = 0;
{
var iter = doc.iterChildren(element_id);
while (iter.next()) |child_id| {
const child = doc.getTag(child_id) orelse continue;
const child_name = child.variant.tag.name;
if (std.mem.eql(u8, child_name, "tileset")) {
tileset_count += 1;
} else {
layer_count += 1;
}
}
}
var tileset_list: std.ArrayList(TilesetReference) = .empty; var tileset_list: std.ArrayList(TilesetReference) = .empty;
try tileset_list.ensureTotalCapacityPrecise(arena, tileset_count);
var layer_list: std.ArrayList(Layer) = .empty; var layer_list: std.ArrayList(Layer) = .empty;
try layer_list.ensureTotalCapacityPrecise(arena, layer_count);
var iter = doc.iterChildren(element_id); while (try iter.next()) |node| {
while (iter.next()) |child_id| { if (node.isTag("tileset")) {
const child = doc.getTag(child_id) orelse continue; try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer));
const child_name = child.variant.tag.name; continue;
} else if (Layer.isLayerNode(node)) {
if (std.mem.eql(u8, child_name, "tileset")) { const layer = try Layer.initFromXml(
tileset_list.appendAssumeCapacity(try TilesetReference.initFromXml(arena, doc, child_id));
} else {
layer_list.appendAssumeCapacity(try Layer.initFromXml(
arena, arena,
doc, scratch,
child_id 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 tilesets = try arena.dupe(TilesetReference, tileset_list.items);
const layers = try arena.dupe(Layer, layer_list.items); const layers = try arena.dupe(Layer, layer_list.items);
@ -219,7 +223,7 @@ fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
return result; return result;
} }
pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: i32, y: i32) ?Tile { pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile {
assert(layer.variant == .tile); assert(layer.variant == .tile);
const tile_variant = layer.variant.tile; const tile_variant = layer.variant.tile;
@ -244,10 +248,10 @@ pub fn getLayer(self: *const Tilemap, name: []const u8) ?*const Layer {
} }
pub const Bounds = struct { pub const Bounds = struct {
min_x: i32, min_x: usize,
min_y: i32, min_y: usize,
max_x: i32, max_x: usize,
max_y: i32, max_y: usize,
const zero = Bounds{ const zero = Bounds{
.min_x = 0, .min_x = 0,
@ -256,16 +260,12 @@ pub const Bounds = struct {
.max_y = 0, .max_y = 0,
}; };
pub fn width(self: Bounds) u32 { pub fn width(self: Bounds) usize {
return @intCast(self.max_x - self.min_x + 1); return self.max_x - self.min_x + 1;
} }
pub fn height(self: Bounds) u32 { pub fn height(self: Bounds) usize {
return @intCast(self.max_y - self.min_y + 1); return self.max_y - self.min_y + 1;
}
pub fn isInside(self: Bounds, x: i32, y: i32) bool {
return (self.min_x <= x and x <= self.max_x) and (self.min_y <= y and y <= self.max_y);
} }
}; };
@ -277,22 +277,26 @@ pub fn getTileBounds(self: *const Tilemap) Bounds {
continue; continue;
} }
for (layer.variant.tile.chunks) |chunk| { for (0..self.height) |y| {
const chunk_bounds = Bounds{ for (0..self.width) |x| {
.min_x = chunk.x, const gid = layer.variant.tile.get(x, y).?;
.min_y = chunk.y, if (gid != 0) {
.max_x = chunk.x + @as(i32, @intCast(chunk.width)),
.max_y = chunk.y + @as(i32, @intCast(chunk.height))
};
if (result) |bounds| { if (result) |bounds| {
result = Bounds{ result = Bounds{
.min_x = @min(chunk_bounds.min_x, bounds.min_x), .min_x = @min(x, bounds.min_x),
.min_y = @min(chunk_bounds.min_y, bounds.min_y), .min_y = @min(y, bounds.min_y),
.max_x = @max(chunk_bounds.max_x, bounds.max_x), .max_x = @max(x, bounds.max_x),
.max_y = @max(chunk_bounds.max_y, bounds.max_y) .max_y = @max(y, bounds.max_y)
}; };
} else { } else {
result = chunk_bounds; result = Bounds{
.min_x = x,
.min_y = y,
.max_x = x,
.max_y = y
};
}
}
} }
} }
} }
@ -300,59 +304,6 @@ pub fn getTileBounds(self: *const Tilemap) Bounds {
return result orelse Bounds.zero; return result orelse Bounds.zero;
} }
fn isRowEmpty(tile_layer: Layer.TileVariant, x: i32, y: i32, length: u32) bool {
for (0..length) |i| {
const gid = tile_layer.get(x + @as(i32, @intCast(i)), y) orelse continue;
if (gid != 0) {
return false;
}
}
return true;
}
fn isColumnEmpty(tile_layer: Layer.TileVariant, x: i32, y: i32, length: u32) bool {
for (0..length) |i| {
const gid = tile_layer.get(x, y + @as(i32, @intCast(i))) orelse continue;
if (gid != 0) {
return false;
}
}
return true;
}
pub fn trimBoundsByEmptyTiles(self: *const Tilemap, bounds: Bounds) Bounds {
var result: Bounds = bounds;
for (self.layers) |*layer| {
if (layer.variant != .tile) {
continue;
}
const tile_layer = layer.variant.tile;
// Trim left
while (result.min_x < result.max_x and isColumnEmpty(tile_layer, result.min_x, result.min_y, result.height())) {
result.min_x += 1;
}
// Trim right
while (result.min_x < result.max_x and isColumnEmpty(tile_layer, result.max_x, result.min_y, result.height())) {
result.max_x -= 1;
}
// Trim top
while (result.min_y < result.max_y and isRowEmpty(tile_layer, result.min_x, result.min_y, result.width())) {
result.min_y += 1;
}
// Trim bottom
while (result.min_y < result.max_y and isRowEmpty(tile_layer, result.min_x, result.max_y, result.width())) {
result.max_y -= 1;
}
}
return result;
}
pub fn deinit(self: *const Tilemap) void { pub fn deinit(self: *const Tilemap) void {
self.arena.deinit(); self.arena.deinit();
} }

View File

@ -14,14 +14,16 @@ pub const Image = struct {
width: u32, width: u32,
height: u32, height: u32,
pub fn intFromXml(arena: std.mem.Allocator, doc: xml.Document, element_id: xml.Element.Id) !Image { pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
const tag = try doc.getTagExpect(element_id, "image"); var iter = xml.TagParser.init(lexer);
const attrs = tag.variant.tag.attributes; const attrs = try iter.begin("image");
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource; const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth; const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight; const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
try iter.finish("image");
return Image{ return Image{
.source = source, .source = source,
.width = width, .width = width,
@ -36,14 +38,26 @@ pub const Tile = struct {
pub fn initFromXml( pub fn initFromXml(
arena: std.mem.Allocator, arena: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer,
) !Tile { ) !Tile {
const tag = try doc.getTagExpect(element_id, "tile"); var iter = xml.TagParser.init(lexer);
const tile_attrs = tag.variant.tag.attributes; const tile_attrs = try iter.begin("tile");
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId; const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
const properties = try Property.List.initFromXmlContainer(arena, doc, element_id);
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{ return Tile{
.id = id, .id = id,
@ -110,16 +124,36 @@ image: Image,
tiles: []Tile, 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( pub fn initFromXml(
gpa: std.mem.Allocator, gpa: std.mem.Allocator,
doc: xml.Document, scratch: *std.heap.ArenaAllocator,
element_id: xml.Element.Id lexer: *xml.Lexer
) !Tileset { ) !Tileset {
var arena_state = std.heap.ArenaAllocator.init(gpa); var arena_state = std.heap.ArenaAllocator.init(gpa);
const arena = arena_state.allocator(); const arena = arena_state.allocator();
const tag = try doc.getTagExpect(element_id, "tileset"); var iter = xml.TagParser.init(lexer);
const tileset_attrs = tag.variant.tag.attributes; const tileset_attrs = try iter.begin("tileset");
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag; 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 tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
@ -131,20 +165,20 @@ pub fn initFromXml(
var image: ?Image = null; var image: ?Image = null;
var tiles_list: std.ArrayList(Tile) = .empty; var tiles_list: std.ArrayList(Tile) = .empty;
try tiles_list.ensureTotalCapacityPrecise(arena, doc.countChildren(element_id));
var iter = doc.iterChildren(element_id); while (try iter.next()) |node| {
while (iter.next()) |child_id| { if (node.isTag("image")) {
const child = doc.getTag(child_id) orelse continue; image = try Image.intFromXml(arena, lexer);
const child_name = child.variant.tag.name; continue;
} else if (node.isTag("tile")) {
const tile = try Tile.initFromXml(arena, scratch, lexer);
try tiles_list.append(scratch.allocator(), tile);
continue;
}
if (std.mem.eql(u8, child_name, "image")) { try iter.skip();
image = try Image.intFromXml(arena, doc, child_id);
} else if (std.mem.eql(u8, child_name, "tile")) {
const tile = try Tile.initFromXml(arena, doc, child_id);
tiles_list.appendAssumeCapacity(tile);
}
} }
try iter.finish("tileset");
const tiles = try arena.dupe(Tile, tiles_list.items); const tiles = try arena.dupe(Tile, tiles_list.items);

View File

@ -1,6 +1,5 @@
const std = @import("std"); const std = @import("std");
const Io = std.Io; const Io = std.Io;
const Allocator = std.mem.Allocator;
const assert = std.debug.assert; const assert = std.debug.assert;
const Color = @import("./color.zig"); const Color = @import("./color.zig");
@ -106,160 +105,102 @@ pub const Attribute = struct {
} }
}; };
pub const Element = struct { pub const Tag = struct {
variant: union(enum) {
tag: struct {
name: []const u8, name: []const u8,
attributes: Attribute.List, attributes: Attribute.List
},
text: []const u8,
},
// TODO: Maybe an element should contain a "inner contents" field?
// With which you could index into the text content.
// But, need to figure out how it would interact with comments.
//
// Maybe comment removal should be a separate pass?
tree: struct {
first_child: Id = .nil,
last_child: Id = .nil,
next_sibling: Id = .nil,
} = .{},
pub const Id = enum(i32) {
_,
pub const nil: Id = @enumFromInt(-1);
pub const root: Id = @enumFromInt(0);
};
}; };
pub const Document = struct { pub const Lexer = struct {
elements: []Element, pub const Buffers = struct {
scratch: std.heap.ArenaAllocator,
text: std.ArrayList(u8),
pub fn get(self: Document, id: Element.Id) ?Element { pub fn init(allocator: std.mem.Allocator) Buffers {
if (id == Element.Id.nil) { return Buffers{
return null; .scratch = std.heap.ArenaAllocator.init(allocator),
} .text = .empty
const index = @intFromEnum(id);
assert(index >= 0);
return self.elements[@intCast(index)];
}
pub fn getTag(self: Document, id: Element.Id) ?Element {
const element = self.get(id) orelse return null;
if (element.variant == .tag) {
return element;
}
return null;
}
pub fn getTagExpect(self: Document, id: Element.Id, name: []const u8) !Element {
const tag = self.getTag(id) orelse return error.TagNotFound;
const tag_name = tag.variant.tag.name;
if (!std.mem.eql(u8, tag_name, name)) {
return error.UnexpectedTagName;
}
return tag;
}
pub fn iterChildren(self: Document, id: Element.Id) SiblingIterator {
var first_child: Element.Id = .nil;
if (self.get(id)) |tag| {
first_child = tag.tree.first_child;
}
return SiblingIterator{
.doc = self,
.current = first_child
}; };
} }
pub fn countChildren(self: Document, id: Element.Id) usize { pub fn clear(self: *Buffers) void {
var count: usize = 0; self.text.clearRetainingCapacity();
var iter = self.iterChildren(id); _ = self.scratch.reset(.retain_capacity);
while (iter.next()) |_| {
count += 1;
}
return count;
} }
const SiblingIterator = struct { pub fn deinit(self: *Buffers) void {
doc: Document, const allocator = self.scratch.child_allocator;
current: Element.Id, self.scratch.deinit();
self.text.deinit(allocator);
pub fn next(self: *SiblingIterator) ?Element.Id {
const element = self.doc.get(self.current) orelse return null;
const current = self.current;
self.current = element.tree.next_sibling;
return current;
} }
}; };
};
const Lexer = struct {
pub const Token = union(enum) { pub const Token = union(enum) {
start_tag_begin: []const u8, start_tag: Tag,
attribute: Attribute,
start_tag_end: []const u8,
end_tag: []const u8, end_tag: []const u8,
text: []const u8, text: []const u8,
// TODO: Add comment type?
pub fn format(self: Token, writer: *Io.Writer) Io.Writer.Error!void { pub fn isStartTag(self: Token, name: []const u8) bool {
switch(self) { if (self == .start_tag) {
.start_tag_begin => |name| try writer.print("Token{{ .start_tag_begin, {s} }}", .{name}), return std.mem.eql(u8, self.start_tag.name, name);
.attribute => |attr| try writer.print("Token{{ .attribute, {s}='{s}' }}", .{attr.name, attr.value}),
.start_tag_end => try writer.print("Token{{ .start_tag_end }}", .{}),
.end_tag => |name| try writer.print("Token{{ .end_tag, {s} }}", .{name}),
.text => |text| try writer.print("Token{{ .text, {s} }}", .{text}),
} }
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;
} }
}; };
text: []const u8, pub const StepResult = struct {
cursor: usize = 0, token: ?Token,
inside_start_tag: ?[]const u8 = null, self_closing: bool = false,
queued_end_tag: ?[]const u8 = null, };
pub fn next(self: *Lexer) !?Token { pub const TestingContext = struct {
if (self.queued_end_tag) |name| { io_reader: Io.Reader,
self.queued_end_tag = null; buffers: Buffers,
return Token{ lexer: Lexer,
.end_tag = name
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
}; };
} }
while (self.cursor < self.text.len) { fn step(self: *Lexer) !StepResult {
if (self.inside_start_tag) |tag_name| { _ = self.buffers.scratch.reset(.retain_capacity);
if (try self.parseAttribute()) |attribute| {
return Token{
.attribute = attribute
};
} else {
try self.skipWhiteSpace();
if (std.mem.eql(u8, try self.peekBytes(1), ">")) { if (try self.peekByte() == '<') {
self.tossBytes(1);
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
self.tossBytes(2);
self.queued_end_tag = tag_name;
} else {
return error.UnfinishedStartTag;
}
self.inside_start_tag = null;
return Token{
.start_tag_end = tag_name
};
}
} else if (try self.peekByte() == '<') {
self.tossByte(); self.tossByte();
if (try self.peekByte() == '/') { if (try self.peekByte() == '/') {
@ -273,7 +214,8 @@ const Lexer = struct {
return error.InvalidEndTag; return error.InvalidEndTag;
} }
return Token{ .end_tag = name }; const token = Token{ .end_tag = name };
return .{ .token = token };
} else if (try self.peekByte() == '?') { } else if (try self.peekByte() == '?') {
// Prolog tag // Prolog tag
@ -282,14 +224,9 @@ const Lexer = struct {
return error.InvalidPrologTag; return error.InvalidPrologTag;
} }
var attributes_buf: [8]Attribute = undefined; const attributes = try self.parseAttributes();
const attributes_len = try self.parseAttributesBounded(&attributes_buf);
try self.skipWhiteSpace(); try self.skipWhiteSpace();
const attributes = Attribute.List{
.items = attributes_buf[0..attributes_len]
};
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) { if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
return error.MissingPrologEnd; return error.MissingPrologEnd;
} }
@ -303,31 +240,104 @@ const Lexer = struct {
return error.InvalidPrologEncoding; return error.InvalidPrologEncoding;
} }
return .{ .token = null };
} else { } else {
// Start tag // Start tag
const name = try self.parseName(); const name = try self.parseName();
self.inside_start_tag = name; const attributes = try self.parseAttributes();
try self.skipWhiteSpace();
return Token{ const token = Token{
.start_tag_begin = name .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 { } else {
try self.skipWhiteSpace(); try self.skipWhiteSpace();
if (!self.hasBytes(1)) {
continue;
}
const text_start = self.cursor; const text_start = self.cursor;
while (try self.peekByte() != '<') { while (try self.peekByte() != '<') {
self.tossByte(); self.tossByte();
} }
var text: []const u8 = self.text[text_start..self.cursor]; var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace); text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
var token: ?Token = null;
if (text.len > 0) { if (text.len > 0) {
return Token{ .text = text }; 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;
} }
} }
@ -371,6 +381,45 @@ const Lexer = struct {
} }
} }
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 { fn isNameStartChar(c: u8) bool {
return c == ':' or c == '_' or std.ascii.isAlphabetic(c); return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
} }
@ -380,14 +429,16 @@ const Lexer = struct {
} }
fn hasBytes(self: *Lexer, n: usize) bool { fn hasBytes(self: *Lexer, n: usize) bool {
return self.cursor + n <= self.text.len; const text = self.buffers.text.items;
return self.cursor + n <= text.len;
} }
fn peekBytes(self: *Lexer, n: usize) ![]const u8 { fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
if (self.hasBytes(n)) { if (self.hasBytes(n)) {
return self.text[self.cursor..][0..n]; const text = self.buffers.text.items;
return text[self.cursor..][0..n];
} }
return error.UnexpectedEOF; return error.EndOfTextBuffer;
} }
fn tossBytes(self: *Lexer, n: usize) void { fn tossBytes(self: *Lexer, n: usize) void {
@ -424,11 +475,11 @@ const Lexer = struct {
} }
} }
return self.text[name_start..self.cursor]; return self.buffers.text.items[name_start..self.cursor];
} }
fn skipWhiteSpace(self: *Lexer) !void { fn skipWhiteSpace(self: *Lexer) !void {
while (self.cursor < self.text.len and std.ascii.isWhitespace(try self.peekByte())) { while (std.ascii.isWhitespace(try self.peekByte())) {
self.tossByte(); self.tossByte();
} }
} }
@ -453,14 +504,18 @@ const Lexer = struct {
value_len += 1; value_len += 1;
} }
return self.text[value_start..][0..value_len]; return self.buffers.text.items[value_start..][0..value_len];
} }
fn parseAttribute(self: *Lexer) !?Attribute { fn parseAttributes(self: *Lexer) !Attribute.List {
const arena = self.buffers.scratch.allocator();
var attributes: std.ArrayList(Attribute) = .empty;
while (true) {
try self.skipWhiteSpace(); try self.skipWhiteSpace();
const name = try self.parseName(); const name = try self.parseName();
if (name.len == 0) { if (name.len == 0) {
return null; break;
} }
try self.skipWhiteSpace(); try self.skipWhiteSpace();
@ -470,287 +525,163 @@ const Lexer = struct {
try self.skipWhiteSpace(); try self.skipWhiteSpace();
const value = try self.parseAttributeValue(); const value = try self.parseAttributeValue();
return Attribute{
.name = name,
.value = value
};
}
fn parseAttributesBounded(self: *Lexer, buffer: []Attribute) !usize {
var attributes: std.ArrayList(Attribute) = .initBuffer(buffer);
while (try self.parseAttribute()) |attr| {
const list = Attribute.List{ .items = attributes.items }; const list = Attribute.List{ .items = attributes.items };
if (list.get(attr.name) != null) { if (list.get(name) != null) {
return error.DuplicateAttribute; return error.DuplicateAttribute;
} }
try attributes.appendBounded(attr); try attributes.append(arena, Attribute{
.name = name,
.value = value
});
} }
return attributes.items.len; return Attribute.List{
} .items = attributes.items
};
fn expectStartTagBegin(lexer: *Lexer, expected_name: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .start_tag_begin);
try std.testing.expectEqualStrings(expected_name, token.start_tag_begin);
}
fn expectStartTagEnd(lexer: *Lexer) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .start_tag_end);
}
fn expectEndTag(lexer: *Lexer, expected_name: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .end_tag);
try std.testing.expectEqualStrings(expected_name, token.end_tag);
}
fn expectDone(lexer: *Lexer) !void {
try std.testing.expect(try lexer.next() == null);
}
fn expectText(lexer: *Lexer, expected_text: []const u8) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .text);
try std.testing.expectEqualStrings(expected_text, token.text);
}
fn expectAttribute(lexer: *Lexer, expected: Attribute) !void {
const token = (try lexer.next()).?;
try std.testing.expect(token == .attribute);
try std.testing.expectEqualStrings(expected.name, token.attribute.name);
try std.testing.expectEqualStrings(expected.value, token.attribute.value);
} }
test "self closing tag" { test "self closing tag" {
const text = const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello /> \\ <hello />
; );
defer ctx.deinit();
var lexer: Lexer = .{ .text = text }; try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try expectStartTagBegin(&lexer, "hello"); try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try expectStartTagEnd(&lexer); try std.testing.expect((try ctx.lexer.next()) == null);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
test "tag" { test "tag" {
const text = const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello></hello> \\ <hello></hello>
; );
defer ctx.deinit();
var lexer: Lexer = .{ .text = text }; try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try expectStartTagBegin(&lexer, "hello"); try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try expectStartTagEnd(&lexer); try std.testing.expect((try ctx.lexer.next()) == null);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
test "tag with prolog" { test "tag with prolog" {
const text = const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <?xml version="1.0" encoding="UTF-8"?> \\ <?xml version="1.0" encoding="UTF-8"?>
\\ <hello></hello> \\ <hello></hello>
; );
defer ctx.deinit();
var lexer: Lexer = .{ .text = text }; try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try expectStartTagBegin(&lexer, "hello"); try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try expectStartTagEnd(&lexer); try std.testing.expect((try ctx.lexer.next()) == null);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
test "text content" { test "text content" {
const text = const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello> Hello World </hello> \\ <hello> Hello World </hello>
; );
defer ctx.deinit();
var lexer: Lexer = .{ .text = text }; try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
try expectStartTagBegin(&lexer, "hello"); try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
try expectStartTagEnd(&lexer); try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
try expectText(&lexer, "Hello World"); try std.testing.expect((try ctx.lexer.next()) == null);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
test "attributes" { test "attributes" {
const text = const allocator = std.testing.allocator;
var ctx: TestingContext = undefined;
ctx.init(allocator,
\\ <hello a='1' b='2'/> \\ <hello a='1' b='2'/>
; );
defer ctx.deinit();
var lexer: Lexer = .{ .text = text }; const token = try ctx.lexer.next();
try expectStartTagBegin(&lexer, "hello"); const attrs = token.?.start_tag.attributes;
try expectAttribute(&lexer, .{ .name = "a", .value = "1" }); try std.testing.expectEqualStrings("1", attrs.get("a").?);
try expectAttribute(&lexer, .{ .name = "b", .value = "2" }); try std.testing.expectEqualStrings("2", attrs.get("b").?);
try expectStartTagEnd(&lexer);
try expectEndTag(&lexer, "hello");
try expectDone(&lexer);
} }
}; };
pub const Parser = struct { // TODO: The API for this is easy to misuse.
elements: []Element, // Design a better API for using Reader
attributes: []Attribute, // As a compromise `assert` was used to guard against some of the ways this can be misused
stack: []Element.Id, pub const TagParser = struct {
lexer: *Lexer,
const InitOptions = struct { begin_called: bool = false,
max_elements: u32 = 4096, finish_called: bool = false,
max_attributes: u32 = 4096,
max_depth: u32 = 256,
};
pub fn initCapacity(gpa: Allocator, opts: InitOptions) !Parser { pub const Node = union(enum) {
const tags = try gpa.alloc(Element, opts.max_elements); tag: Tag,
errdefer gpa.free(tags); text: []const u8,
const attributes = try gpa.alloc(Attribute, opts.max_attributes); pub fn isTag(self: Node, name: []const u8) bool {
errdefer gpa.free(attributes); if (self == .tag) {
return std.mem.eql(u8, self.tag.name, name);
const stack = try gpa.alloc(Element.Id, opts.max_depth);
errdefer gpa.free(stack);
return Parser{
.elements = tags,
.attributes = attributes,
.stack = stack,
};
} }
return false;
pub fn deinit(self: Parser, gpa: Allocator) void {
gpa.free(self.elements);
gpa.free(self.attributes);
gpa.free(self.stack);
}
const ParserContext = struct {
elements: std.ArrayList(Element),
stack: std.ArrayList(Element.Id),
fn getElement(self: *ParserContext, id: Element.Id) *Element {
return &self.elements.items[@intCast(@intFromEnum(id))];
}
pub fn pushElement(self: *ParserContext) !*Element {
const element_index = self.elements.items.len;
const element_id: Element.Id = @enumFromInt(element_index);
const element = self.elements.addOneBounded() catch return error.TooManyElements;
element.tree = .{};
if (self.stack.getLastOrNull()) |parent_id| {
const parent = self.getElement(parent_id);
if (parent.tree.last_child != Element.Id.nil) {
const last_child = &self.elements.items[@intCast(@intFromEnum(parent.tree.last_child))];
last_child.tree.next_sibling = element_id;
} else {
parent.tree.first_child = element_id;
}
parent.tree.last_child = element_id;
}
self.stack.appendBounded(element_id) catch return error.StackTooDeep;
return element;
}
pub fn popElement(self: *ParserContext) !void {
if (self.stack.items.len == 0) {
return error.UnbalancedBeginAndEndTags;
}
_ = self.stack.pop();
} }
}; };
pub fn parse(self: Parser, text: []const u8) !Document { pub fn init(lexer: *Lexer) TagParser {
var ctx = ParserContext{ return TagParser{
.elements = .initBuffer(self.elements), .lexer = lexer,
.stack = .initBuffer(self.stack)
};
var attributes_used: usize = 0;
var attributes: std.ArrayList(Attribute) = .initBuffer(self.attributes[attributes_used..]);
var lexer = Lexer{ .text = text };
while (try lexer.next()) |token| {
switch (token) {
.start_tag_begin => |name| {
_ = name; // autofix
},
.attribute => |attribute| {
attributes.appendBounded(attribute) catch return error.TooManyAttributes;
},
.start_tag_end => |name| {
const element = try ctx.pushElement();
element.variant = .{
.tag = .{
.name = name,
.attributes = .{ .items = attributes.items }
}
};
attributes_used += attributes.items.len;
attributes = .initBuffer(self.attributes[attributes_used..]);
},
.end_tag => {
try ctx.popElement();
},
.text => |element_text| {
const element = try ctx.pushElement();
element.variant = .{
.text = element_text
};
try ctx.popElement();
},
}
}
if (ctx.stack.items.len > 0) {
return error.UnbalancedBeginAndEndTags;
}
return Document{
.elements = ctx.elements.items
}; };
} }
test { pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
const gpa = std.testing.allocator; assert(!self.begin_called);
var parser = try Parser.initCapacity(gpa, .{}); self.begin_called = true;
defer parser.deinit(gpa);
const doc = try parser.parse( return try self.lexer.nextExpectStartTag(name);
\\ <text>Hello, World!</text>
);
const root = try doc.getTagExpect(.root, "text");
const first_child = doc.get(root.tree.first_child).?;
try std.testing.expectEqualStrings("Hello, World!", first_child.variant.text);
} }
test { pub fn finish(self: *TagParser, name: []const u8) !void {
const gpa = std.testing.allocator; assert(self.begin_called);
var parser = try Parser.initCapacity(gpa, .{}); assert(!self.finish_called);
defer parser.deinit(gpa); self.finish_called = true;
const doc = try parser.parse( try self.lexer.skipUntilMatchingEndTag(name);
\\ <container> }
\\ <box>1</box>
\\ <box>2</box> pub fn next(self: *TagParser) !?Node {
\\ <box>3</box> assert(self.begin_called);
\\ </container> assert(!self.finish_called);
);
var iter = doc.iterChildren(.root); const value = try self.lexer.peek() orelse return error.MissingEndTag;
var count: usize = 0; if (value == .end_tag) {
while (iter.next()) |_| { return null;
count += 1; }
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);
} }
try std.testing.expectEqual(3, count);
} }
}; };
test {
_ = Lexer;
_ = Parser;
}

View File

@ -7,7 +7,7 @@ const STBImage = @import("stb_image");
const STBTrueType = @import("stb_truetype"); const STBTrueType = @import("stb_truetype");
const Tiled = @import("tiled"); const Tiled = @import("tiled");
const Platform = @import("platform"); const Platform = @import("platform/root.zig");
const Gfx = Platform.Gfx; const Gfx = Platform.Gfx;
const Vec2 = Platform.Vec2; const Vec2 = Platform.Vec2;
const Audio = Platform.Audio; const Audio = Platform.Audio;
@ -47,11 +47,11 @@ const PlayerSprites = struct {
const Tile = struct { const Tile = struct {
ground_sprite: ?Gfx.Sprite.Id, ground_sprite: ?Gfx.Sprite.Id,
entities_by_layer: std.EnumArray(Entity.Layer, Entity.Id), entity: Entity.Id,
const empty = Tile{ const empty = Tile{
.ground_sprite = null, .ground_sprite = null,
.entities_by_layer = .initFill(.nil) .entity = .nil
}; };
}; };
@ -71,17 +71,6 @@ const Tilemap = struct {
}; };
} }
pub fn clone(self: Tilemap, gpa: Allocator) !Tilemap {
const tiles = try gpa.dupe(Tile, self.tiles);
errdefer gpa.free(tiles);
return Tilemap{
.tiles = tiles,
.width = self.width,
.height = self.height,
};
}
pub fn deinit(self: *Tilemap, gpa: Allocator) void { pub fn deinit(self: *Tilemap, gpa: Allocator) void {
gpa.free(self.tiles); gpa.free(self.tiles);
} }
@ -95,7 +84,6 @@ const Tilemap = struct {
const Entity = struct { const Entity = struct {
type: Type, type: Type,
layer: Layer,
pos_x: i32 = -1, pos_x: i32 = -1,
pos_y: i32 = -1, pos_y: i32 = -1,
@ -108,15 +96,9 @@ const Entity = struct {
const Type = enum { const Type = enum {
player, player,
box, box,
goal_marker,
wall wall
}; };
const Layer = enum {
ground,
surface,
};
const SlotMap = Platform.SlotMapType(u16, u8, Entity); const SlotMap = Platform.SlotMapType(u16, u8, Entity);
const Id = SlotMap.Id; const Id = SlotMap.Id;
}; };
@ -125,45 +107,24 @@ const World = struct {
tilemap: Tilemap, tilemap: Tilemap,
entities: Entity.SlotMap, entities: Entity.SlotMap,
pub fn clone(self: World, gpa: Allocator) !World { fn spawnPlayer(self: *World) !Entity.Id {
var tilemap = try self.tilemap.clone(gpa); return try self.entities.insert(Entity{
errdefer tilemap.deinit(gpa); .type = .player,
});
var entity_slots = try self.entities.slots.clone(gpa);
errdefer entity_slots.deinit(gpa);
var entities = self.entities;
entities.slots = entity_slots;
return World{
.tilemap = tilemap,
.entities = entities
};
} }
pub fn deinit(self: *World, gpa: Allocator) void { fn spawnBox(self: *World, sprite: Gfx.Sprite.Id) !Entity.Id {
self.tilemap.deinit(gpa); return try self.entities.insert(Entity{
self.entities.deinit(gpa); .type = .box,
.sprite = sprite
});
} }
fn spawnEntity(self: *World, opts: Entity) Entity.Id { fn spawnWall(self: *World, sprite: Gfx.Sprite.Id) !Entity.Id {
var e = opts; return try self.entities.insert(Entity{
e.pos_x = -1; .type = .wall,
e.pos_y = -1; .sprite = sprite
const entity_id = self.entities.insert(e) catch |err| { });
log.err("Failed to spawn entity: {}", .{err});
return .nil;
};
if (opts.pos_x != -1 or opts.pos_y != -1) {
assert(opts.pos_x != -1 and opts.pos_y != -1);
if (!self.setEntityPosition(entity_id, opts.pos_x, opts.pos_y)) {
log.err("Entity position: {}, {}", .{opts.pos_x, opts.pos_y});
@panic("Failed to set initial entity position");
}
}
return entity_id;
} }
fn getTile(self: *World, x: i32, y: i32) ?*Tile { fn getTile(self: *World, x: i32, y: i32) ?*Tile {
@ -175,18 +136,18 @@ const World = struct {
return null; return null;
} }
fn setEntityPosition(self: *World, id: Entity.Id, x: i32, y: i32) bool { fn moveEntity(self: *World, id: Entity.Id, x: i32, y: i32) bool {
const e = self.entities.get(id) orelse return false; const e = self.entities.get(id) orelse return false;
const destination_tile = self.getTile(x, y) orelse return false; const destination_tile = self.getTile(x, y) orelse return false;
if (self.entities.exists(destination_tile.entities_by_layer.get(e.layer))) { if (self.entities.exists(destination_tile.entity)) {
// Cant't overwrite entity // Cna't overwrite entity
return false; return false;
} }
destination_tile.entities_by_layer.set(e.layer, id); destination_tile.entity = id;
if (self.getTile(e.pos_x, e.pos_y)) |tile| { if (self.getTile(e.pos_x, e.pos_y)) |tile| {
tile.entities_by_layer.set(e.layer, .nil); tile.entity = .nil;
} }
e.pos_x = x; e.pos_x = x;
@ -195,37 +156,20 @@ const World = struct {
return true; return true;
} }
fn moveEntity(self: *World, id: Entity.Id, dx: i32, dy: i32) bool { fn movePlayer(self: *World, id: Entity.Id, dir_x: i32, dir_y: i32) bool {
const e = self.entities.get(id) orelse return false; const e = self.entities.get(id) orelse return false;
const next_x = e.pos_x+dx; const next_x = e.pos_x+dir_x;
const next_y = e.pos_y+dy; const next_y = e.pos_y+dir_y;
if (!self.moveEntity(id, next_x, next_y)) {
const destination_tile = self.getTile(next_x, next_y) orelse return false;
if (e.type == .player or e.type == .box) {
const destination_entity_id = destination_tile.entities_by_layer.get(e.layer);
if (self.entities.get(destination_entity_id)) |destination_entity| {
if (destination_entity.type == .box) {
if (!self.moveEntity(destination_entity_id, dx, dy)) {
return false; return false;
} }
}
}
}
return self.setEntityPosition(id, next_x, next_y); return true;
} }
}; };
const Level = struct {
number: i32,
world: World,
};
world: World, world: World,
current_level: u32 = 0,
levels: std.ArrayList(Level),
roboto_font: Font, roboto_font: Font,
tilesheet: Tilesheet, tilesheet: Tilesheet,
@ -302,170 +246,80 @@ pub fn init(self: *App, plt: Platform.Init) !?u8 {
.tilesheet = tilesheet, .tilesheet = tilesheet,
.footstep_buffer = footstep_sound, .footstep_buffer = footstep_sound,
.sfx_bus = Audio.addBus(.{ .label = "sfx" }), .sfx_bus = Audio.addBus(.{ .label = "sfx" }),
.world = undefined, .world = .{
.levels = .empty, .tilemap = undefined,
.entities = try .initCapacity(plt.gpa, 256)
}
}; };
self.levels = try self.loadLevels(plt.gpa, plt.assets); var player_pos: Vec2 = .zero;
self.world = try self.levels.items[0].world.clone(plt.gpa); {
var scratch = std.heap.ArenaAllocator.init(plt.gpa);
defer scratch.deinit();
return null; var buffers = Tiled.xml.Lexer.Buffers.init(plt.gpa);
} defer buffers.deinit();
fn loadLevels(self: *App, gpa: Allocator, assets: *Platform.Assets) !std.ArrayList(Level) { const tilemap = try Tiled.Tilemap.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/map.tmx"));
var xml_parser = try Tiled.xml.Parser.initCapacity(gpa, .{});
defer xml_parser.deinit(gpa);
const tilemap = try Tiled.Tilemap.initFromXml(
gpa,
try xml_parser.parse(assets.readFile("tiled/map.tmx")),
.root
);
defer tilemap.deinit(); defer tilemap.deinit();
var tilesets: Tiled.Tileset.List = .empty; var tilesets: Tiled.Tileset.List = .empty;
defer tilesets.deinit(gpa); defer tilesets.deinit(plt.gpa);
try tilesets.add(gpa, "tileset.tsx", try Tiled.Tileset.initFromXml( try tilesets.add(plt.gpa, "tileset.tsx", try Tiled.Tileset.initFromBuffer(plt.gpa, &scratch, &buffers, plt.assets.readFile("tiled/tileset.tsx")));
gpa,
try xml_parser.parse(assets.readFile("tiled/tileset.tsx")),
.root
));
var levels: std.ArrayList(Level) = .empty; const bounds = tilemap.getTileBounds();
const level_bounds = tilemap.getLayer("Level bounds").?; self.world.tilemap = try Tilemap.init(plt.gpa, bounds.width(), bounds.height());
for (level_bounds.variant.object.items) |obj| {
if (obj.shape != .rectangle) {
continue;
}
const level_index = obj.properties.getInt("level") orelse {
log.warn("Level bounds doesn't have index", .{});
continue;
};
var rect = obj.shape.rectangle;
rect.x /= @floatFromInt(tilemap.tile_width);
rect.y /= @floatFromInt(tilemap.tile_height);
rect.width /= @floatFromInt(tilemap.tile_width);
rect.height /= @floatFromInt(tilemap.tile_height);
var level_bound = Tiled.Tilemap.Bounds{
.min_x = @intFromFloat(@floor(rect.x)),
.min_y = @intFromFloat(@floor(rect.y)),
.max_x = @intFromFloat(@ceil(rect.x + rect.width)),
.max_y = @intFromFloat(@ceil(rect.y + rect.height)),
};
level_bound = tilemap.trimBoundsByEmptyTiles(level_bound);
var level = try self.loadLevel(gpa, tilemap, tilesets, level_bound);
level.number = level_index;
try levels.append(gpa, level);
}
const Context = struct {
fn lessThan(_: @This(), lhs: Level, rhs: Level) bool {
return lhs.number < rhs.number;
}
};
std.mem.sort(Level, levels.items, Context{}, Context.lessThan);
return levels;
}
fn loadLevel(
self: *App,
gpa: Allocator,
tilemap: Tiled.Tilemap,
tilesets: Tiled.Tileset.List,
bounds: Tiled.Tilemap.Bounds
) !Level {
var level = Level{
.number = 0,
.world = .{
.tilemap = try .init(gpa, bounds.width(), bounds.height()),
.entities = try .initCapacity(gpa, 256)
}
};
const ground = tilemap.getLayer("Ground").?; const ground = tilemap.getLayer("Ground").?;
const foreground = tilemap.getLayer("Foreground").?; const walls = tilemap.getLayer("Walls").?;
for (0..bounds.height()) |oy| { for (0..bounds.height()) |oy| {
for (0..bounds.width()) |ox| { for (0..bounds.width()) |ox| {
const x = bounds.min_x + @as(i32, @intCast(ox)); const x = bounds.min_x + ox;
const y = bounds.min_y + @as(i32, @intCast(oy)); const y = bounds.min_y + oy;
const tile = level.world.tilemap.get(ox, oy); const tile = self.world.tilemap.get(ox, oy);
if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| { if (tilemap.getTile(ground, tilesets, x, y)) |tiled_tile| {
const pos = tiled_tile.getPosition(); const pos = tiled_tile.getPosition();
tile.ground_sprite = self.tilesheet.get(pos.x, pos.y); tile.ground_sprite = tilesheet.get(pos.x, pos.y);
} }
if (tilemap.getTile(foreground, tilesets, x, y)) |tiled_tile| { if (tilemap.getTile(walls, tilesets, x, y)) |tiled_tile| {
const pos = tiled_tile.getPosition(); const pos = tiled_tile.getPosition();
const props = tiled_tile.getProperties(); const wall_id = try self.world.spawnWall(tilesheet.get(pos.x, pos.y));
const props_type = props.getString("type") orelse ""; _ = self.world.moveEntity(wall_id, @intCast(ox), @intCast(oy));
}
var entity_type: ?Entity.Type = null; }
var layer: Entity.Layer = .surface;
if (std.mem.eql(u8, props_type, "wall")) {
entity_type = .wall;
} else if (std.mem.eql(u8, props_type, "box")) {
entity_type = .box;
} else if (std.mem.eql(u8, props_type, "goal_marker")) {
entity_type = .goal_marker;
layer = .ground;
} else {
log.warn("Unknown tile type: '{s}'", .{props_type});
} }
if (entity_type != null) { const markers = &tilemap.getLayer("Markers").?.variant.object;
_ = level.world.spawnEntity(Entity{ const spawn = markers.getByName("spawn").?;
.type = entity_type.?, player_pos.x = @divFloor(spawn.shape.point.x, @as(f32, @floatFromInt(tilemap.tile_width)));
.layer = layer, player_pos.x -= @floatFromInt(bounds.min_x);
.pos_x = @intCast(ox), player_pos.y = @divFloor(spawn.shape.point.y, @as(f32, @floatFromInt(tilemap.tile_height)));
.pos_y = @intCast(oy), player_pos.y -= @floatFromInt(bounds.min_y);
.sprite = self.tilesheet.get(pos.x, pos.y)
}); const boxes = tilemap.getLayer("Boxes").?;
for (0..bounds.height()) |oy| {
for (0..bounds.width()) |ox| {
const x = bounds.min_x + ox;
const y = bounds.min_y + oy;
if (tilemap.getTile(boxes, tilesets, x, y)) |tiled_tile| {
const pos = tiled_tile.getPosition();
const box_id = try self.world.spawnBox(tilesheet.get(pos.x, pos.y));
_ = self.world.moveEntity(box_id, @intCast(ox), @intCast(oy));
} }
} }
} }
} }
for (tilemap.getLayer("Markers").?.variant.object.items) |obj| { const player_id = try self.world.spawnPlayer();
if (obj.shape != .point) { _ = self.world.moveEntity(player_id, @intFromFloat(player_pos.x), @intFromFloat(player_pos.y));
continue;
}
if (!std.mem.eql(u8, obj.name, "spawn")) {
continue;
}
var position = obj.shape.point; return null;
position.x /= @floatFromInt(tilemap.tile_width);
position.y /= @floatFromInt(tilemap.tile_height);
const pos_x: i32 = @intFromFloat(position.x);
const pos_y: i32 = @intFromFloat(position.y);
if (bounds.isInside(pos_x, pos_y)) {
_ = level.world.spawnEntity(Entity{
.type = .player,
.layer = .surface,
.pos_x = pos_x - bounds.min_x,
.pos_y = pos_y - bounds.min_y
});
}
}
return level;
}
fn updateWorld(world: *World) void {
_ = world; // autofix
} }
pub fn frame(self: *App, plt: Platform.Frame) !void { pub fn frame(self: *App, plt: Platform.Frame) !void {
@ -500,14 +354,21 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
canvas.push(); canvas.push();
defer canvas.pop(clear_color); defer canvas.pop(clear_color);
var goal_count: u32 = 0; for (0..self.world.tilemap.height) |y| {
var box_at_goal_count: u32 = 0; for (0..self.world.tilemap.width) |x| {
{ const tile = self.world.tilemap.get(x, y);
if (tile.ground_sprite) |sprite| {
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
}
}
}
var entity_iter = self.world.entities.iterator(); var entity_iter = self.world.entities.iterator();
while (entity_iter.next()) |entity_id| { while (entity_iter.next()) |entity_id| {
const entity = self.world.entities.getAssumeExists(entity_id); const entity = self.world.entities.getAssumeExists(entity_id);
if (entity.type == .player) { if (entity.type == .player) {
if ((dir_x != 0 or dir_y != 0) and self.world.moveEntity(entity_id, dir_x, dir_y)) { if ((dir_x != 0 or dir_y != 0) and self.world.movePlayer(entity_id, dir_x, dir_y)) {
var new_direction = entity.player_direction; var new_direction = entity.player_direction;
if (dir_x < 0) { if (dir_x < 0) {
new_direction = .left; new_direction = .left;
@ -542,38 +403,9 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
} }
entity.sprite = frames[entity.player_frame_index]; entity.sprite = frames[entity.player_frame_index];
} else if (entity.type == .goal_marker) {
goal_count += 1;
const tile = self.world.getTile(entity.pos_x, entity.pos_y).?;
if (self.world.entities.get(tile.entities_by_layer.get(.surface))) |above_entity| {
if (above_entity.type == .box) {
box_at_goal_count += 1;
}
}
} }
entity.visual_pos = .initFromInt(i32, entity.pos_x, entity.pos_y); entity.visual_pos = .initFromInt(i32, entity.pos_x, entity.pos_y);
}
}
for (0..self.world.tilemap.height) |y| {
for (0..self.world.tilemap.width) |x| {
const tile = self.world.tilemap.get(x, y);
if (tile.ground_sprite) |sprite| {
Gfx.drawSprite(sprite, Vec2.initFromInt(usize, x, y).multiply(tile_size), tile_size, .white);
}
}
}
inline for (.{ Entity.Layer.ground, Entity.Layer.surface }) |layer| {
var entity_iter = self.world.entities.iterator();
while (entity_iter.next()) |entity_id| {
const entity = self.world.entities.getAssumeExists(entity_id);
if (entity.layer != layer) {
continue;
}
if (entity.sprite) |sprite| { if (entity.sprite) |sprite| {
Gfx.drawSprite( Gfx.drawSprite(
@ -584,23 +416,18 @@ pub fn frame(self: *App, plt: Platform.Frame) !void {
); );
} }
} }
}
if (goal_count > 0 and goal_count == box_at_goal_count and self.current_level+1 < self.levels.items.len) { // self.roboto_font.drawText(.{
self.current_level += 1; // .text = try std.fmt.allocPrint(plt.frame, "Hello {:.0}!", .{plt.time()}),
self.world.deinit(plt.gpa); // .pos = .init(0, 0),
self.world = try self.levels.items[self.current_level].world.clone(plt.gpa); // .height = 32,
} // });
} }
pub fn deinit(self: *App, plt: Platform.Deinit) void { pub fn deinit(self: *App, plt: Platform.Deinit) void {
self.roboto_font.deinit(plt.gpa); self.roboto_font.deinit(plt.gpa);
Gfx.deinitSpritesheet(self.roboto_font.spritesheet); Gfx.deinitSpritesheet(self.roboto_font.spritesheet);
self.world.deinit(plt.gpa); self.world.tilemap.deinit(plt.gpa);
self.world.entities.slots.deinit(plt.gpa);
for (self.levels.items) |*level| {
level.world.deinit(plt.gpa);
}
self.levels.deinit(plt.gpa);
} }

View File

@ -1,9 +1,10 @@
const Platform = @import("platform"); const Math = @import("math");
const Vec2 = Math.Vec2;
const Platform = @import("platform/root.zig");
const Gfx = Platform.Gfx; const Gfx = Platform.Gfx;
const Color = Platform.Color; const Color = Platform.Color;
const Vec2 = Platform.Math.Vec2;
const Canvas = @This(); const Canvas = @This();
size: Vec2, size: Vec2,

View File

@ -4,7 +4,7 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Io = std.Io; const Io = std.Io;
const Platform = @import("platform"); const Platform = @import("platform/root.zig");
const Gfx = Platform.Gfx; const Gfx = Platform.Gfx;
const SlotMapType = Platform.SlotMapType; const SlotMapType = Platform.SlotMapType;
const Vec2 = Platform.Vec2; const Vec2 = Platform.Vec2;
@ -92,7 +92,7 @@ pub fn getGlyph(font: *Font, index: Glyph.Index, scale_x: f32, scale_y: f32) ?*F
assert(box_height < Font.Glyph.max_height); assert(box_height < Font.Glyph.max_height);
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
const bitmap = Gfx.Image{ const bitmap = Gfx.ImageData{
.pixels = .{ .r8 = (&bitmap_buffer).ptr }, .pixels = .{ .r8 = (&bitmap_buffer).ptr },
.width = box_width, .width = box_width,
.height = box_height .height = box_height
@ -418,8 +418,8 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insertOrGet(key).?.id; const id1 = cache.insert(key).?;
const id2 = cache.insertOrGet(key).?.id; const id2 = cache.insert(key).?;
try std.testing.expectEqual(id1, id2); try std.testing.expectEqual(id1, id2);
} }
@ -429,9 +429,9 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key = Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insertOrGet(key).?.id; const id1 = cache.insert(key).?;
cache.remove(id1); cache.remove(id1);
const id2 = cache.insertOrGet(key).?.id; const id2 = cache.insert(key).?;
try std.testing.expectEqual(id1, id2); try std.testing.expectEqual(id1, id2);
} }
@ -440,8 +440,8 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insertOrGet(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id1 = cache.insert(Glyph.Key{ .index = 10, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id2 = cache.insertOrGet(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insert(Glyph.Key{ .index = 11, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
try std.testing.expect(id1 != id2); try std.testing.expect(id1 != id2);
} }
@ -452,10 +452,10 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
for (0..limit-1) |i| { for (0..limit-1) |i| {
const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id != null); try std.testing.expect(id != null);
} }
const id = cache.insertOrGet(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insert(Glyph.Key{ .index = limit, .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id == null); try std.testing.expect(id == null);
} }
@ -470,9 +470,9 @@ const GlyphCache = struct {
defer ids.deinit(gpa); defer ids.deinit(gpa);
for (0..(limit-1)) |i| { for (0..(limit-1)) |i| {
const id = cache.insertOrGet(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) }); const id = cache.insert(Glyph.Key{ .index = @intCast(i), .scale_x = .fromF32(1), .scale_y = .fromF32(1) });
try std.testing.expect(id != null); try std.testing.expect(id != null);
try ids.append(gpa, id.?.id); try ids.append(gpa, id.?);
} }
assert(cache.free_head == GlyphSlot.nil_id); assert(cache.free_head == GlyphSlot.nil_id);
@ -498,9 +498,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
defer gpa.free(lru); defer gpa.free(lru);
@ -513,9 +513,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
defer gpa.free(lru); defer gpa.free(lru);
@ -528,7 +528,7 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
try std.testing.expect(cache.lru_head != GlyphSlot.nil_id); try std.testing.expect(cache.lru_head != GlyphSlot.nil_id);
try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id); try std.testing.expect(cache.lru_tail != GlyphSlot.nil_id);
cache.remove(id); cache.remove(id);
@ -542,13 +542,13 @@ const GlyphCache = struct {
defer cache.deinit(gpa); defer cache.deinit(gpa);
const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key1 = Glyph.Key{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id1 = cache.insertOrGet(key1).?.id; const id1 = cache.insert(key1).?;
const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key2 = Glyph.Key{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id2 = cache.insertOrGet(key2).?.id; const id2 = cache.insert(key2).?;
const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }; const key3 = Glyph.Key{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) };
const id3 = cache.insertOrGet(key3).?.id; const id3 = cache.insert(key3).?;
_ = cache.insertOrGet(key2); _ = cache.lookup(key2);
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
@ -556,7 +556,7 @@ const GlyphCache = struct {
try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru); try std.testing.expectEqualSlices(GlyphSlot.Id, &.{ id2, id3, id1 }, lru);
} }
_ = cache.insertOrGet(key1); _ = cache.lookup(key1);
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);
@ -570,9 +570,9 @@ const GlyphCache = struct {
var cache = try GlyphCache.init(gpa, 256); var cache = try GlyphCache.init(gpa, 256);
defer cache.deinit(gpa); defer cache.deinit(gpa);
const id1 = cache.insertOrGet(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id1 = cache.insert(.{ .index = 0, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id2 = cache.insertOrGet(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id2 = cache.insert(.{ .index = 1, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
const id3 = cache.insertOrGet(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?.id; const id3 = cache.insert(.{ .index = 2, .scale_x = .fromF32(1), .scale_y = .fromF32(1) }).?;
{ {
const lru = try cache.getLRUItems(gpa); const lru = try cache.getLRUItems(gpa);

View File

@ -1,6 +1,6 @@
const std = @import("std"); const std = @import("std");
const Platform = @import("platform"); const Platform = @import("platform/root.zig");
const App = @import("app.zig"); const App = @import("app.zig");
pub const std_options: std.Options = .{ pub const std_options: std.Options = .{
@ -21,5 +21,5 @@ pub fn main(init: std.process.Init) void {
test { test {
std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This());
_ = Platform; _ = @import("./platform/slot_map.zig");
} }

View File

@ -15,6 +15,7 @@ const Mat4 = Math.Mat4;
const Rect = Math.Rect; const Rect = Math.Rect;
const STBRectPack = @import("stb_rect_pack"); const STBRectPack = @import("stb_rect_pack");
const STBTrueType = @import("stb_truetype");
const ImGUI = @import("imgui.zig"); const ImGUI = @import("imgui.zig");
pub const Color = @import("./color.zig"); pub const Color = @import("./color.zig");
const shd = @import("shader"); const shd = @import("shader");
@ -125,7 +126,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
bindings.vertex_buffers[0] = sg.makeBuffer(.{ bindings.vertex_buffers[0] = sg.makeBuffer(.{
.size = @sizeOf(VertexQuad) * max_quads, .size = @sizeOf(VertexQuad) * max_quads,
.usage = .{ .vertex_buffer = true, .dynamic_update = true }, .usage = .{ .vertex_buffer = true, .stream_update = true },
.label = "quad-vertices" .label = "quad-vertices"
}); });

View File

@ -2,7 +2,7 @@ const std = @import("std");
const log = std.log.scoped(.app); const log = std.log.scoped(.app);
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Platform = @import("platform"); const Platform = @import("platform/root.zig");
const Gfx = Platform.Gfx; const Gfx = Platform.Gfx;
const Vec2 = Platform.Vec2; const Vec2 = Platform.Vec2;
const Rect = Platform.Rect; const Rect = Platform.Rect;