initial commit
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
zig-out
|
||||
zig-pkg
|
||||
.zig-cache
|
||||
18
README.md
Normal file
@ -0,0 +1,18 @@
|
||||
# Gaem
|
||||
|
||||
## Run
|
||||
|
||||
Linux and Windows:
|
||||
```sh
|
||||
zig build run
|
||||
```
|
||||
|
||||
Web:
|
||||
```sh
|
||||
zig build -Dtarget=wasm32-emscripten run
|
||||
```
|
||||
|
||||
Cross-compile for Windows from Linux:
|
||||
```sh
|
||||
zig build -Dtarget=x86_64-windows
|
||||
```
|
||||
BIN
assets/Prototype_Character/Blue/_aseprite/death.aseprite
Normal file
BIN
assets/Prototype_Character/Blue/_aseprite/hurt.aseprite
Normal file
BIN
assets/Prototype_Character/Blue/_aseprite/idle.aseprite
Normal file
BIN
assets/Prototype_Character/Blue/_aseprite/walk.aseprite
Normal file
BIN
assets/Prototype_Character/Blue/_shadow/shadow_death.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
assets/Prototype_Character/Blue/_shadow/shadow_static.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/Prototype_Character/Blue/death.png
Normal file
|
After Width: | Height: | Size: 932 B |
BIN
assets/Prototype_Character/Blue/hurt.png
Normal file
|
After Width: | Height: | Size: 870 B |
BIN
assets/Prototype_Character/Blue/idle.png
Normal file
|
After Width: | Height: | Size: 718 B |
BIN
assets/Prototype_Character/Blue/walk.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/Prototype_Character/Default/_aseprite/death.aseprite
Normal file
BIN
assets/Prototype_Character/Default/_aseprite/hurt.aseprite
Normal file
BIN
assets/Prototype_Character/Default/_aseprite/idle.aseprite
Normal file
BIN
assets/Prototype_Character/Default/_aseprite/walk.aseprite
Normal file
BIN
assets/Prototype_Character/Default/_shadow/shadow_death.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
assets/Prototype_Character/Default/_shadow/shadow_static.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/Prototype_Character/Default/death.png
Normal file
|
After Width: | Height: | Size: 911 B |
BIN
assets/Prototype_Character/Default/hurt.png
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
assets/Prototype_Character/Default/idle.png
Normal file
|
After Width: | Height: | Size: 690 B |
BIN
assets/Prototype_Character/Default/walk.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/Prototype_Character/Green/_aseprite/death.aseprite
Normal file
BIN
assets/Prototype_Character/Green/_aseprite/hurt.aseprite
Normal file
BIN
assets/Prototype_Character/Green/_aseprite/idle.aseprite
Normal file
BIN
assets/Prototype_Character/Green/_aseprite/walk.aseprite
Normal file
BIN
assets/Prototype_Character/Green/_shadow/shadow_death.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
assets/Prototype_Character/Green/_shadow/shadow_static.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/Prototype_Character/Green/death.png
Normal file
|
After Width: | Height: | Size: 925 B |
BIN
assets/Prototype_Character/Green/hurt.png
Normal file
|
After Width: | Height: | Size: 883 B |
BIN
assets/Prototype_Character/Green/idle.png
Normal file
|
After Width: | Height: | Size: 711 B |
BIN
assets/Prototype_Character/Green/walk.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
assets/Prototype_Character/Red/_aseprite/death.aseprite
Normal file
BIN
assets/Prototype_Character/Red/_aseprite/hurt.aseprite
Normal file
BIN
assets/Prototype_Character/Red/_aseprite/idle.aseprite
Normal file
BIN
assets/Prototype_Character/Red/_aseprite/walk.aseprite
Normal file
BIN
assets/Prototype_Character/Red/_shadow/shadow_death.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
assets/Prototype_Character/Red/_shadow/shadow_static.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/Prototype_Character/Red/death.png
Normal file
|
After Width: | Height: | Size: 918 B |
BIN
assets/Prototype_Character/Red/hurt.png
Normal file
|
After Width: | Height: | Size: 858 B |
BIN
assets/Prototype_Character/Red/idle.png
Normal file
|
After Width: | Height: | Size: 701 B |
BIN
assets/Prototype_Character/Red/walk.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/Prototype_Character/Yellow/_aseprite/death.aseprite
Normal file
BIN
assets/Prototype_Character/Yellow/_aseprite/hurt.aseprite
Normal file
BIN
assets/Prototype_Character/Yellow/_aseprite/idle.aseprite
Normal file
BIN
assets/Prototype_Character/Yellow/_aseprite/walk.aseprite
Normal file
BIN
assets/Prototype_Character/Yellow/_shadow/shadow_death.png
Normal file
|
After Width: | Height: | Size: 240 B |
BIN
assets/Prototype_Character/Yellow/_shadow/shadow_static.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
assets/Prototype_Character/Yellow/death.png
Normal file
|
After Width: | Height: | Size: 931 B |
BIN
assets/Prototype_Character/Yellow/hurt.png
Normal file
|
After Width: | Height: | Size: 871 B |
BIN
assets/Prototype_Character/Yellow/idle.png
Normal file
|
After Width: | Height: | Size: 719 B |
BIN
assets/Prototype_Character/Yellow/walk.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
assets/tranquil_tunnels_transparent.png
Normal file
|
After Width: | Height: | Size: 150 KiB |
393
build.zig
Normal file
@ -0,0 +1,393 @@
|
||||
const std = @import("std");
|
||||
const sokol = @import("sokol");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const assets_dir = "assets";
|
||||
const asset_files = [_][]const u8{
|
||||
"tranquil_tunnels_transparent.png"
|
||||
// "sokoban/sokoban_tilesheet.png",
|
||||
// "roboto-font/Roboto-Regular.ttf",
|
||||
// "tiled/map.tmx",
|
||||
// "tiled/tileset.tsx"
|
||||
};
|
||||
|
||||
const asset_dir_realpath = try b.build_root.join(b.graph.arena, &.{ assets_dir });
|
||||
|
||||
const is_wasm = target.result.cpu.arch.isWasm();
|
||||
const is_debug = (optimize == .Debug);
|
||||
|
||||
const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse is_debug;
|
||||
const assets_from_filesystem = b.option(bool, "assets-filesystem", "Allow reading assets from filesystem") orelse is_debug;
|
||||
|
||||
var has_tracy = false;
|
||||
if (!is_wasm) {
|
||||
has_tracy = b.option(bool, "tracy", "Tracy integration") orelse is_debug;
|
||||
}
|
||||
|
||||
const exe_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const build_options = b.addOptions();
|
||||
build_options.addOption(bool, "has_imgui", has_imgui);
|
||||
build_options.addOption(?[]const u8, "assets_dir", if (assets_from_filesystem) asset_dir_realpath else null);
|
||||
exe_mod.addOptions("build_options", build_options);
|
||||
|
||||
exe_mod.addImport("embedded_assets", try createEmbeddedAssets(b, assets_dir, &asset_files));
|
||||
|
||||
const math_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/math.zig")
|
||||
});
|
||||
exe_mod.addImport("math", math_mod);
|
||||
|
||||
const dep_tiled = b.dependency("tiled", .{});
|
||||
exe_mod.addImport("tiled", dep_tiled.module("tiled"));
|
||||
|
||||
const dep_sokol = b.dependency("sokol", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.with_sokol_imgui = has_imgui
|
||||
});
|
||||
const mod_sokol = dep_sokol.module("sokol");
|
||||
exe_mod.linkLibrary(dep_sokol.artifact("sokol_clib"));
|
||||
exe_mod.addImport("sokol", mod_sokol);
|
||||
|
||||
const dep_shdc = dep_sokol.builder.dependency("shdc", .{});
|
||||
|
||||
var slang = sokol.shdc.Slang{};
|
||||
if (is_wasm) {
|
||||
slang.wgsl = true;
|
||||
} else {
|
||||
slang.glsl410 = true;
|
||||
slang.hlsl4 = true;
|
||||
slang.metal_macos = true;
|
||||
slang.glsl300es = true;
|
||||
}
|
||||
const mod_shader = try sokol.shdc.createModule(b, "shader", mod_sokol, .{
|
||||
.shdc_dep = dep_shdc,
|
||||
.input = "src/shader.glsl",
|
||||
.output = "src/shader.zig",
|
||||
.slang = slang
|
||||
});
|
||||
mod_shader.addImport("math", math_mod);
|
||||
exe_mod.addImport("shader", mod_shader);
|
||||
|
||||
if (has_imgui) {
|
||||
if (b.lazyDependency("cimgui", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |dep_cimgui| {
|
||||
const cimgui = b.lazyImport(@This(), "cimgui").?;
|
||||
const cimgui_conf = cimgui.getConfig(true);
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
const dep_stb = b.dependency("stb", .{});
|
||||
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", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.tracy_enable = has_tracy,
|
||||
.tracy_only_localhost = true
|
||||
});
|
||||
if (has_tracy) {
|
||||
exe_mod.linkLibrary(dep_tracy.artifact("tracy"));
|
||||
}
|
||||
exe_mod.addImport("tracy", dep_tracy.module("tracy"));
|
||||
|
||||
var run_cmd_step: *std.Build.Step = undefined;
|
||||
if (is_wasm) {
|
||||
if (true) {
|
||||
@panic("TODO: test in 0.17");
|
||||
}
|
||||
|
||||
const build_step = buildWasm(b, .{
|
||||
.name = "index",
|
||||
.root_module = exe_mod,
|
||||
.dep_sokol = dep_sokol,
|
||||
});
|
||||
b.getInstallStep().dependOn(build_step);
|
||||
|
||||
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
||||
const emrun_step = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
|
||||
emrun_step.step.dependOn(build_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
|
||||
} else {
|
||||
const exe = buildNative(b, .{
|
||||
.name = "sokol_template_v2",
|
||||
.root_module = exe_mod
|
||||
});
|
||||
b.installArtifact(exe);
|
||||
|
||||
{
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
run_cmd_step = &run_cmd.step;
|
||||
}
|
||||
|
||||
{
|
||||
const exe_tests = b.addTest(.{ .root_module = exe_mod, });
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
}
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run game");
|
||||
run_step.dependOn(run_cmd_step);
|
||||
}
|
||||
|
||||
fn createEmbeddedAssets(
|
||||
b: *std.Build,
|
||||
assets_dir: []const u8,
|
||||
asset_files: []const []const u8
|
||||
) !*std.Build.Module {
|
||||
const write_files = b.addWriteFiles();
|
||||
|
||||
var embedded_assets: std.ArrayList(u8) = .empty;
|
||||
defer embedded_assets.deinit(b.allocator);
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, "pub const files = .{\n");
|
||||
const mod_embedded_assets = b.createModule(.{});
|
||||
for (asset_files) |asset_file| {
|
||||
mod_embedded_assets.addAnonymousImport(asset_file, .{
|
||||
.root_source_file = b.path(b.pathJoin(&.{ assets_dir, asset_file }))
|
||||
});
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, " .{ ");
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, ".name = \"");
|
||||
try embedded_assets.appendSlice(b.allocator, asset_file);
|
||||
try embedded_assets.appendSlice(b.allocator, "\"");
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, ", ");
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, ".contents = @embedFile(\"");
|
||||
try embedded_assets.appendSlice(b.allocator, asset_file);
|
||||
try embedded_assets.appendSlice(b.allocator, "\")");
|
||||
|
||||
try embedded_assets.appendSlice(b.allocator, " },\n");
|
||||
}
|
||||
try embedded_assets.appendSlice(b.allocator, "};\n");
|
||||
mod_embedded_assets.root_source_file = write_files.add("embedded_assets.zig", embedded_assets.items);
|
||||
|
||||
return mod_embedded_assets;
|
||||
}
|
||||
|
||||
const BuildNativeOptions = struct {
|
||||
name: []const u8,
|
||||
root_module: *std.Build.Module,
|
||||
win32_has_console: ?bool = null,
|
||||
win32_png_icon: ?std.Build.LazyPath = null
|
||||
};
|
||||
|
||||
fn buildNative(
|
||||
b: *std.Build,
|
||||
opts: BuildNativeOptions,
|
||||
) *std.Build.Step.Compile {
|
||||
const exe = b.addExecutable(.{
|
||||
.name = opts.name,
|
||||
.root_module = opts.root_module,
|
||||
});
|
||||
|
||||
const target = opts.root_module.resolved_target.?;
|
||||
if (target.result.os.tag == .windows) {
|
||||
const optimize = opts.root_module.optimize.?;
|
||||
const has_console = opts.win32_has_console orelse (optimize == .Debug);
|
||||
exe.subsystem = if (has_console) .Console else .Windows;
|
||||
|
||||
if (opts.win32_png_icon) |win32_png_icon| {
|
||||
const png_to_icon_tool = buildPngToIconTool(b, b.graph.host, .ReleaseSafe);
|
||||
|
||||
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
|
||||
png_to_icon_step.addFileArg(win32_png_icon);
|
||||
const icon_file = png_to_icon_step.addOutputFileArg("icon.ico");
|
||||
|
||||
const add_icon_step = AddExecutableIcon.init(exe, icon_file);
|
||||
exe.step.dependOn(&add_icon_step.step);
|
||||
}
|
||||
}
|
||||
|
||||
return exe;
|
||||
}
|
||||
|
||||
fn buildPngToIconTool(
|
||||
b: *std.Build,
|
||||
target: std.Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
) *std.Build.Step.Compile {
|
||||
const mod = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("tools/png-to-icon.zig"),
|
||||
});
|
||||
|
||||
const dep_stb = b.dependency("stb", .{});
|
||||
mod.addImport("stb_image", dep_stb.module("stb_image"));
|
||||
|
||||
return b.addExecutable(.{
|
||||
.name = "png-to-icon",
|
||||
.root_module = mod,
|
||||
});
|
||||
}
|
||||
|
||||
const AddExecutableIcon = struct {
|
||||
obj: *std.Build.Step.Compile,
|
||||
step: std.Build.Step,
|
||||
icon_file: std.Build.LazyPath,
|
||||
resource_file: std.Build.LazyPath,
|
||||
|
||||
fn init(obj: *std.Build.Step.Compile, icon_file: std.Build.LazyPath) *AddExecutableIcon {
|
||||
const b = obj.step.owner;
|
||||
const self = b.allocator.create(AddExecutableIcon) catch @panic("OOM");
|
||||
|
||||
self.obj = obj;
|
||||
self.step = std.Build.Step.init(.{
|
||||
.id = .custom,
|
||||
.name = "add executable icon",
|
||||
.owner = b,
|
||||
.makeFn = make
|
||||
});
|
||||
|
||||
self.icon_file = icon_file;
|
||||
icon_file.addStepDependencies(&self.step);
|
||||
|
||||
const write_files = b.addWriteFiles();
|
||||
self.resource_file = write_files.add("resource-file.rc", "");
|
||||
self.step.dependOn(&write_files.step);
|
||||
|
||||
self.obj.root_module.addWin32ResourceFile(.{
|
||||
.file = self.resource_file,
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
fn make(step: *std.Build.Step, _: std.Build.Step.MakeOptions) !void {
|
||||
const b = step.owner;
|
||||
const self: *AddExecutableIcon = @fieldParentPtr("step", step);
|
||||
|
||||
const relative_icon_path = try std.fs.path.relative(
|
||||
b.allocator,
|
||||
"",
|
||||
null,
|
||||
self.resource_file.dirname().getPath(b),
|
||||
self.icon_file.getPath(b)
|
||||
);
|
||||
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
|
||||
|
||||
{
|
||||
const cwd = std.Io.Dir.cwd();
|
||||
const resource_file = try cwd.createFile(b.graph.io, self.resource_file.getPath(b), .{ });
|
||||
defer resource_file.close(b.graph.io);
|
||||
|
||||
var buff: [4*4096]u8 = undefined;
|
||||
var file_writer = resource_file.writer(b.graph.io, &buff);
|
||||
const w = &file_writer.interface;
|
||||
|
||||
try w.writeAll("IDI_ICON ICON \"");
|
||||
try w.writeAll(relative_icon_path);
|
||||
try w.writeAll("\"");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const BuildWasmOptions = struct {
|
||||
name: []const u8,
|
||||
root_module: *std.Build.Module,
|
||||
dep_sokol: *std.Build.Dependency,
|
||||
};
|
||||
|
||||
fn buildWasm(b: *std.Build, opts: BuildWasmOptions) *std.Build.Step {
|
||||
opts.root_module.sanitize_c = .off;
|
||||
|
||||
// build the main file into a library, this is because the WASM 'exe'
|
||||
// needs to be linked in a separate build step with the Emscripten linker
|
||||
const main_lib = b.addLibrary(.{
|
||||
.name = opts.name,
|
||||
.root_module = opts.root_module,
|
||||
});
|
||||
|
||||
const dep_sokol = opts.dep_sokol;
|
||||
const dep_emsdk = dep_sokol.builder.dependency("emsdk", .{});
|
||||
|
||||
// TODO:
|
||||
// const emsdk_install_step = sokol.emSdkInstallStep(b, dep_emsdk, .{});
|
||||
// main_lib.step.dependOn(emsdk_install_step);
|
||||
|
||||
patchWasmIncludeDirs(
|
||||
opts.root_module,
|
||||
dep_emsdk.path("upstream/emscripten/cache/sysroot/include"),
|
||||
&(dep_sokol.artifact("sokol_clib").step)
|
||||
);
|
||||
|
||||
const link_step = sokol.emLinkStep(b, .{
|
||||
.lib_main = main_lib,
|
||||
.target = opts.root_module.resolved_target.?,
|
||||
.optimize = opts.root_module.optimize.?,
|
||||
.emsdk = dep_emsdk,
|
||||
.use_webgl2 = true,
|
||||
.use_emmalloc = true,
|
||||
.use_filesystem = false,
|
||||
.shell_file_path = b.path("src/engine/shell.html"),
|
||||
}) catch unreachable;
|
||||
|
||||
return &link_step.step;
|
||||
}
|
||||
|
||||
fn patchWasmIncludeDirs(
|
||||
module: *std.Build.Module,
|
||||
path: std.Build.LazyPath,
|
||||
depend_step: *std.Build.Step
|
||||
) void {
|
||||
if (module.link_libc != null and module.link_libc.?) {
|
||||
// need to inject the Emscripten system header include path into
|
||||
// the cimgui C library otherwise the C/C++ code won't find
|
||||
// C stdlib headers
|
||||
module.addSystemIncludePath(path);
|
||||
}
|
||||
|
||||
for (module.import_table.values()) |imported_module| {
|
||||
patchWasmIncludeDirs(imported_module, path, depend_step);
|
||||
}
|
||||
|
||||
for (module.link_objects.items) |link_object| {
|
||||
if (link_object != .other_step) {
|
||||
continue;
|
||||
}
|
||||
const lib = link_object.other_step;
|
||||
if (&lib.step == depend_step) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lib.root_module.link_libc != null and lib.root_module.link_libc.?) {
|
||||
// need to inject the Emscripten system header include path into
|
||||
// the cimgui C library otherwise the C/C++ code won't find
|
||||
// C stdlib headers
|
||||
lib.root_module.addSystemIncludePath(path);
|
||||
|
||||
// all C libraries need to depend on the sokol library, when building for
|
||||
// WASM this makes sure that the Emscripten SDK has been setup before
|
||||
// C compilation is attempted (since the sokol C library depends on the
|
||||
// Emscripten SDK setup step)
|
||||
lib.step.dependOn(depend_step);
|
||||
}
|
||||
patchWasmIncludeDirs(lib.root_module, path, depend_step);
|
||||
}
|
||||
}
|
||||
31
build.zig.zon
Normal file
@ -0,0 +1,31 @@
|
||||
.{
|
||||
.name = .sokol_template_v2,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xf440b4e6e51f5206, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.dependencies = .{
|
||||
.sokol = .{
|
||||
.url = "git+https://github.com/floooh/sokol-zig.git#ba97ac4e82e36a0f6c62f7fad5dc9c2d82beadd8",
|
||||
.hash = "sokol-0.1.0-pb1HK4prNwDGx2RJFyv7VtYJubDahi19XKioPzPeMCxv",
|
||||
},
|
||||
.stb = .{
|
||||
.path = "./libs/stb",
|
||||
},
|
||||
.tiled = .{
|
||||
.path = "./libs/tiled",
|
||||
},
|
||||
.tracy = .{
|
||||
.path = "./libs/tracy",
|
||||
},
|
||||
.cimgui = .{
|
||||
.url = "git+https://github.com/floooh/dcimgui.git#3c2b41a9333b1195e2c2f8ce6a2d3e50660a73bf",
|
||||
.hash = "cimgui-0.1.0-44ClkR8dmgDqqnfloyLE1bUAtl4zSKTT1wxaHSEC0ikV",
|
||||
.lazy = true
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
37
libs/stb/build.zig
Normal file
@ -0,0 +1,37 @@
|
||||
const std = @import("std");
|
||||
const Build = std.Build;
|
||||
const LazyPath = Build.LazyPath;
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
buildSTBModule(b, target, optimize, "stb_image", b.path("src/stb_image.zig"), b.path("src/stb_image_impl.c"));
|
||||
buildSTBModule(b, target, optimize, "stb_vorbis", b.path("src/stb_vorbis.zig"), b.path("src/stb_vorbis_impl.c"));
|
||||
buildSTBModule(b, target, optimize, "stb_rect_pack", b.path("src/stb_rect_pack.zig"), b.path("src/stb_rect_pack_impl.c"));
|
||||
buildSTBModule(b, target, optimize, "stb_truetype", b.path("src/stb_truetype.zig"), b.path("src/stb_truetype_impl.c"));
|
||||
}
|
||||
|
||||
fn buildSTBModule(
|
||||
b: *std.Build,
|
||||
target: Build.ResolvedTarget,
|
||||
optimize: std.builtin.OptimizeMode,
|
||||
name: []const u8,
|
||||
zig_file: LazyPath,
|
||||
impl_file: LazyPath
|
||||
) void {
|
||||
const stb_dependency = b.dependency("stb", .{});
|
||||
|
||||
const mod = b.addModule(name, .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = zig_file,
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
mod.addIncludePath(stb_dependency.path("."));
|
||||
mod.addCSourceFile(.{
|
||||
.file = impl_file,
|
||||
.flags = &.{}
|
||||
});
|
||||
}
|
||||
17
libs/stb/build.zig.zon
Normal file
@ -0,0 +1,17 @@
|
||||
.{
|
||||
.name = .stb_image,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0xe5d3607840482046, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.stb = .{
|
||||
.url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
|
||||
.hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
31
libs/stb/src/stb_image.zig
Normal file
@ -0,0 +1,31 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cDefine("STBI_NO_STDIO", {});
|
||||
@cInclude("stb_image.h");
|
||||
});
|
||||
|
||||
const STBImage = @This();
|
||||
|
||||
rgba8_pixels: [*c]u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn load(png_data: []const u8) !STBImage {
|
||||
var width: c_int = undefined;
|
||||
var height: c_int = undefined;
|
||||
const pixels = c.stbi_load_from_memory(png_data.ptr, @intCast(png_data.len), &width, &height, null, 4);
|
||||
if (pixels == null) {
|
||||
return error.InvalidPng;
|
||||
}
|
||||
|
||||
return STBImage{
|
||||
.rgba8_pixels = pixels,
|
||||
.width = @intCast(width),
|
||||
.height = @intCast(height)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const STBImage) void {
|
||||
c.stbi_image_free(self.rgba8_pixels);
|
||||
}
|
||||
3
libs/stb/src/stb_image_impl.c
Normal file
@ -0,0 +1,3 @@
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#define STBI_NO_STDIO
|
||||
#include "stb_image.h"
|
||||
36
libs/stb/src/stb_rect_pack.zig
Normal file
@ -0,0 +1,36 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("stb_rect_pack.h");
|
||||
});
|
||||
|
||||
const STBRectPack = @This();
|
||||
|
||||
ctx: c.stbrp_context,
|
||||
|
||||
pub fn init(self: *STBRectPack, width: u32, height: u32, nodes: []Node) void {
|
||||
c.stbrp_init_target(&self.ctx, @intCast(width), @intCast(height), nodes.ptr, @intCast(nodes.len));
|
||||
}
|
||||
|
||||
pub fn setupAllowOutOfMem(self: *STBRectPack, enabled: bool) void {
|
||||
c.stbrp_setup_heuristic(&self.ctx, if (enabled) 1 else 0);
|
||||
}
|
||||
|
||||
pub fn setupHeuristic(self: *STBRectPack, heuristic: Heuristic) void {
|
||||
c.stbrp_setup_heuristic(&self.ctx, heuristic);
|
||||
}
|
||||
|
||||
pub fn packRects(self: *STBRectPack, rects: []Rect) bool {
|
||||
return c.stbrp_pack_rects(&self.ctx, rects.ptr, @intCast(rects.len)) == 1;
|
||||
}
|
||||
|
||||
pub const Heuristic = enum(i32) {
|
||||
Skyline_BL_sortHeight = c.STBRP_HEURISTIC_Skyline_BL_sortHeight,
|
||||
Skyline_BF_sortHeight = c.STBRP_HEURISTIC_Skyline_BF_sortHeight,
|
||||
_,
|
||||
|
||||
pub const default = Heuristic.Skyline_BL_sortHeight;
|
||||
};
|
||||
|
||||
pub const Node = c.stbrp_node;
|
||||
pub const Rect = c.stbrp_rect;
|
||||
2
libs/stb/src/stb_rect_pack_impl.c
Normal file
@ -0,0 +1,2 @@
|
||||
#define STB_RECT_PACK_IMPLEMENTATION
|
||||
#include "stb_rect_pack.h"
|
||||
116
libs/stb/src/stb_truetype.zig
Normal file
@ -0,0 +1,116 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("stb_truetype.h");
|
||||
});
|
||||
|
||||
pub const Box = struct {
|
||||
x0: i32,
|
||||
y0: i32,
|
||||
x1: i32,
|
||||
y1: i32,
|
||||
};
|
||||
|
||||
pub const HMetrics = struct {
|
||||
advance_width: i32,
|
||||
left_side_bearing: i32
|
||||
};
|
||||
|
||||
pub const VMetrics = struct {
|
||||
ascent: i32,
|
||||
descent: i32,
|
||||
line_gap: i32
|
||||
};
|
||||
|
||||
pub const Font = struct {
|
||||
info: c.stbtt_fontinfo,
|
||||
|
||||
pub fn getNumberOfFonts(data: [*c]const u8) !u32 {
|
||||
const result = c.stbtt_GetNumberOfFonts(data);
|
||||
if (result == -1) {
|
||||
return error.stbtt_GetNumberOfFonts;
|
||||
}
|
||||
return @intCast(result);
|
||||
}
|
||||
|
||||
pub fn getFontOffsetForIndex(data: [*c]const u8, index: u32) ?u32 {
|
||||
const result = c.stbtt_GetFontOffsetForIndex(data, @intCast(index));
|
||||
if (result == -1) {
|
||||
return null;
|
||||
}
|
||||
return @intCast(result);
|
||||
}
|
||||
|
||||
pub fn init(data: [*c]const u8, offset: u32) !Font {
|
||||
var font: Font = .{
|
||||
.info = undefined
|
||||
};
|
||||
if (c.stbtt_InitFont(&font.info, data, @intCast(offset)) == 0) {
|
||||
return error.stbtt_InitFont;
|
||||
}
|
||||
return font;
|
||||
}
|
||||
|
||||
pub fn findGlyphIndex(self: Font, codepoint: u21) ?u32 {
|
||||
const result = c.stbtt_FindGlyphIndex(&self.info, codepoint);
|
||||
if (result == 0) {
|
||||
return null;
|
||||
}
|
||||
return @intCast(result);
|
||||
}
|
||||
|
||||
pub fn scaleForPixelHeight(self: Font, pixel_height: f32) f32 {
|
||||
return c.stbtt_ScaleForPixelHeight(&self.info, pixel_height);
|
||||
}
|
||||
|
||||
pub fn getGlyphBitmapBox(self: Font, glyph: u32, scale_x: f32, scale_y: f32) Box {
|
||||
var result: Box = undefined;
|
||||
c.stbtt_GetGlyphBitmapBox(
|
||||
&self.info,
|
||||
@intCast(glyph),
|
||||
scale_x, scale_y,
|
||||
&result.x0, &result.y0,
|
||||
&result.x1, &result.y1,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn makeGlyphBitmap(
|
||||
self: Font,
|
||||
output: [*]u8,
|
||||
out_w: u32,
|
||||
out_h: u32,
|
||||
out_stride: u32,
|
||||
scale_x: f32,
|
||||
scale_y: f32,
|
||||
glyph: u32
|
||||
) void {
|
||||
c.stbtt_MakeGlyphBitmap(
|
||||
&self.info,
|
||||
output,
|
||||
@intCast(out_w),
|
||||
@intCast(out_h),
|
||||
@intCast(out_stride),
|
||||
scale_x,
|
||||
scale_y,
|
||||
@intCast(glyph)
|
||||
);
|
||||
}
|
||||
|
||||
pub fn getGlyphHMetrics(self: Font, glyph: u32) HMetrics {
|
||||
var result: HMetrics = undefined;
|
||||
c.stbtt_GetGlyphHMetrics(&self.info, @intCast(glyph), &result.advance_width, &result.left_side_bearing);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn getFontVMetrics(self: Font) VMetrics {
|
||||
var result: VMetrics = undefined;
|
||||
c.stbtt_GetFontVMetrics(&self.info, &result.ascent, &result.descent, &result.line_gap);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn getGlyphKernAdvance(self: Font, prev_glyph: u32, next_glyph: u32) i32 {
|
||||
return c.stbtt_GetGlyphKernAdvance(&self.info, @intCast(prev_glyph), @intCast(next_glyph));
|
||||
}
|
||||
};
|
||||
|
||||
4
libs/stb/src/stb_truetype_impl.c
Normal file
@ -0,0 +1,4 @@
|
||||
#define STB_TRUETYPE_IMPLEMENTATION
|
||||
// TODO: #define STBTT_STATIC
|
||||
#include "stb_truetype.h"
|
||||
|
||||
170
libs/stb/src/stb_vorbis.zig
Normal file
@ -0,0 +1,170 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const log = std.log.scoped(.stb_vorbis);
|
||||
|
||||
const c = @cImport({
|
||||
@cDefine("STB_VORBIS_NO_INTEGER_CONVERSION", {});
|
||||
@cDefine("STB_VORBIS_NO_STDIO", {});
|
||||
@cDefine("STB_VORBIS_HEADER_ONLY", {});
|
||||
@cInclude("stb_vorbis.c");
|
||||
});
|
||||
|
||||
const STBVorbis = @This();
|
||||
|
||||
pub const Error = error {
|
||||
NeedMoreData,
|
||||
InvalidApiMixing,
|
||||
OutOfMemory,
|
||||
FeatureNotSupported,
|
||||
TooManyChannels,
|
||||
FileOpenFailure,
|
||||
SeekWithoutLength,
|
||||
UnexpectedEof,
|
||||
SeekInvalid,
|
||||
InvalidSetup,
|
||||
InvalidStream,
|
||||
MissingCapturePattern,
|
||||
InvalidStreamStructureVersion,
|
||||
ContinuedPacketFlagInvalid,
|
||||
IncorrectStreamSerialNumber,
|
||||
InvalidFirstPage,
|
||||
BadPacketType,
|
||||
CantFindLastPage,
|
||||
SeekFailed,
|
||||
OggSkeletonNotSupported,
|
||||
|
||||
Unknown
|
||||
};
|
||||
|
||||
fn errorFromInt(err: c_int) ?Error {
|
||||
return switch (err) {
|
||||
c.VORBIS__no_error => null,
|
||||
c.VORBIS_need_more_data => Error.NeedMoreData,
|
||||
c.VORBIS_invalid_api_mixing => Error.InvalidApiMixing,
|
||||
c.VORBIS_outofmem => Error.OutOfMemory,
|
||||
c.VORBIS_feature_not_supported => Error.FeatureNotSupported,
|
||||
c.VORBIS_too_many_channels => Error.TooManyChannels,
|
||||
c.VORBIS_file_open_failure => Error.FileOpenFailure,
|
||||
c.VORBIS_seek_without_length => Error.SeekWithoutLength,
|
||||
c.VORBIS_unexpected_eof => Error.UnexpectedEof,
|
||||
c.VORBIS_seek_invalid => Error.SeekInvalid,
|
||||
c.VORBIS_invalid_setup => Error.InvalidSetup,
|
||||
c.VORBIS_invalid_stream => Error.InvalidStream,
|
||||
c.VORBIS_missing_capture_pattern => Error.MissingCapturePattern,
|
||||
c.VORBIS_invalid_stream_structure_version => Error.InvalidStreamStructureVersion,
|
||||
c.VORBIS_continued_packet_flag_invalid => Error.ContinuedPacketFlagInvalid,
|
||||
c.VORBIS_incorrect_stream_serial_number => Error.IncorrectStreamSerialNumber,
|
||||
c.VORBIS_invalid_first_page => Error.InvalidFirstPage,
|
||||
c.VORBIS_bad_packet_type => Error.BadPacketType,
|
||||
c.VORBIS_cant_find_last_page => Error.CantFindLastPage,
|
||||
c.VORBIS_seek_failed => Error.SeekFailed,
|
||||
c.VORBIS_ogg_skeleton_not_supported => Error.OggSkeletonNotSupported,
|
||||
else => Error.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
handle: *c.stb_vorbis,
|
||||
|
||||
pub fn init(data: []const u8, alloc_buffer: []u8) Error!STBVorbis {
|
||||
const stb_vorbis_alloc: c.stb_vorbis_alloc = .{
|
||||
.alloc_buffer = alloc_buffer.ptr,
|
||||
.alloc_buffer_length_in_bytes = @intCast(alloc_buffer.len)
|
||||
};
|
||||
var error_code: c_int = -1;
|
||||
const handle = c.stb_vorbis_open_memory(
|
||||
data.ptr,
|
||||
@intCast(data.len),
|
||||
&error_code,
|
||||
&stb_vorbis_alloc
|
||||
);
|
||||
if (handle == null) {
|
||||
return errorFromInt(error_code) orelse Error.Unknown;
|
||||
}
|
||||
|
||||
return STBVorbis{
|
||||
.handle = handle.?
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getMinimumAllocBufferSize(self: STBVorbis) u32 {
|
||||
const info = self.getInfo();
|
||||
return info.setup_memory_required + @max(info.setup_temp_memory_required, info.temp_memory_required);
|
||||
}
|
||||
|
||||
fn getLastError(self: STBVorbis) ?Error {
|
||||
const error_code = c.stb_vorbis_get_error(self.handle);
|
||||
return errorFromInt(error_code);
|
||||
}
|
||||
|
||||
pub fn seek(self: STBVorbis, sample_number: u32) !void {
|
||||
const success = c.stb_vorbis_seek(self.handle, sample_number);
|
||||
if (success != 1) {
|
||||
return self.getLastError() orelse Error.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getStreamLengthInSamples(self: STBVorbis) u32 {
|
||||
return c.stb_vorbis_stream_length_in_samples(self.handle);
|
||||
}
|
||||
|
||||
pub fn getStreamLengthInSeconds(self: STBVorbis) f32 {
|
||||
return c.stb_vorbis_stream_length_in_seconds(self.handle);
|
||||
}
|
||||
|
||||
pub fn getSamples(
|
||||
self: STBVorbis,
|
||||
channels: []const [*]f32,
|
||||
max_samples_per_channel: u32
|
||||
) u32 {
|
||||
const samples_per_channel = c.stb_vorbis_get_samples_float(
|
||||
self.handle,
|
||||
@intCast(channels.len),
|
||||
@constCast(@ptrCast(channels.ptr)),
|
||||
@intCast(max_samples_per_channel)
|
||||
);
|
||||
return @intCast(samples_per_channel);
|
||||
}
|
||||
|
||||
const Frame = struct {
|
||||
channels: []const [*c]const f32,
|
||||
samples_per_channel: u32
|
||||
};
|
||||
|
||||
pub fn getFrame(self: STBVorbis) Frame {
|
||||
var output: [*c][*c]f32 = null;
|
||||
var channels: c_int = undefined;
|
||||
const samples_per_channel = c.stb_vorbis_get_frame_float(
|
||||
self.handle,
|
||||
&channels,
|
||||
&output
|
||||
);
|
||||
return Frame{
|
||||
.channels = output[0..@intCast(channels)],
|
||||
.samples_per_channel = @intCast(samples_per_channel)
|
||||
};
|
||||
}
|
||||
|
||||
pub const Info = struct {
|
||||
sample_rate: u32,
|
||||
channels: u32,
|
||||
|
||||
setup_memory_required: u32,
|
||||
setup_temp_memory_required: u32,
|
||||
temp_memory_required: u32,
|
||||
|
||||
max_frame_size: u32
|
||||
};
|
||||
|
||||
pub fn getInfo(self: STBVorbis) Info {
|
||||
const info = c.stb_vorbis_get_info(self.handle);
|
||||
return Info{
|
||||
.sample_rate = info.sample_rate,
|
||||
.channels = @intCast(info.channels),
|
||||
|
||||
.setup_memory_required = info.setup_memory_required,
|
||||
.setup_temp_memory_required = info.setup_temp_memory_required,
|
||||
.temp_memory_required = info.temp_memory_required,
|
||||
|
||||
.max_frame_size = @intCast(info.max_frame_size),
|
||||
};
|
||||
}
|
||||
3
libs/stb/src/stb_vorbis_impl.c
Normal file
@ -0,0 +1,3 @@
|
||||
#define STB_VORBIS_NO_INTEGER_CONVERSION
|
||||
#define STB_VORBIS_NO_STDIO
|
||||
#include "stb_vorbis.c"
|
||||
29
libs/tiled/build.zig
Normal file
@ -0,0 +1,29 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const mod = b.addModule("tiled", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/root.zig")
|
||||
});
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "tiled",
|
||||
.root_module = mod
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
{
|
||||
const tests = b.addTest(.{
|
||||
.root_module = mod
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
}
|
||||
}
|
||||
18
libs/tiled/src/buffers.zig
Normal file
@ -0,0 +1,18 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Buffers = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
points: std.ArrayList(Position) = .empty,
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator) Buffers {
|
||||
return Buffers{
|
||||
.allocator = gpa
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Buffers) void {
|
||||
self.points.deinit(self.allocator);
|
||||
}
|
||||
52
libs/tiled/src/color.zig
Normal file
@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Color = @This();
|
||||
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
a: u8,
|
||||
|
||||
pub const black = Color{
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 0,
|
||||
.a = 255,
|
||||
};
|
||||
|
||||
pub fn parse(str: []const u8, hash_required: bool) !Color {
|
||||
var color = Color{
|
||||
.r = undefined,
|
||||
.g = undefined,
|
||||
.b = undefined,
|
||||
.a = 255,
|
||||
};
|
||||
|
||||
if (str.len < 1) {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
const has_hash = str[0] == '#';
|
||||
if (hash_required and !has_hash) {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
const hex_str = if (has_hash) str[1..] else str;
|
||||
|
||||
if (hex_str.len == 6) {
|
||||
color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||
color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||
color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||
|
||||
} else if (hex_str.len == 8) {
|
||||
color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||
color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||
color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||
color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16);
|
||||
|
||||
} else {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
15
libs/tiled/src/global_tile_id.zig
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
pub const Flag = enum(u32) {
|
||||
flipped_horizontally = 1 << 31, // bit 32
|
||||
flipped_vertically = 1 << 30, // bit 31
|
||||
flipped_diagonally = 1 << 29, // bit 30
|
||||
rotated_hexagonal_120 = 1 << 28, // bit 29
|
||||
_,
|
||||
|
||||
pub const clear: u32 = ~(
|
||||
@intFromEnum(Flag.flipped_horizontally) |
|
||||
@intFromEnum(Flag.flipped_vertically) |
|
||||
@intFromEnum(Flag.flipped_diagonally) |
|
||||
@intFromEnum(Flag.rotated_hexagonal_120)
|
||||
);
|
||||
};
|
||||
283
libs/tiled/src/layer.zig
Normal file
@ -0,0 +1,283 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Property = @import("./property.zig");
|
||||
const xml = @import("./xml.zig");
|
||||
const Color = @import("./color.zig");
|
||||
const Object = @import("./object.zig");
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Layer = @This();
|
||||
|
||||
pub const TileVariant = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []u32,
|
||||
|
||||
fn initDataFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) ![]u32 {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("data");
|
||||
|
||||
const encoding = attrs.get("encoding") orelse "csv";
|
||||
// TODO: compression
|
||||
|
||||
var temp_tiles: std.ArrayList(u32) = .empty;
|
||||
|
||||
if (std.mem.eql(u8, encoding, "csv")) {
|
||||
const text = try lexer.nextExpectText();
|
||||
var split_iter = std.mem.splitScalar(u8, text, ',');
|
||||
while (split_iter.next()) |raw_tile_id| {
|
||||
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace);
|
||||
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
|
||||
try temp_tiles.append(scratch.allocator(), tile_id);
|
||||
}
|
||||
} else {
|
||||
return error.UnknownEncodingType;
|
||||
}
|
||||
|
||||
try iter.finish("data");
|
||||
|
||||
return try arena.dupe(u32, temp_tiles.items);
|
||||
}
|
||||
|
||||
pub fn get(self: TileVariant, x: usize, y: usize) ?u32 {
|
||||
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
|
||||
return self.data[y * self.width + x];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ImageVariant = struct {
|
||||
pub const Image = struct {
|
||||
// TODO: format
|
||||
source: []const u8,
|
||||
transparent_color: ?Color,
|
||||
width: ?u32,
|
||||
height: ?u32,
|
||||
|
||||
fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("image");
|
||||
|
||||
// TODO: format
|
||||
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
|
||||
const width = try attrs.getNumber(u32, "width") orelse null;
|
||||
const height = try attrs.getNumber(u32, "height") orelse null;
|
||||
const transparent_color = try attrs.getColor("trans", false);
|
||||
|
||||
try iter.finish("image");
|
||||
|
||||
return Image{
|
||||
.source = source,
|
||||
.transparent_color = transparent_color,
|
||||
.width = width,
|
||||
.height = height
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
repeat_x: bool,
|
||||
repeat_y: bool,
|
||||
image: ?Image
|
||||
};
|
||||
|
||||
pub const ObjectVariant = struct {
|
||||
pub const DrawOrder = enum {
|
||||
top_down,
|
||||
index,
|
||||
|
||||
const map: std.StaticStringMap(DrawOrder) = .initComptime(.{
|
||||
.{ "topdown", .top_down },
|
||||
.{ "index", .index },
|
||||
});
|
||||
};
|
||||
|
||||
color: ?Color,
|
||||
draw_order: DrawOrder,
|
||||
items: []Object
|
||||
};
|
||||
|
||||
pub const GroupVariant = struct {
|
||||
layers: []Layer
|
||||
};
|
||||
|
||||
pub const Type = enum {
|
||||
tile,
|
||||
object,
|
||||
image,
|
||||
group,
|
||||
|
||||
const name_map: std.StaticStringMap(Type) = .initComptime(.{
|
||||
.{ "layer", .tile },
|
||||
.{ "objectgroup", .object },
|
||||
.{ "imagelayer", .image },
|
||||
.{ "group", .group }
|
||||
});
|
||||
|
||||
fn toXmlName(self: Type) []const u8 {
|
||||
return switch (self) {
|
||||
.tile => "layer",
|
||||
.object => "objectgroup",
|
||||
.image => "imagelayer",
|
||||
.group => "group",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Variant = union(Type) {
|
||||
tile: TileVariant,
|
||||
object: ObjectVariant,
|
||||
image: ImageVariant,
|
||||
group: GroupVariant
|
||||
};
|
||||
|
||||
id: u32,
|
||||
name: []const u8,
|
||||
class: []const u8,
|
||||
opacity: f32,
|
||||
visible: bool,
|
||||
tint_color: ?Color,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
parallax_x: f32,
|
||||
parallax_y: f32,
|
||||
properties: Property.List,
|
||||
variant: Variant,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Layer {
|
||||
const value = try lexer.peek() orelse return error.MissingStartTag;
|
||||
if (value != .start_tag) return error.MissingStartTag;
|
||||
|
||||
var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType;
|
||||
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin(layer_type.toXmlName());
|
||||
|
||||
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
const name = try attrs.getDupe(arena, "name") orelse "";
|
||||
const class = try attrs.getDupe(arena, "class") orelse "";
|
||||
const opacity = try attrs.getNumber(f32, "opacity") orelse 1;
|
||||
const visible = try attrs.getBool("visible", "1", "0") orelse true;
|
||||
const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0;
|
||||
const offset_y = try attrs.getNumber(f32, "offsety") orelse 0;
|
||||
const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1;
|
||||
const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1;
|
||||
const tint_color = try attrs.getColor("tintcolor", true);
|
||||
|
||||
var variant: Variant = undefined;
|
||||
switch (layer_type) {
|
||||
.tile => {
|
||||
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||
variant = .{
|
||||
.tile = TileVariant{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = &[0]u32{}
|
||||
}
|
||||
};
|
||||
},
|
||||
.image => {
|
||||
const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false;
|
||||
const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false;
|
||||
variant = .{
|
||||
.image = ImageVariant{
|
||||
.repeat_x = repeat_x,
|
||||
.repeat_y = repeat_y,
|
||||
.image = null
|
||||
}
|
||||
};
|
||||
},
|
||||
.object => {
|
||||
const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down;
|
||||
const color = try attrs.getColor("color", true);
|
||||
|
||||
variant = .{
|
||||
.object = ObjectVariant{
|
||||
.color = color,
|
||||
.draw_order = draw_order,
|
||||
.items = &.{}
|
||||
}
|
||||
};
|
||||
},
|
||||
.group => {
|
||||
variant = .{
|
||||
.group = .{
|
||||
.layers = &.{}
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
var properties: Property.List = .empty;
|
||||
var objects: std.ArrayList(Object) = .empty;
|
||||
var layers: std.ArrayList(Layer) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("properties")) {
|
||||
properties = try Property.List.initFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .tile and node.isTag("data")) {
|
||||
variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .image and node.isTag("image")) {
|
||||
variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .object and node.isTag("object")) {
|
||||
const object = try Object.initFromXml(arena, scratch, lexer);
|
||||
try objects.append(scratch.allocator(), object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .group and isLayerNode(node)) {
|
||||
const layer = try initFromXml(arena, scratch, lexer);
|
||||
try layers.append(scratch.allocator(), layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish(layer_type.toXmlName());
|
||||
|
||||
if (variant == .object) {
|
||||
variant.object.items = try arena.dupe(Object, objects.items);
|
||||
}
|
||||
|
||||
if (variant == .group) {
|
||||
variant.group.layers = try arena.dupe(Layer, layers.items);
|
||||
}
|
||||
|
||||
return Layer{
|
||||
.id = id,
|
||||
.name = name,
|
||||
.class = class,
|
||||
.opacity = opacity,
|
||||
.visible = visible,
|
||||
.tint_color = tint_color,
|
||||
.offset_x = offset_x,
|
||||
.offset_y = offset_y,
|
||||
.parallax_x = parallax_x,
|
||||
.parallax_y = parallax_y,
|
||||
.properties = properties,
|
||||
.variant = variant,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isLayerNode(node: xml.TagParser.Node) bool {
|
||||
return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group");
|
||||
}
|
||||
421
libs/tiled/src/object.zig
Normal file
@ -0,0 +1,421 @@
|
||||
const std = @import("std");
|
||||
|
||||
const xml = @import("./xml.zig");
|
||||
const Position = @import("./position.zig");
|
||||
const Color = @import("./color.zig");
|
||||
|
||||
const Object = @This();
|
||||
|
||||
pub const Shape = union (Type) {
|
||||
pub const Type = enum {
|
||||
rectangle,
|
||||
point,
|
||||
ellipse,
|
||||
polygon,
|
||||
tile,
|
||||
// TODO: template
|
||||
text
|
||||
};
|
||||
|
||||
pub const Rectangle = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
pub const Tile = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
gid: u32
|
||||
};
|
||||
|
||||
pub const Ellipse = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
pub const Polygon = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
points: []const Position
|
||||
};
|
||||
|
||||
pub const Text = struct {
|
||||
pub const Font = struct {
|
||||
family: []const u8,
|
||||
pixel_size: f32,
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
underline: bool,
|
||||
strikeout: bool,
|
||||
kerning: bool
|
||||
};
|
||||
|
||||
pub const HorizontalAlign = enum {
|
||||
left,
|
||||
center,
|
||||
right,
|
||||
justify,
|
||||
|
||||
const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
|
||||
.{ "left", .left },
|
||||
.{ "center", .center },
|
||||
.{ "right", .right },
|
||||
.{ "justify", .justify },
|
||||
});
|
||||
};
|
||||
|
||||
pub const VerticalAlign = enum {
|
||||
top,
|
||||
center,
|
||||
bottom,
|
||||
|
||||
const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
|
||||
.{ "top", .top },
|
||||
.{ "center", .center },
|
||||
.{ "bottom", .bottom },
|
||||
});
|
||||
};
|
||||
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
word_wrap: bool,
|
||||
color: Color,
|
||||
font: Font,
|
||||
horizontal_align: HorizontalAlign,
|
||||
vertical_align: VerticalAlign,
|
||||
content: []const u8
|
||||
};
|
||||
|
||||
pub const Point = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
};
|
||||
|
||||
rectangle: Rectangle,
|
||||
point: Point,
|
||||
ellipse: Ellipse,
|
||||
polygon: Polygon,
|
||||
tile: Tile,
|
||||
text: Text
|
||||
};
|
||||
|
||||
id: u32,
|
||||
name: []const u8,
|
||||
class: []const u8,
|
||||
rotation: f32, // TODO: maybe this field should be moved to Shape struct
|
||||
visible: bool,
|
||||
shape: Shape,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Object {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("object");
|
||||
|
||||
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
const name = try attrs.getDupe(arena, "name") orelse "";
|
||||
const class = try attrs.getDupe(arena, "type") orelse "";
|
||||
const x = try attrs.getNumber(f32, "x") orelse 0;
|
||||
const y = try attrs.getNumber(f32, "y") orelse 0;
|
||||
const width = try attrs.getNumber(f32, "width") orelse 0;
|
||||
const height = try attrs.getNumber(f32, "height") orelse 0;
|
||||
const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
|
||||
const visible = try attrs.getBool("visible", "1", "0") orelse true;
|
||||
|
||||
var shape: ?Shape = null;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (shape == null) {
|
||||
if (node.isTag("point")) {
|
||||
shape = .{
|
||||
.point = Shape.Point{
|
||||
.x = x,
|
||||
.y = y,
|
||||
}
|
||||
};
|
||||
|
||||
} else if (node.isTag("ellipse")) {
|
||||
shape = .{
|
||||
.ellipse = Shape.Ellipse{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height
|
||||
}
|
||||
};
|
||||
|
||||
} else if (node.isTag("text")) {
|
||||
const HorizontalShape = Shape.Text.HorizontalAlign;
|
||||
const VerticalAlign = Shape.Text.VerticalAlign;
|
||||
|
||||
const text_attrs = node.tag.attributes;
|
||||
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
|
||||
const color = try text_attrs.getColor("color", true) orelse Color.black;
|
||||
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
|
||||
const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
|
||||
const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
|
||||
const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
|
||||
const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
|
||||
const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
|
||||
const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
|
||||
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
|
||||
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
|
||||
|
||||
_ = try lexer.nextExpectStartTag("text");
|
||||
var content: []const u8 = "";
|
||||
const content_value = try lexer.peek();
|
||||
if (content_value != null and content_value.? == .text) {
|
||||
content = try arena.dupe(u8, content_value.?.text);
|
||||
}
|
||||
try lexer.skipUntilMatchingEndTag("text");
|
||||
|
||||
shape = .{
|
||||
.text = Shape.Text{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.word_wrap = word_wrap,
|
||||
.color = color,
|
||||
.horizontal_align = horizontal_align,
|
||||
.vertical_align = vertical_align,
|
||||
.content = try arena.dupe(u8, content),
|
||||
.font = .{
|
||||
.bold = bold,
|
||||
.italic = italic,
|
||||
.strikeout = strikeout,
|
||||
.underline = underline,
|
||||
.pixel_size = pixel_size,
|
||||
.kerning = kerning,
|
||||
.family = font_family,
|
||||
}
|
||||
}
|
||||
};
|
||||
continue;
|
||||
|
||||
} else if (node.isTag("polygon")) {
|
||||
const points_str = node.tag.attributes.get("points") orelse "";
|
||||
|
||||
var points: std.ArrayList(Position) = .empty;
|
||||
var point_iter = std.mem.splitScalar(u8, points_str, ' ');
|
||||
while (point_iter.next()) |point_str| {
|
||||
const point = try Position.parseCommaDelimited(point_str);
|
||||
try points.append(scratch.allocator(), point);
|
||||
}
|
||||
|
||||
shape = .{
|
||||
.polygon = Shape.Polygon{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.points = try arena.dupe(Position, points.items)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
if (shape == null) {
|
||||
if (try attrs.getNumber(u32, "gid")) |gid| {
|
||||
shape = .{
|
||||
.tile = Shape.Tile{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.gid = gid
|
||||
}
|
||||
};
|
||||
} else {
|
||||
shape = .{
|
||||
.rectangle = Shape.Rectangle{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try iter.finish("object");
|
||||
|
||||
return Object{
|
||||
.id = id,
|
||||
.name = name,
|
||||
.class = class,
|
||||
.rotation = rotation,
|
||||
.visible = visible,
|
||||
.shape = shape orelse return error.UnknownShapeType
|
||||
};
|
||||
}
|
||||
|
||||
fn expectParsedEquals(expected: Object, body: []const u8) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: xml.Lexer.TestingContext = undefined;
|
||||
ctx.init(allocator, body);
|
||||
defer ctx.deinit();
|
||||
|
||||
var scratch = std.heap.ArenaAllocator.init(allocator);
|
||||
defer scratch.deinit();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
|
||||
try std.testing.expectEqualDeep(expected, parsed);
|
||||
}
|
||||
|
||||
test Object {
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 10,
|
||||
.name = "rectangle",
|
||||
.class = "object class",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.rectangle = .{
|
||||
.x = 12.34,
|
||||
.y = 56.78,
|
||||
.width = 31.5,
|
||||
.height = 20.25,
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="10" name="rectangle" type="object class" x="12.34" y="56.78" width="31.5" height="20.25"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 3,
|
||||
.name = "point",
|
||||
.class = "foo",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.point = .{
|
||||
.x = 77.125,
|
||||
.y = 99.875
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
|
||||
\\ <point/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 4,
|
||||
.name = "ellipse",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.ellipse = .{
|
||||
.x = 64.25,
|
||||
.y = 108.25,
|
||||
.width = 22.375,
|
||||
.height = 15.375
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
|
||||
\\ <ellipse/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 5,
|
||||
.name = "",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.polygon = .{
|
||||
.x = 40.125,
|
||||
.y = 96.25,
|
||||
.points = &[_]Position{
|
||||
.{ .x = 0, .y = 0 },
|
||||
.{ .x = 13.25, .y = -4.25 },
|
||||
.{ .x = 10.125, .y = 18.625 },
|
||||
.{ .x = 2.25, .y = 17.375 },
|
||||
.{ .x = -0.125, .y = 25.75 },
|
||||
.{ .x = -3.875, .y = 20.75 },
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="5" x="40.125" y="96.25">
|
||||
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 2,
|
||||
.name = "tile",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.tile = .{
|
||||
.x = 60.125,
|
||||
.y = 103.5,
|
||||
.width = 8,
|
||||
.height = 8,
|
||||
.gid = 35
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 6,
|
||||
.name = "text",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.text = .{
|
||||
.x = 64.3906,
|
||||
.y = 92.8594,
|
||||
.width = 87.7188,
|
||||
.height = 21.7813,
|
||||
.content = "Hello World",
|
||||
.word_wrap = true,
|
||||
.color = .black,
|
||||
.horizontal_align = .center,
|
||||
.vertical_align = .top,
|
||||
.font = .{
|
||||
.family = "sans-serif",
|
||||
.pixel_size = 16,
|
||||
.bold = false,
|
||||
.italic = false,
|
||||
.underline = false,
|
||||
.strikeout = false,
|
||||
.kerning = true
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
|
||||
\\ <text wrap="1" halign="center"> Hello World </text>
|
||||
\\ </object>
|
||||
);
|
||||
}
|
||||
20
libs/tiled/src/position.zig
Normal file
@ -0,0 +1,20 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Position = @This();
|
||||
|
||||
x: f32,
|
||||
y: f32,
|
||||
|
||||
pub fn parseCommaDelimited(str: []const u8) !Position {
|
||||
const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma;
|
||||
const x_str = str[0..comma_index];
|
||||
const y_str = str[(comma_index+1)..];
|
||||
|
||||
const x = try std.fmt.parseFloat(f32, x_str);
|
||||
const y = try std.fmt.parseFloat(f32, y_str);
|
||||
|
||||
return Position{
|
||||
.x = x,
|
||||
.y = y
|
||||
};
|
||||
}
|
||||
153
libs/tiled/src/property.zig
Normal file
@ -0,0 +1,153 @@
|
||||
const std = @import("std");
|
||||
const xml = @import("./xml.zig");
|
||||
|
||||
const Property = @This();
|
||||
|
||||
pub const Type = enum {
|
||||
string,
|
||||
int,
|
||||
bool,
|
||||
|
||||
const map: std.StaticStringMap(Type) = .initComptime(.{
|
||||
.{ "string", .string },
|
||||
.{ "int", .int },
|
||||
.{ "bool", .bool },
|
||||
});
|
||||
};
|
||||
|
||||
pub const Value = union(Type) {
|
||||
string: []const u8,
|
||||
int: i32,
|
||||
bool: bool
|
||||
};
|
||||
|
||||
name: []const u8,
|
||||
value: Value,
|
||||
|
||||
pub const List = struct {
|
||||
items: []Property,
|
||||
|
||||
pub const empty = List{
|
||||
.items = &[0]Property{}
|
||||
};
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer
|
||||
) !Property.List {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
_ = try iter.begin("properties");
|
||||
|
||||
var temp_properties: std.ArrayList(Property) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("property")) {
|
||||
const property = try Property.initFromXml(arena, lexer);
|
||||
try temp_properties.append(scratch.allocator(), property);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
try iter.finish("properties");
|
||||
|
||||
const properties = try arena.dupe(Property, temp_properties.items);
|
||||
|
||||
return List{
|
||||
.items = properties
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get(self: List, name: []const u8) ?Value {
|
||||
for (self.items) |item| {
|
||||
if (std.mem.eql(u8, item.name, name)) {
|
||||
return item.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getString(self: List, name: []const u8) ?[]const u8 {
|
||||
if (self.get(name)) |value| {
|
||||
return value.string;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(name: []const u8, value: Value) Property {
|
||||
return Property{
|
||||
.name = name,
|
||||
.value = value
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("property");
|
||||
|
||||
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
|
||||
const prop_type_str = attrs.get("type") orelse "string";
|
||||
const value_str = attrs.get("value") orelse "";
|
||||
|
||||
const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType;
|
||||
const value = switch(prop_type) {
|
||||
.string => Value{
|
||||
.string = try arena.dupe(u8, value_str)
|
||||
},
|
||||
.int => Value{
|
||||
.int = try std.fmt.parseInt(i32, value_str, 10)
|
||||
},
|
||||
.bool => Value{
|
||||
.bool = std.mem.eql(u8, value_str, "true")
|
||||
}
|
||||
};
|
||||
|
||||
try iter.finish("property");
|
||||
|
||||
return Property{
|
||||
.name = name,
|
||||
.value = value
|
||||
};
|
||||
}
|
||||
|
||||
fn expectParsedEquals(expected: Property, body: []const u8) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: xml.Lexer.TestingContext = undefined;
|
||||
ctx.init(allocator, body);
|
||||
defer ctx.deinit();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const parsed = try initFromXml(arena.allocator(), &ctx.lexer);
|
||||
try std.testing.expectEqualDeep(expected, parsed);
|
||||
}
|
||||
|
||||
test Property {
|
||||
try expectParsedEquals(
|
||||
Property.init("solid", .{ .string = "hello" }),
|
||||
\\ <property name="solid" value="hello"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("solid", .{ .string = "hello" }),
|
||||
\\ <property name="solid" type="string" value="hello"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("integer", .{ .int = 123 }),
|
||||
\\ <property name="integer" type="int" value="123"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("boolean", .{ .bool = true }),
|
||||
\\ <property name="boolean" type="bool" value="true"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("boolean", .{ .bool = false }),
|
||||
\\ <property name="boolean" type="bool" value="false"/>
|
||||
);
|
||||
}
|
||||
16
libs/tiled/src/root.zig
Normal file
@ -0,0 +1,16 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Warning:
|
||||
// This library is not complete, it does not cover all features that the specification provides.
|
||||
// But there are enough features implemented so that I could use this for my games.
|
||||
|
||||
// Map format specification:
|
||||
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#
|
||||
|
||||
pub const xml = @import("./xml.zig");
|
||||
pub const Tileset = @import("./tileset.zig");
|
||||
pub const Tilemap = @import("./tilemap.zig");
|
||||
|
||||
test {
|
||||
_ = std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
235
libs/tiled/src/tilemap.zig
Normal file
@ -0,0 +1,235 @@
|
||||
const std = @import("std");
|
||||
const xml = @import("./xml.zig");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Property = @import("./property.zig");
|
||||
const Layer = @import("./layer.zig");
|
||||
const Object = @import("./object.zig");
|
||||
const Position = @import("./position.zig");
|
||||
const Tileset = @import("./tileset.zig");
|
||||
const GlobalTileId = @import("./global_tile_id.zig");
|
||||
|
||||
const Tilemap = @This();
|
||||
|
||||
pub const Orientation = enum {
|
||||
orthogonal,
|
||||
staggered,
|
||||
hexagonal,
|
||||
isometric,
|
||||
|
||||
const map: std.StaticStringMap(Orientation) = .initComptime(.{
|
||||
.{ "orthogonal", .orthogonal },
|
||||
.{ "staggered", .staggered },
|
||||
.{ "hexagonal", .hexagonal },
|
||||
.{ "isometric", .isometric }
|
||||
});
|
||||
};
|
||||
|
||||
pub const RenderOrder = enum {
|
||||
right_down,
|
||||
right_up,
|
||||
left_down,
|
||||
left_up,
|
||||
|
||||
const map: std.StaticStringMap(RenderOrder) = .initComptime(.{
|
||||
.{ "right-down", .right_down },
|
||||
.{ "right-up", .right_up },
|
||||
.{ "left-down", .left_down },
|
||||
.{ "left-up", .left_up },
|
||||
});
|
||||
};
|
||||
|
||||
pub const StaggerAxis = enum {
|
||||
x,
|
||||
y,
|
||||
|
||||
const map: std.StaticStringMap(StaggerAxis) = .initComptime(.{
|
||||
.{ "x", .x },
|
||||
.{ "y", .y }
|
||||
});
|
||||
};
|
||||
|
||||
pub const TilesetReference = struct {
|
||||
source: []const u8,
|
||||
first_gid: u32,
|
||||
|
||||
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !TilesetReference {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("tileset");
|
||||
|
||||
const source = try attrs.getDupe(arena, "source") orelse return error.MissingFirstGid;
|
||||
const first_gid = try attrs.getNumber(u32, "firstgid") orelse return error.MissingFirstGid;
|
||||
|
||||
try iter.finish("tileset");
|
||||
|
||||
return TilesetReference{
|
||||
.source = source,
|
||||
.first_gid = first_gid
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Tile = struct {
|
||||
tileset: *const Tileset,
|
||||
id: u32,
|
||||
|
||||
pub fn getProperties(self: Tile) Property.List {
|
||||
return self.tileset.getTileProperties(self.id) orelse .empty;
|
||||
}
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
version: []const u8,
|
||||
tiled_version: ?[]const u8,
|
||||
orientation: Orientation,
|
||||
render_order: RenderOrder,
|
||||
width: u32,
|
||||
height: u32,
|
||||
tile_width: u32,
|
||||
tile_height: u32,
|
||||
infinite: bool,
|
||||
|
||||
stagger_axis: ?StaggerAxis,
|
||||
next_layer_id: u32,
|
||||
next_object_id: u32,
|
||||
|
||||
tilesets: []TilesetReference,
|
||||
layers: []Layer,
|
||||
|
||||
pub fn initFromBuffer(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
buffer: []const u8
|
||||
) !Tilemap {
|
||||
var reader = Io.Reader.fixed(buffer);
|
||||
return initFromReader(gpa, scratch, xml_buffers, &reader);
|
||||
}
|
||||
|
||||
pub fn initFromReader(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
reader: *Io.Reader,
|
||||
) !Tilemap {
|
||||
var lexer = xml.Lexer.init(reader, xml_buffers);
|
||||
return initFromXml(gpa, scratch, &lexer);
|
||||
}
|
||||
|
||||
// Map specification:
|
||||
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#map
|
||||
pub fn initFromXml(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Tilemap {
|
||||
var arena_allocator = std.heap.ArenaAllocator.init(gpa);
|
||||
errdefer arena_allocator.deinit();
|
||||
|
||||
const arena = arena_allocator.allocator();
|
||||
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const map_attrs = try iter.begin("map");
|
||||
|
||||
const version = try map_attrs.getDupe(arena, "version") orelse return error.MissingVersion;
|
||||
const tiled_version = try map_attrs.getDupe(arena, "tiledversion");
|
||||
const orientation = try map_attrs.getEnum(Orientation, "orientation", Orientation.map) orelse return error.MissingOrientation;
|
||||
const render_order = try map_attrs.getEnum(RenderOrder, "renderorder", RenderOrder.map) orelse return error.MissingRenderOrder;
|
||||
// TODO: compressionlevel
|
||||
const width = try map_attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||
const height = try map_attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||
const tile_width = try map_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTileWidth;
|
||||
const tile_height = try map_attrs.getNumber(u32, "tileheight") orelse return error.MissingTileHeight;
|
||||
// TODO: hexidelength
|
||||
const infinite_int = try map_attrs.getNumber(u32, "infinite") orelse 0;
|
||||
const infinite = infinite_int != 0;
|
||||
const next_layer_id = try map_attrs.getNumber(u32, "nextlayerid") orelse return error.MissingLayerId;
|
||||
const next_object_id = try map_attrs.getNumber(u32, "nextobjectid") orelse return error.MissingObjectId;
|
||||
// TODO: parallaxoriginx
|
||||
// TODO: parallaxoriginy
|
||||
// TODO: backgroundcolor
|
||||
|
||||
var stagger_axis: ?StaggerAxis = null;
|
||||
if (orientation == .hexagonal or orientation == .staggered) {
|
||||
stagger_axis = try map_attrs.getEnum(StaggerAxis, "staggeraxis", StaggerAxis.map) orelse return error.MissingRenderOrder;
|
||||
// TODO: staggerindex
|
||||
}
|
||||
|
||||
var tileset_list: std.ArrayList(TilesetReference) = .empty;
|
||||
var layer_list: std.ArrayList(Layer) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("tileset")) {
|
||||
try tileset_list.append(scratch.allocator(), try TilesetReference.initFromXml(arena, lexer));
|
||||
continue;
|
||||
} else if (Layer.isLayerNode(node)) {
|
||||
const layer = try Layer.initFromXml(
|
||||
arena,
|
||||
scratch,
|
||||
lexer,
|
||||
);
|
||||
try layer_list.append(scratch.allocator(), layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
try iter.finish("map");
|
||||
|
||||
const tilesets = try arena.dupe(TilesetReference, tileset_list.items);
|
||||
const layers = try arena.dupe(Layer, layer_list.items);
|
||||
|
||||
return Tilemap{
|
||||
.arena = arena_allocator,
|
||||
.version = version,
|
||||
.tiled_version = tiled_version,
|
||||
.orientation = orientation,
|
||||
.render_order = render_order,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.tile_width = tile_width,
|
||||
.tile_height = tile_height,
|
||||
.infinite = infinite,
|
||||
.stagger_axis = stagger_axis,
|
||||
.next_object_id = next_object_id,
|
||||
.next_layer_id = next_layer_id,
|
||||
.tilesets = tilesets,
|
||||
.layers = layers,
|
||||
};
|
||||
}
|
||||
|
||||
fn getTilesetByGid(self: *const Tilemap, gid: u32) ?TilesetReference {
|
||||
var result: ?TilesetReference = null;
|
||||
for (self.tilesets) |tileset| {
|
||||
if (gid < tileset.first_gid) {
|
||||
continue;
|
||||
}
|
||||
if (result != null and result.?.first_gid < tileset.first_gid) {
|
||||
continue;
|
||||
}
|
||||
result = tileset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn getTile(self: *const Tilemap, layer: *const Layer, tilesets: Tileset.List, x: usize, y: usize) ?Tile {
|
||||
assert(layer.variant == .tile);
|
||||
const tile_variant = layer.variant.tile;
|
||||
|
||||
const gid = tile_variant.get(x, y) orelse return null;
|
||||
const tileset_ref = self.getTilesetByGid(gid & GlobalTileId.Flag.clear) orelse return null;
|
||||
const tileset = tilesets.get(tileset_ref.source) orelse return null;
|
||||
const id = gid - tileset_ref.first_gid;
|
||||
|
||||
return Tile{
|
||||
.tileset = tileset,
|
||||
.id = id
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Tilemap) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
226
libs/tiled/src/tileset.zig
Normal file
@ -0,0 +1,226 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const xml = @import("./xml.zig");
|
||||
const Property = @import("./property.zig");
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Tileset = @This();
|
||||
|
||||
pub const Image = struct {
|
||||
source: []const u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("image");
|
||||
|
||||
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
|
||||
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||
|
||||
try iter.finish("image");
|
||||
|
||||
return Image{
|
||||
.source = source,
|
||||
.width = width,
|
||||
.height = height
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Tile = struct {
|
||||
id: u32,
|
||||
properties: Property.List,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Tile {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const tile_attrs = try iter.begin("tile");
|
||||
|
||||
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
|
||||
var properties: Property.List = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("properties")) {
|
||||
properties = try Property.List.initFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish("tile");
|
||||
|
||||
return Tile{
|
||||
.id = id,
|
||||
.properties = properties
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const List = struct {
|
||||
const Entry = struct {
|
||||
name: []const u8,
|
||||
tileset: Tileset
|
||||
};
|
||||
|
||||
list: std.ArrayList(Entry),
|
||||
|
||||
pub const empty = List{
|
||||
.list = .empty
|
||||
};
|
||||
|
||||
pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void {
|
||||
if (self.get(name) != null) {
|
||||
return error.DuplicateName;
|
||||
}
|
||||
|
||||
const name_dupe = try gpa.dupe(u8, name);
|
||||
errdefer gpa.free(name_dupe);
|
||||
|
||||
try self.list.append(gpa, .{
|
||||
.name = name_dupe,
|
||||
.tileset = tileset
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, name: []const u8) ?*const Tileset {
|
||||
for (self.list.items) |*entry| {
|
||||
if (std.mem.eql(u8, entry.name, name)) {
|
||||
return &entry.tileset;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *List, gpa: Allocator) void {
|
||||
for (self.list.items) |entry| {
|
||||
gpa.free(entry.name);
|
||||
entry.tileset.deinit();
|
||||
}
|
||||
self.list.deinit(gpa);
|
||||
}
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
version: []const u8,
|
||||
tiled_version: []const u8,
|
||||
name: []const u8,
|
||||
tile_width: u32,
|
||||
tile_height: u32,
|
||||
tile_count: u32,
|
||||
columns: u32,
|
||||
|
||||
image: Image,
|
||||
|
||||
tiles: []Tile,
|
||||
|
||||
pub fn initFromBuffer(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
buffer: []const u8
|
||||
) !Tileset {
|
||||
var reader = Io.Reader.fixed(buffer);
|
||||
return initFromReader(gpa, scratch, xml_buffers, &reader);
|
||||
}
|
||||
|
||||
pub fn initFromReader(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
reader: *Io.Reader,
|
||||
) !Tileset {
|
||||
var lexer = xml.Lexer.init(reader, xml_buffers);
|
||||
return initFromXml(gpa, scratch, &lexer);
|
||||
}
|
||||
|
||||
pub fn initFromXml(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer
|
||||
) !Tileset {
|
||||
var arena_state = std.heap.ArenaAllocator.init(gpa);
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const tileset_attrs = try iter.begin("tileset");
|
||||
|
||||
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag;
|
||||
const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
|
||||
const name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag;
|
||||
const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag;
|
||||
const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag;
|
||||
const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag;
|
||||
const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag;
|
||||
|
||||
var image: ?Image = null;
|
||||
var tiles_list: std.ArrayList(Tile) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("image")) {
|
||||
image = try Image.intFromXml(arena, lexer);
|
||||
continue;
|
||||
} else if (node.isTag("tile")) {
|
||||
const tile = try Tile.initFromXml(arena, scratch, lexer);
|
||||
try tiles_list.append(scratch.allocator(), tile);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
try iter.finish("tileset");
|
||||
|
||||
const tiles = try arena.dupe(Tile, tiles_list.items);
|
||||
|
||||
return Tileset{
|
||||
.arena = arena_state,
|
||||
.version = version,
|
||||
.tiled_version = tiled_version,
|
||||
.name = name,
|
||||
.tile_width = tile_width,
|
||||
.tile_height = tile_height,
|
||||
.tile_count = tile_count,
|
||||
.columns = columns,
|
||||
.image = image orelse return error.MissingImageTag,
|
||||
.tiles = tiles
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
|
||||
for (self.tiles) |tile| {
|
||||
if (tile.id == id) {
|
||||
return tile.properties;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
|
||||
if (id >= self.tile_count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tileset_width = @divExact(self.image.width, self.tile_width);
|
||||
|
||||
const tile_x = @mod(id, tileset_width);
|
||||
const tile_y = @divFloor(id, tileset_width);
|
||||
|
||||
return Position{
|
||||
.x = @floatFromInt(tile_x * self.tile_width),
|
||||
.y = @floatFromInt(tile_y * self.tile_height),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Tileset) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
687
libs/tiled/src/xml.zig
Normal file
@ -0,0 +1,687 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Color = @import("./color.zig");
|
||||
|
||||
pub const Attribute = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
|
||||
pub const List = struct {
|
||||
items: []const Attribute,
|
||||
|
||||
pub fn get(self: List, name: []const u8) ?[]const u8 {
|
||||
for (self.items) |attr| {
|
||||
if (std.mem.eql(u8, attr.name, name)) {
|
||||
return attr.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 {
|
||||
if (self.get(name)) |value| {
|
||||
return try gpa.dupe(u8, value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getNumber(self: List, T: type, name: []const u8) !?T {
|
||||
if (self.get(name)) |value| {
|
||||
if (@typeInfo(T) == .int) {
|
||||
return try std.fmt.parseInt(T, value, 10);
|
||||
} else if (@typeInfo(T) == .float) {
|
||||
return try std.fmt.parseFloat(T, value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool {
|
||||
if (self.get(name)) |value| {
|
||||
if (std.mem.eql(u8, value, true_value)) {
|
||||
return true;
|
||||
} else if (std.mem.eql(u8, value, false_value)) {
|
||||
return false;
|
||||
} else {
|
||||
return error.InvalidBoolean;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T {
|
||||
if (self.get(name)) |value| {
|
||||
return map.get(value) orelse return error.InvalidEnumValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color {
|
||||
if (self.get(name)) |value| {
|
||||
return try Color.parse(value, hash_required);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (self.items.len > 0) {
|
||||
try writer.writeAll("{ ");
|
||||
for (self.items, 0..) |attribute, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeAll(", ");
|
||||
}
|
||||
try writer.print("{f}", .{attribute});
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
} else {
|
||||
try writer.writeAll("{ }");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value });
|
||||
}
|
||||
|
||||
pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (data.len > 0) {
|
||||
try writer.writeAll("{ ");
|
||||
for (data, 0..) |attribute, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeAll(", ");
|
||||
}
|
||||
try writer.print("{f}", .{attribute});
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
} else {
|
||||
try writer.writeAll("{ }");
|
||||
}
|
||||
}
|
||||
|
||||
fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) {
|
||||
return .{ .data = data };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Tag = struct {
|
||||
name: []const u8,
|
||||
attributes: Attribute.List
|
||||
};
|
||||
|
||||
pub const Lexer = struct {
|
||||
pub const Buffers = struct {
|
||||
scratch: std.heap.ArenaAllocator,
|
||||
text: std.ArrayList(u8),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Buffers {
|
||||
return Buffers{
|
||||
.scratch = std.heap.ArenaAllocator.init(allocator),
|
||||
.text = .empty
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(self: *Buffers) void {
|
||||
self.text.clearRetainingCapacity();
|
||||
_ = self.scratch.reset(.retain_capacity);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Buffers) void {
|
||||
const allocator = self.scratch.child_allocator;
|
||||
self.scratch.deinit();
|
||||
self.text.deinit(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Token = union(enum) {
|
||||
start_tag: Tag,
|
||||
end_tag: []const u8,
|
||||
text: []const u8,
|
||||
|
||||
pub fn isStartTag(self: Token, name: []const u8) bool {
|
||||
if (self == .start_tag) {
|
||||
return std.mem.eql(u8, self.start_tag.name, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEndTag(self: Token, name: []const u8) bool {
|
||||
if (self == .end_tag) {
|
||||
return std.mem.eql(u8, self.end_tag, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const StepResult = struct {
|
||||
token: ?Token,
|
||||
self_closing: bool = false,
|
||||
};
|
||||
|
||||
pub const TestingContext = struct {
|
||||
io_reader: Io.Reader,
|
||||
buffers: Buffers,
|
||||
lexer: Lexer,
|
||||
|
||||
pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void {
|
||||
self.* = TestingContext{
|
||||
.lexer = undefined,
|
||||
.io_reader = Io.Reader.fixed(body),
|
||||
.buffers = Buffers.init(allocator)
|
||||
};
|
||||
self.lexer = Lexer.init(&self.io_reader, &self.buffers);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestingContext) void {
|
||||
self.buffers.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
io_reader: *Io.Reader,
|
||||
buffers: *Buffers,
|
||||
|
||||
peeked_value: ?Token,
|
||||
cursor: usize,
|
||||
queued_end_tag: ?[]const u8,
|
||||
|
||||
pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer {
|
||||
buffers.clear();
|
||||
|
||||
return Lexer{
|
||||
.io_reader = reader,
|
||||
.buffers = buffers,
|
||||
.cursor = 0,
|
||||
.queued_end_tag = null,
|
||||
.peeked_value = null
|
||||
};
|
||||
}
|
||||
|
||||
fn step(self: *Lexer) !StepResult {
|
||||
_ = self.buffers.scratch.reset(.retain_capacity);
|
||||
|
||||
if (try self.peekByte() == '<') {
|
||||
self.tossByte();
|
||||
|
||||
if (try self.peekByte() == '/') {
|
||||
// End tag
|
||||
self.tossByte();
|
||||
|
||||
const name = try self.parseName();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
|
||||
return error.InvalidEndTag;
|
||||
}
|
||||
|
||||
const token = Token{ .end_tag = name };
|
||||
return .{ .token = token };
|
||||
|
||||
} else if (try self.peekByte() == '?') {
|
||||
// Prolog tag
|
||||
self.tossByte();
|
||||
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
|
||||
return error.InvalidPrologTag;
|
||||
}
|
||||
|
||||
const attributes = try self.parseAttributes();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
|
||||
return error.MissingPrologEnd;
|
||||
}
|
||||
|
||||
const version = attributes.get("version") orelse return error.InvalidProlog;
|
||||
if (!std.mem.eql(u8, version, "1.0")) {
|
||||
return error.InvalidPrologVersion;
|
||||
}
|
||||
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
|
||||
if (!std.mem.eql(u8, encoding, "UTF-8")) {
|
||||
return error.InvalidPrologEncoding;
|
||||
}
|
||||
|
||||
return .{ .token = null };
|
||||
|
||||
} else {
|
||||
// Start tag
|
||||
const name = try self.parseName();
|
||||
const attributes = try self.parseAttributes();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
const token = Token{
|
||||
.start_tag = .{
|
||||
.name = name,
|
||||
.attributes = attributes
|
||||
}
|
||||
};
|
||||
|
||||
var self_closing = false;
|
||||
if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
|
||||
self.tossBytes(1);
|
||||
|
||||
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
|
||||
self.tossBytes(2);
|
||||
|
||||
self_closing = true;
|
||||
|
||||
} else {
|
||||
return error.UnfinishedStartTag;
|
||||
}
|
||||
|
||||
return .{
|
||||
.token = token,
|
||||
.self_closing = self_closing
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
const text_start = self.cursor;
|
||||
while (try self.peekByte() != '<') {
|
||||
self.tossByte();
|
||||
}
|
||||
var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
|
||||
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
|
||||
|
||||
var token: ?Token = null;
|
||||
if (text.len > 0) {
|
||||
token = Token{ .text = text };
|
||||
}
|
||||
return .{ .token = token };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *Lexer) !?Token {
|
||||
if (self.peeked_value) |value| {
|
||||
self.peeked_value = null;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (self.queued_end_tag) |name| {
|
||||
self.queued_end_tag = null;
|
||||
return Token{
|
||||
.end_tag = name
|
||||
};
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (self.buffers.text.items.len == 0) {
|
||||
self.readIntoTextBuffer() catch |e| switch (e) {
|
||||
error.EndOfStream => break,
|
||||
else => return e
|
||||
};
|
||||
}
|
||||
|
||||
const saved_cursor = self.cursor;
|
||||
const result = self.step() catch |e| switch(e) {
|
||||
error.EndOfTextBuffer => {
|
||||
self.cursor = saved_cursor;
|
||||
|
||||
const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len;
|
||||
if (unused_capacity == 0 and self.cursor > 0) {
|
||||
self.rebaseBuffer();
|
||||
} else {
|
||||
self.readIntoTextBuffer() catch |read_err| switch (read_err) {
|
||||
error.EndOfStream => break,
|
||||
else => return read_err
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
},
|
||||
else => return e
|
||||
};
|
||||
|
||||
if (result.token) |token| {
|
||||
if (token == .start_tag and result.self_closing) {
|
||||
self.queued_end_tag = token.start_tag.name;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void {
|
||||
const value = try self.next() orelse return error.MissingEndTag;
|
||||
if (!value.isEndTag(name)) return error.MissingEndTag;
|
||||
}
|
||||
|
||||
pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List {
|
||||
const value = try self.next() orelse return error.MissingStartTag;
|
||||
if (!value.isStartTag(name)) return error.MissingStartTag;
|
||||
return value.start_tag.attributes;
|
||||
}
|
||||
|
||||
pub fn nextExpectText(self: *Lexer) ![]const u8 {
|
||||
const value = try self.next() orelse return error.MissingTextTag;
|
||||
if (value != .text) return error.MissingTextTag;
|
||||
return value.text;
|
||||
}
|
||||
|
||||
pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void {
|
||||
var depth: usize = 0;
|
||||
while (true) {
|
||||
const value = try self.next() orelse return error.MissingEndTag;
|
||||
|
||||
if (depth == 0 and value == .end_tag) {
|
||||
if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) {
|
||||
return error.MismatchedEndTag;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (value == .start_tag) {
|
||||
depth += 1;
|
||||
} else if (value == .end_tag) {
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(self: *Lexer) !?Token {
|
||||
if (try self.next()) |value| {
|
||||
self.peeked_value = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn readIntoTextBuffer(self: *Lexer) !void {
|
||||
const gpa = self.buffers.scratch.child_allocator;
|
||||
const text = &self.buffers.text;
|
||||
try text.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
var writer = Io.Writer.fixed(text.allocatedSlice());
|
||||
writer.end = text.items.len;
|
||||
|
||||
_ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) {
|
||||
error.WriteFailed => unreachable,
|
||||
else => |ee| return ee
|
||||
};
|
||||
text.items.len = writer.end;
|
||||
}
|
||||
|
||||
fn rebaseBuffer(self: *Lexer) void {
|
||||
if (self.cursor == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = &self.buffers.text;
|
||||
@memmove(
|
||||
text.items[0..(text.items.len - self.cursor)],
|
||||
text.items[self.cursor..]
|
||||
);
|
||||
|
||||
text.items.len -= self.cursor;
|
||||
self.cursor = 0;
|
||||
}
|
||||
|
||||
fn isNameStartChar(c: u8) bool {
|
||||
return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
|
||||
}
|
||||
|
||||
fn isNameChar(c: u8) bool {
|
||||
return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9');
|
||||
}
|
||||
|
||||
fn hasBytes(self: *Lexer, n: usize) bool {
|
||||
const text = self.buffers.text.items;
|
||||
return self.cursor + n <= text.len;
|
||||
}
|
||||
|
||||
fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||
if (self.hasBytes(n)) {
|
||||
const text = self.buffers.text.items;
|
||||
return text[self.cursor..][0..n];
|
||||
}
|
||||
return error.EndOfTextBuffer;
|
||||
}
|
||||
|
||||
fn tossBytes(self: *Lexer, n: usize) void {
|
||||
assert(self.hasBytes(n));
|
||||
self.cursor += n;
|
||||
}
|
||||
|
||||
fn takeBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||
const result = try self.peekBytes(n);
|
||||
self.tossBytes(n);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn peekByte(self: *Lexer) !u8 {
|
||||
return (try self.peekBytes(1))[0];
|
||||
}
|
||||
|
||||
fn tossByte(self: *Lexer) void {
|
||||
self.tossBytes(1);
|
||||
}
|
||||
|
||||
fn takeByte(self: *Lexer) !u8 {
|
||||
return (try self.takeBytes(1))[0];
|
||||
}
|
||||
|
||||
fn parseName(self: *Lexer) ![]const u8 {
|
||||
const name_start = self.cursor;
|
||||
|
||||
if (isNameStartChar(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
|
||||
while (isNameChar(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
}
|
||||
}
|
||||
|
||||
return self.buffers.text.items[name_start..self.cursor];
|
||||
}
|
||||
|
||||
fn skipWhiteSpace(self: *Lexer) !void {
|
||||
while (std.ascii.isWhitespace(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
}
|
||||
}
|
||||
|
||||
fn parseAttributeValue(self: *Lexer) ![]const u8 {
|
||||
const quote = try self.takeByte();
|
||||
if (quote != '"' and quote != '\'') {
|
||||
return error.InvalidAttributeValue;
|
||||
}
|
||||
|
||||
const value_start: usize = self.cursor;
|
||||
var value_len: usize = 0;
|
||||
|
||||
while (true) {
|
||||
const c = try self.takeByte();
|
||||
if (c == '<' or c == '&') {
|
||||
return error.InvalidAttributeValue;
|
||||
}
|
||||
if (c == quote) {
|
||||
break;
|
||||
}
|
||||
value_len += 1;
|
||||
}
|
||||
|
||||
return self.buffers.text.items[value_start..][0..value_len];
|
||||
}
|
||||
|
||||
fn parseAttributes(self: *Lexer) !Attribute.List {
|
||||
const arena = self.buffers.scratch.allocator();
|
||||
var attributes: std.ArrayList(Attribute) = .empty;
|
||||
|
||||
while (true) {
|
||||
try self.skipWhiteSpace();
|
||||
const name = try self.parseName();
|
||||
if (name.len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
try self.skipWhiteSpace();
|
||||
if (try self.takeByte() != '=') {
|
||||
return error.MissingAttributeEquals;
|
||||
}
|
||||
try self.skipWhiteSpace();
|
||||
const value = try self.parseAttributeValue();
|
||||
|
||||
const list = Attribute.List{ .items = attributes.items };
|
||||
if (list.get(name) != null) {
|
||||
return error.DuplicateAttribute;
|
||||
}
|
||||
|
||||
try attributes.append(arena, Attribute{
|
||||
.name = name,
|
||||
.value = value
|
||||
});
|
||||
}
|
||||
|
||||
return Attribute.List{
|
||||
.items = attributes.items
|
||||
};
|
||||
}
|
||||
|
||||
test "self closing tag" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello />
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "tag" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello></hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "tag with prolog" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <?xml version="1.0" encoding="UTF-8"?>
|
||||
\\ <hello></hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "text content" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello> Hello World </hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "attributes" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello a='1' b='2'/>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
const token = try ctx.lexer.next();
|
||||
const attrs = token.?.start_tag.attributes;
|
||||
try std.testing.expectEqualStrings("1", attrs.get("a").?);
|
||||
try std.testing.expectEqualStrings("2", attrs.get("b").?);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: The API for this is easy to misuse.
|
||||
// Design a better API for using Reader
|
||||
// As a compromise `assert` was used to guard against some of the ways this can be misused
|
||||
pub const TagParser = struct {
|
||||
lexer: *Lexer,
|
||||
|
||||
begin_called: bool = false,
|
||||
finish_called: bool = false,
|
||||
|
||||
pub const Node = union(enum) {
|
||||
tag: Tag,
|
||||
text: []const u8,
|
||||
|
||||
pub fn isTag(self: Node, name: []const u8) bool {
|
||||
if (self == .tag) {
|
||||
return std.mem.eql(u8, self.tag.name, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(lexer: *Lexer) TagParser {
|
||||
return TagParser{
|
||||
.lexer = lexer,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
|
||||
assert(!self.begin_called);
|
||||
self.begin_called = true;
|
||||
|
||||
return try self.lexer.nextExpectStartTag(name);
|
||||
}
|
||||
|
||||
pub fn finish(self: *TagParser, name: []const u8) !void {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
self.finish_called = true;
|
||||
|
||||
try self.lexer.skipUntilMatchingEndTag(name);
|
||||
}
|
||||
|
||||
pub fn next(self: *TagParser) !?Node {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
|
||||
const value = try self.lexer.peek() orelse return error.MissingEndTag;
|
||||
if (value == .end_tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (value) {
|
||||
.text => |text| Node{ .text = text },
|
||||
.start_tag => |start_tag| Node{ .tag = start_tag },
|
||||
.end_tag => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn skip(self: *TagParser) !void {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
|
||||
const value = try self.lexer.next() orelse return error.MissingNode;
|
||||
if (value == .end_tag) {
|
||||
return error.UnexpectedEndTag;
|
||||
} else if (value == .start_tag) {
|
||||
// TODO: Make this configurable
|
||||
var name_buffer: [64]u8 = undefined;
|
||||
var name: std.ArrayList(u8) = .initBuffer(&name_buffer);
|
||||
try name.appendSliceBounded(value.start_tag.name);
|
||||
|
||||
try self.lexer.skipUntilMatchingEndTag(name.items);
|
||||
}
|
||||
}
|
||||
};
|
||||
138
libs/tracy/build.zig
Normal file
@ -0,0 +1,138 @@
|
||||
const std = @import("std");
|
||||
const digits2 = std.fmt.digits2;
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const tracy_enable = b.option(bool, "tracy_enable", "Enable profiling") orelse true;
|
||||
const tracy_on_demand = b.option(bool, "tracy_on_demand", "On-demand profiling") orelse false;
|
||||
const tracy_callstack: ?u8 = b.option(u8, "tracy_callstack", "Enforce callstack collection for tracy regions");
|
||||
const tracy_no_callstack = b.option(bool, "tracy_no_callstack", "Disable all callstack related functionality") orelse false;
|
||||
const tracy_no_callstack_inlines = b.option(bool, "tracy_no_callstack_inlines", "Disables the inline functions in callstacks") orelse false;
|
||||
const tracy_only_localhost = b.option(bool, "tracy_only_localhost", "Only listen on the localhost interface") orelse false;
|
||||
const tracy_no_broadcast = b.option(bool, "tracy_no_broadcast", "Disable client discovery by broadcast to local network") orelse false;
|
||||
const tracy_only_ipv4 = b.option(bool, "tracy_only_ipv4", "Tracy will only accept connections on IPv4 addresses (disable IPv6)") orelse false;
|
||||
const tracy_no_code_transfer = b.option(bool, "tracy_no_code_transfer", "Disable collection of source code") orelse false;
|
||||
const tracy_no_context_switch = b.option(bool, "tracy_no_context_switch", "Disable capture of context switches") orelse false;
|
||||
const tracy_no_exit = b.option(bool, "tracy_no_exit", "Client executable does not exit until all profile data is sent to server") orelse false;
|
||||
const tracy_no_sampling = b.option(bool, "tracy_no_sampling", "Disable call stack sampling") orelse false;
|
||||
const tracy_no_verify = b.option(bool, "tracy_no_verify", "Disable zone validation for C API") orelse false;
|
||||
const tracy_no_vsync_capture = b.option(bool, "tracy_no_vsync_capture", "Disable capture of hardware Vsync events") orelse false;
|
||||
const tracy_no_frame_image = b.option(bool, "tracy_no_frame_image", "Disable the frame image support and its thread") orelse false;
|
||||
// NOTE For some reason system tracing on zig projects crashes tracy, will need to investigate
|
||||
const tracy_no_system_tracing = b.option(bool, "tracy_no_system_tracing", "Disable systrace sampling") orelse true;
|
||||
const tracy_delayed_init = b.option(bool, "tracy_delayed_init", "Enable delayed initialization of the library (init on first call)") orelse false;
|
||||
const tracy_manual_lifetime = b.option(bool, "tracy_manual_lifetime", "Enable the manual lifetime management of the profile") orelse false;
|
||||
const tracy_fibers = b.option(bool, "tracy_fibers", "Enable fibers support") orelse false;
|
||||
const tracy_no_crash_handler = b.option(bool, "tracy_no_crash_handler", "Disable crash handling") orelse false;
|
||||
const tracy_timer_fallback = b.option(bool, "tracy_timer_fallback", "Use lower resolution timers") orelse false;
|
||||
const shared = b.option(bool, "shared", "Build the tracy client as a shared libary") orelse false;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(bool, "tracy_enable", tracy_enable);
|
||||
options.addOption(bool, "tracy_on_demand", tracy_on_demand);
|
||||
options.addOption(?u8, "tracy_callstack", tracy_callstack);
|
||||
options.addOption(bool, "tracy_no_callstack", tracy_no_callstack);
|
||||
options.addOption(bool, "tracy_no_callstack_inlines", tracy_no_callstack_inlines);
|
||||
options.addOption(bool, "tracy_only_localhost", tracy_only_localhost);
|
||||
options.addOption(bool, "tracy_no_broadcast", tracy_no_broadcast);
|
||||
options.addOption(bool, "tracy_only_ipv4", tracy_only_ipv4);
|
||||
options.addOption(bool, "tracy_no_code_transfer", tracy_no_code_transfer);
|
||||
options.addOption(bool, "tracy_no_context_switch", tracy_no_context_switch);
|
||||
options.addOption(bool, "tracy_no_exit", tracy_no_exit);
|
||||
options.addOption(bool, "tracy_no_sampling", tracy_no_sampling);
|
||||
options.addOption(bool, "tracy_no_verify", tracy_no_verify);
|
||||
options.addOption(bool, "tracy_no_vsync_capture", tracy_no_vsync_capture);
|
||||
options.addOption(bool, "tracy_no_frame_image", tracy_no_frame_image);
|
||||
options.addOption(bool, "tracy_no_system_tracing", tracy_no_system_tracing);
|
||||
options.addOption(bool, "tracy_delayed_init", tracy_delayed_init);
|
||||
options.addOption(bool, "tracy_manual_lifetime", tracy_manual_lifetime);
|
||||
options.addOption(bool, "tracy_fibers", tracy_fibers);
|
||||
options.addOption(bool, "tracy_no_crash_handler", tracy_no_crash_handler);
|
||||
options.addOption(bool, "tracy_timer_fallback", tracy_timer_fallback);
|
||||
options.addOption(bool, "shared", shared);
|
||||
|
||||
const dep_src = b.dependency("src", .{});
|
||||
|
||||
const tracy_module = b.addModule("tracy", .{
|
||||
.root_source_file = b.path("src/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
tracy_module.addImport("tracy-options", options.createModule());
|
||||
tracy_module.addIncludePath(dep_src.path("./public"));
|
||||
|
||||
const tracy_client = b.addLibrary(.{
|
||||
.linkage = if (shared) .dynamic else .static,
|
||||
.name = "tracy",
|
||||
.root_module = b.createModule(.{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
}),
|
||||
});
|
||||
|
||||
if (target.result.os.tag == .windows) {
|
||||
tracy_client.root_module.linkSystemLibrary("dbghelp", .{});
|
||||
tracy_client.root_module.linkSystemLibrary("ws2_32", .{});
|
||||
}
|
||||
if (target.result.abi != .msvc) {
|
||||
tracy_client.root_module.link_libcpp = true;
|
||||
} else {
|
||||
tracy_client.root_module.link_libc = true;
|
||||
}
|
||||
tracy_client.root_module.addCSourceFile(.{
|
||||
.file = dep_src.path("./public/TracyClient.cpp"),
|
||||
.flags = if (target.result.os.tag == .windows) &.{"-fms-extensions"} else &.{},
|
||||
});
|
||||
tracy_client.installHeadersDirectory(dep_src.path("public"), "", .{
|
||||
.include_extensions = &.{".h", ".hpp"},
|
||||
});
|
||||
if (tracy_enable)
|
||||
tracy_client.root_module.addCMacro("TRACY_ENABLE", "1");
|
||||
if (tracy_on_demand)
|
||||
tracy_client.root_module.addCMacro("TRACY_ON_DEMAND", "1");
|
||||
if (tracy_callstack) |depth| {
|
||||
tracy_client.root_module.addCMacro("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\"");
|
||||
}
|
||||
if (tracy_no_callstack)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK", "1");
|
||||
if (tracy_no_callstack_inlines)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_CALLSTACK_INLINES", "1");
|
||||
if (tracy_only_localhost)
|
||||
tracy_client.root_module.addCMacro("TRACY_ONLY_LOCALHOST", "1");
|
||||
if (tracy_no_broadcast)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_BROADCAST", "1");
|
||||
if (tracy_only_ipv4)
|
||||
tracy_client.root_module.addCMacro("TRACY_ONLY_IPV4", "1");
|
||||
if (tracy_no_code_transfer)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_CODE_TRANSFER", "1");
|
||||
if (tracy_no_context_switch)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_CONTEXT_SWITCH", "1");
|
||||
if (tracy_no_exit)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_EXIT", "1");
|
||||
if (tracy_no_sampling)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_SAMPLING", "1");
|
||||
if (tracy_no_verify)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_VERIFY", "1");
|
||||
if (tracy_no_vsync_capture)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_VSYNC_CAPTURE", "1");
|
||||
if (tracy_no_frame_image)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_FRAME_IMAGE", "1");
|
||||
if (tracy_no_system_tracing)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_SYSTEM_TRACING", "1");
|
||||
if (tracy_delayed_init)
|
||||
tracy_client.root_module.addCMacro("TRACY_DELAYED_INIT", "1");
|
||||
if (tracy_manual_lifetime)
|
||||
tracy_client.root_module.addCMacro("TRACY_MANUAL_LIFETIME", "1");
|
||||
if (tracy_fibers)
|
||||
tracy_client.root_module.addCMacro("TRACY_FIBERS", "1");
|
||||
if (tracy_no_crash_handler)
|
||||
tracy_client.root_module.addCMacro("TRACY_NO_CRASH_HANDLER", "1");
|
||||
if (tracy_timer_fallback)
|
||||
tracy_client.root_module.addCMacro("TRACY_TIMER_FALLBACK", "1");
|
||||
if (shared and target.result.os.tag == .windows)
|
||||
tracy_client.root_module.addCMacro("TRACY_EXPORTS", "1");
|
||||
b.installArtifact(tracy_client);
|
||||
}
|
||||
13
libs/tracy/build.zig.zon
Normal file
@ -0,0 +1,13 @@
|
||||
.{
|
||||
.name = .tracy,
|
||||
.version = "0.0.1",
|
||||
.dependencies = .{
|
||||
.src = .{
|
||||
.url = "https://github.com/wolfpld/tracy/archive/refs/tags/v0.13.1.tar.gz",
|
||||
.hash = "N-V-__8AAOncKwEm1F9c5LrT7HMNmRMYX8-fAoqpc6YyTu9X",
|
||||
},
|
||||
},
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.paths = .{""},
|
||||
.fingerprint = 0x255a89eeaaacb778,
|
||||
}
|
||||
376
libs/tracy/src/root.zig
Normal file
@ -0,0 +1,376 @@
|
||||
const std = @import("std");
|
||||
const digits2 = std.fmt.digits2;
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const options = @import("tracy-options");
|
||||
|
||||
const c = @cImport({
|
||||
if (options.tracy_enable) @cDefine("TRACY_ENABLE", {});
|
||||
if (options.tracy_on_demand) @cDefine("TRACY_ON_DEMAND", {});
|
||||
if (options.tracy_callstack) |depth| @cDefine("TRACY_CALLSTACK", "\"" ++ digits2(depth) ++ "\"");
|
||||
if (options.tracy_no_callstack) @cDefine("TRACY_NO_CALLSTACK", {});
|
||||
if (options.tracy_no_callstack_inlines) @cDefine("TRACY_NO_CALLSTACK_INLINES", {});
|
||||
if (options.tracy_only_localhost) @cDefine("TRACY_ONLY_LOCALHOST", {});
|
||||
if (options.tracy_no_broadcast) @cDefine("TRACY_NO_BROADCAST", {});
|
||||
if (options.tracy_only_ipv4) @cDefine("TRACY_ONLY_IPV4", {});
|
||||
if (options.tracy_no_code_transfer) @cDefine("TRACY_NO_CODE_TRANSFER", {});
|
||||
if (options.tracy_no_context_switch) @cDefine("TRACY_NO_CONTEXT_SWITCH", {});
|
||||
if (options.tracy_no_exit) @cDefine("TRACY_NO_EXIT", {});
|
||||
if (options.tracy_no_sampling) @cDefine("TRACY_NO_SAMPLING", {});
|
||||
if (options.tracy_no_verify) @cDefine("TRACY_NO_VERIFY", {});
|
||||
if (options.tracy_no_vsync_capture) @cDefine("TRACY_NO_VSYNC_CAPTURE", {});
|
||||
if (options.tracy_no_frame_image) @cDefine("TRACY_NO_FRAME_IMAGE", {});
|
||||
if (options.tracy_no_system_tracing) @cDefine("TRACY_NO_SYSTEM_TRACING", {});
|
||||
if (options.tracy_delayed_init) @cDefine("TRACY_DELAYED_INIT", {});
|
||||
if (options.tracy_manual_lifetime) @cDefine("TRACY_MANUAL_LIFETIME", {});
|
||||
if (options.tracy_fibers) @cDefine("TRACY_FIBERS", {});
|
||||
if (options.tracy_no_crash_handler) @cDefine("TRACY_NO_CRASH_HANDLER", {});
|
||||
if (options.tracy_timer_fallback) @cDefine("TRACY_TIMER_FALLBACK", {});
|
||||
if (options.shared and builtin.os.tag == .windows) @cDefine("TRACY_IMPORTS", {});
|
||||
|
||||
@cInclude("tracy/TracyC.h");
|
||||
});
|
||||
|
||||
pub inline fn setThreadName(comptime name: [:0]const u8) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_set_thread_name(name);
|
||||
}
|
||||
|
||||
pub inline fn startupProfiler() void {
|
||||
if (!options.tracy_enable) return;
|
||||
if (!options.tracy_manual_lifetime) return;
|
||||
c.___tracy_startup_profiler();
|
||||
}
|
||||
|
||||
pub inline fn shutdownProfiler() void {
|
||||
if (!options.tracy_enable) return;
|
||||
if (!options.tracy_manual_lifetime) return;
|
||||
c.___tracy_shutdown_profiler();
|
||||
}
|
||||
|
||||
pub inline fn profilerStarted() bool {
|
||||
if (!options.tracy_enable) return false;
|
||||
if (!options.tracy_manual_lifetime) return true;
|
||||
return c.___tracy_profiler_started() != 0;
|
||||
}
|
||||
|
||||
pub inline fn isConnected() bool {
|
||||
if (!options.tracy_enable) return false;
|
||||
return c.___tracy_connected() > 0;
|
||||
}
|
||||
|
||||
pub inline fn frameMark() void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_frame_mark(null);
|
||||
}
|
||||
|
||||
pub inline fn frameMarkNamed(comptime name: [:0]const u8) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_frame_mark(name);
|
||||
}
|
||||
|
||||
const DiscontinuousFrame = struct {
|
||||
name: [:0]const u8,
|
||||
|
||||
pub inline fn deinit(frame: *const DiscontinuousFrame) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_frame_mark_end(frame.name);
|
||||
}
|
||||
};
|
||||
|
||||
pub inline fn initDiscontinuousFrame(comptime name: [:0]const u8) DiscontinuousFrame {
|
||||
if (!options.tracy_enable) return .{ .name = name };
|
||||
c.___tracy_emit_frame_mark_start(name);
|
||||
return .{ .name = name };
|
||||
}
|
||||
|
||||
pub inline fn frameImage(image: *anyopaque, width: u16, height: u16, offset: u8, flip: bool) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_frame_mark_image(image, width, height, offset, @as(c_int, @intFromBool(flip)));
|
||||
}
|
||||
|
||||
pub const ZoneOptions = struct {
|
||||
active: bool = true,
|
||||
name: ?[]const u8 = null,
|
||||
color: ?u32 = null,
|
||||
};
|
||||
|
||||
const ZoneContext = if (options.tracy_enable) extern struct {
|
||||
ctx: c.___tracy_c_zone_context,
|
||||
|
||||
pub inline fn deinit(zone: *const ZoneContext) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_zone_end(zone.ctx);
|
||||
}
|
||||
|
||||
pub inline fn name(zone: *const ZoneContext, zone_name: []const u8) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_zone_name(zone.ctx, zone_name.ptr, zone_name.len);
|
||||
}
|
||||
|
||||
pub inline fn text(zone: *const ZoneContext, zone_text: []const u8) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_zone_text(zone.ctx, zone_text.ptr, zone_text.len);
|
||||
}
|
||||
|
||||
pub inline fn color(zone: *const ZoneContext, zone_color: u32) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_zone_color(zone.ctx, zone_color);
|
||||
}
|
||||
|
||||
pub inline fn value(zone: *const ZoneContext, zone_value: u64) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_zone_value(zone.ctx, zone_value);
|
||||
}
|
||||
} else struct {
|
||||
pub inline fn deinit(_: *const ZoneContext) void {}
|
||||
pub inline fn name(_: *const ZoneContext, _: []const u8) void {}
|
||||
pub inline fn text(_: *const ZoneContext, _: []const u8) void {}
|
||||
pub inline fn color(_: *const ZoneContext, _: u32) void {}
|
||||
pub inline fn value(_: *const ZoneContext, _: u64) void {}
|
||||
};
|
||||
|
||||
pub inline fn initZone(comptime src: std.builtin.SourceLocation, comptime opts: ZoneOptions) ZoneContext {
|
||||
if (!options.tracy_enable) return .{};
|
||||
const active: c_int = @intFromBool(opts.active);
|
||||
|
||||
const static = struct {
|
||||
var src_loc = c.___tracy_source_location_data{
|
||||
.name = if (opts.name) |name| name.ptr else null,
|
||||
.function = src.fn_name.ptr,
|
||||
.file = src.file,
|
||||
.line = 0,
|
||||
.color = opts.color orelse 0,
|
||||
};
|
||||
};
|
||||
|
||||
// src.line magically is not comptime https://github.com/ziglang/zig/pull/12016#issuecomment-1178092847
|
||||
static.src_loc.line = src.line;
|
||||
|
||||
if (!options.tracy_no_callstack) {
|
||||
if (options.tracy_callstack) |depth| {
|
||||
return .{
|
||||
.ctx = c.___tracy_emit_zone_begin_callstack(&static.src_loc, depth, active),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.ctx = c.___tracy_emit_zone_begin(&static.src_loc, active),
|
||||
};
|
||||
}
|
||||
|
||||
pub inline fn plot(comptime T: type, comptime name: [:0]const u8, value: T) void {
|
||||
if (!options.tracy_enable) return;
|
||||
|
||||
const type_info = @typeInfo(T);
|
||||
switch (type_info) {
|
||||
.int => |int_type| {
|
||||
if (int_type.bits > 64) @compileError("Too large int to plot");
|
||||
if (int_type.signedness == .unsigned and int_type.bits > 63) @compileError("Too large unsigned int to plot");
|
||||
c.___tracy_emit_plot_int(name, value);
|
||||
},
|
||||
.float => |float_type| {
|
||||
if (float_type.bits <= 32) {
|
||||
c.___tracy_emit_plot_float(name, value);
|
||||
} else if (float_type.bits <= 64) {
|
||||
c.___tracy_emit_plot(name, value);
|
||||
} else {
|
||||
@compileError("Too large float to plot");
|
||||
}
|
||||
},
|
||||
else => @compileError("Unsupported plot value type"),
|
||||
}
|
||||
}
|
||||
|
||||
pub const PlotType = enum(c_int) {
|
||||
Number,
|
||||
Memory,
|
||||
Percentage,
|
||||
Watt,
|
||||
};
|
||||
|
||||
pub const PlotConfig = struct {
|
||||
plot_type: PlotType,
|
||||
step: c_int,
|
||||
fill: c_int,
|
||||
color: u32,
|
||||
};
|
||||
|
||||
pub inline fn plotConfig(comptime name: [:0]const u8, comptime config: PlotConfig) void {
|
||||
if (!options.tracy_enable) return;
|
||||
c.___tracy_emit_plot_config(
|
||||
name,
|
||||
@intFromEnum(config.plot_type),
|
||||
config.step,
|
||||
config.fill,
|
||||
config.color,
|
||||
);
|
||||
}
|
||||
|
||||
pub inline fn message(comptime msg: [:0]const u8) void {
|
||||
if (!options.tracy_enable) return;
|
||||
const depth = options.tracy_callstack orelse 0;
|
||||
c.___tracy_emit_messageL(msg, depth);
|
||||
}
|
||||
|
||||
pub inline fn messageColor(comptime msg: [:0]const u8, color: u32) void {
|
||||
if (!options.tracy_enable) return;
|
||||
const depth = options.tracy_callstack orelse 0;
|
||||
c.___tracy_emit_messageLC(msg, color, depth);
|
||||
}
|
||||
|
||||
const tracy_message_buffer_size = if (options.tracy_enable) 4096 else 0;
|
||||
threadlocal var tracy_message_buffer: [tracy_message_buffer_size]u8 = undefined;
|
||||
|
||||
pub inline fn print(comptime fmt: []const u8, args: anytype) void {
|
||||
if (!options.tracy_enable) return;
|
||||
const depth = options.tracy_callstack orelse 0;
|
||||
|
||||
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
|
||||
stream.writer().print(fmt, args) catch {};
|
||||
|
||||
const written = stream.getWritten();
|
||||
c.___tracy_emit_message(written.ptr, written.len, depth);
|
||||
}
|
||||
|
||||
pub inline fn printColor(comptime fmt: []const u8, args: anytype, color: u32) void {
|
||||
if (!options.tracy_enable) return;
|
||||
const depth = options.tracy_callstack orelse 0;
|
||||
|
||||
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
|
||||
stream.writer().print(fmt, args) catch {};
|
||||
|
||||
const written = stream.getWritten();
|
||||
c.___tracy_emit_messageC(written.ptr, written.len, color, depth);
|
||||
}
|
||||
|
||||
pub inline fn printAppInfo(comptime fmt: []const u8, args: anytype) void {
|
||||
if (!options.tracy_enable) return;
|
||||
|
||||
var stream = std.io.fixedBufferStream(&tracy_message_buffer);
|
||||
stream.reset();
|
||||
stream.writer().print(fmt, args) catch {};
|
||||
|
||||
const written = stream.getWritten();
|
||||
c.___tracy_emit_message_appinfo(written.ptr, written.len);
|
||||
}
|
||||
|
||||
pub const TracingAllocator = struct {
|
||||
parent_allocator: std.mem.Allocator,
|
||||
pool_name: ?[:0]const u8,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn init(parent_allocator: std.mem.Allocator) Self {
|
||||
return .{
|
||||
.parent_allocator = parent_allocator,
|
||||
.pool_name = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initNamed(comptime pool_name: [:0]const u8, parent_allocator: std.mem.Allocator) Self {
|
||||
return .{
|
||||
.parent_allocator = parent_allocator,
|
||||
.pool_name = pool_name,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn allocator(self: *Self) std.mem.Allocator {
|
||||
return .{
|
||||
.ptr = self,
|
||||
.vtable = &.{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.remap = remap,
|
||||
.free = free,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn alloc(
|
||||
ctx: *anyopaque,
|
||||
len: usize,
|
||||
ptr_align: std.mem.Alignment,
|
||||
ret_addr: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
const result = self.parent_allocator.rawAlloc(len, ptr_align, ret_addr);
|
||||
if (!options.tracy_enable) return result;
|
||||
|
||||
if (self.pool_name) |name| {
|
||||
c.___tracy_emit_memory_alloc_named(result, len, 0, name.ptr);
|
||||
} else {
|
||||
c.___tracy_emit_memory_alloc(result, len, 0);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn resize(
|
||||
ctx: *anyopaque,
|
||||
buf: []u8,
|
||||
buf_align: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ret_addr: usize,
|
||||
) bool {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
const result = self.parent_allocator.rawResize(buf, buf_align, new_len, ret_addr);
|
||||
if (!result) return false;
|
||||
|
||||
if (!options.tracy_enable) return true;
|
||||
|
||||
if (self.pool_name) |name| {
|
||||
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
|
||||
c.___tracy_emit_memory_alloc_named(buf.ptr, new_len, 0, name.ptr);
|
||||
} else {
|
||||
c.___tracy_emit_memory_free(buf.ptr, 0);
|
||||
c.___tracy_emit_memory_alloc(buf.ptr, new_len, 0);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn remap(
|
||||
ctx: *anyopaque,
|
||||
buf: []u8,
|
||||
buf_align: std.mem.Alignment,
|
||||
new_len: usize,
|
||||
ret_addr: usize,
|
||||
) ?[*]u8 {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
const result = self.parent_allocator.rawRemap(buf, buf_align, new_len, ret_addr);
|
||||
const new_buf = result orelse return null;
|
||||
|
||||
if (!options.tracy_enable) return new_buf;
|
||||
|
||||
if (self.pool_name) |name| {
|
||||
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
|
||||
c.___tracy_emit_memory_alloc_named(new_buf, new_len, 0, name.ptr);
|
||||
} else {
|
||||
c.___tracy_emit_memory_free(buf.ptr, 0);
|
||||
c.___tracy_emit_memory_alloc(new_buf, new_len, 0);
|
||||
}
|
||||
|
||||
return new_buf;
|
||||
}
|
||||
|
||||
fn free(
|
||||
ctx: *anyopaque,
|
||||
buf: []u8,
|
||||
buf_align: std.mem.Alignment,
|
||||
ret_addr: usize,
|
||||
) void {
|
||||
const self: *Self = @ptrCast(@alignCast(ctx));
|
||||
|
||||
if (options.tracy_enable) {
|
||||
if (self.pool_name) |name| {
|
||||
c.___tracy_emit_memory_free_named(buf.ptr, 0, name.ptr);
|
||||
} else {
|
||||
c.___tracy_emit_memory_free(buf.ptr, 0);
|
||||
}
|
||||
}
|
||||
|
||||
self.parent_allocator.rawFree(buf, buf_align, ret_addr);
|
||||
}
|
||||
};
|
||||
57
scripts/png-to-icon.zig
Normal file
@ -0,0 +1,57 @@
|
||||
const std = @import("std");
|
||||
const STBImage = @import("stb_image");
|
||||
|
||||
// https://en.wikipedia.org/wiki/ICO_(file_format)#Icon_file_structure
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
const allocator = gpa.allocator();
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
const args = try std.process.argsAlloc(allocator);
|
||||
defer std.process.argsFree(allocator, args);
|
||||
|
||||
if (args.len != 3) {
|
||||
std.debug.print("Usage: ./png-to-icon <png-file> <output-ico>", .{});
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
const cwd = std.fs.cwd();
|
||||
const input_png_path = args[1];
|
||||
const output_ico_path = args[2];
|
||||
|
||||
const input_png_data = try cwd.readFileAlloc(allocator, input_png_path, 1024 * 1024 * 5);
|
||||
defer allocator.free(input_png_data);
|
||||
|
||||
const png_image = try STBImage.load(input_png_data);
|
||||
defer png_image.deinit();
|
||||
|
||||
std.debug.assert(png_image.width > 0 and png_image.width <= 256);
|
||||
std.debug.assert(png_image.height > 0 and png_image.height <= 256);
|
||||
|
||||
const output_ico_file = try std.fs.cwd().createFile(output_ico_path, .{ });
|
||||
defer output_ico_file.close();
|
||||
|
||||
var buffer: [4096 * 4]u8 = undefined;
|
||||
var writer = output_ico_file.writer(&buffer);
|
||||
|
||||
// ICONDIR structure
|
||||
try writer.interface.writeInt(u16, 0, .little); // Must always be zero
|
||||
try writer.interface.writeInt(u16, 1, .little); // Image type. 1 for .ICO
|
||||
try writer.interface.writeInt(u16, 1, .little); // Number of images
|
||||
|
||||
// ICONDIRENTRY structure
|
||||
try writer.interface.writeInt(u8, @truncate(png_image.width), .little); // Image width
|
||||
try writer.interface.writeInt(u8, @truncate(png_image.height), .little); // Image height
|
||||
try writer.interface.writeInt(u8, 0, .little); // Number of colors in color pallete. 0 means that color pallete is not used
|
||||
try writer.interface.writeInt(u8, 0, .little); // Must always be zero
|
||||
try writer.interface.writeInt(u16, 0, .little); // Color plane
|
||||
try writer.interface.writeInt(u16, 32, .little); // Bits per pixel
|
||||
try writer.interface.writeInt(u32, @intCast(input_png_data.len), .little); // Image size in bytes
|
||||
try writer.interface.writeInt(u32, 22, .little); // Offset to image data from the start
|
||||
|
||||
// PNG image data
|
||||
try writer.interface.writeAll(input_png_data);
|
||||
try writer.interface.flush();
|
||||
}
|
||||
|
||||
115
src/app.zig
Normal file
@ -0,0 +1,115 @@
|
||||
const std = @import("std");
|
||||
const CLI = @import("./cli.zig");
|
||||
const log = std.log.scoped(.app);
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const Math = @import("math");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const STBImage = @import("stb_image");
|
||||
const STBTrueType = @import("stb_truetype");
|
||||
const Tiled = @import("tiled");
|
||||
|
||||
const Platform = @import("./platform.zig");
|
||||
const Gfx = Platform.Gfx;
|
||||
const ImGUI = Platform.ImGUI;
|
||||
const Rect = Platform.Rect;
|
||||
|
||||
const App = @This();
|
||||
|
||||
tilesheet: Tilesheet,
|
||||
|
||||
const Tilesheet = struct {
|
||||
image: Gfx.ImageData,
|
||||
sprites: []Gfx.Sprite.Id,
|
||||
tile_size: Vec2,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn init(gpa: Allocator, image: Gfx.ImageData, width: u32, height: u32, tile_size: Vec2) !Tilesheet {
|
||||
const sprites = try gpa.alloc(Gfx.Sprite.Id, width * height);
|
||||
errdefer gpa.free(sprites);
|
||||
@memset(sprites, Gfx.getNilSprite());
|
||||
|
||||
return Tilesheet{
|
||||
.sprites = sprites,
|
||||
.image = image,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.tile_size = tile_size
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Tilesheet) void {
|
||||
for (self.sprites) |sprite| {
|
||||
Gfx.deinitSprite(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: Tilesheet, x: u32, y: u32) Gfx.Sprite.Id {
|
||||
if (x >= self.width or y >= self.height) {
|
||||
log.warn("Attempt to get tile which is out of bounds", .{});
|
||||
return Gfx.getNilSprite();
|
||||
}
|
||||
|
||||
const sprite_index = y * self.width + x;
|
||||
if (self.sprites[sprite_index] == Gfx.getNilSprite()) {
|
||||
const sprite = Gfx.initSprite(.{ .padding = 1 });
|
||||
Gfx.setSprite(sprite, .{
|
||||
.image = self.image,
|
||||
.rect = Rect.init(@floatFromInt(x), @floatFromInt(y), 1, 1).multiply(self.tile_size)
|
||||
});
|
||||
self.sprites[sprite_index] = sprite;
|
||||
}
|
||||
|
||||
return self.sprites[sprite_index];
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(self: *App, plt: Platform.Init) !?u8 {
|
||||
// const robot_font = Gfx.initFont();
|
||||
// Gfx.setFont(robot_font, plt.assets.readFile("roboto-font/Roboto-Regular.ttf"));
|
||||
|
||||
const tilesheet_png = try STBImage.load(plt.assets.readFile("tranquil_tunnels_transparent.png"));
|
||||
const tilesheet = try Tilesheet.init(
|
||||
plt.arena,
|
||||
Gfx.ImageData{
|
||||
.width = tilesheet_png.width,
|
||||
.height = tilesheet_png.height,
|
||||
.pixels = .{ .rgba8 = tilesheet_png.rgba8_pixels }
|
||||
},
|
||||
13,
|
||||
8,
|
||||
.init(64, 64)
|
||||
);
|
||||
|
||||
self.* = App{
|
||||
.tilesheet = tilesheet
|
||||
};
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn frame(self: *App, plt: Platform.Frame) !void {
|
||||
_ = self; // autofix
|
||||
const input = plt.input;
|
||||
_ = input; // autofix
|
||||
|
||||
const dt = plt.deltaTime();
|
||||
_ = dt; // autofix
|
||||
|
||||
Gfx.setClearColor(.rgb(40, 40, 40));
|
||||
Gfx.drawLine(.init(0, 200), .init(300, 200), .white, 5);
|
||||
// Gfx.drawSprite(frames[self.player_frame_index], self.player_pos, .init(64, 64), .white);
|
||||
// Gfx.drawSprite(self.tilesheet.get(11, 6), self.player_pos.add(.init(0, 200)), .init(64, 64), .white);
|
||||
// Gfx.drawText(self.roboto_font, .{
|
||||
// .pos = .init(300, 100),
|
||||
// .text = "Hello, World!",
|
||||
// .height = 64,
|
||||
// });
|
||||
}
|
||||
|
||||
pub fn deinit(self: *App, plt: Platform.Deinit) void {
|
||||
_ = plt; // autofix
|
||||
_ = self; // autofix
|
||||
}
|
||||
302
src/cli.zig
Normal file
@ -0,0 +1,302 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const log = std.log.scoped(.engine);
|
||||
|
||||
const CLI = @This();
|
||||
|
||||
gpa: std.mem.Allocator,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
options: std.ArrayList(Option),
|
||||
commands: std.ArrayList(Command),
|
||||
|
||||
stdout: Channel,
|
||||
stderr: Channel,
|
||||
|
||||
pub const Command = struct {
|
||||
name: []const u8,
|
||||
description: []const u8,
|
||||
|
||||
const Id = enum (u32) { _ };
|
||||
};
|
||||
|
||||
pub const Option = struct {
|
||||
long_name: []const u8,
|
||||
description: []const u8,
|
||||
kind: Kind,
|
||||
|
||||
pub const Kind = enum {
|
||||
toggle,
|
||||
single_argument
|
||||
};
|
||||
|
||||
pub const Id = enum (u32) { _ };
|
||||
|
||||
const Value = struct {
|
||||
argument: ?[]const u8 = null
|
||||
};
|
||||
};
|
||||
|
||||
const Channel = struct {
|
||||
file: Io.File,
|
||||
buffer: [4 * 4096]u8,
|
||||
writer: Io.File.Writer,
|
||||
|
||||
pub fn init(self: *Channel, io: Io, file: Io.File) void {
|
||||
self.* = Channel{
|
||||
.file = file,
|
||||
.buffer = undefined,
|
||||
.writer = file.writer(io, &self.buffer)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn write(self: *Channel, comptime fmt: []const u8, args: anytype) void {
|
||||
self.writer.interface.print(fmt, args) catch |e| {
|
||||
log.err("Failed to write to channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn flush(self: *Channel) void {
|
||||
self.writer.interface.flush() catch |e| {
|
||||
log.err("Failed to flush channel: {}", .{e});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(self: *CLI, io: Io, gpa: std.mem.Allocator) void {
|
||||
self.* = CLI{
|
||||
.gpa = gpa,
|
||||
.arena = .init(gpa),
|
||||
.options = .empty,
|
||||
.commands = .empty,
|
||||
.stdout = undefined,
|
||||
.stderr = undefined
|
||||
};
|
||||
self.stdout.init(io, Io.File.stdout());
|
||||
self.stderr.init(io, Io.File.stderr());
|
||||
}
|
||||
|
||||
pub fn deinit(self: *CLI) void {
|
||||
self.stderr.flush();
|
||||
self.stdout.flush();
|
||||
|
||||
self.arena.deinit();
|
||||
|
||||
self.options.deinit(self.gpa);
|
||||
self.commands.deinit(self.gpa);
|
||||
}
|
||||
|
||||
pub const AddOptionOptions = struct {
|
||||
long_name: []const u8,
|
||||
description: ?[]const u8 = null,
|
||||
kind: Option.Kind = .toggle,
|
||||
};
|
||||
|
||||
pub fn addOption(self: *CLI, opts: AddOptionOptions) !Option.Id {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
var owned_description: []const u8 = "";
|
||||
if (opts.description) |description| {
|
||||
owned_description = try arena.dupe(u8, description);
|
||||
}
|
||||
|
||||
const index = self.options.items.len;
|
||||
try self.options.append(self.gpa, .{
|
||||
.long_name = try arena.dupe(u8, opts.long_name),
|
||||
.description = owned_description,
|
||||
.kind = opts.kind
|
||||
});
|
||||
|
||||
return @enumFromInt(index);
|
||||
}
|
||||
|
||||
fn getOptionByLongName(self: *CLI, long_name: []const u8) ?Option.Id {
|
||||
for (0.., self.options.items) |i, option| {
|
||||
if (std.mem.eql(u8, long_name, option.long_name)) {
|
||||
return @enumFromInt(i);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub const AddCommandOptions = struct {
|
||||
name: []const u8,
|
||||
description: ?[]const u8 = null
|
||||
};
|
||||
|
||||
pub fn addCommand(self: *CLI, opts: AddCommandOptions) !Command.Id {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
var owned_description: []const u8 = "";
|
||||
if (opts.description) |description| {
|
||||
owned_description = try arena.dupe(u8, description);
|
||||
}
|
||||
|
||||
const index = self.commands.items.len;
|
||||
try self.commands.append(self.gpa, Command{
|
||||
.name = try arena.dupe(u8, opts.name),
|
||||
.description = owned_description,
|
||||
});
|
||||
|
||||
return @enumFromInt(index);
|
||||
}
|
||||
|
||||
pub fn showUsage(self: *CLI, program_name: []const u8) void {
|
||||
self.stderr.write("Usage: {s}", .{program_name});
|
||||
if (self.options.items.len > 0) {
|
||||
self.stderr.write(" [options]", .{});
|
||||
}
|
||||
if (self.commands.items.len > 0) {
|
||||
self.stderr.write(" <command>", .{});
|
||||
}
|
||||
self.stderr.write("\n", .{});
|
||||
|
||||
if (self.commands.items.len > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
self.stderr.write("Commands:\n", .{});
|
||||
for (self.commands.items) |command| {
|
||||
self.stderr.write(" {s} {s}\n", .{command.name, command.description});
|
||||
}
|
||||
}
|
||||
|
||||
if (self.options.items.len > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
self.stderr.write("Options:\n", .{});
|
||||
|
||||
for (0.., self.options.items) |i, option| {
|
||||
if (i > 0) {
|
||||
self.stderr.write("\n", .{});
|
||||
}
|
||||
|
||||
self.stderr.write(" --{s}", .{option.long_name});
|
||||
if (option.kind == .single_argument) {
|
||||
// TODO: Make this hint renameable
|
||||
self.stderr.write(" <value>", .{});
|
||||
}
|
||||
self.stderr.write("\n", .{});
|
||||
if (option.description.len > 0) {
|
||||
self.stderr.write(" {s}\n", .{option.description});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.stderr.flush();
|
||||
}
|
||||
|
||||
pub const ParseResult = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
command: ?Command.Id,
|
||||
options: []?Option.Value,
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator, options_len: usize) !ParseResult {
|
||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const options = try arena.allocator().alloc(?Option.Value, options_len);
|
||||
@memset(options, null);
|
||||
|
||||
return ParseResult{
|
||||
.arena = arena,
|
||||
.command = null,
|
||||
.options = options
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ParseResult) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn getOption(self: ParseResult, id: Option.Id) ?Option.Value {
|
||||
return self.options[@intFromEnum(id)];
|
||||
}
|
||||
|
||||
pub fn isSet(self: ParseResult, id: Option.Id) bool {
|
||||
return self.getOption(id) != null;
|
||||
}
|
||||
|
||||
// TODO: I don't like this function name
|
||||
pub fn getOptionArgument(self: ParseResult, id: Option.Id) ?[]const u8 {
|
||||
if (self.getOption(id)) |option| {
|
||||
return option.argument;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const Parser = struct {
|
||||
args: []const []const u8,
|
||||
cursor: u32,
|
||||
|
||||
pub fn peek(self: *Parser) ?[]const u8 {
|
||||
if (self.cursor < self.args.len) {
|
||||
return self.args[self.cursor];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn take(self: *Parser) ?[]const u8 {
|
||||
if (self.peek()) |value| {
|
||||
self.cursor += 1;
|
||||
return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fn printError(self: *CLI, comptime fmt: []const u8, args: anytype) void {
|
||||
self.stderr.write("ERROR: " ++ fmt ++ "\n", args);
|
||||
self.stderr.flush();
|
||||
}
|
||||
|
||||
pub fn parse(self: *CLI, gpa: std.mem.Allocator, args: []const []const u8) !ParseResult {
|
||||
var result = try ParseResult.init(gpa, self.options.items.len);
|
||||
errdefer result.deinit();
|
||||
|
||||
var parser = Parser{
|
||||
.args = args,
|
||||
.cursor = 1
|
||||
};
|
||||
|
||||
next_arg: while (parser.take()) |arg| {
|
||||
if (std.mem.startsWith(u8, arg, "--")) {
|
||||
const long_option_name = arg[2..];
|
||||
if (self.getOptionByLongName(long_option_name)) |option_id| {
|
||||
const option = self.options.items[@intFromEnum(option_id)];
|
||||
var result_value = Option.Value{};
|
||||
|
||||
if (option.kind == .single_argument) {
|
||||
const argument = parser.take() orelse {
|
||||
self.printError("Missing required argument for '{s}'", .{arg});
|
||||
return error.MissingArgument;
|
||||
};
|
||||
|
||||
result_value.argument = argument;
|
||||
}
|
||||
|
||||
result.options[@intFromEnum(option_id)] = result_value;
|
||||
continue :next_arg;
|
||||
}
|
||||
|
||||
self.printError("Unknown option '{s}'", .{arg});
|
||||
return error.UnknownOption;
|
||||
}
|
||||
|
||||
if (result.command == null) {
|
||||
for (0.., self.commands.items) |i, command| {
|
||||
if (std.mem.eql(u8, command.name, arg)) {
|
||||
result.command = @enumFromInt(i);
|
||||
continue :next_arg;
|
||||
}
|
||||
}
|
||||
|
||||
self.printError("Unknown command '{s}'", .{arg});
|
||||
return error.UnknownCommand;
|
||||
} else {
|
||||
self.printError("Unknown argument '{s}'", .{arg});
|
||||
return error.UnknownArgument;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
40
src/color.zig
Normal file
@ -0,0 +1,40 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Color = @This();
|
||||
|
||||
r: f32,
|
||||
g: f32,
|
||||
b: f32,
|
||||
a: f32,
|
||||
|
||||
pub const black = rgb(0, 0, 0);
|
||||
pub const white = rgb(255, 255, 255);
|
||||
pub const purple = rgb(255, 0, 255);
|
||||
|
||||
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color {
|
||||
assert(0 <= a and a <= 1);
|
||||
return .{
|
||||
.r = @as(f32, @floatFromInt(r)) / 255,
|
||||
.g = @as(f32, @floatFromInt(g)) / 255,
|
||||
.b = @as(f32, @floatFromInt(b)) / 255,
|
||||
.a = a,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn rgb(r: u8, g: u8, b: u8) Color {
|
||||
return rgba(r, g, b, 1);
|
||||
}
|
||||
|
||||
pub fn rgb_hex(text: []const u8) ?Color {
|
||||
if (text.len != 7) {
|
||||
return null;
|
||||
}
|
||||
if (text[0] != '#') {
|
||||
return null;
|
||||
}
|
||||
const r = std.fmt.parseInt(u8, text[1..3], 16) catch return null;
|
||||
const g = std.fmt.parseInt(u8, text[3..5], 16) catch return null;
|
||||
const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null;
|
||||
return rgb(r, g, b);
|
||||
}
|
||||
320
src/file_watcher.zig
Normal file
@ -0,0 +1,320 @@
|
||||
const std = @import("std");
|
||||
const INotify = @import("inotify.zig");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const FileWatcher = @This();
|
||||
|
||||
root_dir: []u8,
|
||||
inotify: INotify,
|
||||
files: std.ArrayList(File),
|
||||
watches: std.ArrayList(Watch),
|
||||
|
||||
const File = struct {
|
||||
path: []u8,
|
||||
debounce: Io.Duration,
|
||||
last_event_at: ?Io.Timestamp,
|
||||
|
||||
pub fn deinit(self: File, gpa: Allocator) void {
|
||||
gpa.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
const Watch = struct {
|
||||
wd: ?u32,
|
||||
path: []u8,
|
||||
|
||||
pub fn deinit(self: Watch, gpa: Allocator) void {
|
||||
gpa.free(self.path);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(gpa: Allocator, root_dir: []const u8) !FileWatcher {
|
||||
if (!std.fs.path.isAbsolute(root_dir)) {
|
||||
return error.PathIsNotAbsolute;
|
||||
}
|
||||
|
||||
const inotify_buf = try gpa.alloc(u8, INotify.max_event_size * 10);
|
||||
errdefer gpa.free(inotify_buf);
|
||||
|
||||
const root_dir_owned = try gpa.dupe(u8, root_dir);
|
||||
errdefer gpa.free(root_dir_owned);
|
||||
|
||||
return FileWatcher{
|
||||
.root_dir = root_dir_owned,
|
||||
.inotify = try .init(inotify_buf),
|
||||
.files = .empty,
|
||||
.watches = .empty
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *FileWatcher, gpa: Allocator) void {
|
||||
gpa.free(self.root_dir);
|
||||
|
||||
gpa.free(self.inotify.buf);
|
||||
self.inotify.deinit();
|
||||
|
||||
for (self.files.items) |file| {
|
||||
file.deinit(gpa);
|
||||
}
|
||||
self.files.deinit(gpa);
|
||||
|
||||
for (self.watches.items) |watch| {
|
||||
watch.deinit(gpa);
|
||||
}
|
||||
self.watches.deinit(gpa);
|
||||
}
|
||||
|
||||
fn findFileIndex(self: *FileWatcher, path: []const u8) ?usize {
|
||||
for (0.., self.files.items) |i, file| {
|
||||
if (std.mem.eql(u8, file.path, path)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findFile(self: *FileWatcher, path: []const u8) ?*File {
|
||||
if (self.findFileIndex(path)) |file_index| {
|
||||
return &self.files.items[file_index];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
fn findWatchByPath(self: *FileWatcher, path: []const u8) ?usize {
|
||||
for (0.., self.watches.items) |i, watch| {
|
||||
if (std.mem.eql(u8, watch.path, path)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn findWatchByDescriptor(self: *FileWatcher, wd: u32) ?usize {
|
||||
for (0.., self.watches.items) |i, watch| {
|
||||
if (watch.wd == wd) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn bufJoinPaths(path_buff: []u8, paths: []const []const u8) ![:0]u8 {
|
||||
return try std.fmt.bufPrintZ(path_buff, "{f}", .{
|
||||
std.fs.path.fmtJoin(paths)
|
||||
});
|
||||
}
|
||||
|
||||
fn attemptRegisteringWatch(self: *FileWatcher, watch: *Watch) !void {
|
||||
if (watch.wd != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ self.root_dir, watch.path });
|
||||
|
||||
watch.wd = self.inotify.add(path, INotify.Mask{
|
||||
.create = true,
|
||||
.modify = true,
|
||||
.delete_self = true,
|
||||
.move_self = true,
|
||||
.moved_to = true,
|
||||
.moved_from = true,
|
||||
}) catch |err| switch (err) {
|
||||
error.FileNotFound => null,
|
||||
else => return err
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toRelativePath(outer_path: []const u8, inner_path: []const u8) ?[]const u8 {
|
||||
if (inner_path.len == outer_path.len) {
|
||||
if (std.mem.eql(u8, inner_path, outer_path)) {
|
||||
return "";
|
||||
}
|
||||
} else if (inner_path.len > outer_path.len) {
|
||||
if (std.mem.startsWith(u8, inner_path, outer_path) and inner_path[outer_path.len] == std.fs.path.sep) {
|
||||
return inner_path[(outer_path.len+1)..];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn isInside(outer_path: []const u8, inner_path: []const u8) bool {
|
||||
return toRelativePath(outer_path, inner_path) != null;
|
||||
}
|
||||
|
||||
fn unregisterWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
||||
for (self.watches.items) |*watch| {
|
||||
const wd = watch.wd orelse continue;
|
||||
|
||||
if (isInside(dir, watch.path)) {
|
||||
try self.inotify.remove(wd);
|
||||
watch.wd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn registerWatchesRecursively(self: *FileWatcher, dir: []const u8) !void {
|
||||
for (self.watches.items) |*watch| {
|
||||
if (watch.wd == null and isInside(dir, watch.path)) {
|
||||
try self.attemptRegisteringWatch(watch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensureWatchExists(self: *FileWatcher, gpa: Allocator, path: []const u8) !usize {
|
||||
var watch_index: usize = undefined;
|
||||
if (self.findWatchByPath(path)) |found_watch_index| {
|
||||
watch_index = found_watch_index;
|
||||
} else {
|
||||
try self.watches.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
const path_owned = try gpa.dupe(u8, path);
|
||||
errdefer gpa.free(path_owned);
|
||||
|
||||
watch_index = self.watches.items.len;
|
||||
self.watches.appendAssumeCapacity(Watch{
|
||||
.wd = null,
|
||||
.path = path_owned
|
||||
});
|
||||
}
|
||||
|
||||
try self.attemptRegisteringWatch(&self.watches.items[watch_index]);
|
||||
|
||||
return watch_index;
|
||||
}
|
||||
|
||||
fn removeWatch(self: *FileWatcher, gpa: Allocator, index: usize) void {
|
||||
const watch = self.watches.swapRemove(index);
|
||||
watch.deinit(gpa);
|
||||
}
|
||||
|
||||
pub fn add(self: *FileWatcher, gpa: Allocator, path: []const u8, debounce: Io.Duration) !void {
|
||||
if (std.fs.path.isAbsolute(path)) {
|
||||
return error.PathIsAbsolute;
|
||||
}
|
||||
|
||||
if (self.findFileIndex(path) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dir_iter: ?[]const u8 = path;
|
||||
while (dir_iter) |current| {
|
||||
_ = try self.ensureWatchExists(gpa, current);
|
||||
|
||||
dir_iter = std.fs.path.dirname(current);
|
||||
}
|
||||
|
||||
_ = try self.ensureWatchExists(gpa, "");
|
||||
|
||||
try self.files.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
const path_owned = try gpa.dupe(u8, path);
|
||||
errdefer gpa.free(path_owned);
|
||||
|
||||
self.files.appendAssumeCapacity(File{
|
||||
.path = path_owned,
|
||||
.debounce = debounce,
|
||||
.last_event_at = null
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove(self: *FileWatcher, gpa: Allocator, path: []const u8) void {
|
||||
const index = self.findFileIndex(path);
|
||||
if (index == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const file = self.files.items[index];
|
||||
self.files.swapRemove(index);
|
||||
|
||||
file.deinit(gpa);
|
||||
}
|
||||
|
||||
fn nextEvents(self: *FileWatcher) !?*File {
|
||||
while (try self.inotify.next()) |e| {
|
||||
const watch_index = self.findWatchByDescriptor(e.wd) orelse continue;
|
||||
var watch = &self.watches.items[watch_index];
|
||||
|
||||
if (e.mask.ignored) {
|
||||
watch.wd = null;
|
||||
|
||||
} else if (e.mask.q_overflow) {
|
||||
return error.QueueOverflow;
|
||||
|
||||
} else if (e.mask.delete_self or e.mask.modify) {
|
||||
if (self.findFile(watch.path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.create) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
if (self.findWatchByPath(path)) |child_watch_index| {
|
||||
try self.attemptRegisteringWatch(&self.watches.items[child_watch_index]);
|
||||
}
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.moved_from) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
try self.unregisterWatchesRecursively(path);
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
|
||||
} else if (e.mask.moved_to) {
|
||||
assert(e.name != null);
|
||||
var path_buff: [Io.Dir.max_path_bytes+1]u8 = undefined;
|
||||
const path = try bufJoinPaths(&path_buff, &.{ watch.path, std.mem.span(e.name.?) });
|
||||
try self.registerWatchesRecursively(path);
|
||||
|
||||
if (self.findFile(path)) |file| {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn next(self: *FileWatcher, io: Io) !?[]const u8 {
|
||||
const now = Io.Clock.real.now(io);
|
||||
|
||||
var queue_overflow = false;
|
||||
while (true) {
|
||||
const file = (self.nextEvents() catch |err| switch (err) {
|
||||
error.QueueOverflow => {
|
||||
queue_overflow = true;
|
||||
break;
|
||||
},
|
||||
else => return err
|
||||
}) orelse break;
|
||||
file.last_event_at = now;
|
||||
}
|
||||
|
||||
if (queue_overflow) {
|
||||
for (self.files.items) |*file| {
|
||||
file.last_event_at = now;
|
||||
}
|
||||
}
|
||||
|
||||
for (self.files.items) |*file| {
|
||||
const last_event_at = file.last_event_at orelse continue;
|
||||
if (now.nanoseconds > last_event_at.addDuration(file.debounce).nanoseconds) {
|
||||
file.last_event_at = null;
|
||||
return file.path;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
1996
src/graphics.zig
Normal file
339
src/imgui.zig
Normal file
@ -0,0 +1,339 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.imgui);
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const build_options = @import("build_options");
|
||||
const Color = @import("./color.zig");
|
||||
const Math = @import("math");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const sapp = sokol.app;
|
||||
const simgui = sokol.imgui;
|
||||
|
||||
const ig = @import("cimgui");
|
||||
|
||||
const ImGUI = @This();
|
||||
|
||||
const State = struct {
|
||||
enabled: bool,
|
||||
frame: ?Allocator
|
||||
};
|
||||
|
||||
var g_state: State = undefined;
|
||||
|
||||
pub fn init(logger: simgui.Logger) void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = &g_state;
|
||||
|
||||
simgui.setup(.{
|
||||
.logger = logger
|
||||
});
|
||||
|
||||
if (@hasDecl(ig, "ImGuiConfigFlags_DockingEnable")) {
|
||||
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_DockingEnable;
|
||||
ig.igGetIO().*.ConfigFlags |= ig.ImGuiConfigFlags_ViewportsEnable;
|
||||
}
|
||||
|
||||
self.* = State{
|
||||
.enabled = true,
|
||||
.frame = null
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
|
||||
simgui.shutdown();
|
||||
}
|
||||
|
||||
pub fn event(e: sapp.Event) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return simgui.handleEvent(e);
|
||||
}
|
||||
|
||||
pub fn beginFrame(enabled: bool, frame: Allocator) void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
|
||||
const self = &g_state;
|
||||
|
||||
self.frame = frame;
|
||||
self.enabled = enabled;
|
||||
|
||||
if (!self.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
simgui.newFrame(.{
|
||||
.width = sapp.width(),
|
||||
.height = sapp.height(),
|
||||
.delta_time = sapp.frameDuration(),
|
||||
.dpi_scale = sapp.dpiScale(),
|
||||
});
|
||||
|
||||
const viewport = ig.igGetMainViewport();
|
||||
ig.igSetNextWindowPos(viewport[0].WorkPos, ig.ImGuiCond_Always);
|
||||
ig.igSetNextWindowSize(viewport[0].WorkSize, ig.ImGuiCond_Always);
|
||||
ig.igSetNextWindowViewport(viewport[0].ID);
|
||||
|
||||
const window_flags =
|
||||
ig.ImGuiWindowFlags_NoDocking |
|
||||
ig.ImGuiWindowFlags_NoTitleBar |
|
||||
ig.ImGuiWindowFlags_NoCollapse |
|
||||
ig.ImGuiWindowFlags_NoResize |
|
||||
ig.ImGuiWindowFlags_NoMove |
|
||||
ig.ImGuiWindowFlags_NoBackground |
|
||||
ig.ImGuiWindowFlags_NoBringToFrontOnFocus |
|
||||
ig.ImGuiWindowFlags_NoNavFocus;
|
||||
|
||||
ig.igPushStyleVar(ig.ImGuiStyleVar_WindowRounding, 0.0);
|
||||
ig.igPushStyleVar(ig.ImGuiStyleVar_WindowBorderSize, 0.0);
|
||||
ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_WindowPadding, .{ .x = 0, .y = 0 });
|
||||
_ = ig.igBegin("DockSpace", null, window_flags);
|
||||
ig.igPopStyleVarEx(3);
|
||||
|
||||
const dockspace_id = ig.igGetID("MyDockSpace");
|
||||
_ = ig.igDockSpaceEx(dockspace_id, ig.ImVec2{ .x = 0, .y = 0 }, ig.ImGuiDockNodeFlags_PassthruCentralNode, null);
|
||||
}
|
||||
|
||||
pub fn endFrame() void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// if (ig.igBegin("Game", null, ig.ImGuiWindowFlags_None)) {
|
||||
// // std.debug.print("{}\n", .{ig.igGetContentRegionAvail()});
|
||||
// }
|
||||
// ig.igEnd();
|
||||
|
||||
ig.igEnd();
|
||||
|
||||
// if (ig.igBeginMainMenuBar()) {
|
||||
// defer ig.igEndMainMenuBar();
|
||||
//
|
||||
// if (ig.igBeginMenu("foo")) {
|
||||
// defer ig.igEndMenu();
|
||||
//
|
||||
// if (ig.igMenuItem("bar")) {
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
simgui.render();
|
||||
}
|
||||
|
||||
pub const WindowOptions = struct {
|
||||
name: []const u8,
|
||||
pos: ?Vec2 = null,
|
||||
size: ?Vec2 = null,
|
||||
collapsed: ?bool = null,
|
||||
open: ?*bool = null,
|
||||
};
|
||||
|
||||
pub fn beginWindow(opts: WindowOptions) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts.open != null and opts.open.?.* == false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts.pos) |pos| {
|
||||
ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once);
|
||||
}
|
||||
if (opts.size) |size| {
|
||||
ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once);
|
||||
}
|
||||
if (opts.collapsed) |collapsed| {
|
||||
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
|
||||
}
|
||||
|
||||
ig.igSetNextWindowBgAlpha(1);
|
||||
|
||||
const frame = g_state.frame.?;
|
||||
const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: {
|
||||
log.err("dupeSentinel() failed",.{});
|
||||
break :blk "<unknown>";
|
||||
};
|
||||
|
||||
var open = ig.igBegin(namez, opts.open, ig.ImGuiWindowFlags_None);
|
||||
if (opts.open) |opts_open| {
|
||||
if (opts_open.* == false) {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
if (!open) {
|
||||
endWindow();
|
||||
}
|
||||
|
||||
return open;
|
||||
}
|
||||
|
||||
pub fn endWindow() void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEnd();
|
||||
}
|
||||
|
||||
fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 {
|
||||
const frame = g_state.frame.?;
|
||||
return std.fmt.allocPrintSentinel(frame, fmt, args, 0) catch blk: {
|
||||
log.err("allocPrintSentinel() failed", .{});
|
||||
break :blk "<unknown>";
|
||||
};
|
||||
}
|
||||
|
||||
pub fn text(comptime fmt: []const u8, args: anytype) void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igTextUnformatted(formatString(fmt, args));
|
||||
}
|
||||
|
||||
pub fn button(label: []const u8) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igButton(formatString("{s}", .{label}));
|
||||
}
|
||||
|
||||
pub const SliderOptions = struct {
|
||||
label: []const u8,
|
||||
value: *f32,
|
||||
min: f32,
|
||||
max: f32,
|
||||
};
|
||||
|
||||
pub fn slider(opts: SliderOptions) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igSliderFloat(
|
||||
formatString("{s}", .{opts.label}),
|
||||
opts.value,
|
||||
opts.min,
|
||||
opts.max
|
||||
);
|
||||
}
|
||||
|
||||
pub fn checkbox(label: []const u8, value: *bool) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igCheckbox(
|
||||
formatString("{s}", .{label}),
|
||||
value,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn separator() void {
|
||||
if (!build_options.has_imgui) {
|
||||
return;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igSeparator();
|
||||
}
|
||||
|
||||
const ColorEditOptions = struct {
|
||||
label: []const u8,
|
||||
value: *Color,
|
||||
has_alpha: bool = false
|
||||
};
|
||||
|
||||
pub fn colorEdit(opts: ColorEditOptions) bool {
|
||||
if (!build_options.has_imgui) {
|
||||
return false;
|
||||
}
|
||||
const self = &g_state;
|
||||
if (!self.enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const label = formatString("{s}", .{opts.label});
|
||||
var color: [4]f32 = .{
|
||||
opts.value.r,
|
||||
opts.value.g,
|
||||
opts.value.b,
|
||||
opts.value.a,
|
||||
};
|
||||
|
||||
var changed: bool = undefined;
|
||||
if (opts.has_alpha) {
|
||||
changed = ig.igColorEdit4(label, &color, ig.ImGuiColorEditFlags_None);
|
||||
} else {
|
||||
changed = ig.igColorEdit3(label, &color, ig.ImGuiColorEditFlags_None);
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
opts.value.r = color[0];
|
||||
opts.value.g = color[1];
|
||||
opts.value.b = color[2];
|
||||
opts.value.a = color[3];
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
fn toImVec2(vec2: Vec2) ig.ImVec2 {
|
||||
return ig.ImVec2{
|
||||
.x = vec2.x,
|
||||
.y = vec2.y,
|
||||
};
|
||||
}
|
||||
186
src/inotify.zig
Normal file
@ -0,0 +1,186 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
const linux = std.os.linux;
|
||||
|
||||
const INotify = @This();
|
||||
|
||||
fd: i32,
|
||||
|
||||
buf: []u8,
|
||||
buf_offset: usize,
|
||||
buf_used: usize,
|
||||
|
||||
pub const Mask = packed struct(u32) {
|
||||
access: bool = false,
|
||||
modify: bool = false,
|
||||
attrib: bool = false,
|
||||
close_write: bool = false,
|
||||
close_nowrite: bool = false,
|
||||
open: bool = false,
|
||||
moved_from: bool = false,
|
||||
moved_to: bool = false,
|
||||
create: bool = false,
|
||||
delete: bool = false,
|
||||
delete_self: bool = false,
|
||||
move_self: bool = false,
|
||||
|
||||
_unused1: u1 = 0,
|
||||
|
||||
unmount: bool = false,
|
||||
q_overflow: bool = false,
|
||||
ignored: bool = false,
|
||||
|
||||
_unused2: u8 = 0,
|
||||
|
||||
onlydir: bool = false,
|
||||
dont_follow: bool = false,
|
||||
excl_unlink: bool = false,
|
||||
|
||||
_unused3: u1 = 0,
|
||||
|
||||
mask_create: bool = false,
|
||||
mask_add: bool = false,
|
||||
isdir: bool = false,
|
||||
oneshot: bool = false,
|
||||
|
||||
pub fn format(self: Mask, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.writeAll(".{");
|
||||
var first = true;
|
||||
inline for (@typeInfo(Mask).@"struct".fields) |field| {
|
||||
if (@typeInfo(field.type) == .bool and @field(self, field.name)) {
|
||||
if (!first) {
|
||||
try writer.writeAll(",");
|
||||
}
|
||||
try writer.print(" .{s} = true", .{field.name});
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
|
||||
fn testMask(expected: u32, actual: Mask) !void {
|
||||
try std.testing.expectEqual(expected, @as(u32, @bitCast(actual)));
|
||||
}
|
||||
|
||||
test {
|
||||
try testMask(linux.IN.ACCESS, Mask{ .access = true });
|
||||
try testMask(linux.IN.MODIFY, Mask{ .modify = true });
|
||||
try testMask(linux.IN.ATTRIB, Mask{ .attrib = true });
|
||||
try testMask(linux.IN.CLOSE_WRITE, Mask{ .close_write = true });
|
||||
try testMask(linux.IN.CLOSE_NOWRITE, Mask{ .close_nowrite = true });
|
||||
try testMask(linux.IN.OPEN, Mask{ .open = true });
|
||||
try testMask(linux.IN.MOVED_FROM, Mask{ .moved_from = true });
|
||||
try testMask(linux.IN.MOVED_TO, Mask{ .moved_to = true });
|
||||
try testMask(linux.IN.CREATE, Mask{ .create = true });
|
||||
try testMask(linux.IN.DELETE, Mask{ .delete = true });
|
||||
try testMask(linux.IN.DELETE_SELF, Mask{ .delete_self = true });
|
||||
try testMask(linux.IN.MOVE_SELF, Mask{ .move_self = true });
|
||||
try testMask(linux.IN.UNMOUNT, Mask{ .unmount = true });
|
||||
try testMask(linux.IN.Q_OVERFLOW, Mask{ .q_overflow = true });
|
||||
try testMask(linux.IN.IGNORED, Mask{ .ignored = true });
|
||||
try testMask(linux.IN.ONLYDIR, Mask{ .onlydir = true });
|
||||
try testMask(linux.IN.DONT_FOLLOW, Mask{ .dont_follow = true });
|
||||
try testMask(linux.IN.EXCL_UNLINK, Mask{ .excl_unlink = true });
|
||||
try testMask(linux.IN.MASK_CREATE, Mask{ .mask_create = true });
|
||||
try testMask(linux.IN.MASK_ADD, Mask{ .mask_add = true });
|
||||
try testMask(linux.IN.ISDIR, Mask{ .isdir = true });
|
||||
try testMask(linux.IN.ONESHOT, Mask{ .oneshot = true });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Event = struct {
|
||||
wd: u32,
|
||||
mask: Mask,
|
||||
cookie: u32,
|
||||
name: ?[*:0]const u8,
|
||||
};
|
||||
|
||||
pub const max_event_size = @sizeOf(linux.inotify_event) + linux.NAME_MAX + 1;
|
||||
|
||||
pub fn init(buf: []u8) !INotify {
|
||||
const fd = linux.inotify_init1(linux.IN.NONBLOCK);
|
||||
if (linux.errno(fd) != .SUCCESS) {
|
||||
return error.InotifyInit;
|
||||
}
|
||||
return INotify{
|
||||
.fd = @intCast(fd),
|
||||
.buf = buf,
|
||||
.buf_offset = 0,
|
||||
.buf_used = 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn add(self: INotify, path: [*:0]const u8, mask: Mask) !u32 {
|
||||
const wd = linux.inotify_add_watch(
|
||||
self.fd,
|
||||
path,
|
||||
@bitCast(mask)
|
||||
);
|
||||
|
||||
switch (linux.errno(wd)) {
|
||||
.SUCCESS => return @intCast(wd),
|
||||
.ACCES => return error.AccessDenied,
|
||||
.BADF => unreachable, // The given file descriptor is not valid.
|
||||
.EXIST => return error.MarkAlreadyExists,
|
||||
.FAULT => unreachable, // path points outside of the process's accessible address space.
|
||||
.INVAL => unreachable, // The given event mask contains no valid events;
|
||||
.NAMETOOLONG => return error.NameTooLong,
|
||||
.NOENT => return error.FileNotFound,
|
||||
.NOMEM => return error.SystemResources,
|
||||
.NOSPC => return error.UserMarkQuotaExceeded,
|
||||
.NOTDIR => return error.NotDir,
|
||||
else => |err| return std.posix.unexpectedErrno(err),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(self: INotify, wd: u32) !void {
|
||||
const rc = linux.inotify_rm_watch(self.fd, @bitCast(wd));
|
||||
if (linux.errno(rc) != .SUCCESS) {
|
||||
return error.InotifyRemoveWatch;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *INotify) !?Event {
|
||||
if (self.buf_used <= self.buf.len) {
|
||||
const n = std.posix.read(self.fd, self.buf[self.buf_used..]) catch |err| switch(err) {
|
||||
error.WouldBlock => 0,
|
||||
else => return err
|
||||
};
|
||||
if (n > 0) {
|
||||
self.buf_used += n;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.buf_offset < self.buf_used) {
|
||||
const event = std.mem.bytesAsValue(
|
||||
linux.inotify_event,
|
||||
self.buf[self.buf_offset..][0..@sizeOf(linux.inotify_event)]
|
||||
);
|
||||
self.buf_offset += @sizeOf(linux.inotify_event) + event.len;
|
||||
assert(self.buf_offset <= self.buf_used);
|
||||
|
||||
if (self.buf_used == self.buf_offset) {
|
||||
self.buf_offset = 0;
|
||||
self.buf_used = 0;
|
||||
}
|
||||
|
||||
var name: ?[*:0]const u8 = null;
|
||||
if (event.len > 0) {
|
||||
name = std.mem.span(@as([*:0]const u8, @ptrCast(event)) + @sizeOf(linux.inotify_event));
|
||||
}
|
||||
|
||||
return Event{
|
||||
.wd = @bitCast(event.wd),
|
||||
.mask = @bitCast(event.mask),
|
||||
.cookie = event.cookie,
|
||||
.name = name
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: INotify) void {
|
||||
_ = linux.close(self.fd);
|
||||
}
|
||||
238
src/input.zig
Normal file
@ -0,0 +1,238 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const sokol = @import("sokol");
|
||||
|
||||
const Math = @import("math");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const Input = @This();
|
||||
|
||||
key_code_mapping: std.EnumMap(KeyCode, u21),
|
||||
|
||||
window_size: Vec2,
|
||||
focused: bool,
|
||||
keyboard: KeyStateType(KeyCode),
|
||||
mouse_buttons: KeyStateType(MouseButton),
|
||||
mouse_position: ?Vec2,
|
||||
mouse_scroll: Vec2,
|
||||
|
||||
pub fn init(window_size: Vec2) Input {
|
||||
return Input{
|
||||
.key_code_mapping = .{},
|
||||
.focused = false,
|
||||
.keyboard = .empty,
|
||||
.mouse_buttons = .empty,
|
||||
.mouse_position = null,
|
||||
.mouse_scroll = .init(0, 0),
|
||||
.window_size = window_size
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isKeyPressed(self: Input, key: KeyCode) bool {
|
||||
return self.keyboard.pressed.contains(key);
|
||||
}
|
||||
|
||||
pub fn isKeyReleased(self: Input, key: KeyCode) bool {
|
||||
return self.keyboard.released.contains(key);
|
||||
}
|
||||
|
||||
pub fn isKeyDown(self: Input, key: KeyCode) bool {
|
||||
return self.keyboard.down.contains(key);
|
||||
}
|
||||
|
||||
pub fn isMousePressed(self: Input, button: MouseButton) bool {
|
||||
return self.mouse_buttons.pressed.contains(button);
|
||||
}
|
||||
|
||||
pub fn iMouseReleased(self: Input, button: MouseButton) bool {
|
||||
return self.mouse_buttons.released.contains(button);
|
||||
}
|
||||
|
||||
pub fn isMouseDown(self: Input, button: MouseButton) bool {
|
||||
return self.mouse_buttons.down.contains(button);
|
||||
}
|
||||
|
||||
pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) {
|
||||
SPACE = 32,
|
||||
APOSTROPHE = 39,
|
||||
COMMA = 44,
|
||||
MINUS = 45,
|
||||
PERIOD = 46,
|
||||
SLASH = 47,
|
||||
_0 = 48,
|
||||
_1 = 49,
|
||||
_2 = 50,
|
||||
_3 = 51,
|
||||
_4 = 52,
|
||||
_5 = 53,
|
||||
_6 = 54,
|
||||
_7 = 55,
|
||||
_8 = 56,
|
||||
_9 = 57,
|
||||
SEMICOLON = 59,
|
||||
EQUAL = 61,
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
LEFT_BRACKET = 91,
|
||||
BACKSLASH = 92,
|
||||
RIGHT_BRACKET = 93,
|
||||
GRAVE_ACCENT = 96,
|
||||
WORLD_1 = 161,
|
||||
WORLD_2 = 162,
|
||||
ESCAPE = 256,
|
||||
ENTER = 257,
|
||||
TAB = 258,
|
||||
BACKSPACE = 259,
|
||||
INSERT = 260,
|
||||
DELETE = 261,
|
||||
RIGHT = 262,
|
||||
LEFT = 263,
|
||||
DOWN = 264,
|
||||
UP = 265,
|
||||
PAGE_UP = 266,
|
||||
PAGE_DOWN = 267,
|
||||
HOME = 268,
|
||||
END = 269,
|
||||
CAPS_LOCK = 280,
|
||||
SCROLL_LOCK = 281,
|
||||
NUM_LOCK = 282,
|
||||
PRINT_SCREEN = 283,
|
||||
PAUSE = 284,
|
||||
F1 = 290,
|
||||
F2 = 291,
|
||||
F3 = 292,
|
||||
F4 = 293,
|
||||
F5 = 294,
|
||||
F6 = 295,
|
||||
F7 = 296,
|
||||
F8 = 297,
|
||||
F9 = 298,
|
||||
F10 = 299,
|
||||
F11 = 300,
|
||||
F12 = 301,
|
||||
F13 = 302,
|
||||
F14 = 303,
|
||||
F15 = 304,
|
||||
F16 = 305,
|
||||
F17 = 306,
|
||||
F18 = 307,
|
||||
F19 = 308,
|
||||
F20 = 309,
|
||||
F21 = 310,
|
||||
F22 = 311,
|
||||
F23 = 312,
|
||||
F24 = 313,
|
||||
F25 = 314,
|
||||
KP_0 = 320,
|
||||
KP_1 = 321,
|
||||
KP_2 = 322,
|
||||
KP_3 = 323,
|
||||
KP_4 = 324,
|
||||
KP_5 = 325,
|
||||
KP_6 = 326,
|
||||
KP_7 = 327,
|
||||
KP_8 = 328,
|
||||
KP_9 = 329,
|
||||
KP_DECIMAL = 330,
|
||||
KP_DIVIDE = 331,
|
||||
KP_MULTIPLY = 332,
|
||||
KP_SUBTRACT = 333,
|
||||
KP_ADD = 334,
|
||||
KP_ENTER = 335,
|
||||
KP_EQUAL = 336,
|
||||
LEFT_SHIFT = 340,
|
||||
LEFT_CONTROL = 341,
|
||||
LEFT_ALT = 342,
|
||||
LEFT_SUPER = 343,
|
||||
RIGHT_SHIFT = 344,
|
||||
RIGHT_CONTROL = 345,
|
||||
RIGHT_ALT = 346,
|
||||
RIGHT_SUPER = 347,
|
||||
MENU = 348,
|
||||
};
|
||||
|
||||
pub const MouseButton = enum {
|
||||
left,
|
||||
right,
|
||||
middle,
|
||||
|
||||
pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton {
|
||||
return switch(mouse_button) {
|
||||
.LEFT => .left,
|
||||
.RIGHT => .right,
|
||||
.MIDDLE => .middle,
|
||||
else => null
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Event = union(enum) {
|
||||
mouse_pressed: MouseButton,
|
||||
mouse_released: MouseButton,
|
||||
mouse_move: Vec2,
|
||||
mouse_enter: Vec2,
|
||||
mouse_leave,
|
||||
mouse_scroll: Vec2,
|
||||
key_pressed: KeyCode,
|
||||
key_released: KeyCode,
|
||||
char: u21,
|
||||
window_resize: Vec2,
|
||||
focused,
|
||||
unfocused,
|
||||
};
|
||||
|
||||
fn KeyStateType(T: type) type {
|
||||
return struct {
|
||||
pressed: std.EnumSet(T),
|
||||
released: std.EnumSet(T),
|
||||
down: std.EnumSet(T),
|
||||
|
||||
pub const empty = @This(){
|
||||
.pressed = .empty,
|
||||
.released = .empty,
|
||||
.down = .empty,
|
||||
};
|
||||
|
||||
pub fn press(self: *@This(), key: T) void {
|
||||
self.pressed.insert(key);
|
||||
self.down.insert(key);
|
||||
}
|
||||
|
||||
pub fn release(self: *@This(), key: T) void {
|
||||
self.released.insert(key);
|
||||
self.down.remove(key);
|
||||
}
|
||||
|
||||
pub fn releaseAll(self: *@This()) void {
|
||||
var iter = self.down.iterator();
|
||||
while (iter.next()) |key| {
|
||||
self.release(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
25
src/main.zig
Normal file
@ -0,0 +1,25 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Platform = @import("platform.zig");
|
||||
const App = @import("app.zig");
|
||||
|
||||
pub const std_options: std.Options = .{
|
||||
.log_scope_levels = &.{
|
||||
.{ .scope = .graphics, .level = .info }
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main(init: std.process.Init) void {
|
||||
Platform.run(App, .{
|
||||
.init = init,
|
||||
|
||||
.window_title = "Gaem",
|
||||
.width = 800,
|
||||
.height = 600,
|
||||
});
|
||||
}
|
||||
|
||||
test {
|
||||
std.testing.refAllDecls(@This());
|
||||
_ = @import("./slot_map.zig");
|
||||
}
|
||||
447
src/math.zig
Normal file
@ -0,0 +1,447 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
pub const bytes_per_kib = 1024;
|
||||
pub const bytes_per_mib = bytes_per_kib * 1024;
|
||||
pub const bytes_per_gib = bytes_per_mib * 1024;
|
||||
|
||||
pub const bytes_per_kb = 1000;
|
||||
pub const bytes_per_mb = bytes_per_kb * 1000;
|
||||
pub const bytes_per_gb = bytes_per_mb * 1000;
|
||||
|
||||
pub const Range = struct {
|
||||
from: f32,
|
||||
to: f32,
|
||||
|
||||
pub const zero = init(0, 0);
|
||||
|
||||
pub fn init(from: f32, to: f32) Range {
|
||||
return Range{
|
||||
.from = from,
|
||||
.to = to
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSize(self: Range) f32 {
|
||||
return @abs(self.from - self.to);
|
||||
}
|
||||
|
||||
pub fn random(self: Range, rng: std.Random) f32 {
|
||||
return self.from + rng.float(f32) * (self.to - self.from);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Vec2 = extern struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
|
||||
pub const zero = init(0, 0);
|
||||
|
||||
pub fn init(x: f32, y: f32) Vec2 {
|
||||
return Vec2{
|
||||
.x = x,
|
||||
.y = y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initFromInt(T: type, x: T, y: T) Vec2 {
|
||||
return .init(@floatFromInt(x), @floatFromInt(y));
|
||||
}
|
||||
|
||||
pub fn initAngle(angle: f32) Vec2 {
|
||||
return Vec2{
|
||||
.x = @cos(angle),
|
||||
.y = @sin(angle),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn rotateLeft90(self: Vec2) Vec2 {
|
||||
return Vec2.init(self.y, -self.x);
|
||||
}
|
||||
|
||||
pub fn rotateRight90(self: Vec2) Vec2 {
|
||||
return Vec2.init(-self.y, self.x);
|
||||
}
|
||||
|
||||
pub fn rotate(self: Vec2, angle: f32) Vec2 {
|
||||
return init(
|
||||
@cos(angle) * self.x - @sin(angle) * self.y,
|
||||
@sin(angle) * self.x + @cos(angle) * self.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn rotateAround(self: Vec2, angle: f32, origin: Vec2) Vec2 {
|
||||
return self.sub(origin).rotate(angle).add(origin);
|
||||
}
|
||||
|
||||
pub fn getAngle(self: Vec2) f32 {
|
||||
return std.math.atan2(self.y, self.x);
|
||||
}
|
||||
|
||||
pub fn flip(self: Vec2) Vec2 {
|
||||
return Vec2.init(-self.x, -self.y);
|
||||
}
|
||||
|
||||
pub fn add(self: Vec2, other: Vec2) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x + other.x,
|
||||
self.y + other.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn sub(self: Vec2, other: Vec2) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x - other.x,
|
||||
self.y - other.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn multiplyScalar(self: Vec2, value: f32) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x * value,
|
||||
self.y * value,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn multiply(self: Vec2, other: Vec2) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x * other.x,
|
||||
self.y * other.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn divide(self: Vec2, other: Vec2) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x / other.x,
|
||||
self.y / other.y,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn divideScalar(self: Vec2, value: f32) Vec2 {
|
||||
return Vec2.init(
|
||||
self.x / value,
|
||||
self.y / value,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn lengthSqr(self: Vec2) f32 {
|
||||
return self.x*self.x + self.y*self.y;
|
||||
}
|
||||
|
||||
pub fn length(self: Vec2) f32 {
|
||||
return @sqrt(self.lengthSqr());
|
||||
}
|
||||
|
||||
pub fn distance(self: Vec2, other: Vec2) f32 {
|
||||
return self.sub(other).length();
|
||||
}
|
||||
|
||||
pub fn distanceSqr(self: Vec2, other: Vec2) f32 {
|
||||
return self.sub(other).lengthSqr();
|
||||
}
|
||||
|
||||
pub fn limitLength(self: Vec2, max_length: f32) Vec2 {
|
||||
const self_length = self.length();
|
||||
if (self_length > max_length) {
|
||||
if (self_length == 0) {
|
||||
return Vec2.init(0, 0);
|
||||
}
|
||||
return self.divideScalar(self_length / max_length);
|
||||
} else {
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalized(self: Vec2) Vec2 {
|
||||
const self_length = self.length();
|
||||
if (self_length == 0) {
|
||||
return Vec2.init(0, 0);
|
||||
}
|
||||
return self.divideScalar(self_length);
|
||||
}
|
||||
|
||||
pub fn initScalar(value: f32) Vec2 {
|
||||
return Vec2.init(value, value);
|
||||
}
|
||||
|
||||
pub fn eql(self: Vec2, other: Vec2) bool {
|
||||
return self.x == other.x and self.y == other.y;
|
||||
}
|
||||
|
||||
pub fn format(self: Vec2, writer: *std.io.Writer) std.io.Writer.Error!void {
|
||||
try writer.print("Vec2{{ {d}, {d} }}", .{ self.x, self.y });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Vec3 = extern struct {
|
||||
x: f32, y: f32, z: f32,
|
||||
|
||||
pub const zero = init(0, 0, 0);
|
||||
|
||||
pub fn init(x: f32, y: f32, z: f32) Vec3 {
|
||||
return Vec3{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.z = z,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initScalar(value: f32) Vec3 {
|
||||
return Vec3.init(value, value, value);
|
||||
}
|
||||
|
||||
pub fn asArray(self: *Vec3) []f32 {
|
||||
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
|
||||
return ptr[0..3];
|
||||
}
|
||||
|
||||
pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 {
|
||||
return Vec3.init(
|
||||
std.math.lerp(a.x, b.x, t),
|
||||
std.math.lerp(a.y, b.y, t),
|
||||
std.math.lerp(a.z, b.z, t),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn clamp(self: Vec3, min_value: f32, max_value: f32) Vec3 {
|
||||
return Vec3.init(
|
||||
std.math.clamp(self.x, min_value, max_value),
|
||||
std.math.clamp(self.y, min_value, max_value),
|
||||
std.math.clamp(self.z, min_value, max_value),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Vec4 = extern struct {
|
||||
x: f32, y: f32, z: f32, w: f32,
|
||||
|
||||
pub const zero = init(0, 0, 0, 0);
|
||||
|
||||
pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 {
|
||||
return Vec4{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.z = z,
|
||||
.w = w
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initVec3XYZ(vec3: Vec3, w: f32) Vec4 {
|
||||
return init(vec3.x, vec3.y, vec3.z, w);
|
||||
}
|
||||
|
||||
pub fn initScalar(value: f32) Vec4 {
|
||||
return Vec4.init(value, value, value, value);
|
||||
}
|
||||
|
||||
pub fn multiplyMat4(left: Vec4, right: Mat4) Vec4 {
|
||||
var result: Vec4 = undefined;
|
||||
|
||||
// TODO: SIMD
|
||||
|
||||
result.x = left.x * right.columns[0][0];
|
||||
result.y = left.x * right.columns[0][1];
|
||||
result.z = left.x * right.columns[0][2];
|
||||
result.w = left.x * right.columns[0][3];
|
||||
|
||||
result.x += left.y * right.columns[1][0];
|
||||
result.y += left.y * right.columns[1][1];
|
||||
result.z += left.y * right.columns[1][2];
|
||||
result.w += left.y * right.columns[1][3];
|
||||
|
||||
result.x += left.z * right.columns[2][0];
|
||||
result.y += left.z * right.columns[2][1];
|
||||
result.z += left.z * right.columns[2][2];
|
||||
result.w += left.z * right.columns[2][3];
|
||||
|
||||
result.x += left.w * right.columns[3][0];
|
||||
result.y += left.w * right.columns[3][1];
|
||||
result.z += left.w * right.columns[3][2];
|
||||
result.w += left.w * right.columns[3][3];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn multiply(left: Vec4, right: Vec4) Vec4 {
|
||||
return init(
|
||||
left.x * right.x,
|
||||
left.y * right.y,
|
||||
left.z * right.z,
|
||||
left.w * right.w
|
||||
);
|
||||
}
|
||||
|
||||
pub fn asArray(self: *Vec4) []f32 {
|
||||
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self))));
|
||||
return ptr[0..4];
|
||||
}
|
||||
|
||||
pub fn initArray(array: []const f32) Vec4 {
|
||||
return Vec4.init(array[0], array[1], array[2], array[3]);
|
||||
}
|
||||
|
||||
pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 {
|
||||
return Vec4.init(
|
||||
std.math.lerp(a.x, b.x, t),
|
||||
std.math.lerp(a.y, b.y, t),
|
||||
std.math.lerp(a.z, b.z, t),
|
||||
std.math.lerp(a.w, b.w, t),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn clamp(self: Vec4, min_value: f32, max_value: f32) Vec4 {
|
||||
return Vec4.init(
|
||||
std.math.clamp(self.x, min_value, max_value),
|
||||
std.math.clamp(self.y, min_value, max_value),
|
||||
std.math.clamp(self.z, min_value, max_value),
|
||||
std.math.clamp(self.w, min_value, max_value),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn toVec3XYZ(self: Vec4) Vec3 {
|
||||
return Vec3.init(self.x, self.y, self.z);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Mat4 = extern struct {
|
||||
columns: [4][4]f32,
|
||||
|
||||
pub fn initZero() Mat4 {
|
||||
var self: Mat4 = undefined;
|
||||
@memset(self.asArray(), 0);
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn initIdentity() Mat4 {
|
||||
return Mat4.initDiagonal(1);
|
||||
}
|
||||
|
||||
pub fn initDiagonal(value: f32) Mat4 {
|
||||
var self = Mat4.initZero();
|
||||
self.columns[0][0] = value;
|
||||
self.columns[1][1] = value;
|
||||
self.columns[2][2] = value;
|
||||
self.columns[3][3] = value;
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn multiply(left: Mat4, right: Mat4) Mat4 {
|
||||
var self: Mat4 = undefined;
|
||||
|
||||
inline for (.{ 0, 1, 2, 3 }) |i| {
|
||||
var column = Vec4.initArray(&right.columns[i]).multiplyMat4(left);
|
||||
@memcpy(&self.columns[i], column.asArray());
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn initScale(scale: Vec3) Mat4 {
|
||||
var self = Mat4.initIdentity();
|
||||
self.columns[0][0] = scale.x;
|
||||
self.columns[1][1] = scale.y;
|
||||
self.columns[2][2] = scale.z;
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn initTranslate(offset: Vec3) Mat4 {
|
||||
var self = Mat4.initIdentity();
|
||||
self.columns[3][0] = offset.x;
|
||||
self.columns[3][1] = offset.y;
|
||||
self.columns[3][2] = offset.z;
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn asArray(self: *Mat4) []f32 {
|
||||
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns))));
|
||||
return ptr[0..16];
|
||||
}
|
||||
};
|
||||
|
||||
pub const Rect = struct {
|
||||
pos: Vec2,
|
||||
size: Vec2,
|
||||
|
||||
pub const zero = Rect{
|
||||
.pos = Vec2.zero,
|
||||
.size = Vec2.zero
|
||||
};
|
||||
|
||||
pub const unit = Rect{
|
||||
.pos = Vec2.zero,
|
||||
.size = Vec2.init(1, 1)
|
||||
};
|
||||
|
||||
pub fn init(x: f32, y: f32, width: f32, height: f32) Rect {
|
||||
return Rect{
|
||||
.pos = Vec2.init(x, y),
|
||||
.size = Vec2.init(width, height)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clip(self: Rect, other: Rect) Rect {
|
||||
const left_edge = @max(self.left(), other.left());
|
||||
const right_edge = @min(self.right(), other.right());
|
||||
const top_edge = @max(self.top(), other.top());
|
||||
const bottom_edge = @min(self.bottom(), other.bottom());
|
||||
return Rect.init(
|
||||
left_edge,
|
||||
top_edge,
|
||||
right_edge - left_edge,
|
||||
bottom_edge - top_edge
|
||||
);
|
||||
}
|
||||
|
||||
pub fn left(self: Rect) f32 {
|
||||
return self.pos.x;
|
||||
}
|
||||
|
||||
pub fn right(self: Rect) f32 {
|
||||
return self.pos.x + self.size.x;
|
||||
}
|
||||
|
||||
pub fn top(self: Rect) f32 {
|
||||
return self.pos.y;
|
||||
}
|
||||
|
||||
pub fn bottom(self: Rect) f32 {
|
||||
return self.pos.y + self.size.y;
|
||||
}
|
||||
|
||||
pub fn center(self: Rect) Vec2 {
|
||||
return self.pos.add(self.size.multiplyScalar(0.5));
|
||||
}
|
||||
|
||||
pub fn multiply(self: Rect, xy: Vec2) Rect {
|
||||
return Rect{
|
||||
.pos = self.pos.multiply(xy),
|
||||
.size = self.size.multiply(xy),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn divide(self: Rect, xy: Vec2) Rect {
|
||||
return Rect{
|
||||
.pos = self.pos.divide(xy),
|
||||
.size = self.size.divide(xy),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isInside(self: Rect, pos: Vec2) bool {
|
||||
const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x;
|
||||
const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;
|
||||
return x_overlap and y_overlap;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Line = struct {
|
||||
p0: Vec2,
|
||||
p1: Vec2
|
||||
};
|
||||
|
||||
pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool {
|
||||
const rect = Rect{
|
||||
.pos = rect_pos,
|
||||
.size = rect_size
|
||||
};
|
||||
return rect.isInside(pos);
|
||||
}
|
||||
674
src/platform.zig
Normal file
@ -0,0 +1,674 @@
|
||||
const builtin = @import("builtin");
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const log = std.log.scoped(.platform);
|
||||
const assert = std.debug.assert;
|
||||
const build_options = @import("build_options");
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const sapp = sokol.app;
|
||||
const sg = sokol.gfx;
|
||||
const sglue = sokol.glue;
|
||||
const sgl = sokol.gl;
|
||||
|
||||
pub const Math = @import("math");
|
||||
pub const Vec2 = Math.Vec2;
|
||||
pub const Vec4 = Math.Vec4;
|
||||
pub const Mat4 = Math.Mat4;
|
||||
pub const Rect = Math.Rect;
|
||||
|
||||
const tracy = @import("tracy");
|
||||
|
||||
pub const Color = @import("./color.zig");
|
||||
const shd = @import("shader");
|
||||
|
||||
pub const ImGUI = @import("./imgui.zig");
|
||||
pub const Gfx = @import("./graphics.zig");
|
||||
pub const Input = @import("./input.zig");
|
||||
|
||||
const EmbeddedAssets = @import("embedded_assets");
|
||||
|
||||
fn PlatformType(App: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
gpa: Allocator,
|
||||
arena: *std.heap.ArenaAllocator,
|
||||
io: Io,
|
||||
cli_args: []const [:0]const u8,
|
||||
|
||||
app: App,
|
||||
|
||||
started_at: Io.Timestamp,
|
||||
last_frame_at: Io.Timestamp,
|
||||
|
||||
assets: Assets,
|
||||
input: Input,
|
||||
events_buffer: [256]Input.Event,
|
||||
events_overflow: bool,
|
||||
events: std.ArrayList(Input.Event),
|
||||
last_key_press_is_repeat: bool,
|
||||
last_key_pressed: ?Input.KeyCode,
|
||||
|
||||
frame_arena: std.heap.ArenaAllocator,
|
||||
|
||||
show_imgui: bool,
|
||||
|
||||
fn init(self: *Self) !void {
|
||||
const window_size = Vec2.init(sapp.widthf(), sapp.heightf());
|
||||
|
||||
self.started_at = Io.Timestamp.now(self.io, .awake);
|
||||
self.last_frame_at = self.started_at;
|
||||
self.input = Input.init(window_size);
|
||||
|
||||
try Gfx.init(self.gpa, .{
|
||||
.func = sokolLogCallback
|
||||
});
|
||||
ImGUI.init(.{
|
||||
.func = sokolLogCallback
|
||||
});
|
||||
|
||||
if (@hasDecl(App, "init")) {
|
||||
const plt = Init{
|
||||
.gpa = self.gpa,
|
||||
.arena = self.arena.allocator(),
|
||||
.io = self.io,
|
||||
.assets = &self.assets
|
||||
};
|
||||
if (try self.app.init(plt)) |exit_code| {
|
||||
std.process.exit(exit_code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sokolInit(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const self: *Self = @ptrCast(@alignCast(user_data));
|
||||
|
||||
self.init() catch |err| {
|
||||
log.err("init() error: {}", .{err});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpErrorReturnTrace(trace);
|
||||
}
|
||||
sapp.quit();
|
||||
};
|
||||
}
|
||||
|
||||
fn frame(self: *Self) !void {
|
||||
tracy.frameMark();
|
||||
|
||||
_ = self.frame_arena.reset(.free_all); // TODO: make arena reset mode configurable
|
||||
|
||||
const now = Io.Timestamp.now(self.io, .awake);
|
||||
const t = self.started_at.durationTo(now);
|
||||
const dt = self.last_frame_at.durationTo(now);
|
||||
self.last_frame_at = now;
|
||||
|
||||
if (self.input.keyboard.pressed.contains(.F4)) {
|
||||
self.show_imgui = !self.show_imgui;
|
||||
}
|
||||
|
||||
{
|
||||
Gfx.beginFrame();
|
||||
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
|
||||
|
||||
if (@hasDecl(App, "frame")) {
|
||||
const plt = Frame{
|
||||
.t = t.nanoseconds,
|
||||
.dt = dt.nanoseconds,
|
||||
.gpa = self.gpa,
|
||||
.arena = self.arena.allocator(),
|
||||
.io = self.io,
|
||||
.input = &self.input,
|
||||
.input_events = self.events.items,
|
||||
.frame = self.frame_arena.allocator(),
|
||||
.assets = &self.assets
|
||||
};
|
||||
try self.app.frame(plt);
|
||||
}
|
||||
|
||||
Gfx.flush(.{});
|
||||
Gfx.showDebug();
|
||||
|
||||
ImGUI.endFrame();
|
||||
Gfx.endFrame();
|
||||
}
|
||||
|
||||
self.events_overflow = false;
|
||||
self.events.clearRetainingCapacity();
|
||||
self.input.keyboard.pressed = .empty;
|
||||
self.input.keyboard.released = .empty;
|
||||
self.input.mouse_buttons.pressed = .empty;
|
||||
self.input.mouse_buttons.released = .empty;
|
||||
}
|
||||
|
||||
fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const self: *Self = @ptrCast(@alignCast(user_data));
|
||||
self.frame() catch |err| {
|
||||
log.err("frame() error: {}", .{err});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpErrorReturnTrace(trace);
|
||||
}
|
||||
sapp.quit();
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: I don't like the names of these 'event' related functions.
|
||||
// All of them "append an event", but they do different things.
|
||||
fn applyEventToState(self: *Self, e: Input.Event) void {
|
||||
const input = &self.input;
|
||||
|
||||
switch (e) {
|
||||
.key_pressed => |key| {
|
||||
assert(!input.isKeyDown(key));
|
||||
input.keyboard.press(key);
|
||||
self.last_key_pressed = key;
|
||||
},
|
||||
.key_released => |key| {
|
||||
assert(input.isKeyDown(key));
|
||||
input.keyboard.release(key);
|
||||
},
|
||||
.mouse_pressed => |button| {
|
||||
assert(input.mouse_position != null);
|
||||
assert(!input.isMouseDown(button));
|
||||
input.mouse_buttons.press(button);
|
||||
},
|
||||
.mouse_released => |button| {
|
||||
assert(input.mouse_position != null);
|
||||
assert(input.isMouseDown(button));
|
||||
input.mouse_buttons.release(button);
|
||||
},
|
||||
.mouse_enter => |pos| {
|
||||
assert(input.mouse_position == null);
|
||||
input.mouse_position = pos;
|
||||
},
|
||||
.mouse_move => |pos| {
|
||||
assert(input.mouse_position != null);
|
||||
input.mouse_position = pos;
|
||||
},
|
||||
.mouse_leave => {
|
||||
assert(input.mouse_position != null);
|
||||
input.mouse_position = null;
|
||||
},
|
||||
.char => |char| {
|
||||
// A 'key_pressed' event always occurs before a 'char' event.
|
||||
// This allows us to differentiate between keycodes and scan codes.
|
||||
// And how to map key codes to character codes.
|
||||
assert(self.last_key_pressed != null);
|
||||
input.key_code_mapping.put(self.last_key_pressed.?, char);
|
||||
self.last_key_pressed = null;
|
||||
},
|
||||
.mouse_scroll => |offset| {
|
||||
assert(input.mouse_position != null);
|
||||
input.mouse_scroll = input.mouse_scroll.add(offset);
|
||||
},
|
||||
.window_resize => |window_size| {
|
||||
self.input.window_size = window_size;
|
||||
},
|
||||
.focused => {
|
||||
assert(!input.focused);
|
||||
input.focused = true;
|
||||
},
|
||||
.unfocused => {
|
||||
assert(input.focused);
|
||||
assert(input.mouse_position == null);
|
||||
assert(input.keyboard.down.eql(.empty));
|
||||
assert(input.mouse_buttons.down.eql(.empty));
|
||||
input.focused = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn appendEvent(self: *Self, e: Input.Event) void {
|
||||
if (self.events.capacity == self.events.items.len) {
|
||||
if (!self.events_overflow) {
|
||||
log.warn("too many events, limit: {}", .{self.events.capacity});
|
||||
}
|
||||
self.events_overflow = true;
|
||||
return;
|
||||
}
|
||||
|
||||
self.events.appendAssumeCapacity(e);
|
||||
self.applyEventToState(e);
|
||||
}
|
||||
|
||||
fn pushEvent(self: *Self, e: Input.Event) void {
|
||||
const input = &self.input;
|
||||
|
||||
if (input.focused) {
|
||||
if (e == .focused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e == .key_pressed and input.isKeyDown(e.key_pressed)) {
|
||||
return;
|
||||
}
|
||||
if (e == .key_released and !input.isKeyDown(e.key_released)) {
|
||||
return;
|
||||
}
|
||||
if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e == .unfocused) {
|
||||
var key_iter = input.keyboard.down.iterator();
|
||||
while (key_iter.next()) |key| {
|
||||
self.appendEvent(.{ .key_released = key });
|
||||
}
|
||||
var mouse_buttons_iter = input.mouse_buttons.down.iterator();
|
||||
while (mouse_buttons_iter.next()) |button| {
|
||||
self.appendEvent(.{ .mouse_released = button });
|
||||
}
|
||||
if (input.mouse_position != null) {
|
||||
self.appendEvent(.{ .mouse_leave = {} });
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
if (e == .unfocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e != .focused) {
|
||||
self.appendEvent(.focused);
|
||||
|
||||
if (e == .mouse_move) {
|
||||
self.appendEvent(.{ .mouse_enter = e.mouse_move });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.appendEvent(e);
|
||||
}
|
||||
|
||||
fn event(self: *Self, e: sapp.Event) !bool {
|
||||
if (self.show_imgui) {
|
||||
if (ImGUI.event(e)) {
|
||||
self.pushEvent(.{
|
||||
.unfocused = {}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const input = &self.input;
|
||||
|
||||
switch (e.type) {
|
||||
.MOUSE_DOWN => {
|
||||
if (input.mouse_position == null) {
|
||||
self.pushEvent(.{
|
||||
.mouse_enter = .init(e.mouse_x, e.mouse_y)
|
||||
});
|
||||
}
|
||||
|
||||
self.pushEvent(.{
|
||||
.mouse_pressed = Input.MouseButton.fromSokol(e.mouse_button) orelse return false,
|
||||
});
|
||||
},
|
||||
.MOUSE_UP => {
|
||||
if (input.mouse_position == null) {
|
||||
self.pushEvent(.{
|
||||
.mouse_enter = .init(e.mouse_x, e.mouse_y)
|
||||
});
|
||||
}
|
||||
|
||||
self.pushEvent(.{
|
||||
.mouse_released = Input.MouseButton.fromSokol(e.mouse_button) orelse return false,
|
||||
});
|
||||
},
|
||||
.MOUSE_MOVE => {
|
||||
if (input.mouse_position == null) {
|
||||
self.pushEvent(.{
|
||||
.mouse_enter = .init(e.mouse_x, e.mouse_y)
|
||||
});
|
||||
}
|
||||
|
||||
self.pushEvent(.{
|
||||
.mouse_move = .init(e.mouse_x, e.mouse_y)
|
||||
});
|
||||
},
|
||||
.MOUSE_ENTER => {
|
||||
self.pushEvent(.{
|
||||
.mouse_enter = .init(e.mouse_x, e.mouse_y)
|
||||
});
|
||||
},
|
||||
.MOUSE_LEAVE => {
|
||||
self.pushEvent(.{
|
||||
.mouse_leave = {}
|
||||
});
|
||||
},
|
||||
.MOUSE_SCROLL => {
|
||||
self.pushEvent(.{
|
||||
.mouse_scroll = .init(e.scroll_x, e.scroll_y)
|
||||
});
|
||||
},
|
||||
.KEY_DOWN => {
|
||||
self.last_key_press_is_repeat = e.key_repeat;
|
||||
if (!e.key_repeat) {
|
||||
self.pushEvent(.{
|
||||
.key_pressed = @enumFromInt(@intFromEnum(e.key_code))
|
||||
});
|
||||
}
|
||||
},
|
||||
.KEY_UP => {
|
||||
self.pushEvent(.{
|
||||
.key_released = @enumFromInt(@intFromEnum(e.key_code)),
|
||||
});
|
||||
},
|
||||
.CHAR => {
|
||||
if (!self.last_key_press_is_repeat) {
|
||||
self.pushEvent(.{
|
||||
.char = @intCast(e.char_code)
|
||||
});
|
||||
}
|
||||
},
|
||||
.FOCUSED => {
|
||||
self.pushEvent(.{
|
||||
.focused = {}
|
||||
});
|
||||
},
|
||||
.UNFOCUSED => {
|
||||
self.pushEvent(.{
|
||||
.unfocused = {}
|
||||
});
|
||||
},
|
||||
else => {}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
fn sokolEvent(ev: [*c]const sapp.Event, user_data: ?*anyopaque) callconv(.c) void {
|
||||
const self: *Self = @ptrCast(@alignCast(user_data));
|
||||
|
||||
const consumed_event = self.event(ev.*) catch |err| blk: {
|
||||
log.err("event() error: {}", .{err});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpErrorReturnTrace(trace);
|
||||
}
|
||||
sapp.quit();
|
||||
break :blk false;
|
||||
};
|
||||
|
||||
if (consumed_event) {
|
||||
sapp.consumeEvent();
|
||||
}
|
||||
}
|
||||
|
||||
fn sokolCleanup(user_data: ?*anyopaque) callconv(.c) void {
|
||||
const self: *Self = @ptrCast(@alignCast(user_data));
|
||||
if (@hasDecl(App, "deinit")) {
|
||||
const plt = Deinit{
|
||||
.gpa = self.gpa,
|
||||
.io = self.io
|
||||
};
|
||||
self.app.deinit(plt);
|
||||
}
|
||||
self.frame_arena.deinit();
|
||||
self.assets.deinit();
|
||||
|
||||
ImGUI.deinit();
|
||||
Gfx.deinit();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const RunOptions = struct {
|
||||
init: std.process.Init,
|
||||
|
||||
window_title: [*c]const u8 = null,
|
||||
width: i32 = 800,
|
||||
height: i32 = 600,
|
||||
};
|
||||
|
||||
pub const Assets = struct {
|
||||
arena: std.heap.ArenaAllocator,
|
||||
gpa: Allocator,
|
||||
io: Io,
|
||||
|
||||
dir: ?Io.Dir,
|
||||
files: std.array_hash_map.String(File),
|
||||
|
||||
fn init(gpa: Allocator, io: Io, dir: ?Io.Dir) Assets {
|
||||
return Assets{
|
||||
.gpa = gpa,
|
||||
.io = io,
|
||||
.arena = .init(gpa),
|
||||
.dir = dir,
|
||||
.files = .empty
|
||||
};
|
||||
}
|
||||
|
||||
fn readFileFromAssetDir(self: *Assets, filename: []const u8) !?[:0]const u8 {
|
||||
const dir = self.dir orelse return null;
|
||||
|
||||
const arena = self.arena.allocator();
|
||||
const contents = try dir.readFileAllocOptions(
|
||||
self.io,
|
||||
filename,
|
||||
arena,
|
||||
.unlimited,
|
||||
.of(u8),
|
||||
0
|
||||
);
|
||||
|
||||
const filename_owned = try arena.dupe(u8, filename);
|
||||
try self.files.put(self.gpa, filename_owned, File{
|
||||
.contents = contents
|
||||
});
|
||||
|
||||
return contents;
|
||||
}
|
||||
|
||||
pub fn readFile(self: *Assets, filename: []const u8) [:0]const u8 {
|
||||
if (self.files.get(filename)) |file| {
|
||||
return file.contents;
|
||||
} else {
|
||||
if (self.readFileFromAssetDir(filename)) |maybe_contents| {
|
||||
if (maybe_contents) |contents| {
|
||||
log.warn("Read file which isn't defined in assets: '{s}'", .{filename});
|
||||
return contents;
|
||||
} else {
|
||||
log.err("Attempt to read file '{s}', which doesn't exist", .{filename});
|
||||
return "";
|
||||
}
|
||||
} else |err| {
|
||||
log.err("Failed to read file from filesystem: {}", .{err});
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insertStaticFile(self: *Assets, filename: []const u8, contents: [:0]const u8) !void {
|
||||
try self.files.put(self.gpa, filename, File{
|
||||
.contents = contents
|
||||
});
|
||||
}
|
||||
|
||||
fn deinit(self: *Assets) void {
|
||||
self.arena.deinit();
|
||||
self.files.deinit(self.gpa);
|
||||
}
|
||||
|
||||
const File = struct {
|
||||
contents: [:0]const u8
|
||||
};
|
||||
};
|
||||
|
||||
pub const Init = struct {
|
||||
gpa: Allocator,
|
||||
arena: Allocator,
|
||||
io: Io,
|
||||
assets: *Assets
|
||||
};
|
||||
|
||||
pub const Deinit = struct {
|
||||
gpa: Allocator,
|
||||
io: Io,
|
||||
};
|
||||
|
||||
pub const Nanoseconds = i96;
|
||||
|
||||
pub const Frame = struct {
|
||||
t: Nanoseconds,
|
||||
dt: Nanoseconds,
|
||||
|
||||
gpa: Allocator,
|
||||
arena: Allocator,
|
||||
frame: Allocator,
|
||||
io: Io,
|
||||
assets: *Assets,
|
||||
input: *Input,
|
||||
input_events: []Input.Event,
|
||||
|
||||
fn nanosecondsToSeconds(nanoseconds: i96) f32 {
|
||||
return @floatCast(@as(f64, @floatFromInt(nanoseconds)) / std.time.ns_per_s);
|
||||
}
|
||||
|
||||
pub fn deltaTime(self: Frame) f32 {
|
||||
return nanosecondsToSeconds(self.dt);
|
||||
}
|
||||
|
||||
pub fn time(self: Frame) f32 {
|
||||
return nanosecondsToSeconds(self.t);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn run(App: type, opts: RunOptions) void {
|
||||
const arena = opts.init.arena.allocator();
|
||||
const gpa = opts.init.gpa;
|
||||
const io = opts.init.io;
|
||||
|
||||
var assets_dir: ?Io.Dir = null;
|
||||
if (build_options.assets_dir) |assets_dir_path| {
|
||||
const cwd = Io.Dir.cwd();
|
||||
assets_dir = cwd.openDir(io, assets_dir_path, .{}) catch |e| blk: {
|
||||
log.err("Failed to open assets directory: {}", .{e});
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
|
||||
const PlatformApp = PlatformType(App);
|
||||
const plt = arena.create(PlatformApp) catch @panic("OOM");
|
||||
plt.* = PlatformApp{
|
||||
.gpa = gpa,
|
||||
.arena = opts.init.arena,
|
||||
.io = io,
|
||||
.cli_args = opts.init.minimal.args.toSlice(arena) catch @panic("OOM"),
|
||||
.app = undefined,
|
||||
.input = undefined,
|
||||
.events_buffer = undefined,
|
||||
.events_overflow = false,
|
||||
.events = .initBuffer(&plt.events_buffer),
|
||||
.started_at = undefined,
|
||||
.last_frame_at = undefined,
|
||||
.last_key_press_is_repeat = false,
|
||||
.last_key_pressed = null,
|
||||
.frame_arena = .init(gpa),
|
||||
.show_imgui = builtin.mode == .Debug,
|
||||
.assets = .init(gpa, io, assets_dir)
|
||||
};
|
||||
|
||||
inline for (EmbeddedAssets.files) |file| {
|
||||
plt.assets.insertStaticFile(file.name, file.contents) catch @panic("OOM");
|
||||
}
|
||||
|
||||
var icon: sapp.IconDesc = .{};
|
||||
icon.sokol_default = true;
|
||||
// TODO: Don't hard code icon path, allow changing through options
|
||||
// var icon_data = try STBImage.load(@embedFile("../assets/icon.png"));
|
||||
// defer icon_data.deinit();
|
||||
// TODO:
|
||||
// icon.images[0] = .{
|
||||
// .width = @intCast(icon_data.width),
|
||||
// .height = @intCast(icon_data.height),
|
||||
// .pixels = .{
|
||||
// .ptr = icon_data.rgba8_pixels,
|
||||
// .size = icon_data.width * icon_data.height * 4
|
||||
// }
|
||||
// };
|
||||
|
||||
tracy.setThreadName("Main");
|
||||
|
||||
const is_wasm = builtin.cpu.arch.isWasm();
|
||||
if (builtin.os.tag == .linux and !is_wasm) {
|
||||
var sa: std.posix.Sigaction = .{
|
||||
.handler = .{ .handler = posixSignalHandler },
|
||||
.mask = std.posix.sigemptyset(),
|
||||
.flags = std.posix.SA.RESTART,
|
||||
};
|
||||
std.posix.sigaction(std.posix.SIG.INT, &sa, null);
|
||||
}
|
||||
|
||||
sapp.run(.{
|
||||
.init_userdata_cb = PlatformApp.sokolInit,
|
||||
.frame_userdata_cb = PlatformApp.sokolFrame,
|
||||
.cleanup_userdata_cb = PlatformApp.sokolCleanup,
|
||||
.event_userdata_cb = PlatformApp.sokolEvent,
|
||||
.user_data = plt,
|
||||
.window_title = opts.window_title,
|
||||
.width = opts.width,
|
||||
.height = opts.height,
|
||||
.icon = icon,
|
||||
.logger = .{ .func = sokolLogCallback },
|
||||
});
|
||||
}
|
||||
|
||||
fn posixSignalHandler(sig: std.posix.SIG) callconv(.c) void {
|
||||
_ = sig;
|
||||
sapp.requestQuit();
|
||||
}
|
||||
|
||||
fn toSokolColor(color: Color) sokol.gfx.Color {
|
||||
// TODO: Assert and do cast
|
||||
return sokol.gfx.Color{
|
||||
.r = color.r,
|
||||
.g = color.g,
|
||||
.b = color.b,
|
||||
.a = color.a,
|
||||
};
|
||||
}
|
||||
|
||||
fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void {
|
||||
if (log_level == 0) {
|
||||
log.err(format, args);
|
||||
} else if (log_level == 1) {
|
||||
log.err(format, args);
|
||||
} else if (log_level == 2) {
|
||||
log.warn(format, args);
|
||||
} else {
|
||||
log.info(format, args);
|
||||
}
|
||||
}
|
||||
|
||||
fn cStrToZig(c_str: [*c]const u8) [:0]const u8 {
|
||||
return std.mem.span(c_str);
|
||||
}
|
||||
|
||||
fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void {
|
||||
_ = user_data;
|
||||
|
||||
if (filename != null) {
|
||||
sokolLogFmt(
|
||||
log_level,
|
||||
"[{s}][id:{}] {s}:{}: {s}",
|
||||
.{
|
||||
cStrToZig(tag orelse "-"),
|
||||
log_item,
|
||||
std.fs.path.basename(cStrToZig(filename orelse "-")),
|
||||
line_nr,
|
||||
cStrToZig(message orelse "")
|
||||
}
|
||||
);
|
||||
} else {
|
||||
sokolLogFmt(
|
||||
log_level,
|
||||
"[{s}][id:{}] {s}",
|
||||
.{
|
||||
cStrToZig(tag orelse "-"),
|
||||
log_item,
|
||||
cStrToZig(message orelse "")
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/shader.glsl
Normal file
@ -0,0 +1,50 @@
|
||||
@header const m = @import("math")
|
||||
@ctype mat4 m.Mat4
|
||||
|
||||
/* main vertex shader */
|
||||
@vs vs
|
||||
in vec2 position;
|
||||
in vec2 texcoord0;
|
||||
in vec4 color0;
|
||||
out vec4 color;
|
||||
out vec4 uv;
|
||||
|
||||
layout(binding = 0) uniform vs_params {
|
||||
mat4 view;
|
||||
mat4 projection;
|
||||
};
|
||||
|
||||
void main() {
|
||||
gl_Position = projection * view * vec4(position, 0, 1);
|
||||
color = color0;
|
||||
uv = vec4(texcoord0, 0.0, 1.0);
|
||||
}
|
||||
@end
|
||||
|
||||
/* main fragment shader */
|
||||
@fs fs
|
||||
layout(binding=0) uniform texture2D tex;
|
||||
layout(binding=0) uniform sampler smp;
|
||||
in vec4 color;
|
||||
in vec4 uv;
|
||||
out vec4 frag_color;
|
||||
|
||||
layout(binding = 1) uniform fs_params {
|
||||
// Textures modes:
|
||||
// 0 - rgba8
|
||||
// 1 - r8 - use red channel as opacity, used for fonts.
|
||||
int texture_mode;
|
||||
};
|
||||
|
||||
void main() {
|
||||
vec4 tex_color = texture(sampler2D(tex, smp), uv.xy);
|
||||
if (texture_mode == 0) {
|
||||
frag_color = tex_color * color;
|
||||
} else if (texture_mode == 1) {
|
||||
frag_color = vec4(1, 1, 1, tex_color.r) * color;
|
||||
}
|
||||
}
|
||||
@end
|
||||
|
||||
/* main shader program */
|
||||
@program main vs fs
|
||||
305
src/slot_map.zig
Normal file
@ -0,0 +1,305 @@
|
||||
const std = @import("std");
|
||||
const tracy = @import("tracy");
|
||||
const builtin = @import("builtin");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// This array provides:
|
||||
/// - O(1) insertion
|
||||
/// - O(1) removal
|
||||
/// - O(1) lookup
|
||||
/// - Pointers and IDs stay stable when inserting and removing.
|
||||
/// - Unique IDs for every inserted item (assuming that generation doesn't overflow)
|
||||
///
|
||||
/// For more details about this data structure checkout this podcast episode from Wookash:
|
||||
/// * https://www.youtube.com/watch?v=ShSGHb65f3M
|
||||
pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
|
||||
assert(@typeInfo(Generation) == .int);
|
||||
assert(@typeInfo(Index) == .int);
|
||||
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
// TODO: This struct will probably have paddings bytes inserted.
|
||||
// It's not ideal for a data structure like this which will be commonly used.
|
||||
//
|
||||
// The `used` field could be packed into a bitset.
|
||||
// But that would introduce a second `std.ArrayList` which would make managing the memory a PITA.
|
||||
//
|
||||
// So in the end idk if it's worth it.
|
||||
pub const Slot = struct {
|
||||
value: Value,
|
||||
generation: Generation,
|
||||
next_hole: ?Index,
|
||||
used: bool
|
||||
};
|
||||
|
||||
slots: std.ArrayList(Slot),
|
||||
first_hole: ?Index,
|
||||
last_hole: ?Index,
|
||||
hole_count: usize,
|
||||
|
||||
const empty = Self{
|
||||
.slots = .empty,
|
||||
.first_hole = null,
|
||||
.last_hole = null,
|
||||
.hole_count = 0,
|
||||
};
|
||||
|
||||
pub const Id = packed struct {
|
||||
generation: Generation,
|
||||
index: Index,
|
||||
|
||||
pub fn format(self: Id, writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
try writer.print("Id({s}){{ {}, {} }}", .{ @typeName(Value), self.index, self.generation });
|
||||
}
|
||||
};
|
||||
|
||||
pub const Iterator = struct {
|
||||
slot_map: *Self,
|
||||
index: Index,
|
||||
|
||||
pub fn next(self: *Iterator) ?Id {
|
||||
while (self.index < self.slot_map.slots.items.len) {
|
||||
const index = self.index;
|
||||
const slot = self.slot_map.slots.items[index];
|
||||
self.index += 1;
|
||||
|
||||
if (slot.used) {
|
||||
return Id{
|
||||
.index = @intCast(index),
|
||||
.generation = slot.generation
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn clearRetainingCapacity(self: *Self) void {
|
||||
var slots = self.slots;
|
||||
slots.clearRetainingCapacity();
|
||||
|
||||
self.* = .init(slots);
|
||||
}
|
||||
|
||||
fn insertHole(self: *Self, index: Index) void {
|
||||
if (self.last_hole) |last_hole| {
|
||||
self.slots.items[index].next_hole = last_hole;
|
||||
self.last_hole = index;
|
||||
} else {
|
||||
self.first_hole = index;
|
||||
self.last_hole = index;
|
||||
}
|
||||
self.hole_count += 1;
|
||||
}
|
||||
|
||||
fn removeUnused(self: *Self) ?Index {
|
||||
if (self.first_hole) |first_hole| {
|
||||
self.first_hole = self.slots.items[first_hole].next_hole;
|
||||
if (self.first_hole == null) {
|
||||
self.last_hole = null;
|
||||
}
|
||||
self.hole_count -= 1;
|
||||
return first_hole;
|
||||
}
|
||||
|
||||
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
||||
if (self.slots.items.len < capacity) {
|
||||
const index: Index = @intCast(self.slots.items.len);
|
||||
self.slots.items.len += 1;
|
||||
self.slots.items[index] = Slot{
|
||||
.value = undefined,
|
||||
.generation = 0,
|
||||
.next_hole = null,
|
||||
.used = false
|
||||
};
|
||||
return index;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void {
|
||||
if (additional_count > self.hole_count) {
|
||||
const new_capacity, const overflow = @addWithOverflow(
|
||||
self.slots.capacity,
|
||||
additional_count - self.hole_count
|
||||
);
|
||||
if (overflow != 0) return error.OutOfMemory;
|
||||
if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory;
|
||||
|
||||
try self.slots.ensureTotalCapacity(gpa, new_capacity);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unusedCapacity(self: *Self) usize {
|
||||
const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
|
||||
return capacity - self.slots.items.len + self.hole_count;
|
||||
}
|
||||
|
||||
pub fn insertAssumeCapacity(self: *Self) Id {
|
||||
const index = self.removeUnused().?;
|
||||
const slot = &self.slots.items[index];
|
||||
assert(!slot.used);
|
||||
slot.used = true;
|
||||
|
||||
return Id{
|
||||
.index = @intCast(index),
|
||||
.generation = slot.generation
|
||||
};
|
||||
}
|
||||
|
||||
pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id {
|
||||
try self.ensureUnusedCapacity(gpa, 1);
|
||||
return self.insertAssumeCapacity();
|
||||
}
|
||||
|
||||
pub fn insertBounded(self: *Self) Allocator.Error!Id {
|
||||
if (self.unusedCapacity() == 0) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
return self.insertAssumeCapacity();
|
||||
}
|
||||
|
||||
pub fn exists(self: *Self, id: Id) bool {
|
||||
if (id.index >= self.slots.items.len) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const slot = self.slots.items[id.index];
|
||||
|
||||
return slot.used and slot.generation == id.generation;
|
||||
}
|
||||
|
||||
pub fn get(self: *Self, id: Id) ?*Value {
|
||||
if (self.exists(id)) {
|
||||
return &self.slots.items[id.index].value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getAssumeExists(self: *Self, id: Id) *Value {
|
||||
assert(self.exists(id));
|
||||
return self.get(id).?;
|
||||
}
|
||||
|
||||
pub fn removeAssumeExists(self: *Self, id: Id) void {
|
||||
assert(self.exists(id));
|
||||
|
||||
const slot = &self.slots.items[id.index];
|
||||
|
||||
slot.used = false;
|
||||
slot.generation +%= 1;
|
||||
self.insertHole(id.index);
|
||||
}
|
||||
|
||||
pub fn remove(self: *Self, id: Id) bool {
|
||||
if (!self.exists(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.removeAssumeExists(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn iterator(self: *Self) Iterator {
|
||||
return Iterator{
|
||||
.slot_map = self,
|
||||
.index = 0
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(slots: std.ArrayList(Slot)) Self {
|
||||
var self: Self = .empty;
|
||||
self.slots = slots;
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self, gpa: Allocator) void {
|
||||
self.slots.deinit(gpa);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const TestMap = SlotMapType(u24, u8, void);
|
||||
|
||||
// TODO: Add more rigorous test for check if generation usage is nicely distributed.
|
||||
|
||||
test "insert & remove" {
|
||||
const expect = std.testing.expect;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
defer map.deinit(gpa);
|
||||
|
||||
const id1 = try map.insert(gpa);
|
||||
try expect(map.exists(id1));
|
||||
try expect(map.remove(id1));
|
||||
try expect(!map.exists(id1));
|
||||
try expect(!map.remove(id1));
|
||||
|
||||
const id2 = try map.insert(gpa);
|
||||
try expect(map.exists(id2));
|
||||
try expect(!map.exists(id1));
|
||||
}
|
||||
|
||||
test "generation wrap around" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
defer map.deinit(gpa);
|
||||
|
||||
// Grow array list so that at least 1 slot exists
|
||||
const id1 = try map.insert(gpa);
|
||||
map.removeAssumeExists(id1);
|
||||
|
||||
// Artificially increase generation count
|
||||
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
|
||||
|
||||
// Check if generation wraps around
|
||||
const id2 = try map.insert(gpa);
|
||||
map.removeAssumeExists(id2);
|
||||
try expectEqual(id1.index, id2.index);
|
||||
try expectEqual(0, map.slots.items[id1.index].generation);
|
||||
}
|
||||
|
||||
test "iterator" {
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
defer map.deinit(gpa);
|
||||
|
||||
// Create array which has a hole
|
||||
const id1 = try map.insert(gpa);
|
||||
const id2 = try map.insert(gpa);
|
||||
const id3 = try map.insert(gpa);
|
||||
|
||||
map.removeAssumeExists(id2);
|
||||
|
||||
var iter = map.iterator();
|
||||
try expectEqual(id1, iter.next().?);
|
||||
try expectEqual(id3, iter.next().?);
|
||||
try expectEqual(null, iter.next());
|
||||
}
|
||||
|
||||
test "clear retaining capacity" {
|
||||
const expect = std.testing.expect;
|
||||
const expectEqual = std.testing.expectEqual;
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var map: TestMap = .empty;
|
||||
defer map.deinit(gpa);
|
||||
|
||||
const id1 = try map.insert(gpa);
|
||||
try expect(map.exists(id1));
|
||||
map.clearRetainingCapacity();
|
||||
|
||||
const id2 = try map.insert(gpa);
|
||||
try expect(map.exists(id2));
|
||||
|
||||
try expectEqual(id1, id2);
|
||||
}
|
||||