Initial commit
This commit is contained in:
commit
052c3ad624
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
zig-out
|
||||
.zig-cache
|
||||
*.tiled-session
|
||||
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# Gaem
|
||||
|
||||
Weapons:
|
||||
* Laser mask
|
||||
* Bomb mask
|
||||
* Piston mask
|
||||
|
||||
Enemies:
|
||||
* Singular pawns that keep distance
|
||||
* Cluster of many enemies
|
||||
* Snake-like row of enemies
|
||||
|
||||
## 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
|
||||
```
|
||||
276
build.zig
Normal file
276
build.zig
Normal file
@ -0,0 +1,276 @@
|
||||
const std = @import("std");
|
||||
const sokol = @import("sokol");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
const project_name = "game-2026-01-18";
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse (optimize == .Debug);
|
||||
var has_tracy = b.option(bool, "tracy", "Tracy integration") orelse (optimize == .Debug);
|
||||
const has_console = b.option(bool, "console", "Show console (Window only)") orelse (optimize == .Debug);
|
||||
|
||||
const isWasm = target.result.cpu.arch.isWasm();
|
||||
|
||||
if (isWasm) {
|
||||
has_tracy = false;
|
||||
}
|
||||
|
||||
const mod_main = b.createModule(.{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, .with_sokol_imgui = has_imgui, .vulkan = true });
|
||||
mod_main.linkLibrary(dep_sokol.artifact("sokol_clib"));
|
||||
mod_main.addImport("sokol", dep_sokol.module("sokol"));
|
||||
|
||||
if (has_imgui) {
|
||||
if (b.lazyDependency("cimgui", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
})) |dep_cimgui| {
|
||||
const cimgui = b.lazyImport(@This(), "cimgui").?;
|
||||
const cimgui_conf = cimgui.getConfig(false);
|
||||
mod_main.addImport("cimgui", dep_cimgui.module(cimgui_conf.module_name));
|
||||
dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path(cimgui_conf.include_dir));
|
||||
}
|
||||
}
|
||||
|
||||
const dep_tracy = b.dependency("tracy", .{ .target = target, .optimize = optimize, .tracy_enable = has_tracy, .tracy_only_localhost = true });
|
||||
if (has_tracy) {
|
||||
mod_main.linkLibrary(dep_tracy.artifact("tracy"));
|
||||
}
|
||||
mod_main.addImport("tracy", dep_tracy.module("tracy"));
|
||||
|
||||
const dep_tiled = b.dependency("tiled", .{});
|
||||
mod_main.addImport("tiled", dep_tiled.module("tiled"));
|
||||
|
||||
const dep_stb = b.dependency("stb", .{});
|
||||
mod_main.addImport("stb_image", dep_stb.module("stb_image"));
|
||||
mod_main.addImport("stb_vorbis", dep_stb.module("stb_vorbis"));
|
||||
mod_main.addImport("stb_rect_pack", dep_stb.module("stb_rect_pack"));
|
||||
|
||||
const dep_fontstash_c = b.dependency("fontstash_c", .{});
|
||||
mod_main.addIncludePath(dep_fontstash_c.path("src"));
|
||||
|
||||
const dep_sokol_c = b.dependency("sokol_c", .{});
|
||||
{
|
||||
var cflags_buffer: [64][]const u8 = undefined;
|
||||
var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer);
|
||||
switch (sokol.resolveSokolBackend(.vulkan, target.result)) {
|
||||
.d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"),
|
||||
.metal => try cflags.appendBounded("-DSOKOL_METAL"),
|
||||
.gl => try cflags.appendBounded("-DSOKOL_GLCORE"),
|
||||
.gles3 => try cflags.appendBounded("-DSOKOL_GLES3"),
|
||||
.wgpu => try cflags.appendBounded("-DSOKOL_WGPU"),
|
||||
.vulkan => try cflags.appendBounded("-DSOKOL_VULKAN"),
|
||||
else => @panic("unknown sokol backend"),
|
||||
}
|
||||
|
||||
mod_main.addIncludePath(dep_sokol_c.path("util"));
|
||||
mod_main.addCSourceFile(.{ .file = b.path("src/engine/fontstash/sokol_fontstash_impl.c"), .flags = cflags.items });
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// const sdl = b.dependency("sdl", .{
|
||||
// .optimize = optimize,
|
||||
// .target = target,
|
||||
// .linkage = .static,
|
||||
// .default_target_config = !isWasm
|
||||
// });
|
||||
// mod_main.linkLibrary(sdl.artifact("SDL3"));
|
||||
// if (isWasm) {
|
||||
// // TODO: Define buid config for wasm
|
||||
// }
|
||||
|
||||
var options = b.addOptions();
|
||||
options.addOption(bool, "has_imgui", has_imgui);
|
||||
options.addOption(bool, "has_tracy", has_tracy);
|
||||
mod_main.addOptions("build_options", options);
|
||||
|
||||
// from here on different handling for native vs wasm builds
|
||||
if (target.result.cpu.arch.isWasm()) {
|
||||
try buildWasm(b, .{
|
||||
.name = project_name,
|
||||
.mod_main = mod_main,
|
||||
.dep_sokol = dep_sokol,
|
||||
});
|
||||
} else {
|
||||
try buildNative(b, project_name, mod_main, has_console);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildNative(b: *std.Build, name: []const u8, mod: *std.Build.Module, has_console: bool) !void {
|
||||
const exe = b.addExecutable(.{ .name = name, .root_module = mod });
|
||||
const target = mod.resolved_target.?;
|
||||
if (target.result.os.tag == .windows) {
|
||||
exe.subsystem = if (has_console) .Console else .Windows;
|
||||
|
||||
const png_to_icon_tool = b.addExecutable(.{
|
||||
.name = "png-to-icon",
|
||||
.root_module = b.createModule(.{
|
||||
.target = b.graph.host,
|
||||
.root_source_file = b.path("tools/png-to-icon.zig"),
|
||||
}),
|
||||
});
|
||||
const dep_stb_image = b.dependency("stb_image", .{});
|
||||
png_to_icon_tool.root_module.addImport("stb_image", dep_stb_image.module("stb_image"));
|
||||
|
||||
const png_to_icon_step = b.addRunArtifact(png_to_icon_tool);
|
||||
png_to_icon_step.addFileArg(b.path("src/assets/icon.png"));
|
||||
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);
|
||||
}
|
||||
b.installArtifact(exe);
|
||||
|
||||
{
|
||||
const run_step = b.step("run", "Run game");
|
||||
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
|
||||
if (b.args) |args| {
|
||||
run_cmd.addArgs(args);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const exe_tests = b.addTest(.{
|
||||
.root_module = exe.root_module,
|
||||
});
|
||||
|
||||
const run_exe_tests = b.addRunArtifact(exe_tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_exe_tests.step);
|
||||
}
|
||||
}
|
||||
|
||||
const BuildWasmOptions = struct {
|
||||
name: []const u8,
|
||||
mod_main: *std.Build.Module,
|
||||
dep_sokol: *std.Build.Dependency,
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
fn buildWasm(b: *std.Build, opts: BuildWasmOptions) !void {
|
||||
opts.mod_main.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 = "index",
|
||||
.root_module = opts.mod_main,
|
||||
});
|
||||
|
||||
const dep_emsdk = opts.dep_sokol.builder.dependency("emsdk", .{});
|
||||
|
||||
patchWasmIncludeDirs(opts.mod_main, dep_emsdk.path("upstream/emscripten/cache/sysroot/include"), &(opts.dep_sokol.artifact("sokol_clib").step));
|
||||
|
||||
// create a build step which invokes the Emscripten linker
|
||||
const link_step = try sokol.emLinkStep(b, .{
|
||||
.lib_main = main_lib,
|
||||
.target = opts.mod_main.resolved_target.?,
|
||||
.optimize = opts.mod_main.optimize.?,
|
||||
.emsdk = dep_emsdk,
|
||||
.use_webgl2 = true,
|
||||
.use_emmalloc = true,
|
||||
.use_filesystem = false,
|
||||
.shell_file_path = b.path("src/engine/shell.html"),
|
||||
});
|
||||
// attach to default target
|
||||
b.getInstallStep().dependOn(&link_step.step);
|
||||
// ...and a special run step to start the web build output via 'emrun'
|
||||
const run = sokol.emRunStep(b, .{ .name = "index", .emsdk = dep_emsdk });
|
||||
run.step.dependOn(&link_step.step);
|
||||
b.step("run", "Run game").dependOn(&run.step);
|
||||
|
||||
// TODO: Create a zip archive of all of the files. Would be useful for easier itch.io upload
|
||||
}
|
||||
|
||||
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.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 resource_file = try std.fs.cwd().createFile(self.resource_file.getPath(b), .{});
|
||||
defer resource_file.close();
|
||||
|
||||
const relative_icon_path = try std.fs.path.relative(b.allocator, self.resource_file.dirname().getPath(b), self.icon_file.getPath(b));
|
||||
std.mem.replaceScalar(u8, relative_icon_path, '\\', '/');
|
||||
|
||||
try resource_file.writeAll("IDI_ICON ICON \"");
|
||||
try resource_file.writeAll(relative_icon_path);
|
||||
try resource_file.writeAll("\"");
|
||||
}
|
||||
};
|
||||
44
build.zig.zon
Normal file
44
build.zig.zon
Normal file
@ -0,0 +1,44 @@
|
||||
.{
|
||||
.name = .sokol_template,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x60a8e079a691c8d9, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.sokol = .{
|
||||
.url = "git+https://github.com/floooh/sokol-zig.git#3f893819d1469cf96ab4d18ec1a54311c9c1c9c8",
|
||||
.hash = "sokol-0.1.0-pb1HK_qcNgDWA4dFM6Lr-aUxnequltCKH87jDcYsV7t5",
|
||||
},
|
||||
.sokol_c = .{
|
||||
.url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
|
||||
.hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
|
||||
},
|
||||
.fontstash_c = .{
|
||||
.url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
|
||||
.hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
|
||||
},
|
||||
.cimgui = .{
|
||||
.url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13",
|
||||
.hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL",
|
||||
.lazy = true,
|
||||
},
|
||||
.tracy = .{
|
||||
.url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
|
||||
.hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
|
||||
},
|
||||
.tiled = .{
|
||||
.path = "./libs/tiled",
|
||||
},
|
||||
.stb = .{
|
||||
.path = "./libs/stb",
|
||||
},
|
||||
// .sdl = .{
|
||||
// .url = "git+https://github.com/allyourcodebase/SDL3.git#f85824b0db782b7d01c60aaad8bcb537892394e8",
|
||||
// .hash = "sdl-0.0.0-i4QD0UuFqADRQysNyJ1OvCOZnq-clcVhq3BfPcBOf9zr",
|
||||
// },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
53
libs/stb/build.zig
Normal file
53
libs/stb/build.zig
Normal file
@ -0,0 +1,53 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const stb_dependency = b.dependency("stb", .{});
|
||||
|
||||
{
|
||||
const mod_stb_image = b.addModule("stb_image", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/stb_image.zig"),
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
mod_stb_image.addIncludePath(stb_dependency.path("."));
|
||||
mod_stb_image.addCSourceFile(.{
|
||||
.file = b.path("src/stb_image_impl.c"),
|
||||
.flags = &.{}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const mod_stb_image = b.addModule("stb_vorbis", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/stb_vorbis.zig"),
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
mod_stb_image.addIncludePath(stb_dependency.path("."));
|
||||
mod_stb_image.addCSourceFile(.{
|
||||
.file = b.path("src/stb_vorbis_impl.c"),
|
||||
.flags = &.{}
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const mod_stb_rect_pack = b.addModule("stb_rect_pack", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/stb_rect_pack.zig"),
|
||||
.link_libc = true,
|
||||
});
|
||||
|
||||
mod_stb_rect_pack.addIncludePath(stb_dependency.path("."));
|
||||
mod_stb_rect_pack.addCSourceFile(.{
|
||||
.file = b.path("src/stb_rect_pack_impl.c"),
|
||||
.flags = &.{}
|
||||
});
|
||||
}
|
||||
}
|
||||
17
libs/stb/build.zig.zon
Normal file
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
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
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"
|
||||
52
libs/stb/src/stb_rect_pack.zig
Normal file
52
libs/stb/src/stb_rect_pack.zig
Normal file
@ -0,0 +1,52 @@
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("stb_rect_pack.h");
|
||||
});
|
||||
|
||||
const STBRectPack = @This();
|
||||
|
||||
pub const Node = c.stbrp_node;
|
||||
pub const Rect = c.stbrp_rect;
|
||||
|
||||
pub const Options = struct {
|
||||
const Heuristic = enum(i32) {
|
||||
bl_sort_height = c.STBRP_HEURISTIC_Skyline_BL_sortHeight,
|
||||
bf_sort_height = c.STBRP_HEURISTIC_Skyline_BF_sortHeight,
|
||||
|
||||
pub const default: Heuristic = @enumFromInt(c.STBRP_HEURISTIC_Skyline_default);
|
||||
};
|
||||
|
||||
width: u31,
|
||||
height: u31,
|
||||
nodes: []Node,
|
||||
allow_out_of_memory: bool = false,
|
||||
heuristic: Heuristic = .default,
|
||||
};
|
||||
|
||||
ctx: c.stbrp_context,
|
||||
|
||||
pub fn init(options: Options) STBRectPack {
|
||||
var ctx: c.stbrp_context = undefined;
|
||||
c.stbrp_init_target(
|
||||
&ctx,
|
||||
options.width,
|
||||
options.height,
|
||||
options.nodes.ptr,
|
||||
options.nodes.len
|
||||
);
|
||||
|
||||
if (options.allow_out_of_memory) {
|
||||
c.stbrp_setup_allow_out_of_mem(&ctx, 1);
|
||||
}
|
||||
|
||||
if (options.heuristic != .default) {
|
||||
c.stbrp_setup_heuristic(&ctx, options.heuristic);
|
||||
}
|
||||
|
||||
return STBRectPack{ .ctx = ctx };
|
||||
}
|
||||
|
||||
pub fn packRects(self: *STBRectPack, rects: []Rect) bool {
|
||||
const success = c.stbrp_pack_rects(&self.ctx, rects.ptr, rects.len);
|
||||
return success != 0;
|
||||
}
|
||||
2
libs/stb/src/stb_rect_pack_impl.c
Normal file
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"
|
||||
170
libs/stb/src/stb_vorbis.zig
Normal file
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
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
29
libs/tiled/build.zig
Normal file
@ -0,0 +1,29 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const mod = b.addModule("tiled", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.root_source_file = b.path("src/root.zig")
|
||||
});
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "tiled",
|
||||
.root_module = mod
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
{
|
||||
const tests = b.addTest(.{
|
||||
.root_module = mod
|
||||
});
|
||||
|
||||
const run_tests = b.addRunArtifact(tests);
|
||||
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&run_tests.step);
|
||||
}
|
||||
}
|
||||
18
libs/tiled/src/buffers.zig
Normal file
18
libs/tiled/src/buffers.zig
Normal file
@ -0,0 +1,18 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Buffers = @This();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
points: std.ArrayList(Position) = .empty,
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator) Buffers {
|
||||
return Buffers{
|
||||
.allocator = gpa
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Buffers) void {
|
||||
self.points.deinit(self.allocator);
|
||||
}
|
||||
52
libs/tiled/src/color.zig
Normal file
52
libs/tiled/src/color.zig
Normal file
@ -0,0 +1,52 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Color = @This();
|
||||
|
||||
r: u8,
|
||||
g: u8,
|
||||
b: u8,
|
||||
a: u8,
|
||||
|
||||
pub const black = Color{
|
||||
.r = 0,
|
||||
.g = 0,
|
||||
.b = 0,
|
||||
.a = 255,
|
||||
};
|
||||
|
||||
pub fn parse(str: []const u8, hash_required: bool) !Color {
|
||||
var color = Color{
|
||||
.r = undefined,
|
||||
.g = undefined,
|
||||
.b = undefined,
|
||||
.a = 255,
|
||||
};
|
||||
|
||||
if (str.len < 1) {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
const has_hash = str[0] == '#';
|
||||
if (hash_required and !has_hash) {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
const hex_str = if (has_hash) str[1..] else str;
|
||||
|
||||
if (hex_str.len == 6) {
|
||||
color.r = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||
color.g = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||
color.b = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||
|
||||
} else if (hex_str.len == 8) {
|
||||
color.a = try std.fmt.parseInt(u8, hex_str[0..2], 16);
|
||||
color.r = try std.fmt.parseInt(u8, hex_str[2..4], 16);
|
||||
color.g = try std.fmt.parseInt(u8, hex_str[4..6], 16);
|
||||
color.b = try std.fmt.parseInt(u8, hex_str[6..8], 16);
|
||||
|
||||
} else {
|
||||
return error.InvalidColorFormat;
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
15
libs/tiled/src/global_tile_id.zig
Normal file
15
libs/tiled/src/global_tile_id.zig
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
pub const Flag = enum(u32) {
|
||||
flipped_horizontally = 1 << 31, // bit 32
|
||||
flipped_vertically = 1 << 30, // bit 31
|
||||
flipped_diagonally = 1 << 29, // bit 30
|
||||
rotated_hexagonal_120 = 1 << 28, // bit 29
|
||||
_,
|
||||
|
||||
pub const clear: u32 = ~(
|
||||
@intFromEnum(Flag.flipped_horizontally) |
|
||||
@intFromEnum(Flag.flipped_vertically) |
|
||||
@intFromEnum(Flag.flipped_diagonally) |
|
||||
@intFromEnum(Flag.rotated_hexagonal_120)
|
||||
);
|
||||
};
|
||||
464
libs/tiled/src/layer.zig
Normal file
464
libs/tiled/src/layer.zig
Normal file
@ -0,0 +1,464 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Property = @import("./property.zig");
|
||||
const xml = @import("./xml.zig");
|
||||
const Color = @import("./color.zig");
|
||||
const Object = @import("./object.zig");
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Layer = @This();
|
||||
|
||||
pub const Bounds = struct {
|
||||
left: i32,
|
||||
right: i32,
|
||||
top: i32,
|
||||
bottom: i32,
|
||||
|
||||
pub const zero = Bounds{
|
||||
.left = 0,
|
||||
.right = 0,
|
||||
.top = 0,
|
||||
.bottom = 0,
|
||||
};
|
||||
|
||||
pub fn initFromRect(x: i32, y: i32, width: i32, height: i32) Bounds {
|
||||
return Bounds{
|
||||
.left = x,
|
||||
.right = x + width,
|
||||
.top = y,
|
||||
.bottom = y + height,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getWidth(self: Bounds) u32 {
|
||||
return @intCast(self.right - self.left + 1);
|
||||
}
|
||||
|
||||
pub fn getHeight(self: Bounds) u32 {
|
||||
return @intCast(self.bottom - self.top + 1);
|
||||
}
|
||||
|
||||
pub fn combine(lhs: Bounds, rhs: Bounds) Bounds {
|
||||
return Bounds{
|
||||
.left = @min(lhs.left, rhs.left),
|
||||
.right = @max(lhs.right, rhs.right),
|
||||
.top = @min(lhs.top, rhs.top),
|
||||
.bottom = @max(lhs.bottom, rhs.bottom)
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const TileVariant = struct {
|
||||
pub const Chunk = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: []u32,
|
||||
|
||||
pub fn getBounds(self: Chunk) Bounds {
|
||||
return Bounds.initFromRect(self.x, self.y, @intCast(self.width), @intCast(self.height));
|
||||
}
|
||||
};
|
||||
|
||||
pub const Data = union(enum) {
|
||||
fixed: []u32,
|
||||
chunks: []Chunk
|
||||
};
|
||||
|
||||
const Encoding = enum {
|
||||
csv,
|
||||
|
||||
const map: std.StaticStringMap(Encoding) = .initComptime(.{
|
||||
.{ "csv", .csv },
|
||||
});
|
||||
};
|
||||
|
||||
width: u32,
|
||||
height: u32,
|
||||
data: Data,
|
||||
|
||||
fn initChunkDataFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
encoding: Encoding
|
||||
) !Chunk {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("chunk");
|
||||
|
||||
const x = try attrs.getNumber(i32, "x") orelse return error.MissingAttribute;
|
||||
const y = try attrs.getNumber(i32, "y") orelse return error.MissingAttribute;
|
||||
const width = try attrs.getNumber(u32, "width") orelse return error.MissingAttribute;
|
||||
const height = try attrs.getNumber(u32, "height") orelse return error.MissingAttribute;
|
||||
|
||||
var temp_tiles: std.ArrayList(u32) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node == .text) {
|
||||
const encoded_tiles = try lexer.nextExpectText();
|
||||
const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles);
|
||||
try temp_tiles.appendSlice(scratch.allocator(), tiles.items);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish("chunk");
|
||||
|
||||
return Chunk{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = try arena.dupe(u32, temp_tiles.items)
|
||||
};
|
||||
}
|
||||
|
||||
fn initDataFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Data {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("data");
|
||||
|
||||
const encoding = try attrs.getEnum(Encoding, "encoding", Encoding.map) orelse .csv;
|
||||
// TODO: compression
|
||||
|
||||
var temp_chunks: std.ArrayList(Chunk) = .empty;
|
||||
var temp_tiles: std.ArrayList(u32) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node == .text) {
|
||||
const encoded_tiles = try lexer.nextExpectText();
|
||||
const tiles = try parseEncoding(encoding, scratch.allocator(), encoded_tiles);
|
||||
try temp_tiles.appendSlice(scratch.allocator(), tiles.items);
|
||||
|
||||
} else if (node.isTag("chunk")) {
|
||||
const chunk = try initChunkDataFromXml(arena, scratch, lexer, encoding);
|
||||
try temp_chunks.append(scratch.allocator(), chunk);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish("data");
|
||||
|
||||
if (temp_chunks.items.len > 0) {
|
||||
return .{
|
||||
.chunks = try arena.dupe(Chunk, temp_chunks.items)
|
||||
};
|
||||
} else {
|
||||
return .{
|
||||
.fixed = try arena.dupe(u32, temp_tiles.items)
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn parseEncoding(
|
||||
encoding: Encoding,
|
||||
allocator: std.mem.Allocator,
|
||||
text: []const u8
|
||||
) !std.ArrayList(u32) {
|
||||
return switch (encoding) {
|
||||
.csv => try parseCSVEncoding(allocator, text)
|
||||
};
|
||||
}
|
||||
|
||||
fn parseCSVEncoding(
|
||||
allocator: std.mem.Allocator,
|
||||
text: []const u8
|
||||
) !std.ArrayList(u32) {
|
||||
var result: std.ArrayList(u32) = .empty;
|
||||
|
||||
var split_iter = std.mem.splitScalar(u8, text, ',');
|
||||
while (split_iter.next()) |raw_tile_id| {
|
||||
const tile_id_str = std.mem.trim(u8, raw_tile_id, &std.ascii.whitespace);
|
||||
const tile_id = try std.fmt.parseInt(u32, tile_id_str, 10);
|
||||
try result.append(allocator, tile_id);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn getBounds(self: TileVariant) Bounds {
|
||||
if (self.data == .fixed) {
|
||||
return Bounds.initFromRect(0, 0, @intCast(self.width), @intCast(self.height));
|
||||
|
||||
} else if (self.data == .chunks) {
|
||||
const chunks = self.data.chunks;
|
||||
|
||||
var result: Bounds = .zero;
|
||||
if (chunks.len > 0) {
|
||||
result = chunks[0].getBounds();
|
||||
for (chunks[1..]) |chunk| {
|
||||
result = result.combine(chunk.getBounds());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} else {
|
||||
unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(self: TileVariant, x: i32, y: i32) ?u32 {
|
||||
if (self.data == .fixed) {
|
||||
if ((0 <= x and x < self.width) and (0 <= y and y < self.height)) {
|
||||
const x_u32: u32 = @intCast(x);
|
||||
const y_u32: u32 = @intCast(y);
|
||||
return self.data.fixed[y_u32 * self.width + x_u32];
|
||||
}
|
||||
} else if (self.data == .chunks) {
|
||||
for (self.data.chunks) |chunk| {
|
||||
const ox = x - chunk.x;
|
||||
const oy = y - chunk.y;
|
||||
if ((0 <= ox and ox < chunk.width) and (0 <= oy and oy < chunk.height)) {
|
||||
const ox_u32: u32 = @intCast(ox);
|
||||
const oy_u32: u32 = @intCast(oy);
|
||||
return chunk.data[oy_u32 * chunk.width + ox_u32];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unreachable;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ImageVariant = struct {
|
||||
pub const Image = struct {
|
||||
// TODO: format
|
||||
source: []const u8,
|
||||
transparent_color: ?Color,
|
||||
width: ?u32,
|
||||
height: ?u32,
|
||||
|
||||
fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("image");
|
||||
|
||||
// TODO: format
|
||||
const source = try attrs.getDupe(arena, "source") orelse return error.MissingSource;
|
||||
const width = try attrs.getNumber(u32, "width") orelse null;
|
||||
const height = try attrs.getNumber(u32, "height") orelse null;
|
||||
const transparent_color = try attrs.getColor("trans", false);
|
||||
|
||||
try iter.finish("image");
|
||||
|
||||
return Image{
|
||||
.source = source,
|
||||
.transparent_color = transparent_color,
|
||||
.width = width,
|
||||
.height = height
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
repeat_x: bool,
|
||||
repeat_y: bool,
|
||||
image: ?Image
|
||||
};
|
||||
|
||||
pub const ObjectVariant = struct {
|
||||
pub const DrawOrder = enum {
|
||||
top_down,
|
||||
index,
|
||||
|
||||
const map: std.StaticStringMap(DrawOrder) = .initComptime(.{
|
||||
.{ "topdown", .top_down },
|
||||
.{ "index", .index },
|
||||
});
|
||||
};
|
||||
|
||||
color: ?Color,
|
||||
draw_order: DrawOrder,
|
||||
items: []Object
|
||||
};
|
||||
|
||||
pub const GroupVariant = struct {
|
||||
layers: []Layer
|
||||
};
|
||||
|
||||
pub const Type = enum {
|
||||
tile,
|
||||
object,
|
||||
image,
|
||||
group,
|
||||
|
||||
const name_map: std.StaticStringMap(Type) = .initComptime(.{
|
||||
.{ "layer", .tile },
|
||||
.{ "objectgroup", .object },
|
||||
.{ "imagelayer", .image },
|
||||
.{ "group", .group }
|
||||
});
|
||||
|
||||
fn toXmlName(self: Type) []const u8 {
|
||||
return switch (self) {
|
||||
.tile => "layer",
|
||||
.object => "objectgroup",
|
||||
.image => "imagelayer",
|
||||
.group => "group",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Variant = union(Type) {
|
||||
tile: TileVariant,
|
||||
object: ObjectVariant,
|
||||
image: ImageVariant,
|
||||
group: GroupVariant
|
||||
};
|
||||
|
||||
id: u32,
|
||||
name: []const u8,
|
||||
class: []const u8,
|
||||
opacity: f32,
|
||||
visible: bool,
|
||||
tint_color: ?Color,
|
||||
offset_x: f32,
|
||||
offset_y: f32,
|
||||
parallax_x: f32,
|
||||
parallax_y: f32,
|
||||
properties: Property.List,
|
||||
variant: Variant,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Layer {
|
||||
const value = try lexer.peek() orelse return error.MissingStartTag;
|
||||
if (value != .start_tag) return error.MissingStartTag;
|
||||
|
||||
var layer_type = Type.name_map.get(value.start_tag.name) orelse return error.UnknownLayerType;
|
||||
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin(layer_type.toXmlName());
|
||||
|
||||
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
const name = try attrs.getDupe(arena, "name") orelse "";
|
||||
const class = try attrs.getDupe(arena, "class") orelse "";
|
||||
const opacity = try attrs.getNumber(f32, "opacity") orelse 1;
|
||||
const visible = try attrs.getBool("visible", "1", "0") orelse true;
|
||||
const offset_x = try attrs.getNumber(f32, "offsetx") orelse 0;
|
||||
const offset_y = try attrs.getNumber(f32, "offsety") orelse 0;
|
||||
const parallax_x = try attrs.getNumber(f32, "parallaxx") orelse 1;
|
||||
const parallax_y = try attrs.getNumber(f32, "parallaxy") orelse 1;
|
||||
const tint_color = try attrs.getColor("tintcolor", true);
|
||||
|
||||
var variant: Variant = undefined;
|
||||
switch (layer_type) {
|
||||
.tile => {
|
||||
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||
variant = .{
|
||||
.tile = TileVariant{
|
||||
.width = width,
|
||||
.height = height,
|
||||
.data = .{ .fixed = &[0]u32{} }
|
||||
}
|
||||
};
|
||||
},
|
||||
.image => {
|
||||
const repeat_x = try attrs.getBool("repeatx", "1", "0") orelse false;
|
||||
const repeat_y = try attrs.getBool("repeaty", "1", "0") orelse false;
|
||||
variant = .{
|
||||
.image = ImageVariant{
|
||||
.repeat_x = repeat_x,
|
||||
.repeat_y = repeat_y,
|
||||
.image = null
|
||||
}
|
||||
};
|
||||
},
|
||||
.object => {
|
||||
const draw_order = try attrs.getEnum(ObjectVariant.DrawOrder, "draworder", ObjectVariant.DrawOrder.map) orelse .top_down;
|
||||
const color = try attrs.getColor("color", true);
|
||||
|
||||
variant = .{
|
||||
.object = ObjectVariant{
|
||||
.color = color,
|
||||
.draw_order = draw_order,
|
||||
.items = &.{}
|
||||
}
|
||||
};
|
||||
},
|
||||
.group => {
|
||||
variant = .{
|
||||
.group = .{
|
||||
.layers = &.{}
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
|
||||
var properties: Property.List = .empty;
|
||||
var objects: std.ArrayList(Object) = .empty;
|
||||
var layers: std.ArrayList(Layer) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("properties")) {
|
||||
properties = try Property.List.initFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .tile and node.isTag("data")) {
|
||||
variant.tile.data = try TileVariant.initDataFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .image and node.isTag("image")) {
|
||||
variant.image.image = try ImageVariant.Image.initFromXml(arena, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .object and node.isTag("object")) {
|
||||
const object = try Object.initFromXml(arena, scratch, lexer);
|
||||
try objects.append(scratch.allocator(), object);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (variant == .group and isLayerNode(node)) {
|
||||
const layer = try initFromXml(arena, scratch, lexer);
|
||||
try layers.append(scratch.allocator(), layer);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish(layer_type.toXmlName());
|
||||
|
||||
if (variant == .object) {
|
||||
variant.object.items = try arena.dupe(Object, objects.items);
|
||||
}
|
||||
|
||||
if (variant == .group) {
|
||||
variant.group.layers = try arena.dupe(Layer, layers.items);
|
||||
}
|
||||
|
||||
return Layer{
|
||||
.id = id,
|
||||
.name = name,
|
||||
.class = class,
|
||||
.opacity = opacity,
|
||||
.visible = visible,
|
||||
.tint_color = tint_color,
|
||||
.offset_x = offset_x,
|
||||
.offset_y = offset_y,
|
||||
.parallax_x = parallax_x,
|
||||
.parallax_y = parallax_y,
|
||||
.properties = properties,
|
||||
.variant = variant,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isLayerNode(node: xml.TagParser.Node) bool {
|
||||
return node.isTag("layer") or node.isTag("objectgroup") or node.isTag("imagelayer") or node.isTag("group");
|
||||
}
|
||||
421
libs/tiled/src/object.zig
Normal file
421
libs/tiled/src/object.zig
Normal file
@ -0,0 +1,421 @@
|
||||
const std = @import("std");
|
||||
|
||||
const xml = @import("./xml.zig");
|
||||
const Position = @import("./position.zig");
|
||||
const Color = @import("./color.zig");
|
||||
|
||||
const Object = @This();
|
||||
|
||||
pub const Shape = union (Type) {
|
||||
pub const Type = enum {
|
||||
rectangle,
|
||||
point,
|
||||
ellipse,
|
||||
polygon,
|
||||
tile,
|
||||
// TODO: template
|
||||
text
|
||||
};
|
||||
|
||||
pub const Rectangle = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
pub const Tile = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
gid: u32
|
||||
};
|
||||
|
||||
pub const Ellipse = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
};
|
||||
|
||||
pub const Polygon = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
points: []const Position
|
||||
};
|
||||
|
||||
pub const Text = struct {
|
||||
pub const Font = struct {
|
||||
family: []const u8,
|
||||
pixel_size: f32,
|
||||
bold: bool,
|
||||
italic: bool,
|
||||
underline: bool,
|
||||
strikeout: bool,
|
||||
kerning: bool
|
||||
};
|
||||
|
||||
pub const HorizontalAlign = enum {
|
||||
left,
|
||||
center,
|
||||
right,
|
||||
justify,
|
||||
|
||||
const map: std.StaticStringMap(HorizontalAlign) = .initComptime(.{
|
||||
.{ "left", .left },
|
||||
.{ "center", .center },
|
||||
.{ "right", .right },
|
||||
.{ "justify", .justify },
|
||||
});
|
||||
};
|
||||
|
||||
pub const VerticalAlign = enum {
|
||||
top,
|
||||
center,
|
||||
bottom,
|
||||
|
||||
const map: std.StaticStringMap(VerticalAlign) = .initComptime(.{
|
||||
.{ "top", .top },
|
||||
.{ "center", .center },
|
||||
.{ "bottom", .bottom },
|
||||
});
|
||||
};
|
||||
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
word_wrap: bool,
|
||||
color: Color,
|
||||
font: Font,
|
||||
horizontal_align: HorizontalAlign,
|
||||
vertical_align: VerticalAlign,
|
||||
content: []const u8
|
||||
};
|
||||
|
||||
pub const Point = struct {
|
||||
x: f32,
|
||||
y: f32,
|
||||
};
|
||||
|
||||
rectangle: Rectangle,
|
||||
point: Point,
|
||||
ellipse: Ellipse,
|
||||
polygon: Polygon,
|
||||
tile: Tile,
|
||||
text: Text
|
||||
};
|
||||
|
||||
id: u32,
|
||||
name: []const u8,
|
||||
class: []const u8,
|
||||
rotation: f32, // TODO: maybe this field should be moved to Shape struct
|
||||
visible: bool,
|
||||
shape: Shape,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Object {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("object");
|
||||
|
||||
const id = try attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
const name = try attrs.getDupe(arena, "name") orelse "";
|
||||
const class = try attrs.getDupe(arena, "type") orelse "";
|
||||
const x = try attrs.getNumber(f32, "x") orelse 0;
|
||||
const y = try attrs.getNumber(f32, "y") orelse 0;
|
||||
const width = try attrs.getNumber(f32, "width") orelse 0;
|
||||
const height = try attrs.getNumber(f32, "height") orelse 0;
|
||||
const rotation = try attrs.getNumber(f32, "rotation") orelse 0;
|
||||
const visible = try attrs.getBool("visible", "1", "0") orelse true;
|
||||
|
||||
var shape: ?Shape = null;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (shape == null) {
|
||||
if (node.isTag("point")) {
|
||||
shape = .{
|
||||
.point = Shape.Point{
|
||||
.x = x,
|
||||
.y = y,
|
||||
}
|
||||
};
|
||||
|
||||
} else if (node.isTag("ellipse")) {
|
||||
shape = .{
|
||||
.ellipse = Shape.Ellipse{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height
|
||||
}
|
||||
};
|
||||
|
||||
} else if (node.isTag("text")) {
|
||||
const HorizontalShape = Shape.Text.HorizontalAlign;
|
||||
const VerticalAlign = Shape.Text.VerticalAlign;
|
||||
|
||||
const text_attrs = node.tag.attributes;
|
||||
const word_wrap = try text_attrs.getBool("wrap", "1", "0") orelse false;
|
||||
const color = try text_attrs.getColor("color", true) orelse Color.black;
|
||||
const horizontal_align = try text_attrs.getEnum(HorizontalShape, "halign", HorizontalShape.map) orelse .left;
|
||||
const vertical_align = try text_attrs.getEnum(VerticalAlign, "valign", VerticalAlign.map) orelse .top;
|
||||
const bold = try text_attrs.getBool("bold", "1", "0") orelse false;
|
||||
const italic = try text_attrs.getBool("italic", "1", "0") orelse false;
|
||||
const strikeout = try text_attrs.getBool("strikeout", "1", "0") orelse false;
|
||||
const underline = try text_attrs.getBool("underline", "1", "0") orelse false;
|
||||
const kerning = try text_attrs.getBool("kerning", "1", "0") orelse true;
|
||||
const pixel_size = try text_attrs.getNumber(f32, "pixelsize") orelse 16;
|
||||
const font_family = try text_attrs.getDupe(arena, "fontfamily") orelse "sans-serif";
|
||||
|
||||
_ = try lexer.nextExpectStartTag("text");
|
||||
var content: []const u8 = "";
|
||||
const content_value = try lexer.peek();
|
||||
if (content_value != null and content_value.? == .text) {
|
||||
content = try arena.dupe(u8, content_value.?.text);
|
||||
}
|
||||
try lexer.skipUntilMatchingEndTag("text");
|
||||
|
||||
shape = .{
|
||||
.text = Shape.Text{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.word_wrap = word_wrap,
|
||||
.color = color,
|
||||
.horizontal_align = horizontal_align,
|
||||
.vertical_align = vertical_align,
|
||||
.content = try arena.dupe(u8, content),
|
||||
.font = .{
|
||||
.bold = bold,
|
||||
.italic = italic,
|
||||
.strikeout = strikeout,
|
||||
.underline = underline,
|
||||
.pixel_size = pixel_size,
|
||||
.kerning = kerning,
|
||||
.family = font_family,
|
||||
}
|
||||
}
|
||||
};
|
||||
continue;
|
||||
|
||||
} else if (node.isTag("polygon")) {
|
||||
const points_str = node.tag.attributes.get("points") orelse "";
|
||||
|
||||
var points: std.ArrayList(Position) = .empty;
|
||||
var point_iter = std.mem.splitScalar(u8, points_str, ' ');
|
||||
while (point_iter.next()) |point_str| {
|
||||
const point = try Position.parseCommaDelimited(point_str);
|
||||
try points.append(scratch.allocator(), point);
|
||||
}
|
||||
|
||||
shape = .{
|
||||
.polygon = Shape.Polygon{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.points = try arena.dupe(Position, points.items)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
if (shape == null) {
|
||||
if (try attrs.getNumber(u32, "gid")) |gid| {
|
||||
shape = .{
|
||||
.tile = Shape.Tile{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height,
|
||||
.gid = gid
|
||||
}
|
||||
};
|
||||
} else {
|
||||
shape = .{
|
||||
.rectangle = Shape.Rectangle{
|
||||
.x = x,
|
||||
.y = y,
|
||||
.width = width,
|
||||
.height = height
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try iter.finish("object");
|
||||
|
||||
return Object{
|
||||
.id = id,
|
||||
.name = name,
|
||||
.class = class,
|
||||
.rotation = rotation,
|
||||
.visible = visible,
|
||||
.shape = shape orelse return error.UnknownShapeType
|
||||
};
|
||||
}
|
||||
|
||||
fn expectParsedEquals(expected: Object, body: []const u8) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: xml.Lexer.TestingContext = undefined;
|
||||
ctx.init(allocator, body);
|
||||
defer ctx.deinit();
|
||||
|
||||
var scratch = std.heap.ArenaAllocator.init(allocator);
|
||||
defer scratch.deinit();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const parsed = try initFromXml(arena.allocator(), &scratch, &ctx.lexer);
|
||||
try std.testing.expectEqualDeep(expected, parsed);
|
||||
}
|
||||
|
||||
test Object {
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 10,
|
||||
.name = "rectangle",
|
||||
.class = "object class",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.rectangle = .{
|
||||
.x = 12.34,
|
||||
.y = 56.78,
|
||||
.width = 31.5,
|
||||
.height = 20.25,
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="10" name="rectangle" type="object class" x="12.34" y="56.78" width="31.5" height="20.25"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 3,
|
||||
.name = "point",
|
||||
.class = "foo",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.point = .{
|
||||
.x = 77.125,
|
||||
.y = 99.875
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="3" name="point" type="foo" x="77.125" y="99.875">
|
||||
\\ <point/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 4,
|
||||
.name = "ellipse",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.ellipse = .{
|
||||
.x = 64.25,
|
||||
.y = 108.25,
|
||||
.width = 22.375,
|
||||
.height = 15.375
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="4" name="ellipse" x="64.25" y="108.25" width="22.375" height="15.375">
|
||||
\\ <ellipse/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 5,
|
||||
.name = "",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.polygon = .{
|
||||
.x = 40.125,
|
||||
.y = 96.25,
|
||||
.points = &[_]Position{
|
||||
.{ .x = 0, .y = 0 },
|
||||
.{ .x = 13.25, .y = -4.25 },
|
||||
.{ .x = 10.125, .y = 18.625 },
|
||||
.{ .x = 2.25, .y = 17.375 },
|
||||
.{ .x = -0.125, .y = 25.75 },
|
||||
.{ .x = -3.875, .y = 20.75 },
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="5" x="40.125" y="96.25">
|
||||
\\ <polygon points="0,0 13.25,-4.25 10.125,18.625 2.25,17.375 -0.125,25.75 -3.875,20.75"/>
|
||||
\\ </object>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 2,
|
||||
.name = "tile",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.tile = .{
|
||||
.x = 60.125,
|
||||
.y = 103.5,
|
||||
.width = 8,
|
||||
.height = 8,
|
||||
.gid = 35
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="2" name="tile" gid="35" x="60.125" y="103.5" width="8" height="8"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Object{
|
||||
.id = 6,
|
||||
.name = "text",
|
||||
.class = "",
|
||||
.rotation = 0,
|
||||
.visible = true,
|
||||
.shape = .{
|
||||
.text = .{
|
||||
.x = 64.3906,
|
||||
.y = 92.8594,
|
||||
.width = 87.7188,
|
||||
.height = 21.7813,
|
||||
.content = "Hello World",
|
||||
.word_wrap = true,
|
||||
.color = .black,
|
||||
.horizontal_align = .center,
|
||||
.vertical_align = .top,
|
||||
.font = .{
|
||||
.family = "sans-serif",
|
||||
.pixel_size = 16,
|
||||
.bold = false,
|
||||
.italic = false,
|
||||
.underline = false,
|
||||
.strikeout = false,
|
||||
.kerning = true
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
\\ <object id="6" name="text" x="64.3906" y="92.8594" width="87.7188" height="21.7813">
|
||||
\\ <text wrap="1" halign="center"> Hello World </text>
|
||||
\\ </object>
|
||||
);
|
||||
}
|
||||
20
libs/tiled/src/position.zig
Normal file
20
libs/tiled/src/position.zig
Normal file
@ -0,0 +1,20 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Position = @This();
|
||||
|
||||
x: f32,
|
||||
y: f32,
|
||||
|
||||
pub fn parseCommaDelimited(str: []const u8) !Position {
|
||||
const comma_index = std.mem.indexOfScalar(u8, str, ',') orelse return error.MissingComma;
|
||||
const x_str = str[0..comma_index];
|
||||
const y_str = str[(comma_index+1)..];
|
||||
|
||||
const x = try std.fmt.parseFloat(f32, x_str);
|
||||
const y = try std.fmt.parseFloat(f32, y_str);
|
||||
|
||||
return Position{
|
||||
.x = x,
|
||||
.y = y
|
||||
};
|
||||
}
|
||||
162
libs/tiled/src/property.zig
Normal file
162
libs/tiled/src/property.zig
Normal file
@ -0,0 +1,162 @@
|
||||
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 getBool(self: List, name: []const u8) ?bool {
|
||||
if (self.get(name)) |value| {
|
||||
if (value == .bool) {
|
||||
return value.bool;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(name: []const u8, value: Value) Property {
|
||||
return Property{
|
||||
.name = name,
|
||||
.value = value
|
||||
};
|
||||
}
|
||||
|
||||
pub fn initFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Property {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("property");
|
||||
|
||||
const name = try attrs.getDupe(arena, "name") orelse return error.MissingName;
|
||||
const prop_type_str = attrs.get("type") orelse "string";
|
||||
const value_str = attrs.get("value") orelse "";
|
||||
|
||||
const prop_type = Type.map.get(prop_type_str) orelse return error.UnknownPropertyType;
|
||||
const value = switch(prop_type) {
|
||||
.string => Value{
|
||||
.string = try arena.dupe(u8, value_str)
|
||||
},
|
||||
.int => Value{
|
||||
.int = try std.fmt.parseInt(i32, value_str, 10)
|
||||
},
|
||||
.bool => Value{
|
||||
.bool = std.mem.eql(u8, value_str, "true")
|
||||
}
|
||||
};
|
||||
|
||||
try iter.finish("property");
|
||||
|
||||
return Property{
|
||||
.name = name,
|
||||
.value = value
|
||||
};
|
||||
}
|
||||
|
||||
fn expectParsedEquals(expected: Property, body: []const u8) !void {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: xml.Lexer.TestingContext = undefined;
|
||||
ctx.init(allocator, body);
|
||||
defer ctx.deinit();
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(allocator);
|
||||
defer arena.deinit();
|
||||
|
||||
const parsed = try initFromXml(arena.allocator(), &ctx.lexer);
|
||||
try std.testing.expectEqualDeep(expected, parsed);
|
||||
}
|
||||
|
||||
test Property {
|
||||
try expectParsedEquals(
|
||||
Property.init("solid", .{ .string = "hello" }),
|
||||
\\ <property name="solid" value="hello"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("solid", .{ .string = "hello" }),
|
||||
\\ <property name="solid" type="string" value="hello"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("integer", .{ .int = 123 }),
|
||||
\\ <property name="integer" type="int" value="123"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("boolean", .{ .bool = true }),
|
||||
\\ <property name="boolean" type="bool" value="true"/>
|
||||
);
|
||||
|
||||
try expectParsedEquals(
|
||||
Property.init("boolean", .{ .bool = false }),
|
||||
\\ <property name="boolean" type="bool" value="false"/>
|
||||
);
|
||||
}
|
||||
16
libs/tiled/src/root.zig
Normal file
16
libs/tiled/src/root.zig
Normal file
@ -0,0 +1,16 @@
|
||||
const std = @import("std");
|
||||
|
||||
// Warning:
|
||||
// This library is not complete, it does not cover all features that the specification provides.
|
||||
// But there are enough features implemented so that I could use this for my games.
|
||||
|
||||
// Map format specification:
|
||||
// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#
|
||||
|
||||
pub const xml = @import("./xml.zig");
|
||||
pub const Tileset = @import("./tileset.zig");
|
||||
pub const Tilemap = @import("./tilemap.zig");
|
||||
|
||||
test {
|
||||
_ = std.testing.refAllDeclsRecursive(@This());
|
||||
}
|
||||
266
libs/tiled/src/tilemap.zig
Normal file
266
libs/tiled/src/tilemap.zig
Normal file
@ -0,0 +1,266 @@
|
||||
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, tilesets: Tileset.List, gid: u32) ?Tile {
|
||||
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 getTileByPosition(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 getTileBounds(self: *const Tilemap) Layer.Bounds {
|
||||
var result: ?Layer.Bounds = null;
|
||||
|
||||
for (self.layers) |layer| {
|
||||
if (layer.variant != .tile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const layer_bounds = layer.variant.tile.getBounds();
|
||||
if (result == null) {
|
||||
result = layer_bounds;
|
||||
} else {
|
||||
result = layer_bounds.combine(result.?);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return result orelse .zero;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Tilemap) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
226
libs/tiled/src/tileset.zig
Normal file
226
libs/tiled/src/tileset.zig
Normal file
@ -0,0 +1,226 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const xml = @import("./xml.zig");
|
||||
const Property = @import("./property.zig");
|
||||
const Position = @import("./position.zig");
|
||||
|
||||
const Tileset = @This();
|
||||
|
||||
pub const Image = struct {
|
||||
source: []const u8,
|
||||
width: u32,
|
||||
height: u32,
|
||||
|
||||
pub fn intFromXml(arena: std.mem.Allocator, lexer: *xml.Lexer) !Image {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const attrs = try iter.begin("image");
|
||||
|
||||
const source = try attrs.getDupe(arena, "width") orelse return error.MissingSource;
|
||||
const width = try attrs.getNumber(u32, "width") orelse return error.MissingWidth;
|
||||
const height = try attrs.getNumber(u32, "height") orelse return error.MissingHeight;
|
||||
|
||||
try iter.finish("image");
|
||||
|
||||
return Image{
|
||||
.source = source,
|
||||
.width = width,
|
||||
.height = height
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Tile = struct {
|
||||
id: u32,
|
||||
properties: Property.List,
|
||||
|
||||
pub fn initFromXml(
|
||||
arena: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer,
|
||||
) !Tile {
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const tile_attrs = try iter.begin("tile");
|
||||
|
||||
const id = try tile_attrs.getNumber(u32, "id") orelse return error.MissingId;
|
||||
|
||||
var properties: Property.List = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("properties")) {
|
||||
properties = try Property.List.initFromXml(arena, scratch, lexer);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
|
||||
try iter.finish("tile");
|
||||
|
||||
return Tile{
|
||||
.id = id,
|
||||
.properties = properties
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const List = struct {
|
||||
const Entry = struct {
|
||||
name: []const u8,
|
||||
tileset: Tileset
|
||||
};
|
||||
|
||||
list: std.ArrayList(Entry),
|
||||
|
||||
pub const empty = List{
|
||||
.list = .empty
|
||||
};
|
||||
|
||||
pub fn add(self: *List, gpa: Allocator, name: []const u8, tileset: Tileset) !void {
|
||||
if (self.get(name) != null) {
|
||||
return error.DuplicateName;
|
||||
}
|
||||
|
||||
const name_dupe = try gpa.dupe(u8, name);
|
||||
errdefer gpa.free(name_dupe);
|
||||
|
||||
try self.list.append(gpa, .{
|
||||
.name = name_dupe,
|
||||
.tileset = tileset
|
||||
});
|
||||
}
|
||||
|
||||
pub fn get(self: *const List, name: []const u8) ?*const Tileset {
|
||||
for (self.list.items) |*entry| {
|
||||
if (std.mem.eql(u8, entry.name, name)) {
|
||||
return &entry.tileset;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *List, gpa: Allocator) void {
|
||||
for (self.list.items) |entry| {
|
||||
gpa.free(entry.name);
|
||||
entry.tileset.deinit();
|
||||
}
|
||||
self.list.deinit(gpa);
|
||||
}
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
version: []const u8,
|
||||
tiled_version: []const u8,
|
||||
name: []const u8,
|
||||
tile_width: u32,
|
||||
tile_height: u32,
|
||||
tile_count: u32,
|
||||
columns: u32,
|
||||
|
||||
image: Image,
|
||||
|
||||
tiles: []Tile,
|
||||
|
||||
pub fn initFromBuffer(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
buffer: []const u8
|
||||
) !Tileset {
|
||||
var reader = Io.Reader.fixed(buffer);
|
||||
return initFromReader(gpa, scratch, xml_buffers, &reader);
|
||||
}
|
||||
|
||||
pub fn initFromReader(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
xml_buffers: *xml.Lexer.Buffers,
|
||||
reader: *Io.Reader,
|
||||
) !Tileset {
|
||||
var lexer = xml.Lexer.init(reader, xml_buffers);
|
||||
return initFromXml(gpa, scratch, &lexer);
|
||||
}
|
||||
|
||||
pub fn initFromXml(
|
||||
gpa: std.mem.Allocator,
|
||||
scratch: *std.heap.ArenaAllocator,
|
||||
lexer: *xml.Lexer
|
||||
) !Tileset {
|
||||
var arena_state = std.heap.ArenaAllocator.init(gpa);
|
||||
const arena = arena_state.allocator();
|
||||
|
||||
var iter = xml.TagParser.init(lexer);
|
||||
const tileset_attrs = try iter.begin("tileset");
|
||||
|
||||
const version = try tileset_attrs.getDupe(arena, "version") orelse return error.MissingTilesetTag;
|
||||
const tiled_version = try tileset_attrs.getDupe(arena, "tiledversion") orelse return error.MissingTilesetTag;
|
||||
const name = try tileset_attrs.getDupe(arena, "name") orelse return error.MissingTilesetTag;
|
||||
const tile_width = try tileset_attrs.getNumber(u32, "tilewidth") orelse return error.MissingTilesetTag;
|
||||
const tile_height = try tileset_attrs.getNumber(u32, "tileheight") orelse return error.MissingTilesetTag;
|
||||
const tile_count = try tileset_attrs.getNumber(u32, "tilecount") orelse return error.MissingTilesetTag;
|
||||
const columns = try tileset_attrs.getNumber(u32, "columns") orelse return error.MissingTilesetTag;
|
||||
|
||||
var image: ?Image = null;
|
||||
var tiles_list: std.ArrayList(Tile) = .empty;
|
||||
|
||||
while (try iter.next()) |node| {
|
||||
if (node.isTag("image")) {
|
||||
image = try Image.intFromXml(arena, lexer);
|
||||
continue;
|
||||
} else if (node.isTag("tile")) {
|
||||
const tile = try Tile.initFromXml(arena, scratch, lexer);
|
||||
try tiles_list.append(scratch.allocator(), tile);
|
||||
continue;
|
||||
}
|
||||
|
||||
try iter.skip();
|
||||
}
|
||||
try iter.finish("tileset");
|
||||
|
||||
const tiles = try arena.dupe(Tile, tiles_list.items);
|
||||
|
||||
return Tileset{
|
||||
.arena = arena_state,
|
||||
.version = version,
|
||||
.tiled_version = tiled_version,
|
||||
.name = name,
|
||||
.tile_width = tile_width,
|
||||
.tile_height = tile_height,
|
||||
.tile_count = tile_count,
|
||||
.columns = columns,
|
||||
.image = image orelse return error.MissingImageTag,
|
||||
.tiles = tiles
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getTileProperties(self: *const Tileset, id: u32) ?Property.List {
|
||||
for (self.tiles) |tile| {
|
||||
if (tile.id == id) {
|
||||
return tile.properties;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getTilePositionInImage(self: *const Tileset, id: u32) ?Position {
|
||||
if (id >= self.tile_count) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tileset_width = @divExact(self.image.width, self.tile_width);
|
||||
|
||||
const tile_x = @mod(id, tileset_width);
|
||||
const tile_y = @divFloor(id, tileset_width);
|
||||
|
||||
return Position{
|
||||
.x = @floatFromInt(tile_x * self.tile_width),
|
||||
.y = @floatFromInt(tile_y * self.tile_height),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Tileset) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
687
libs/tiled/src/xml.zig
Normal file
687
libs/tiled/src/xml.zig
Normal file
@ -0,0 +1,687 @@
|
||||
const std = @import("std");
|
||||
const Io = std.Io;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Color = @import("./color.zig");
|
||||
|
||||
pub const Attribute = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
|
||||
pub const List = struct {
|
||||
items: []const Attribute,
|
||||
|
||||
pub fn get(self: List, name: []const u8) ?[]const u8 {
|
||||
for (self.items) |attr| {
|
||||
if (std.mem.eql(u8, attr.name, name)) {
|
||||
return attr.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getDupe(self: List, gpa: std.mem.Allocator, name: []const u8) !?[]u8 {
|
||||
if (self.get(name)) |value| {
|
||||
return try gpa.dupe(u8, value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getNumber(self: List, T: type, name: []const u8) !?T {
|
||||
if (self.get(name)) |value| {
|
||||
if (@typeInfo(T) == .int) {
|
||||
return try std.fmt.parseInt(T, value, 10);
|
||||
} else if (@typeInfo(T) == .float) {
|
||||
return try std.fmt.parseFloat(T, value);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getBool(self: List, name: []const u8, true_value: []const u8, false_value: []const u8) !?bool {
|
||||
if (self.get(name)) |value| {
|
||||
if (std.mem.eql(u8, value, true_value)) {
|
||||
return true;
|
||||
} else if (std.mem.eql(u8, value, false_value)) {
|
||||
return false;
|
||||
} else {
|
||||
return error.InvalidBoolean;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getEnum(self: List, T: type, name: []const u8, map: std.StaticStringMap(T)) !?T {
|
||||
if (self.get(name)) |value| {
|
||||
return map.get(value) orelse return error.InvalidEnumValue;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn getColor(self: List, name: []const u8, hash_required: bool) !?Color {
|
||||
if (self.get(name)) |value| {
|
||||
return try Color.parse(value, hash_required);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn format(self: List, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (self.items.len > 0) {
|
||||
try writer.writeAll("{ ");
|
||||
for (self.items, 0..) |attribute, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeAll(", ");
|
||||
}
|
||||
try writer.print("{f}", .{attribute});
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
} else {
|
||||
try writer.writeAll("{ }");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn format(self: *const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
try writer.print("{s}{{ .name='{s}', .value='{s}' }}", .{ @typeName(Attribute), self.name, self.value });
|
||||
}
|
||||
|
||||
pub fn formatSlice(data: []const Attribute, writer: *Io.Writer) Io.Writer.Error!void {
|
||||
if (data.len > 0) {
|
||||
try writer.writeAll("{ ");
|
||||
for (data, 0..) |attribute, i| {
|
||||
if (i > 0) {
|
||||
try writer.writeAll(", ");
|
||||
}
|
||||
try writer.print("{f}", .{attribute});
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
} else {
|
||||
try writer.writeAll("{ }");
|
||||
}
|
||||
}
|
||||
|
||||
fn altSlice(data: []const Attribute) std.fmt.Alt(Attribute.List, Attribute.List.format) {
|
||||
return .{ .data = data };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Tag = struct {
|
||||
name: []const u8,
|
||||
attributes: Attribute.List
|
||||
};
|
||||
|
||||
pub const Lexer = struct {
|
||||
pub const Buffers = struct {
|
||||
scratch: std.heap.ArenaAllocator,
|
||||
text: std.ArrayList(u8),
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) Buffers {
|
||||
return Buffers{
|
||||
.scratch = std.heap.ArenaAllocator.init(allocator),
|
||||
.text = .empty
|
||||
};
|
||||
}
|
||||
|
||||
pub fn clear(self: *Buffers) void {
|
||||
self.text.clearRetainingCapacity();
|
||||
_ = self.scratch.reset(.retain_capacity);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Buffers) void {
|
||||
const allocator = self.scratch.child_allocator;
|
||||
self.scratch.deinit();
|
||||
self.text.deinit(allocator);
|
||||
}
|
||||
};
|
||||
|
||||
pub const Token = union(enum) {
|
||||
start_tag: Tag,
|
||||
end_tag: []const u8,
|
||||
text: []const u8,
|
||||
|
||||
pub fn isStartTag(self: Token, name: []const u8) bool {
|
||||
if (self == .start_tag) {
|
||||
return std.mem.eql(u8, self.start_tag.name, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn isEndTag(self: Token, name: []const u8) bool {
|
||||
if (self == .end_tag) {
|
||||
return std.mem.eql(u8, self.end_tag, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const StepResult = struct {
|
||||
token: ?Token,
|
||||
self_closing: bool = false,
|
||||
};
|
||||
|
||||
pub const TestingContext = struct {
|
||||
io_reader: Io.Reader,
|
||||
buffers: Buffers,
|
||||
lexer: Lexer,
|
||||
|
||||
pub fn init(self: *TestingContext, allocator: std.mem.Allocator, body: []const u8) void {
|
||||
self.* = TestingContext{
|
||||
.lexer = undefined,
|
||||
.io_reader = Io.Reader.fixed(body),
|
||||
.buffers = Buffers.init(allocator)
|
||||
};
|
||||
self.lexer = Lexer.init(&self.io_reader, &self.buffers);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *TestingContext) void {
|
||||
self.buffers.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
io_reader: *Io.Reader,
|
||||
buffers: *Buffers,
|
||||
|
||||
peeked_value: ?Token,
|
||||
cursor: usize,
|
||||
queued_end_tag: ?[]const u8,
|
||||
|
||||
pub fn init(reader: *Io.Reader, buffers: *Buffers) Lexer {
|
||||
buffers.clear();
|
||||
|
||||
return Lexer{
|
||||
.io_reader = reader,
|
||||
.buffers = buffers,
|
||||
.cursor = 0,
|
||||
.queued_end_tag = null,
|
||||
.peeked_value = null
|
||||
};
|
||||
}
|
||||
|
||||
fn step(self: *Lexer) !StepResult {
|
||||
_ = self.buffers.scratch.reset(.retain_capacity);
|
||||
|
||||
if (try self.peekByte() == '<') {
|
||||
self.tossByte();
|
||||
|
||||
if (try self.peekByte() == '/') {
|
||||
// End tag
|
||||
self.tossByte();
|
||||
|
||||
const name = try self.parseName();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
if (!std.mem.eql(u8, try self.takeBytes(1), ">")) {
|
||||
return error.InvalidEndTag;
|
||||
}
|
||||
|
||||
const token = Token{ .end_tag = name };
|
||||
return .{ .token = token };
|
||||
|
||||
} else if (try self.peekByte() == '?') {
|
||||
// Prolog tag
|
||||
self.tossByte();
|
||||
if (!std.mem.eql(u8, try self.takeBytes(4), "xml ")) {
|
||||
return error.InvalidPrologTag;
|
||||
}
|
||||
|
||||
const attributes = try self.parseAttributes();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
if (!std.mem.eql(u8, try self.takeBytes(2), "?>")) {
|
||||
return error.MissingPrologEnd;
|
||||
}
|
||||
|
||||
const version = attributes.get("version") orelse return error.InvalidProlog;
|
||||
if (!std.mem.eql(u8, version, "1.0")) {
|
||||
return error.InvalidPrologVersion;
|
||||
}
|
||||
const encoding = attributes.get("encoding") orelse return error.InvalidProlog;
|
||||
if (!std.mem.eql(u8, encoding, "UTF-8")) {
|
||||
return error.InvalidPrologEncoding;
|
||||
}
|
||||
|
||||
return .{ .token = null };
|
||||
|
||||
} else {
|
||||
// Start tag
|
||||
const name = try self.parseName();
|
||||
const attributes = try self.parseAttributes();
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
const token = Token{
|
||||
.start_tag = .{
|
||||
.name = name,
|
||||
.attributes = attributes
|
||||
}
|
||||
};
|
||||
|
||||
var self_closing = false;
|
||||
if (std.mem.eql(u8, try self.peekBytes(1), ">")) {
|
||||
self.tossBytes(1);
|
||||
|
||||
} else if (std.mem.eql(u8, try self.peekBytes(2), "/>")) {
|
||||
self.tossBytes(2);
|
||||
|
||||
self_closing = true;
|
||||
|
||||
} else {
|
||||
return error.UnfinishedStartTag;
|
||||
}
|
||||
|
||||
return .{
|
||||
.token = token,
|
||||
.self_closing = self_closing
|
||||
};
|
||||
}
|
||||
|
||||
} else {
|
||||
try self.skipWhiteSpace();
|
||||
|
||||
const text_start = self.cursor;
|
||||
while (try self.peekByte() != '<') {
|
||||
self.tossByte();
|
||||
}
|
||||
var text: []const u8 = self.buffers.text.items[text_start..self.cursor];
|
||||
text = std.mem.trimEnd(u8, text, &std.ascii.whitespace);
|
||||
|
||||
var token: ?Token = null;
|
||||
if (text.len > 0) {
|
||||
token = Token{ .text = text };
|
||||
}
|
||||
return .{ .token = token };
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next(self: *Lexer) !?Token {
|
||||
if (self.peeked_value) |value| {
|
||||
self.peeked_value = null;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (self.queued_end_tag) |name| {
|
||||
self.queued_end_tag = null;
|
||||
return Token{
|
||||
.end_tag = name
|
||||
};
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if (self.buffers.text.items.len == 0) {
|
||||
self.readIntoTextBuffer() catch |e| switch (e) {
|
||||
error.EndOfStream => break,
|
||||
else => return e
|
||||
};
|
||||
}
|
||||
|
||||
const saved_cursor = self.cursor;
|
||||
const result = self.step() catch |e| switch(e) {
|
||||
error.EndOfTextBuffer => {
|
||||
self.cursor = saved_cursor;
|
||||
|
||||
const unused_capacity = self.buffers.text.capacity - self.buffers.text.items.len;
|
||||
if (unused_capacity == 0 and self.cursor > 0) {
|
||||
self.rebaseBuffer();
|
||||
} else {
|
||||
self.readIntoTextBuffer() catch |read_err| switch (read_err) {
|
||||
error.EndOfStream => break,
|
||||
else => return read_err
|
||||
};
|
||||
}
|
||||
|
||||
continue;
|
||||
},
|
||||
else => return e
|
||||
};
|
||||
|
||||
if (result.token) |token| {
|
||||
if (token == .start_tag and result.self_closing) {
|
||||
self.queued_end_tag = token.start_tag.name;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn nextExpectEndTag(self: *Lexer, name: []const u8) !void {
|
||||
const value = try self.next() orelse return error.MissingEndTag;
|
||||
if (!value.isEndTag(name)) return error.MissingEndTag;
|
||||
}
|
||||
|
||||
pub fn nextExpectStartTag(self: *Lexer, name: []const u8) !Attribute.List {
|
||||
const value = try self.next() orelse return error.MissingStartTag;
|
||||
if (!value.isStartTag(name)) return error.MissingStartTag;
|
||||
return value.start_tag.attributes;
|
||||
}
|
||||
|
||||
pub fn nextExpectText(self: *Lexer) ![]const u8 {
|
||||
const value = try self.next() orelse return error.MissingTextTag;
|
||||
if (value != .text) return error.MissingTextTag;
|
||||
return value.text;
|
||||
}
|
||||
|
||||
pub fn skipUntilMatchingEndTag(self: *Lexer, name: ?[]const u8) !void {
|
||||
var depth: usize = 0;
|
||||
while (true) {
|
||||
const value = try self.next() orelse return error.MissingEndTag;
|
||||
|
||||
if (depth == 0 and value == .end_tag) {
|
||||
if (name != null and !std.mem.eql(u8, value.end_tag, name.?)) {
|
||||
return error.MismatchedEndTag;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (value == .start_tag) {
|
||||
depth += 1;
|
||||
} else if (value == .end_tag) {
|
||||
depth -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peek(self: *Lexer) !?Token {
|
||||
if (try self.next()) |value| {
|
||||
self.peeked_value = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
fn readIntoTextBuffer(self: *Lexer) !void {
|
||||
const gpa = self.buffers.scratch.child_allocator;
|
||||
const text = &self.buffers.text;
|
||||
try text.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
var writer = Io.Writer.fixed(text.allocatedSlice());
|
||||
writer.end = text.items.len;
|
||||
|
||||
_ = self.io_reader.stream(&writer, .limited(text.capacity - text.items.len)) catch |e| switch (e) {
|
||||
error.WriteFailed => unreachable,
|
||||
else => |ee| return ee
|
||||
};
|
||||
text.items.len = writer.end;
|
||||
}
|
||||
|
||||
fn rebaseBuffer(self: *Lexer) void {
|
||||
if (self.cursor == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = &self.buffers.text;
|
||||
@memmove(
|
||||
text.items[0..(text.items.len - self.cursor)],
|
||||
text.items[self.cursor..]
|
||||
);
|
||||
|
||||
text.items.len -= self.cursor;
|
||||
self.cursor = 0;
|
||||
}
|
||||
|
||||
fn isNameStartChar(c: u8) bool {
|
||||
return c == ':' or c == '_' or std.ascii.isAlphabetic(c);
|
||||
}
|
||||
|
||||
fn isNameChar(c: u8) bool {
|
||||
return isNameStartChar(c) or c == '-' or c == '.' or ('0' <= c and c <= '9');
|
||||
}
|
||||
|
||||
fn hasBytes(self: *Lexer, n: usize) bool {
|
||||
const text = self.buffers.text.items;
|
||||
return self.cursor + n <= text.len;
|
||||
}
|
||||
|
||||
fn peekBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||
if (self.hasBytes(n)) {
|
||||
const text = self.buffers.text.items;
|
||||
return text[self.cursor..][0..n];
|
||||
}
|
||||
return error.EndOfTextBuffer;
|
||||
}
|
||||
|
||||
fn tossBytes(self: *Lexer, n: usize) void {
|
||||
assert(self.hasBytes(n));
|
||||
self.cursor += n;
|
||||
}
|
||||
|
||||
fn takeBytes(self: *Lexer, n: usize) ![]const u8 {
|
||||
const result = try self.peekBytes(n);
|
||||
self.tossBytes(n);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn peekByte(self: *Lexer) !u8 {
|
||||
return (try self.peekBytes(1))[0];
|
||||
}
|
||||
|
||||
fn tossByte(self: *Lexer) void {
|
||||
self.tossBytes(1);
|
||||
}
|
||||
|
||||
fn takeByte(self: *Lexer) !u8 {
|
||||
return (try self.takeBytes(1))[0];
|
||||
}
|
||||
|
||||
fn parseName(self: *Lexer) ![]const u8 {
|
||||
const name_start = self.cursor;
|
||||
|
||||
if (isNameStartChar(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
|
||||
while (isNameChar(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
}
|
||||
}
|
||||
|
||||
return self.buffers.text.items[name_start..self.cursor];
|
||||
}
|
||||
|
||||
fn skipWhiteSpace(self: *Lexer) !void {
|
||||
while (std.ascii.isWhitespace(try self.peekByte())) {
|
||||
self.tossByte();
|
||||
}
|
||||
}
|
||||
|
||||
fn parseAttributeValue(self: *Lexer) ![]const u8 {
|
||||
const quote = try self.takeByte();
|
||||
if (quote != '"' and quote != '\'') {
|
||||
return error.InvalidAttributeValue;
|
||||
}
|
||||
|
||||
const value_start: usize = self.cursor;
|
||||
var value_len: usize = 0;
|
||||
|
||||
while (true) {
|
||||
const c = try self.takeByte();
|
||||
if (c == '<' or c == '&') {
|
||||
return error.InvalidAttributeValue;
|
||||
}
|
||||
if (c == quote) {
|
||||
break;
|
||||
}
|
||||
value_len += 1;
|
||||
}
|
||||
|
||||
return self.buffers.text.items[value_start..][0..value_len];
|
||||
}
|
||||
|
||||
fn parseAttributes(self: *Lexer) !Attribute.List {
|
||||
const arena = self.buffers.scratch.allocator();
|
||||
var attributes: std.ArrayList(Attribute) = .empty;
|
||||
|
||||
while (true) {
|
||||
try self.skipWhiteSpace();
|
||||
const name = try self.parseName();
|
||||
if (name.len == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
try self.skipWhiteSpace();
|
||||
if (try self.takeByte() != '=') {
|
||||
return error.MissingAttributeEquals;
|
||||
}
|
||||
try self.skipWhiteSpace();
|
||||
const value = try self.parseAttributeValue();
|
||||
|
||||
const list = Attribute.List{ .items = attributes.items };
|
||||
if (list.get(name) != null) {
|
||||
return error.DuplicateAttribute;
|
||||
}
|
||||
|
||||
try attributes.append(arena, Attribute{
|
||||
.name = name,
|
||||
.value = value
|
||||
});
|
||||
}
|
||||
|
||||
return Attribute.List{
|
||||
.items = attributes.items
|
||||
};
|
||||
}
|
||||
|
||||
test "self closing tag" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello />
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "tag" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello></hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "tag with prolog" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <?xml version="1.0" encoding="UTF-8"?>
|
||||
\\ <hello></hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "text content" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello> Hello World </hello>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isStartTag("hello"));
|
||||
try std.testing.expectEqualStrings("Hello World", (try ctx.lexer.next()).?.text);
|
||||
try std.testing.expect((try ctx.lexer.next()).?.isEndTag("hello"));
|
||||
try std.testing.expect((try ctx.lexer.next()) == null);
|
||||
}
|
||||
|
||||
test "attributes" {
|
||||
const allocator = std.testing.allocator;
|
||||
var ctx: TestingContext = undefined;
|
||||
ctx.init(allocator,
|
||||
\\ <hello a='1' b='2'/>
|
||||
);
|
||||
defer ctx.deinit();
|
||||
|
||||
const token = try ctx.lexer.next();
|
||||
const attrs = token.?.start_tag.attributes;
|
||||
try std.testing.expectEqualStrings("1", attrs.get("a").?);
|
||||
try std.testing.expectEqualStrings("2", attrs.get("b").?);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: The API for this is easy to misuse.
|
||||
// Design a better API for using Reader
|
||||
// As a compromise `assert` was used to guard against some of the ways this can be misused
|
||||
pub const TagParser = struct {
|
||||
lexer: *Lexer,
|
||||
|
||||
begin_called: bool = false,
|
||||
finish_called: bool = false,
|
||||
|
||||
pub const Node = union(enum) {
|
||||
tag: Tag,
|
||||
text: []const u8,
|
||||
|
||||
pub fn isTag(self: Node, name: []const u8) bool {
|
||||
if (self == .tag) {
|
||||
return std.mem.eql(u8, self.tag.name, name);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init(lexer: *Lexer) TagParser {
|
||||
return TagParser{
|
||||
.lexer = lexer,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn begin(self: *TagParser, name: []const u8) !Attribute.List {
|
||||
assert(!self.begin_called);
|
||||
self.begin_called = true;
|
||||
|
||||
return try self.lexer.nextExpectStartTag(name);
|
||||
}
|
||||
|
||||
pub fn finish(self: *TagParser, name: []const u8) !void {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
self.finish_called = true;
|
||||
|
||||
try self.lexer.skipUntilMatchingEndTag(name);
|
||||
}
|
||||
|
||||
pub fn next(self: *TagParser) !?Node {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
|
||||
const value = try self.lexer.peek() orelse return error.MissingEndTag;
|
||||
if (value == .end_tag) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return switch (value) {
|
||||
.text => |text| Node{ .text = text },
|
||||
.start_tag => |start_tag| Node{ .tag = start_tag },
|
||||
.end_tag => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn skip(self: *TagParser) !void {
|
||||
assert(self.begin_called);
|
||||
assert(!self.finish_called);
|
||||
|
||||
const value = try self.lexer.next() orelse return error.MissingNode;
|
||||
if (value == .end_tag) {
|
||||
return error.UnexpectedEndTag;
|
||||
} else if (value == .start_tag) {
|
||||
// TODO: Make this configurable
|
||||
var name_buffer: [64]u8 = undefined;
|
||||
var name: std.ArrayList(u8) = .initBuffer(&name_buffer);
|
||||
try name.appendSliceBounded(value.start_tag.name);
|
||||
|
||||
try self.lexer.skipUntilMatchingEndTag(name.items);
|
||||
}
|
||||
}
|
||||
};
|
||||
155
src/assets.zig
Normal file
155
src/assets.zig
Normal file
@ -0,0 +1,155 @@
|
||||
const std = @import("std");
|
||||
|
||||
const tiled = @import("tiled");
|
||||
|
||||
const Math = @import("./engine/math.zig");
|
||||
const Engine = @import("./engine/root.zig");
|
||||
const STBImage = @import("stb_image");
|
||||
const Gfx = Engine.Graphics;
|
||||
const Audio = Engine.Audio;
|
||||
const Vec2 = Engine.Vec2;
|
||||
const Rect = Engine.Math.Rect;
|
||||
|
||||
const Assets = @This();
|
||||
|
||||
const FontName = enum {
|
||||
regular,
|
||||
bold,
|
||||
italic,
|
||||
|
||||
const EnumArray = std.EnumArray(FontName, Gfx.Font.Id);
|
||||
};
|
||||
|
||||
pub const Tilemap = struct {
|
||||
texture: Gfx.TextureId,
|
||||
tile_size: Engine.Vec2,
|
||||
|
||||
|
||||
pub fn getTileUV(self: Tilemap, tile_x: f32, tile_y: f32) Rect {
|
||||
const texture_info = Engine.Graphics.getTextureInfo(self.texture);
|
||||
const tilemap_size = Vec2.initFromInt(u32, texture_info.width, texture_info.height);
|
||||
|
||||
return .{
|
||||
.pos = Vec2.init(tile_x, tile_y).multiply(self.tile_size).divide(tilemap_size),
|
||||
.size = self.tile_size.divide(tilemap_size),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
font_id: FontName.EnumArray,
|
||||
wood01: Audio.Data.Id,
|
||||
map: tiled.Tilemap,
|
||||
tilesets: tiled.Tileset.List,
|
||||
move_sound: []Audio.Data.Id,
|
||||
|
||||
terrain_tilemap: Tilemap,
|
||||
players_tilemap: Tilemap,
|
||||
weapons_tilemap: Tilemap,
|
||||
|
||||
pub fn init(gpa: std.mem.Allocator) !Assets {
|
||||
var arena = std.heap.ArenaAllocator.init(gpa);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const font_id_array: FontName.EnumArray = .init(.{
|
||||
.regular = try Gfx.addFont("regular", @embedFile("assets/roboto-font/Roboto-Regular.ttf")),
|
||||
.bold = try Gfx.addFont("bold", @embedFile("assets/roboto-font/Roboto-Bold.ttf")),
|
||||
.italic = try Gfx.addFont("italic", @embedFile("assets/roboto-font/Roboto-Italic.ttf")),
|
||||
});
|
||||
|
||||
const wood01 = try Audio.load(.{
|
||||
.format = .vorbis,
|
||||
.data = @embedFile("assets/wood01.ogg"),
|
||||
});
|
||||
|
||||
var scratch = std.heap.ArenaAllocator.init(gpa);
|
||||
defer scratch.deinit();
|
||||
|
||||
var xml_buffers = tiled.xml.Lexer.Buffers.init(gpa);
|
||||
defer xml_buffers.deinit();
|
||||
|
||||
const map = try tiled.Tilemap.initFromBuffer(
|
||||
gpa,
|
||||
&scratch,
|
||||
&xml_buffers,
|
||||
@embedFile("assets/map.tmx")
|
||||
);
|
||||
|
||||
const tileset = try tiled.Tileset.initFromBuffer(
|
||||
gpa,
|
||||
&scratch,
|
||||
&xml_buffers,
|
||||
@embedFile("assets/tileset.tsx")
|
||||
);
|
||||
|
||||
var tilesets: tiled.Tileset.List = .empty;
|
||||
try tilesets.add(gpa, "tilemap.tsx", tileset);
|
||||
|
||||
const players_image = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Players/tilemap_packed.png"));
|
||||
defer players_image.deinit();
|
||||
const players_texture = try Gfx.addTexture(&.{
|
||||
.{
|
||||
.width = players_image.width,
|
||||
.height = players_image.height,
|
||||
.rgba = players_image.rgba8_pixels
|
||||
}
|
||||
});
|
||||
|
||||
const tileset_image = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Tiles/tilemap_packed.png"));
|
||||
defer tileset_image.deinit();
|
||||
const tileset_texture = try Gfx.addTexture(&.{
|
||||
.{
|
||||
.width = tileset_image.width,
|
||||
.height = tileset_image.height,
|
||||
.rgba = tileset_image.rgba8_pixels
|
||||
}
|
||||
});
|
||||
|
||||
const weapons_tileset = try STBImage.load(@embedFile("assets/kenney_desert-shooter-pack_1.0/PNG/Weapons/tilemap_packed.png"));
|
||||
defer weapons_tileset.deinit();
|
||||
const weapons_texture = try Gfx.addTexture(&.{
|
||||
.{
|
||||
.width = weapons_tileset.width,
|
||||
.height = weapons_tileset.height,
|
||||
.rgba = weapons_tileset.rgba8_pixels
|
||||
}
|
||||
});
|
||||
|
||||
const move_c = try Audio.load(.{
|
||||
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg"),
|
||||
.format = .vorbis,
|
||||
});
|
||||
const move_d = try Audio.load(.{
|
||||
.data = @embedFile("assets/kenney_desert-shooter-pack_1.0/Sounds/move-d.ogg"),
|
||||
.format = .vorbis,
|
||||
});
|
||||
const move_sound = try arena.allocator().dupe(Audio.Data.Id, &.{ move_c, move_d });
|
||||
|
||||
return Assets{
|
||||
.arena = arena,
|
||||
.font_id = font_id_array,
|
||||
.wood01 = wood01,
|
||||
.map = map,
|
||||
.tilesets = tilesets,
|
||||
.move_sound = move_sound,
|
||||
.terrain_tilemap = .{
|
||||
.texture = tileset_texture,
|
||||
.tile_size = .initFromInt(u32, tileset.tile_width, tileset.tile_height)
|
||||
},
|
||||
.players_tilemap = .{
|
||||
.texture = players_texture,
|
||||
.tile_size = .init(24, 24)
|
||||
},
|
||||
.weapons_tilemap = .{
|
||||
.texture = weapons_texture,
|
||||
.tile_size = .init(24, 24)
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Assets, gpa: std.mem.Allocator) void {
|
||||
self.map.deinit();
|
||||
self.tilesets.deinit(gpa);
|
||||
self.arena.deinit();
|
||||
}
|
||||
14
src/assets/game-2026-01-18.tiled-project
Normal file
14
src/assets/game-2026-01-18.tiled-project
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"automappingRulesFile": "",
|
||||
"commands": [
|
||||
],
|
||||
"compatibilityVersion": 1100,
|
||||
"extensionsPath": "extensions",
|
||||
"folders": [
|
||||
"."
|
||||
],
|
||||
"properties": [
|
||||
],
|
||||
"propertyTypes": [
|
||||
]
|
||||
}
|
||||
BIN
src/assets/icon.png
Normal file
BIN
src/assets/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 795 B |
23
src/assets/kenney_desert-shooter-pack_1.0/License.txt
Normal file
23
src/assets/kenney_desert-shooter-pack_1.0/License.txt
Normal file
@ -0,0 +1,23 @@
|
||||
|
||||
|
||||
Desert Shooter Pack (1.0)
|
||||
|
||||
Created/distributed by Kenney (www.kenney.nl)
|
||||
Sponsored by: GameMaker (www.gamemaker.io)
|
||||
Creation date: 24-04-2024
|
||||
|
||||
------------------------------
|
||||
|
||||
License: (Creative Commons Zero, CC0)
|
||||
http://creativecommons.org/publicdomain/zero/1.0/
|
||||
|
||||
This content is free to use in personal, educational and commercial projects.
|
||||
Support us by crediting Kenney or www.kenney.nl (this is not mandatory)
|
||||
|
||||
------------------------------
|
||||
|
||||
Donate: http://support.kenney.nl
|
||||
Patreon: http://patreon.com/kenney/
|
||||
|
||||
Follow on Twitter for updates:
|
||||
http://twitter.com/KenneyNL
|
||||
@ -0,0 +1,9 @@
|
||||
Tilesheet information:
|
||||
|
||||
Tile size • 24px × 24px
|
||||
Space between tiles • 1px × 1px
|
||||
---
|
||||
Total tiles (horizontal) • 4 tiles
|
||||
Total tiles (vertical) • 4 tiles
|
||||
---
|
||||
Total tiles in sheet • 16 tiles
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
@ -0,0 +1,9 @@
|
||||
Tilesheet information:
|
||||
|
||||
Tile size • 16px × 16px
|
||||
Space between tiles • 1px × 1px
|
||||
---
|
||||
Total tiles (horizontal) • 18 tiles
|
||||
Total tiles (vertical) • 11 tiles
|
||||
---
|
||||
Total tiles in sheet • 198 tiles
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
@ -0,0 +1,9 @@
|
||||
Tilesheet information:
|
||||
|
||||
Tile size • 24px × 24px
|
||||
Space between tiles • 1px × 1px
|
||||
---
|
||||
Total tiles (horizontal) • 4 tiles
|
||||
Total tiles (vertical) • 4 tiles
|
||||
---
|
||||
Total tiles in sheet • 16 tiles
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,9 @@
|
||||
Tilesheet information:
|
||||
|
||||
Tile size • 16px × 16px
|
||||
Space between tiles • 1px × 1px
|
||||
---
|
||||
Total tiles (horizontal) • 18 tiles
|
||||
Total tiles (vertical) • 13 tiles
|
||||
---
|
||||
Total tiles in sheet • 234 tiles
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@ -0,0 +1,9 @@
|
||||
Tilesheet information:
|
||||
|
||||
Tile size • 24px × 24px
|
||||
Space between tiles • 1px × 1px
|
||||
---
|
||||
Total tiles (horizontal) • 10 tiles
|
||||
Total tiles (vertical) • 4 tiles
|
||||
---
|
||||
Total tiles in sheet • 40 tiles
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/coin-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/error-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/explosion-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/fall-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/fall-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/fall-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/fall-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-e.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/hurt-e.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-e.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-e.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-f.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/jump-f.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/lose-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/move-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/select-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/select-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-a.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-a.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-b.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-b.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-c.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-c.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-d.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-d.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-e.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-e.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-f.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-f.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-g.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-g.ogg
Normal file
Binary file not shown.
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-h.ogg
Normal file
BIN
src/assets/kenney_desert-shooter-pack_1.0/Sounds/shoot-h.ogg
Normal file
Binary file not shown.
225
src/assets/map.tmx
Normal file
225
src/assets/map.tmx
Normal file
@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.10" tiledversion="1.11.2" orientation="orthogonal" renderorder="right-down" width="30" height="20" tilewidth="16" tileheight="16" infinite="1" nextlayerid="5" nextobjectid="2">
|
||||
<tileset firstgid="1" source="tilemap.tsx"/>
|
||||
<layer id="1" name="ground" width="30" height="20">
|
||||
<data encoding="csv">
|
||||
<chunk x="-32" y="-32" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,91,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109
|
||||
</chunk>
|
||||
<chunk x="-16" y="-32" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,65,65,0,0,0,0,0,0,
|
||||
92,92,92,92,92,92,92,93,65,65,0,0,0,0,0,0,
|
||||
110,20,110,110,110,110,110,112,110,65,0,0,0,0,0,0,
|
||||
110,110,110,110,20,110,110,110,110,65,65,0,0,0,0,0
|
||||
</chunk>
|
||||
<chunk x="-32" y="-16" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,109,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,127,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
<chunk x="-16" y="-16" width="16" height="16">
|
||||
110,110,20,110,110,110,110,94,110,65,65,0,0,0,65,0,
|
||||
110,20,110,110,110,110,20,111,65,65,65,0,0,0,65,0,
|
||||
128,128,128,128,128,128,128,129,65,65,65,0,65,65,65,0,
|
||||
0,0,0,65,65,65,65,65,65,65,65,65,195,65,65,0,
|
||||
0,0,0,65,65,65,65,65,65,65,65,65,65,65,65,0,
|
||||
0,0,65,65,65,65,65,65,65,65,65,65,66,65,65,65,
|
||||
0,0,65,65,65,65,66,65,65,66,65,65,65,65,65,65,
|
||||
0,0,0,65,65,65,65,65,65,65,66,65,65,65,65,65,
|
||||
0,0,0,65,65,195,65,65,65,65,65,65,65,65,65,65,
|
||||
0,0,0,65,65,65,65,66,65,65,65,65,65,65,65,65,
|
||||
0,0,0,65,65,65,65,65,65,66,65,65,195,65,65,65,
|
||||
0,0,0,65,65,65,65,65,65,65,65,65,65,65,66,65,
|
||||
0,65,65,65,65,65,66,65,65,65,65,65,65,65,65,65,
|
||||
0,0,65,65,65,65,65,65,65,65,65,65,65,65,65,65,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
<chunk x="0" y="-16" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,65,65,0,0,0,0,0,0,0,0,0,0,0,
|
||||
65,65,65,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
65,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="3" name="walls" width="30" height="20">
|
||||
<properties>
|
||||
<property name="solid" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
<chunk x="-16" y="-32" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,1,2,2,2,2,3,0,0,
|
||||
0,0,0,0,0,0,0,0,19,110,20,110,20,21,0,0,
|
||||
0,0,0,0,0,0,0,0,19,110,110,20,20,21,0,0,
|
||||
0,0,0,0,0,0,0,0,37,38,38,38,38,39,0,0,
|
||||
0,0,0,0,0,0,0,0,73,56,174,174,174,57,0,0,
|
||||
0,0,0,0,0,0,0,0,73,174,174,56,174,57,0,0,
|
||||
0,0,0,0,0,0,0,0,73,174,174,174,174,57,0,0
|
||||
</chunk>
|
||||
<chunk x="-16" y="-16" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,73,174,56,56,174,57,0,0,
|
||||
0,0,0,0,0,0,0,0,73,174,174,174,56,57,0,0,
|
||||
0,0,0,0,0,0,0,0,73,74,74,74,74,75,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="4" name="walls 2" width="30" height="20">
|
||||
<properties>
|
||||
<property name="solid" type="bool" value="true"/>
|
||||
</properties>
|
||||
<data encoding="csv">
|
||||
<chunk x="-16" y="-32" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,37,38,38
|
||||
</chunk>
|
||||
<chunk x="0" y="-32" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
2,2,2,3,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
38,38,38,39,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
<chunk x="-16" y="-16" width="16" height="16">
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,73,174,174,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,174,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,73,56,56,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,73,74,74,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
<chunk x="0" y="-16" width="16" height="16">
|
||||
56,56,174,75,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
174,174,174,75,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
174,174,56,75,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
174,56,56,75,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
74,74,74,75,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
|
||||
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0
|
||||
</chunk>
|
||||
</data>
|
||||
</layer>
|
||||
<objectgroup id="2" name="Object Layer 1">
|
||||
<object id="1" name="spawnpoint" x="-118" y="-120.5">
|
||||
<point/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</map>
|
||||
93
src/assets/roboto-font/LICENSE.txt
Normal file
93
src/assets/roboto-font/LICENSE.txt
Normal file
@ -0,0 +1,93 @@
|
||||
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
src/assets/roboto-font/Roboto-Bold.ttf
Normal file
BIN
src/assets/roboto-font/Roboto-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/roboto-font/Roboto-Italic.ttf
Normal file
BIN
src/assets/roboto-font/Roboto-Italic.ttf
Normal file
Binary file not shown.
BIN
src/assets/roboto-font/Roboto-Regular.ttf
Normal file
BIN
src/assets/roboto-font/Roboto-Regular.ttf
Normal file
Binary file not shown.
4
src/assets/tilemap.tsx
Normal file
4
src/assets/tilemap.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.11.2" name="tilemap" tilewidth="16" tileheight="16" tilecount="234" columns="18">
|
||||
<image source="kenney_desert-shooter-pack_1.0/PNG/Tiles/tilemap_packed.png" width="288" height="208"/>
|
||||
</tileset>
|
||||
4
src/assets/tileset.tsx
Normal file
4
src/assets/tileset.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.10" tiledversion="1.11.2" name="tilemap" tilewidth="16" tileheight="16" tilecount="234" columns="18">
|
||||
<image source="kenney_desert-shooter-pack_1.0/PNG/Tiles/Tilemap/tilemap_packed.png" width="288" height="208"/>
|
||||
</tileset>
|
||||
BIN
src/assets/wood01.ogg
Normal file
BIN
src/assets/wood01.ogg
Normal file
Binary file not shown.
BIN
src/assets/wood02.ogg
Normal file
BIN
src/assets/wood02.ogg
Normal file
Binary file not shown.
BIN
src/assets/wood03.ogg
Normal file
BIN
src/assets/wood03.ogg
Normal file
Binary file not shown.
74
src/engine/audio/data.zig
Normal file
74
src/engine/audio/data.zig
Normal file
@ -0,0 +1,74 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
|
||||
pub const Data = union(enum) {
|
||||
raw: struct {
|
||||
channels: [][*]f32,
|
||||
sample_count: u32,
|
||||
sample_rate: u32
|
||||
},
|
||||
vorbis: struct {
|
||||
alloc_buffer: []u8,
|
||||
stb_vorbis: STBVorbis,
|
||||
},
|
||||
|
||||
pub fn streamChannel(
|
||||
self: Data,
|
||||
buffer: []f32,
|
||||
cursor: u32,
|
||||
channel_index: u32,
|
||||
sample_rate: u32
|
||||
) []f32 {
|
||||
// var result: std.ArrayList(f32) = .initBuffer(buffer);
|
||||
switch (self) {
|
||||
.raw => |opts| {
|
||||
if (opts.sample_rate == sample_rate) {
|
||||
assert(channel_index < opts.channels.len); // TODO:
|
||||
const channel = opts.channels[channel_index];
|
||||
|
||||
var memcpy_len: usize = 0;
|
||||
if (cursor + buffer.len <= opts.sample_count) {
|
||||
memcpy_len = buffer.len;
|
||||
} else if (cursor < opts.sample_count) {
|
||||
memcpy_len = opts.sample_count - cursor;
|
||||
}
|
||||
|
||||
@memcpy(buffer[0..memcpy_len], channel[cursor..][0..memcpy_len]);
|
||||
return buffer[0..memcpy_len];
|
||||
} else {
|
||||
// const in_sample_rate: f32 = @floatFromInt(opts.sample_rate);
|
||||
// const out_sample_rate: f32 = @floatFromInt(sample_rate);
|
||||
// const increment = in_sample_rate / out_sample_rate;
|
||||
// _ = increment; // autofix
|
||||
unreachable;
|
||||
}
|
||||
},
|
||||
.vorbis => |opts| {
|
||||
_ = opts; // autofix
|
||||
unreachable;
|
||||
},
|
||||
}
|
||||
// return result.items;
|
||||
}
|
||||
|
||||
pub fn getSampleCount(self: Data) u32 {
|
||||
return switch (self) {
|
||||
.raw => |opts| opts.sample_count,
|
||||
.vorbis => |opts| opts.stb_vorbis.getStreamLengthInSamples()
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getSampleRate(self: Data) u32 {
|
||||
return switch (self) {
|
||||
.raw => |opts| opts.sample_rate,
|
||||
.vorbis => |opts| blk: {
|
||||
const info = opts.stb_vorbis.getInfo();
|
||||
break :blk info.sample_rate;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Id = enum (u16) { _ };
|
||||
};
|
||||
152
src/engine/audio/mixer.zig
Normal file
152
src/engine/audio/mixer.zig
Normal file
@ -0,0 +1,152 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.audio);
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const saudio = sokol.audio;
|
||||
|
||||
const AudioData = @import("./data.zig").Data;
|
||||
pub const Store = @import("./store.zig");
|
||||
|
||||
const Mixer = @This();
|
||||
|
||||
pub const Instance = struct {
|
||||
data_id: AudioData.Id,
|
||||
volume: f32 = 0,
|
||||
cursor: u32 = 0,
|
||||
};
|
||||
|
||||
pub const Command = union(enum) {
|
||||
pub const Play = struct {
|
||||
id: AudioData.Id,
|
||||
volume: f32 = 1
|
||||
};
|
||||
|
||||
play: Play,
|
||||
|
||||
pub const RingBuffer = struct {
|
||||
// TODO: This ring buffer will work in a single producer single consumer configuration
|
||||
// For my game this will be good enough
|
||||
|
||||
items: []Command,
|
||||
head: std.atomic.Value(usize) = .init(0),
|
||||
tail: std.atomic.Value(usize) = .init(0),
|
||||
|
||||
pub fn push(self: *RingBuffer, command: Command) error{OutOfMemory}!void {
|
||||
const head = self.head.load(.monotonic);
|
||||
const tail = self.tail.load(.monotonic);
|
||||
const next_head = @mod(head + 1, self.items.len);
|
||||
|
||||
// A single slot in the .items array will always not be used.
|
||||
if (next_head == tail) {
|
||||
return error.OutOfMemory;
|
||||
}
|
||||
|
||||
self.items[head] = command;
|
||||
self.head.store(next_head, .monotonic);
|
||||
}
|
||||
|
||||
pub fn pop(self: *RingBuffer) ?Command {
|
||||
const head = self.head.load(.monotonic);
|
||||
const tail = self.tail.load(.monotonic);
|
||||
|
||||
if (head == tail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = self.items[tail];
|
||||
self.tail.store(@mod(tail + 1, self.items.len), .monotonic);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// TODO: Tracks
|
||||
|
||||
instances: std.ArrayList(Instance),
|
||||
commands: Command.RingBuffer,
|
||||
working_buffer: []f32,
|
||||
|
||||
pub fn init(
|
||||
gpa: Allocator,
|
||||
max_instances: u32,
|
||||
max_commands: u32,
|
||||
working_buffer_size: u32
|
||||
) !Mixer {
|
||||
var instances = try std.ArrayList(Instance).initCapacity(gpa, max_instances);
|
||||
errdefer instances.deinit(gpa);
|
||||
|
||||
const commands = try gpa.alloc(Command, max_commands);
|
||||
errdefer gpa.free(commands);
|
||||
|
||||
const working_buffer = try gpa.alloc(f32, working_buffer_size);
|
||||
errdefer gpa.free(working_buffer);
|
||||
|
||||
return Mixer{
|
||||
.working_buffer = working_buffer,
|
||||
.instances = instances,
|
||||
.commands = .{
|
||||
.items = commands
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Mixer, gpa: Allocator) void {
|
||||
self.instances.deinit(gpa);
|
||||
gpa.free(self.commands.items);
|
||||
gpa.free(self.working_buffer);
|
||||
}
|
||||
|
||||
pub fn queue(self: *Mixer, command: Command) void {
|
||||
self.commands.push(command) catch log.warn("Maximum number of audio commands reached!", .{});
|
||||
}
|
||||
|
||||
pub fn stream(self: *Mixer, store: Store, buffer: []f32, num_frames: u32, num_channels: u32) !void {
|
||||
while (self.commands.pop()) |command| {
|
||||
switch (command) {
|
||||
.play => |opts| {
|
||||
const volume = @max(opts.volume, 0);
|
||||
if (volume == 0) {
|
||||
log.warn("Attempt to play audio with 0 volume", .{});
|
||||
continue;
|
||||
}
|
||||
|
||||
self.instances.appendBounded(.{
|
||||
.data_id = opts.id,
|
||||
.volume = volume,
|
||||
}) catch log.warn("Maximum number of audio instances reached!", .{});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert(num_channels == 1); // TODO:
|
||||
const sample_rate: u32 = @intCast(saudio.sampleRate());
|
||||
|
||||
@memset(buffer, 0);
|
||||
assert(self.working_buffer.len >= num_frames);
|
||||
|
||||
for (self.instances.items) |*instance| {
|
||||
const audio_data = store.get(instance.data_id);
|
||||
const samples = audio_data.streamChannel(self.working_buffer[0..num_frames], instance.cursor, 0, sample_rate);
|
||||
for (0.., samples) |i, sample| {
|
||||
buffer[i] += sample * instance.volume;
|
||||
}
|
||||
instance.cursor += @intCast(samples.len);
|
||||
}
|
||||
|
||||
{
|
||||
var i: usize = 0;
|
||||
while (i < self.instances.items.len) {
|
||||
const instance = self.instances.items[i];
|
||||
const audio_data = store.get(instance.data_id);
|
||||
const is_complete = instance.cursor == audio_data.getSampleCount();
|
||||
|
||||
if (is_complete) {
|
||||
_ = self.instances.swapRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/engine/audio/root.zig
Normal file
111
src/engine/audio/root.zig
Normal file
@ -0,0 +1,111 @@
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.audio);
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const tracy = @import("tracy");
|
||||
|
||||
const Math = @import("../math.zig");
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
|
||||
pub const Data = @import("./data.zig").Data;
|
||||
pub const Store = @import("./store.zig");
|
||||
pub const Mixer = @import("./mixer.zig");
|
||||
|
||||
pub const Command = Mixer.Command;
|
||||
|
||||
const Nanoseconds = @import("../root.zig").Nanoseconds;
|
||||
const sokol = @import("sokol");
|
||||
const saudio = sokol.audio;
|
||||
|
||||
var stopped: bool = true;
|
||||
var gpa: std.mem.Allocator = undefined;
|
||||
var store: Store = undefined;
|
||||
pub var mixer: Mixer = undefined;
|
||||
|
||||
const Options = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
logger: saudio.Logger = .{},
|
||||
channels: u32 = 1,
|
||||
max_vorbis_alloc_buffer_size: u32 = 1 * Math.bytes_per_mib,
|
||||
buffer_frames: u32 = 2048,
|
||||
max_instances: u32 = 64
|
||||
};
|
||||
|
||||
pub fn init(opts: Options) !void {
|
||||
gpa = opts.allocator;
|
||||
|
||||
store = try Store.init(.{
|
||||
.allocator = opts.allocator,
|
||||
.max_vorbis_alloc_buffer_size = opts.max_vorbis_alloc_buffer_size,
|
||||
});
|
||||
|
||||
mixer = try Mixer.init(gpa,
|
||||
opts.max_instances,
|
||||
opts.max_instances,
|
||||
opts.buffer_frames
|
||||
);
|
||||
|
||||
saudio.setup(.{
|
||||
.logger = opts.logger,
|
||||
.stream_cb = sokolStreamCallback,
|
||||
.num_channels = @intCast(opts.channels),
|
||||
.buffer_frames = @intCast(opts.buffer_frames)
|
||||
});
|
||||
stopped = false;
|
||||
|
||||
const sample_rate: f32 = @floatFromInt(saudio.sampleRate());
|
||||
const audio_latency: f32 = @as(f32, @floatFromInt(opts.buffer_frames)) / sample_rate;
|
||||
log.debug("Audio latency = {D}", .{@as(u64, @intFromFloat(audio_latency * std.time.ns_per_s))});
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
stopped = true;
|
||||
saudio.shutdown();
|
||||
mixer.deinit(gpa);
|
||||
store.deinit();
|
||||
}
|
||||
|
||||
pub fn load(opts: Store.LoadOptions) !Data.Id {
|
||||
return try store.load(opts);
|
||||
}
|
||||
|
||||
const Info = struct {
|
||||
sample_count: u32,
|
||||
sample_rate: u32,
|
||||
|
||||
pub fn getDuration(self: Info) Nanoseconds {
|
||||
return @as(Nanoseconds, self.sample_count) * std.time.ns_per_s / self.sample_rate;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn getInfo(id: Data.Id) Info {
|
||||
const data = store.get(id);
|
||||
return Info{
|
||||
.sample_count = data.getSampleCount(),
|
||||
.sample_rate = data.getSampleRate(),
|
||||
};
|
||||
}
|
||||
|
||||
fn sokolStreamCallback(buffer: [*c]f32, num_frames: i32, num_channels: i32) callconv(.c) void {
|
||||
if (stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zone = tracy.initZone(@src(), .{ });
|
||||
defer zone.deinit();
|
||||
|
||||
const num_frames_u32: u32 = @intCast(num_frames);
|
||||
const num_channels_u32: u32 = @intCast(num_channels);
|
||||
|
||||
mixer.stream(
|
||||
store,
|
||||
buffer[0..(num_frames_u32 * num_channels_u32)],
|
||||
num_frames_u32,
|
||||
num_channels_u32
|
||||
) catch |e| {
|
||||
log.err("mixer.stream() failed: {}", .{e});
|
||||
if (@errorReturnTrace()) |trace| {
|
||||
std.debug.dumpStackTrace(trace.*);
|
||||
}
|
||||
};
|
||||
}
|
||||
106
src/engine/audio/store.zig
Normal file
106
src/engine/audio/store.zig
Normal file
@ -0,0 +1,106 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const Math = @import("../math.zig");
|
||||
const STBVorbis = @import("stb_vorbis");
|
||||
|
||||
const AudioData = @import("./data.zig").Data;
|
||||
|
||||
const Store = @This();
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
list: std.ArrayList(AudioData),
|
||||
temp_vorbis_alloc_buffer: []u8,
|
||||
|
||||
const Options = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
max_vorbis_alloc_buffer_size: u32,
|
||||
};
|
||||
|
||||
pub fn init(opts: Options) !Store {
|
||||
const gpa = opts.allocator;
|
||||
|
||||
const temp_vorbis_alloc_buffer = try gpa.alloc(u8, opts.max_vorbis_alloc_buffer_size);
|
||||
errdefer gpa.free(temp_vorbis_alloc_buffer);
|
||||
|
||||
return Store{
|
||||
.arena = std.heap.ArenaAllocator.init(gpa),
|
||||
.list = .empty,
|
||||
.temp_vorbis_alloc_buffer = temp_vorbis_alloc_buffer
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Store) void {
|
||||
const gpa = self.arena.child_allocator;
|
||||
gpa.free(self.temp_vorbis_alloc_buffer);
|
||||
self.list.deinit(gpa);
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub const LoadOptions = struct {
|
||||
const PlaybackStyle = enum {
|
||||
stream,
|
||||
decode_once,
|
||||
|
||||
// If the decoded size is less than `stream_threshold`, then .decode_once will by default be used.
|
||||
const stream_threshold = 10 * Math.bytes_per_mib;
|
||||
};
|
||||
|
||||
const Format = enum {
|
||||
vorbis
|
||||
};
|
||||
|
||||
format: Format,
|
||||
data: []const u8,
|
||||
playback_style: ?PlaybackStyle = null,
|
||||
};
|
||||
|
||||
pub fn load(self: *Store, opts: LoadOptions) !AudioData.Id {
|
||||
const gpa = self.arena.child_allocator;
|
||||
|
||||
const id = self.list.items.len;
|
||||
try self.list.ensureUnusedCapacity(gpa, 1);
|
||||
|
||||
const PlaybackStyle = LoadOptions.PlaybackStyle;
|
||||
const temp_stb_vorbis = try STBVorbis.init(opts.data, self.temp_vorbis_alloc_buffer);
|
||||
const info = temp_stb_vorbis.getInfo();
|
||||
const duration_in_samples = temp_stb_vorbis.getStreamLengthInSamples();
|
||||
const decoded_size = info.channels * duration_in_samples * @sizeOf(f32);
|
||||
const stream_threshold = PlaybackStyle.stream_threshold;
|
||||
const default_playback_style: PlaybackStyle = if (decoded_size < stream_threshold) .decode_once else .stream;
|
||||
|
||||
const arena_allocator = self.arena.allocator();
|
||||
const playback_style = opts.playback_style orelse default_playback_style;
|
||||
if (playback_style == .decode_once) {
|
||||
const channels = try arena_allocator.alloc([*]f32, info.channels);
|
||||
for (channels) |*channel| {
|
||||
channel.* = (try arena_allocator.alloc(f32, duration_in_samples)).ptr;
|
||||
}
|
||||
const samples_decoded = temp_stb_vorbis.getSamples(channels, duration_in_samples);
|
||||
assert(samples_decoded == duration_in_samples);
|
||||
|
||||
self.list.appendAssumeCapacity(AudioData{
|
||||
.raw = .{
|
||||
.channels = channels,
|
||||
.sample_count = duration_in_samples,
|
||||
.sample_rate = info.sample_rate
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const alloc_buffer = try arena_allocator.alloc(u8, temp_stb_vorbis.getMinimumAllocBufferSize());
|
||||
const stb_vorbis = STBVorbis.init(opts.data, alloc_buffer) catch unreachable;
|
||||
|
||||
self.list.appendAssumeCapacity(AudioData{
|
||||
.vorbis = .{
|
||||
.alloc_buffer = alloc_buffer,
|
||||
.stb_vorbis = stb_vorbis
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return @enumFromInt(id);
|
||||
}
|
||||
|
||||
pub fn get(self: Store, id: AudioData.Id) AudioData {
|
||||
return self.list.items[@intFromEnum(id)];
|
||||
}
|
||||
250
src/engine/fontstash/context.zig
Normal file
250
src/engine/fontstash/context.zig
Normal file
@ -0,0 +1,250 @@
|
||||
const std = @import("std");
|
||||
|
||||
const Font = @import("./font.zig");
|
||||
const Align = Font.Align;
|
||||
|
||||
const Context = @This();
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("fontstash.h");
|
||||
|
||||
@cInclude("sokol/sokol_gfx.h");
|
||||
@cInclude("sokol/sokol_gl.h");
|
||||
@cInclude("sokol_fontstash.h");
|
||||
});
|
||||
|
||||
pub const FONScontext = c.FONScontext;
|
||||
|
||||
pub const Size = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
pub const TextBounds = struct {
|
||||
advance: f32,
|
||||
min_x: f32,
|
||||
max_x: f32,
|
||||
min_y: f32,
|
||||
max_y: f32,
|
||||
};
|
||||
|
||||
pub const LineBounds = struct {
|
||||
min_y: f32,
|
||||
max_y: f32,
|
||||
};
|
||||
|
||||
pub const VertMetrics = struct {
|
||||
ascender: f32,
|
||||
descender: f32,
|
||||
lineh: f32
|
||||
};
|
||||
|
||||
pub const Quad = struct {
|
||||
x0: f32,
|
||||
y0: f32,
|
||||
s0: f32,
|
||||
t0: f32,
|
||||
|
||||
x1: f32,
|
||||
y1: f32,
|
||||
s1: f32,
|
||||
t1: f32,
|
||||
};
|
||||
|
||||
pub const TextIterator = struct {
|
||||
context: Context,
|
||||
iter: c.FONStextIter,
|
||||
|
||||
pub fn init(ctx: Context, x: f32, y: f32, text: []const u8) TextIterator {
|
||||
var self = TextIterator{
|
||||
.context = ctx,
|
||||
.iter = undefined
|
||||
};
|
||||
|
||||
const success = c.fonsTextIterInit(
|
||||
self.context.ctx,
|
||||
&self.iter,
|
||||
x,
|
||||
y,
|
||||
text.ptr,
|
||||
text.ptr + text.len
|
||||
);
|
||||
if (success != 1) {
|
||||
return error.fonsTextIterInit;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn next(self: TextIterator) ?Quad {
|
||||
var quad: c.FONSquad = undefined;
|
||||
const success = c.fonsTextIterNext(self.context, &self.iter, &quad);
|
||||
if (success != 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Quad{
|
||||
.x0 = quad.x0,
|
||||
.y0 = quad.y0,
|
||||
.s0 = quad.s0,
|
||||
.t0 = quad.t0,
|
||||
|
||||
.x1 = quad.x0,
|
||||
.y1 = quad.y0,
|
||||
.s1 = quad.s0,
|
||||
.t1 = quad.t0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
ctx: *FONScontext,
|
||||
|
||||
pub fn init(desc: c.sfons_desc_t) !Context {
|
||||
const ctx = c.sfons_create(&desc);
|
||||
if (ctx == null) {
|
||||
return error.sfons_create;
|
||||
}
|
||||
|
||||
return Context{
|
||||
.ctx = ctx.?
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: Context) void {
|
||||
c.sfons_destroy(self.ctx);
|
||||
}
|
||||
|
||||
pub fn flush(self: Context) void {
|
||||
c.sfons_flush(self.ctx);
|
||||
}
|
||||
|
||||
pub fn addFont(self: Context, name: [*c]const u8, data: []const u8) !Font.Id {
|
||||
const font_id = c.fonsAddFontMem(
|
||||
self.ctx,
|
||||
name,
|
||||
@constCast(data.ptr),
|
||||
@intCast(data.len),
|
||||
0
|
||||
);
|
||||
if (font_id == c.FONS_INVALID) {
|
||||
return error.fonsAddFontMem;
|
||||
}
|
||||
return @enumFromInt(font_id);
|
||||
}
|
||||
|
||||
pub fn addFallbackFont(self: Context, base: Font.Id, fallback: Font.Id) void {
|
||||
const success = c.fonsAddFallbackFont(self.ctx, @intFromEnum(base), @intFromEnum(fallback));
|
||||
if (success != 1) {
|
||||
return error.fonsAddFallbackFont;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getFontByName(self: Context, name: [*c]const u8) ?Font.Id {
|
||||
const font_id = c.fonsGetFontByName(self.ctx, name);
|
||||
if (font_id == c.FONS_INVALID) {
|
||||
return null;
|
||||
}
|
||||
return @enumFromInt(font_id);
|
||||
}
|
||||
|
||||
// TODO: fonsSetErrorCallback
|
||||
|
||||
pub fn getAtlasSize(self: Context) Size {
|
||||
var result: Size = .{
|
||||
.width = 0,
|
||||
.height = 0
|
||||
};
|
||||
c.fonsGetAtlasSize(self.ctx, &result.width, &result.height);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn expandAtlas(self: Context, width: u32, height: u32) !void {
|
||||
const success = c.fonsExpandAtlas(self.ctx, @bitCast(width), @bitCast(height));
|
||||
if (success != 1) {
|
||||
return error.fonsExpandAtlas;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resetAtlas(self: Context) !void {
|
||||
const success = c.fonsResetAtlas(self.ctx);
|
||||
if (success != 1) {
|
||||
return error.fonsResetAtlas;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pushState(self: Context) void {
|
||||
c.fonsPushState(self.ctx);
|
||||
}
|
||||
|
||||
pub fn popState(self: Context) void {
|
||||
c.fonsPopState(self.ctx);
|
||||
}
|
||||
|
||||
pub fn clearState(self: Context) void {
|
||||
c.fonsClearState(self.ctx);
|
||||
}
|
||||
|
||||
pub fn setSize(self: Context, size: f32) void {
|
||||
c.fonsSetSize(self.ctx, size);
|
||||
}
|
||||
|
||||
pub fn setColor(self: Context, color: u32) void {
|
||||
c.fonsSetColor(self.ctx, color);
|
||||
}
|
||||
|
||||
pub fn setSpacing(self: Context, spacing: f32) void {
|
||||
c.fonsSetSpacing(self.ctx, spacing);
|
||||
}
|
||||
|
||||
pub fn setBlur(self: Context, blur: f32) void {
|
||||
c.fonsSetSpacing(self.ctx, blur);
|
||||
}
|
||||
|
||||
pub fn setAlign(self: Context, alignment: Align) void {
|
||||
c.fonsSetAlign(self.ctx, @intFromEnum(alignment.x) | @intFromEnum(alignment.y));
|
||||
}
|
||||
|
||||
pub fn setFont(self: Context, id: Font.Id) void {
|
||||
c.fonsSetFont(self.ctx, @intFromEnum(id));
|
||||
}
|
||||
|
||||
pub fn drawText(self: Context, x: f32, y: f32, text: []const u8) void {
|
||||
const advance = c.fonsDrawText(self.ctx, x, y, text.ptr, text.ptr + text.len);
|
||||
_ = advance;
|
||||
}
|
||||
|
||||
pub fn textBounds(self: Context, x: f32, y: f32, text: []const u8) TextBounds {
|
||||
var bounds: f32[4] = undefined;
|
||||
const advance = c.fonsTextBounds(self.ctx, x, y, text.ptr, text.ptr + text.len, &bounds);
|
||||
|
||||
return TextBounds{
|
||||
.advance = advance,
|
||||
.min_x = bounds[0],
|
||||
.max_x = bounds[1],
|
||||
.min_y = bounds[2],
|
||||
.max_y = bounds[3]
|
||||
};
|
||||
}
|
||||
|
||||
pub fn lineBounds(self: Context, y: f32) LineBounds {
|
||||
var result: LineBounds = .{
|
||||
.max_y = 0,
|
||||
.min_y = 0
|
||||
};
|
||||
c.fonsLineBounds(self.ctx, y, &result.min_y, &result.max_y);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn vertMetrics(self: Context) void {
|
||||
var result: VertMetrics = .{
|
||||
.ascender = 0,
|
||||
.descender = 0,
|
||||
.lineh = 0
|
||||
};
|
||||
c.fonsVertMetrics(self.ctx, &result.ascender, &result.descender, &result.lineh);
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn drawDebug(self: Context, x: f32, y: f32) void {
|
||||
c.fonsDrawDebug(self.ctx, x, y);
|
||||
}
|
||||
33
src/engine/fontstash/font.zig
Normal file
33
src/engine/fontstash/font.zig
Normal file
@ -0,0 +1,33 @@
|
||||
const std = @import("std");
|
||||
|
||||
const c = @cImport({
|
||||
@cInclude("fontstash.h");
|
||||
});
|
||||
|
||||
const Font = @This();
|
||||
|
||||
pub const Id = enum(c_int) {
|
||||
_,
|
||||
|
||||
pub const invalid: Id = @enumFromInt(c.FONS_INVALID);
|
||||
};
|
||||
|
||||
pub const AlignX = enum(c_int) {
|
||||
left = c.FONS_ALIGN_LEFT,
|
||||
right = c.FONS_ALIGN_RIGHT,
|
||||
center = c.FONS_ALIGN_CENTER,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const AlignY = enum(c_int) {
|
||||
top = c.FONS_ALIGN_TOP,
|
||||
middle = c.FONS_ALIGN_MIDDLE,
|
||||
bottom = c.FONS_ALIGN_BOTTOM,
|
||||
baseline = c.FONS_ALIGN_BASELINE,
|
||||
_,
|
||||
};
|
||||
|
||||
pub const Align = struct {
|
||||
x: AlignX,
|
||||
y: AlignY,
|
||||
};
|
||||
3
src/engine/fontstash/root.zig
Normal file
3
src/engine/fontstash/root.zig
Normal file
@ -0,0 +1,3 @@
|
||||
|
||||
pub const Font = @import("./font.zig");
|
||||
pub const Context = @import("./context.zig");
|
||||
12
src/engine/fontstash/sokol_fontstash_impl.c
Normal file
12
src/engine/fontstash/sokol_fontstash_impl.c
Normal file
@ -0,0 +1,12 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
#define FONTSTASH_IMPLEMENTATION
|
||||
#include "fontstash.h"
|
||||
|
||||
#include "sokol/sokol_gfx.h"
|
||||
#include "sokol/sokol_gl.h"
|
||||
#define SOKOL_FONTSTASH_IMPL
|
||||
#include "sokol_fontstash.h"
|
||||
291
src/engine/frame.zig
Normal file
291
src/engine/frame.zig
Normal file
@ -0,0 +1,291 @@
|
||||
const std = @import("std");
|
||||
const build_options = @import("build_options");
|
||||
const log = std.log.scoped(.engine);
|
||||
|
||||
const InputSystem = @import("./input.zig");
|
||||
const KeyCode = InputSystem.KeyCode;
|
||||
const MouseButton = InputSystem.MouseButton;
|
||||
|
||||
const AudioSystem = @import("./audio/root.zig");
|
||||
const AudioData = AudioSystem.Data;
|
||||
const AudioCommand = AudioSystem.Command;
|
||||
|
||||
const GraphicsSystem = @import("./graphics.zig");
|
||||
const TextureId = GraphicsSystem.TextureId;
|
||||
const GraphicsCommand = GraphicsSystem.Command;
|
||||
const Font = GraphicsSystem.Font;
|
||||
const Sprite = GraphicsSystem.Sprite;
|
||||
|
||||
const Math = @import("./math.zig");
|
||||
const Rect = Math.Rect;
|
||||
const Vec4 = Math.Vec4;
|
||||
const Vec2 = Math.Vec2;
|
||||
const rgb = Math.rgb;
|
||||
|
||||
pub const Nanoseconds = u64;
|
||||
|
||||
const Frame = @This();
|
||||
|
||||
pub const Input = struct {
|
||||
keyboard: InputSystem.ButtonStateSet(KeyCode),
|
||||
mouse_button: InputSystem.ButtonStateSet(MouseButton),
|
||||
mouse_position: ?Vec2,
|
||||
|
||||
pub const empty = Input{
|
||||
.keyboard = .empty,
|
||||
.mouse_button = .empty,
|
||||
.mouse_position = null,
|
||||
};
|
||||
};
|
||||
|
||||
pub const KeyState = struct {
|
||||
down: bool,
|
||||
pressed: bool,
|
||||
released: bool,
|
||||
down_duration: ?f64,
|
||||
|
||||
pub const RepeatOptions = struct {
|
||||
first_at: f64 = 0,
|
||||
period: f64
|
||||
};
|
||||
|
||||
pub fn repeat(self: KeyState, last_repeat_at: *?f64, opts: RepeatOptions) bool {
|
||||
if (!self.down) {
|
||||
last_repeat_at.* = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
const down_duration = self.down_duration.?;
|
||||
if (last_repeat_at.* != null) {
|
||||
if (down_duration >= last_repeat_at.*.? + opts.period) {
|
||||
last_repeat_at.* = last_repeat_at.*.? + opts.period;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
if (down_duration >= opts.first_at) {
|
||||
last_repeat_at.* = opts.first_at;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Audio = struct {
|
||||
commands: std.ArrayList(AudioCommand),
|
||||
|
||||
pub const empty = Audio{
|
||||
.commands = .empty
|
||||
};
|
||||
};
|
||||
|
||||
pub const Graphics = struct {
|
||||
clear_color: Vec4,
|
||||
screen_size: Vec2,
|
||||
canvas_size: ?Vec2,
|
||||
|
||||
scissor_stack: std.ArrayList(Rect),
|
||||
commands: std.ArrayList(GraphicsCommand),
|
||||
|
||||
pub const empty = Graphics{
|
||||
.clear_color = rgb(0, 0, 0),
|
||||
.screen_size = .init(0, 0),
|
||||
.canvas_size = null,
|
||||
.scissor_stack = .empty,
|
||||
.commands = .empty
|
||||
};
|
||||
};
|
||||
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
time_ns: Nanoseconds,
|
||||
dt_ns: Nanoseconds,
|
||||
input: Input,
|
||||
audio: Audio,
|
||||
graphics: Graphics,
|
||||
|
||||
show_debug: bool,
|
||||
hide_cursor: bool,
|
||||
|
||||
pub fn init(self: *Frame, gpa: std.mem.Allocator) void {
|
||||
self.* = Frame{
|
||||
.arena = std.heap.ArenaAllocator.init(gpa),
|
||||
.time_ns = 0,
|
||||
.dt_ns = 0,
|
||||
.input = .empty,
|
||||
.audio = .empty,
|
||||
.graphics = .empty,
|
||||
.show_debug = false,
|
||||
.hide_cursor = false
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Frame) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn deltaTime(self: Frame) f32 {
|
||||
return @as(f32, @floatFromInt(self.dt_ns)) / std.time.ns_per_s;
|
||||
}
|
||||
|
||||
pub fn time(self: Frame) f64 {
|
||||
return @as(f64, @floatFromInt(self.time_ns)) / std.time.ns_per_s;
|
||||
}
|
||||
|
||||
pub fn isKeyDown(self: Frame, key_code: KeyCode) bool {
|
||||
const keyboard = &self.input.keyboard;
|
||||
return keyboard.down.contains(key_code);
|
||||
}
|
||||
|
||||
pub fn getKeyDownDuration(self: Frame, key_code: KeyCode) ?f32 {
|
||||
if (!self.isKeyDown(key_code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyboard = &self.input.keyboard;
|
||||
const pressed_at_ns = keyboard.pressed_at.get(key_code).?;
|
||||
const duration_ns = self.time_ns - pressed_at_ns;
|
||||
|
||||
return @as(f32, @floatFromInt(duration_ns)) / std.time.ns_per_s;
|
||||
}
|
||||
|
||||
pub fn isKeyPressed(self: Frame, key_code: KeyCode) bool {
|
||||
const keyboard = &self.input.keyboard;
|
||||
return keyboard.pressed.contains(key_code);
|
||||
}
|
||||
|
||||
pub fn isKeyReleased(self: Frame, key_code: KeyCode) bool {
|
||||
const keyboard = &self.input.keyboard;
|
||||
return keyboard.released.contains(key_code);
|
||||
}
|
||||
|
||||
pub fn getKeyState(self: Frame, key_code: KeyCode) KeyState {
|
||||
return KeyState{
|
||||
.down = self.isKeyDown(key_code),
|
||||
.released = self.isKeyReleased(key_code),
|
||||
.pressed = self.isKeyPressed(key_code),
|
||||
.down_duration = self.getKeyDownDuration(key_code)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn isMousePressed(self: Frame, button: MouseButton) bool {
|
||||
return self.input.mouse_button.pressed.contains(button);
|
||||
}
|
||||
|
||||
pub fn isMouseDown(self: Frame, button: MouseButton) bool {
|
||||
return self.input.mouse_button.down.contains(button);
|
||||
}
|
||||
|
||||
fn pushAudioCommand(self: *Frame, command: AudioCommand) void {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
self.audio.commands.append(arena, command) catch |e| {
|
||||
log.warn("Failed to play audio: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn pushGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
self.graphics.commands.append(arena, command) catch |e|{
|
||||
log.warn("Failed to push graphics command: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn prependGraphicsCommand(self: *Frame, command: GraphicsCommand) void {
|
||||
const arena = self.arena.allocator();
|
||||
|
||||
self.graphics.commands.insert(arena, 0, command) catch |e|{
|
||||
log.warn("Failed to push graphics command: {}", .{e});
|
||||
};
|
||||
}
|
||||
|
||||
pub fn playAudio(self: *Frame, options: AudioCommand.Play) void {
|
||||
self.pushAudioCommand(.{
|
||||
.play = options,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pushScissor(self: *Frame, rect: Rect) void {
|
||||
const arena = self.arena.allocator();
|
||||
self.graphics.scissor_stack.append(arena, rect) catch |e| {
|
||||
log.warn("Failed to push scissor region: {}", .{e});
|
||||
return;
|
||||
};
|
||||
|
||||
self.pushGraphicsCommand(.{
|
||||
.set_scissor = rect
|
||||
});
|
||||
}
|
||||
|
||||
pub fn popScissor(self: *Frame) void {
|
||||
_ = self.graphics.scissor_stack.pop().?;
|
||||
const rect = self.graphics.scissor_stack.getLast();
|
||||
|
||||
self.pushGraphicsCommand(.{
|
||||
.set_scissor = rect
|
||||
});
|
||||
}
|
||||
|
||||
pub fn drawRectangle(self: *Frame, opts: GraphicsCommand.DrawRectangle) void {
|
||||
self.pushGraphicsCommand(.{ .draw_rectangle = opts });
|
||||
}
|
||||
|
||||
pub fn drawLine(self: *Frame, pos1: Vec2, pos2: Vec2, color: Vec4, width: f32) void {
|
||||
self.pushGraphicsCommand(.{
|
||||
.draw_line = .{
|
||||
.pos1 = pos1,
|
||||
.pos2 = pos2,
|
||||
.width = width,
|
||||
.color = color
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn drawRectanglOutline(self: *Frame, pos: Vec2, size: Vec2, color: Vec4, width: f32) void {
|
||||
// TODO: Don't use line segments
|
||||
self.drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width);
|
||||
self.drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width);
|
||||
self.drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width);
|
||||
self.drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width);
|
||||
}
|
||||
|
||||
pub const DrawTextOptions = struct {
|
||||
font: Font.Id,
|
||||
size: f32 = 16,
|
||||
color: Vec4 = rgb(255, 255, 255),
|
||||
};
|
||||
|
||||
pub fn drawText(self: *Frame, position: Vec2, text: []const u8, opts: DrawTextOptions) void {
|
||||
const arena = self.arena.allocator();
|
||||
const text_dupe = arena.dupe(u8, text) catch |e| {
|
||||
log.warn("Failed to draw text: {}", .{e});
|
||||
return;
|
||||
};
|
||||
|
||||
self.pushGraphicsCommand(.{
|
||||
.draw_text = .{
|
||||
.pos = position,
|
||||
.text = text_dupe,
|
||||
.size = opts.size,
|
||||
.font = opts.font,
|
||||
.color = opts.color,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pushTransform(self: *Frame, translation: Vec2, scale: Vec2) void {
|
||||
self.pushGraphicsCommand(.{
|
||||
.push_transformation = .{
|
||||
.translation = translation,
|
||||
.scale = scale
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn popTransform(self: *Frame) void {
|
||||
self.pushGraphicsCommand(.{
|
||||
.pop_transformation = {}
|
||||
});
|
||||
}
|
||||
380
src/engine/graphics.zig
Normal file
380
src/engine/graphics.zig
Normal file
@ -0,0 +1,380 @@
|
||||
const sokol = @import("sokol");
|
||||
const sg = sokol.gfx;
|
||||
const sglue = sokol.glue;
|
||||
const slog = sokol.log;
|
||||
const sapp = sokol.app;
|
||||
const simgui = sokol.imgui;
|
||||
const sgl = sokol.gl;
|
||||
|
||||
const Math = @import("./math.zig");
|
||||
const Vec2 = Math.Vec2;
|
||||
const Vec4 = Math.Vec4;
|
||||
const rgb = Math.rgb;
|
||||
const Rect = Math.Rect;
|
||||
|
||||
const std = @import("std");
|
||||
const log = std.log.scoped(.graphics);
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const imgui = @import("imgui.zig");
|
||||
const tracy = @import("tracy");
|
||||
const fontstash = @import("./fontstash/root.zig");
|
||||
pub const Font = fontstash.Font;
|
||||
|
||||
const GraphicsFrame = @import("./frame.zig").Graphics;
|
||||
|
||||
// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug.
|
||||
// From other peoples research it seems that disabling vsync when a resize event occurs fixes it.
|
||||
// Maybe a patch for sokol could be made?
|
||||
// More info:
|
||||
// * https://github.com/libsdl-org/SDL/issues/11618
|
||||
// * https://github.com/nimgl/nimgl/issues/59
|
||||
|
||||
const Options = struct {
|
||||
const ImguiFont = struct { ttf_data: []const u8, size: f32 = 16 };
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
logger: sg.Logger = .{},
|
||||
imgui_font: ?ImguiFont = null,
|
||||
};
|
||||
|
||||
pub const Command = union(enum) {
|
||||
pub const DrawRectangle = struct {
|
||||
rect: Rect,
|
||||
color: Vec4,
|
||||
sprite: ?Sprite = null,
|
||||
rotation: f32 = 0,
|
||||
origin: Vec2 = .init(0, 0),
|
||||
};
|
||||
|
||||
set_scissor: Rect,
|
||||
draw_rectangle: DrawRectangle,
|
||||
draw_line: struct { pos1: Vec2, pos2: Vec2, color: Vec4, width: f32 },
|
||||
draw_text: struct {
|
||||
pos: Vec2,
|
||||
text: []const u8,
|
||||
font: Font.Id,
|
||||
size: f32,
|
||||
color: Vec4,
|
||||
},
|
||||
push_transformation: struct { translation: Vec2, scale: Vec2 },
|
||||
pop_transformation: void,
|
||||
};
|
||||
|
||||
const Texture = struct {
|
||||
image: sg.Image,
|
||||
view: sg.View,
|
||||
info: Info,
|
||||
|
||||
const Info = struct {
|
||||
width: u32,
|
||||
height: u32,
|
||||
};
|
||||
|
||||
const Id = enum(u32) { _ };
|
||||
const Data = struct { width: u32, height: u32, rgba: [*]u8 };
|
||||
};
|
||||
pub const TextureId = Texture.Id;
|
||||
pub const TextureInfo = Texture.Info;
|
||||
|
||||
pub const Sprite = struct {
|
||||
texture: TextureId,
|
||||
uv: Rect,
|
||||
};
|
||||
|
||||
var gpa: std.mem.Allocator = undefined;
|
||||
|
||||
var main_pipeline: sgl.Pipeline = .{};
|
||||
var linear_sampler: sg.Sampler = .{};
|
||||
var nearest_sampler: sg.Sampler = .{};
|
||||
var font_context: fontstash.Context = undefined;
|
||||
var textures: std.ArrayList(Texture) = .empty;
|
||||
|
||||
var scale_stack_buffer: [32]Vec2 = undefined;
|
||||
var scale_stack: std.ArrayList(Vec2) = .empty;
|
||||
|
||||
pub fn init(options: Options) !void {
|
||||
gpa = options.allocator;
|
||||
|
||||
sg.setup(.{
|
||||
.logger = options.logger,
|
||||
.environment = sglue.environment(),
|
||||
});
|
||||
|
||||
sgl.setup(.{ .logger = .{ .func = options.logger.func, .user_data = options.logger.user_data } });
|
||||
|
||||
main_pipeline = sgl.makePipeline(.{
|
||||
.colors = init: {
|
||||
var colors: [8]sg.ColorTargetState = @splat(.{});
|
||||
colors[0] = .{
|
||||
.blend = .{
|
||||
.enabled = true,
|
||||
.src_factor_rgb = .SRC_ALPHA,
|
||||
.dst_factor_rgb = .ONE_MINUS_SRC_ALPHA,
|
||||
.op_rgb = .ADD,
|
||||
.src_factor_alpha = .ONE,
|
||||
.dst_factor_alpha = .ONE_MINUS_SRC_ALPHA,
|
||||
.op_alpha = .ADD,
|
||||
},
|
||||
};
|
||||
break :init colors;
|
||||
},
|
||||
});
|
||||
|
||||
imgui.setup(options.allocator, .{
|
||||
.logger = .{ .func = options.logger.func, .user_data = options.logger.user_data },
|
||||
.no_default_font = options.imgui_font != null,
|
||||
|
||||
// TODO: Figure out a way to make imgui play nicely with UI
|
||||
// Ideally when mouse is inside a Imgui window, then the imgui cursor should be used.
|
||||
// Otherwise our own cursor should be used.
|
||||
.disable_set_mouse_cursor = true,
|
||||
});
|
||||
|
||||
if (options.imgui_font) |imgui_font| {
|
||||
imgui.addFont(imgui_font.ttf_data, imgui_font.size);
|
||||
}
|
||||
|
||||
linear_sampler = sg.makeSampler(.{
|
||||
.min_filter = .LINEAR,
|
||||
.mag_filter = .LINEAR,
|
||||
.mipmap_filter = .LINEAR,
|
||||
.label = "linear-sampler",
|
||||
});
|
||||
|
||||
nearest_sampler = sg.makeSampler(.{
|
||||
.min_filter = .NEAREST,
|
||||
.mag_filter = .NEAREST,
|
||||
.mipmap_filter = .NEAREST,
|
||||
.label = "nearest-sampler",
|
||||
});
|
||||
|
||||
const dpi_scale = sapp.dpiScale();
|
||||
const atlas_size = 512;
|
||||
const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(atlas_size * dpi_scale));
|
||||
font_context = try fontstash.Context.init(.{
|
||||
.width = @intCast(atlas_dim),
|
||||
.height = @intCast(atlas_dim),
|
||||
});
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
textures.deinit(gpa);
|
||||
imgui.shutdown();
|
||||
font_context.deinit();
|
||||
sgl.shutdown();
|
||||
sg.shutdown();
|
||||
}
|
||||
|
||||
pub fn drawCommand(command: Command) void {
|
||||
switch (command) {
|
||||
.push_transformation => |opts| {
|
||||
pushTransform(opts.translation, opts.scale);
|
||||
// font_resolution_scale = font_resolution_scale.multiply(opts.scale);
|
||||
},
|
||||
.pop_transformation => {
|
||||
popTransform();
|
||||
},
|
||||
.draw_rectangle => |opts| {
|
||||
drawRectangle(opts);
|
||||
},
|
||||
.set_scissor => |opts| {
|
||||
sgl.scissorRectf(opts.pos.x, opts.pos.y, opts.size.x, opts.size.y, true);
|
||||
},
|
||||
.draw_line => |opts| {
|
||||
drawLine(opts.pos1, opts.pos2, opts.color, opts.width);
|
||||
},
|
||||
.draw_text => |opts| {
|
||||
const font_resolution_scale = scale_stack.getLast();
|
||||
|
||||
sgl.pushMatrix();
|
||||
defer sgl.popMatrix();
|
||||
|
||||
sgl.scale(1 / font_resolution_scale.x, 1 / font_resolution_scale.y, 1);
|
||||
|
||||
font_context.setFont(opts.font);
|
||||
font_context.setSize(opts.size * font_resolution_scale.y);
|
||||
font_context.setAlign(.{ .x = .left, .y = .top });
|
||||
font_context.setSpacing(0);
|
||||
|
||||
const r: u8 = @intFromFloat(opts.color.x * 255);
|
||||
const g: u8 = @intFromFloat(opts.color.y * 255);
|
||||
const b: u8 = @intFromFloat(opts.color.z * 255);
|
||||
const a: u8 = @intFromFloat(opts.color.w * 255);
|
||||
const color: u32 = r | (@as(u32, g) << 8) | (@as(u32, b) << 16) | (@as(u32, a) << 24);
|
||||
font_context.setColor(color);
|
||||
font_context.drawText(opts.pos.x * font_resolution_scale.x, opts.pos.y * font_resolution_scale.y, opts.text);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawCommands(commands: []const Command) void {
|
||||
for (commands) |command| {
|
||||
drawCommand(command);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn beginFrame() void {
|
||||
const zone = tracy.initZone(@src(), .{});
|
||||
defer zone.deinit();
|
||||
|
||||
imgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), .delta_time = sapp.frameDuration(), .dpi_scale = sapp.dpiScale() });
|
||||
|
||||
scale_stack = .initBuffer(&scale_stack_buffer);
|
||||
scale_stack.appendAssumeCapacity(.init(1, 1));
|
||||
|
||||
font_context.clearState();
|
||||
sgl.defaults();
|
||||
sgl.matrixModeProjection();
|
||||
sgl.ortho(0, sapp.widthf(), sapp.heightf(), 0, -1, 1);
|
||||
sgl.loadPipeline(main_pipeline);
|
||||
}
|
||||
|
||||
pub fn endFrame(clear_color: Vec4) void {
|
||||
const zone = tracy.initZone(@src(), .{});
|
||||
defer zone.deinit();
|
||||
|
||||
var pass_action: sg.PassAction = .{};
|
||||
|
||||
pass_action.colors[0] = sg.ColorAttachmentAction{ .load_action = .CLEAR, .clear_value = .{ .r = clear_color.x, .g = clear_color.y, .b = clear_color.z, .a = clear_color.w } };
|
||||
|
||||
font_context.flush();
|
||||
|
||||
{
|
||||
sg.beginPass(.{ .action = pass_action, .swapchain = sglue.swapchain() });
|
||||
defer sg.endPass();
|
||||
|
||||
sgl.draw();
|
||||
|
||||
imgui.render();
|
||||
}
|
||||
sg.commit();
|
||||
}
|
||||
|
||||
fn pushTransform(translation: Vec2, scale: Vec2) void {
|
||||
sgl.pushMatrix();
|
||||
sgl.translate(translation.x, translation.y, 0);
|
||||
sgl.scale(scale.x, scale.y, 1);
|
||||
|
||||
scale_stack.appendAssumeCapacity(scale_stack.getLast().multiply(scale));
|
||||
}
|
||||
|
||||
fn popTransform() void {
|
||||
sgl.popMatrix();
|
||||
_ = scale_stack.pop().?;
|
||||
}
|
||||
|
||||
const Vertex = struct { pos: Vec2, uv: Vec2 };
|
||||
|
||||
fn drawQuad(quad: [4]Vertex, color: Vec4, texture_id: TextureId) void {
|
||||
sgl.enableTexture();
|
||||
defer sgl.disableTexture();
|
||||
|
||||
const view = textures.items[@intFromEnum(texture_id)].view;
|
||||
// TODO: Make sampler configurable
|
||||
sgl.texture(view, nearest_sampler);
|
||||
|
||||
sgl.beginQuads();
|
||||
defer sgl.end();
|
||||
|
||||
sgl.c4f(color.x, color.y, color.z, color.w);
|
||||
for (quad) |vertex| {
|
||||
const pos = vertex.pos;
|
||||
const uv = vertex.uv;
|
||||
sgl.v2fT2f(pos.x, pos.y, uv.x, uv.y);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawQuadNoUVs(quad: [4]Vec2, color: Vec4) void {
|
||||
sgl.beginQuads();
|
||||
defer sgl.end();
|
||||
|
||||
sgl.c4f(color.x, color.y, color.z, color.w);
|
||||
for (quad) |pos| {
|
||||
sgl.v2f(pos.x, pos.y);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawRectangle(opts: Command.DrawRectangle) void {
|
||||
const pos = opts.rect.pos;
|
||||
const size = opts.rect.size;
|
||||
|
||||
const top_left = Vec2.init(0, 0).rotateAround(opts.rotation, opts.origin);
|
||||
const top_right = Vec2.init(size.x, 0).rotateAround(opts.rotation, opts.origin);
|
||||
const bottom_right = size.rotateAround(opts.rotation, opts.origin);
|
||||
const bottom_left = Vec2.init(0, size.y).rotateAround(opts.rotation, opts.origin);
|
||||
|
||||
if (opts.sprite) |sprite| {
|
||||
const uv = sprite.uv;
|
||||
const quad = [4]Vertex{ .{ .pos = pos.add(top_left), .uv = .init(uv.left(), uv.top()) }, .{ .pos = pos.add(top_right), .uv = .init(uv.right(), uv.top()) }, .{ .pos = pos.add(bottom_right), .uv = .init(uv.right(), uv.bottom()) }, .{ .pos = pos.add(bottom_left), .uv = .init(uv.left(), uv.bottom()) } };
|
||||
drawQuad(quad, opts.color, sprite.texture);
|
||||
} else {
|
||||
const quad = .{ pos.add(top_left), pos.add(top_right), pos.add(bottom_right), pos.add(bottom_left) };
|
||||
drawQuadNoUVs(quad, opts.color);
|
||||
}
|
||||
}
|
||||
|
||||
fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
|
||||
const step = to.sub(from).normalized().multiplyScalar(width / 2);
|
||||
|
||||
const top_left = from.add(step.rotateLeft90());
|
||||
const bottom_left = from.add(step.rotateRight90());
|
||||
const top_right = to.add(step.rotateLeft90());
|
||||
const bottom_right = to.add(step.rotateRight90());
|
||||
|
||||
drawQuadNoUVs(.{ top_right, top_left, bottom_left, bottom_right }, color);
|
||||
}
|
||||
|
||||
pub fn addFont(name: [*c]const u8, data: []const u8) !Font.Id {
|
||||
return try font_context.addFont(name, data);
|
||||
}
|
||||
|
||||
fn makeView(image: sg.Image) !sg.View {
|
||||
const image_view = sg.makeView(.{ .texture = .{ .image = image } });
|
||||
if (image_view.id == sg.invalid_id) {
|
||||
return error.InvalidView;
|
||||
}
|
||||
return image_view;
|
||||
}
|
||||
|
||||
fn makeImageWithMipMaps(mipmaps: []const Texture.Data) !sg.Image {
|
||||
if (mipmaps.len == 0) {
|
||||
return error.NoMipMaps;
|
||||
}
|
||||
|
||||
var data: sg.ImageData = .{};
|
||||
var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&data.mip_levels);
|
||||
|
||||
for (mipmaps) |mipmap| {
|
||||
try mip_levels.appendBounded(.{ .ptr = mipmap.rgba, .size = mipmap.width * mipmap.height * 4 });
|
||||
}
|
||||
|
||||
const image = sg.makeImage(.{ .width = @intCast(mipmaps[0].width), .height = @intCast(mipmaps[0].height), .pixel_format = .RGBA8, .usage = .{ .immutable = true }, .num_mipmaps = @intCast(mip_levels.items.len), .data = data });
|
||||
if (image.id == sg.invalid_id) {
|
||||
return error.InvalidImage;
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
pub fn addTexture(mipmaps: []const Texture.Data) !TextureId {
|
||||
const image = try makeImageWithMipMaps(mipmaps);
|
||||
errdefer sg.deallocImage(image);
|
||||
|
||||
const view = try makeView(image);
|
||||
errdefer sg.deallocView(view);
|
||||
|
||||
assert(mipmaps.len > 0);
|
||||
const index = textures.items.len;
|
||||
try textures.append(gpa, .{ .image = image, .view = view, .info = .{
|
||||
.width = mipmaps[0].width,
|
||||
.height = mipmaps[0].height,
|
||||
} });
|
||||
|
||||
return @enumFromInt(index);
|
||||
}
|
||||
|
||||
pub fn getTextureInfo(id: TextureId) TextureInfo {
|
||||
const texture = textures.items[@intFromEnum(id)];
|
||||
return texture.info;
|
||||
}
|
||||
504
src/engine/imgui.zig
Normal file
504
src/engine/imgui.zig
Normal file
@ -0,0 +1,504 @@
|
||||
const std = @import("std");
|
||||
const Math = @import("./math.zig");
|
||||
const build_options = @import("build_options");
|
||||
pub const ig = @import("cimgui");
|
||||
const Vec2 = Math.Vec2;
|
||||
const Vec3 = Math.Vec3;
|
||||
const Vec4 = Math.Vec4;
|
||||
|
||||
const sokol = @import("sokol");
|
||||
const sapp = sokol.app;
|
||||
const simgui = sokol.imgui;
|
||||
|
||||
const enabled = build_options.has_imgui;
|
||||
|
||||
var global_allocator: ?std.mem.Allocator = null;
|
||||
|
||||
pub const WindowOptions = struct {
|
||||
name: [*c]const u8,
|
||||
pos: ?Vec2 = null,
|
||||
size: ?Vec2 = null,
|
||||
collapsed: ?bool = null,
|
||||
open: ?*bool = null
|
||||
};
|
||||
|
||||
pub const SliderOptions = struct {
|
||||
label: [*c]const u8,
|
||||
value: *f32,
|
||||
min: f32,
|
||||
max: f32,
|
||||
};
|
||||
|
||||
fn toImVec2(vec2: Vec2) ig.ImVec2 {
|
||||
return ig.ImVec2{
|
||||
.x = vec2.x,
|
||||
.y = vec2.y,
|
||||
};
|
||||
}
|
||||
|
||||
inline fn structCast(T: type, value: anytype) T {
|
||||
return @as(*T, @ptrFromInt(@intFromPtr(&value))).*;
|
||||
}
|
||||
|
||||
pub fn setup(gpa: std.mem.Allocator, desc: simgui.Desc) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
global_allocator = gpa;
|
||||
simgui.setup(desc);
|
||||
}
|
||||
|
||||
pub fn addFont(ttf_data: []const u8, font_size: f32) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var font_config: ig.ImFontConfig = .{};
|
||||
font_config.FontDataOwnedByAtlas = false;
|
||||
font_config.OversampleH = 2;
|
||||
font_config.OversampleV = 2;
|
||||
font_config.GlyphMaxAdvanceX = std.math.floatMax(f32);
|
||||
font_config.RasterizerMultiply = 1.0;
|
||||
font_config.RasterizerDensity = 1.0;
|
||||
font_config.EllipsisChar = 0;
|
||||
|
||||
const io = ig.igGetIO();
|
||||
_ = ig.ImFontAtlas_AddFontFromMemoryTTF(
|
||||
io.*.Fonts,
|
||||
@constCast(@ptrCast(ttf_data.ptr)),
|
||||
@intCast(ttf_data.len),
|
||||
font_size,
|
||||
&font_config,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
pub fn shutdown() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
simgui.shutdown();
|
||||
}
|
||||
|
||||
pub fn handleEvent(ev: sapp.Event) bool {
|
||||
if (enabled) {
|
||||
return simgui.handleEvent(ev);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn newFrame(desc: simgui.FrameDesc) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
simgui.newFrame(desc);
|
||||
}
|
||||
|
||||
pub fn render() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
simgui.render();
|
||||
}
|
||||
|
||||
pub fn beginWindow(opts: WindowOptions) bool {
|
||||
if (!enabled) {
|
||||
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);
|
||||
|
||||
var open = ig.igBegin(opts.name, 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 (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEnd();
|
||||
}
|
||||
|
||||
pub fn textFmt(comptime fmt: []const u8, args: anytype) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gpa = global_allocator orelse return;
|
||||
|
||||
const formatted = std.fmt.allocPrintSentinel(gpa, fmt, args, 0) catch return;
|
||||
defer gpa.free(formatted);
|
||||
|
||||
text(formatted);
|
||||
}
|
||||
|
||||
pub fn text(text_z: [*c]const u8) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igText("%s", text_z);
|
||||
}
|
||||
|
||||
pub fn beginDisabled(disabled: bool) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igBeginDisabled(disabled);
|
||||
}
|
||||
|
||||
pub fn endDisabled() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEndDisabled();
|
||||
}
|
||||
|
||||
pub fn button(label: [*c]const u8) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igButton(label);
|
||||
}
|
||||
|
||||
pub fn slider(opts: SliderOptions) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igSliderFloat(opts.label, opts.value, opts.min, opts.max);
|
||||
}
|
||||
|
||||
pub fn checkbox(label: [*c]const u8, value: *bool) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igCheckbox(label, value);
|
||||
}
|
||||
|
||||
pub fn beginTabBar(id: [*c]const u8) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igBeginTabBar(id, ig.ImGuiTabBarFlags_None);
|
||||
}
|
||||
|
||||
pub fn endTabBar() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEndTabBar();
|
||||
}
|
||||
|
||||
pub fn beginTabItem(label: [*c]const u8) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igBeginTabItem(label, null, ig.ImGuiTabItemFlags_None);
|
||||
}
|
||||
|
||||
pub fn endTabItem() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return ig.igEndTabItem();
|
||||
}
|
||||
|
||||
pub fn beginGroup() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igBeginGroup();
|
||||
}
|
||||
|
||||
pub fn endGroup() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEndGroup();
|
||||
}
|
||||
|
||||
pub fn sameLine() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igSameLine();
|
||||
}
|
||||
|
||||
pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igBeginTable(id, @intCast(columns), flags);
|
||||
}
|
||||
|
||||
pub fn endTable() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEndTable();
|
||||
}
|
||||
|
||||
pub fn tableNextColumn() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ig.igTableNextColumn();
|
||||
}
|
||||
|
||||
pub fn tableNextRow() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ig.igTableNextRow();
|
||||
}
|
||||
|
||||
pub fn tableSetColumnIndex(index: usize) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ig.igTableSetColumnIndex(@intCast(index));
|
||||
}
|
||||
|
||||
pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igTableSetupColumn(label, flags);
|
||||
}
|
||||
|
||||
pub fn tableHeadersRow() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igTableHeadersRow();
|
||||
}
|
||||
|
||||
pub const ID = union(enum) {
|
||||
string: []const u8,
|
||||
int: i32
|
||||
};
|
||||
|
||||
pub fn pushID(id: ID) void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (id) {
|
||||
.string => |str| ig.igPushIDStr(str.ptr, str.ptr + str.len),
|
||||
.int => |int| ig.igPushIDInt(int)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn popID() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igPopID();
|
||||
}
|
||||
|
||||
pub const TreeNodeFlags = packed struct {
|
||||
selected: bool = false,
|
||||
framed: bool = false,
|
||||
allow_overlap: bool = false,
|
||||
no_tree_pushOnOpen: bool = false,
|
||||
no_auto_open_on_log: bool = false,
|
||||
default_open: bool = false,
|
||||
open_on_double_click: bool = false,
|
||||
open_on_arrow: bool = false,
|
||||
leaf: bool = false,
|
||||
bullet: bool = false,
|
||||
frame_padding: bool = false,
|
||||
span_avail_width: bool = false,
|
||||
span_full_width: bool = false,
|
||||
span_label_width: bool = false,
|
||||
span_all_columns: bool = false,
|
||||
label_span_all_columns: bool = false,
|
||||
nav_left_jumps_back_here: bool = false,
|
||||
collapsing_header: bool = false,
|
||||
|
||||
fn toInt(self: TreeNodeFlags) u32 {
|
||||
// TODO: Try using comptime to reduce this duplication.
|
||||
// Would be great if `toInt()` could be replaced with just a @bitCast
|
||||
//
|
||||
// If the underlying C enum is exhaustive, maybe a bitcast could be performed?
|
||||
// If the order of enums is correct
|
||||
const flags = .{
|
||||
.{ self.selected, ig.ImGuiTreeNodeFlags_Selected },
|
||||
.{ self.framed, ig.ImGuiTreeNodeFlags_Framed },
|
||||
.{ self.allow_overlap, ig.ImGuiTreeNodeFlags_AllowOverlap },
|
||||
.{ self.no_tree_pushOnOpen, ig.ImGuiTreeNodeFlags_NoTreePushOnOpen },
|
||||
.{ self.no_auto_open_on_log, ig.ImGuiTreeNodeFlags_NoAutoOpenOnLog },
|
||||
.{ self.default_open, ig.ImGuiTreeNodeFlags_DefaultOpen },
|
||||
.{ self.open_on_double_click, ig.ImGuiTreeNodeFlags_OpenOnDoubleClick },
|
||||
.{ self.open_on_arrow, ig.ImGuiTreeNodeFlags_OpenOnArrow },
|
||||
.{ self.leaf, ig.ImGuiTreeNodeFlags_Leaf },
|
||||
.{ self.bullet, ig.ImGuiTreeNodeFlags_Bullet },
|
||||
.{ self.frame_padding, ig.ImGuiTreeNodeFlags_FramePadding },
|
||||
.{ self.span_avail_width, ig.ImGuiTreeNodeFlags_SpanAvailWidth },
|
||||
.{ self.span_full_width, ig.ImGuiTreeNodeFlags_SpanFullWidth },
|
||||
.{ self.span_label_width, ig.ImGuiTreeNodeFlags_SpanLabelWidth },
|
||||
.{ self.span_all_columns, ig.ImGuiTreeNodeFlags_SpanAllColumns },
|
||||
.{ self.label_span_all_columns, ig.ImGuiTreeNodeFlags_LabelSpanAllColumns },
|
||||
.{ self.nav_left_jumps_back_here, ig.ImGuiTreeNodeFlags_NavLeftJumpsBackHere },
|
||||
.{ self.collapsing_header, ig.ImGuiTreeNodeFlags_CollapsingHeader },
|
||||
};
|
||||
|
||||
var sum: u32 = 0;
|
||||
inline for (flags) |flag_pair| {
|
||||
if (flag_pair[0]) {
|
||||
sum += flag_pair[1];
|
||||
}
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn treeNode(label: [*c]const u8, flags: TreeNodeFlags) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igTreeNodeEx(label, @intCast(flags.toInt()));
|
||||
}
|
||||
|
||||
pub fn treePop() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igTreePop();
|
||||
}
|
||||
|
||||
pub fn isItemClicked() bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igIsItemClicked();
|
||||
}
|
||||
|
||||
pub fn isItemToggledOpen() bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igIsItemToggledOpen();
|
||||
}
|
||||
|
||||
pub fn colorPicker4(label: [*c]const u8, color: *Vec4) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igColorPicker4(label, color.asArray().ptr, 0, null);
|
||||
}
|
||||
|
||||
pub fn colorEdit4(label: [*c]const u8, color: *Vec4) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igColorEdit4(label, color.asArray().ptr, 0);
|
||||
}
|
||||
|
||||
pub fn beginCombo(label: [*c]const u8, preview_value: [*c]const u8) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igBeginCombo(label, preview_value, 0);
|
||||
}
|
||||
|
||||
pub fn endCombo() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igEndCombo();
|
||||
}
|
||||
|
||||
pub fn selectable(label: [*c]const u8, selected: bool) bool {
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ig.igSelectableEx(label, selected, 0, .{ });
|
||||
}
|
||||
|
||||
pub fn setItemDefaultFocus() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igSetItemDefaultFocus();
|
||||
}
|
||||
|
||||
pub fn combo(label: [*c]const u8, items: []const [*c]const u8, selected: *usize) void {
|
||||
if (beginCombo(label, items[selected.*])) {
|
||||
defer endCombo();
|
||||
|
||||
for (0.., items) |i, item| {
|
||||
const is_selected = selected.* == i;
|
||||
if (selectable(item, is_selected)) {
|
||||
selected.* = i;
|
||||
}
|
||||
|
||||
if (is_selected) {
|
||||
setItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn separator() void {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
ig.igSeparator();
|
||||
}
|
||||
124
src/engine/input.zig
Normal file
124
src/engine/input.zig
Normal file
@ -0,0 +1,124 @@
|
||||
const std = @import("std");
|
||||
const sokol = @import("sokol");
|
||||
|
||||
const Frame = @import("./frame.zig");
|
||||
const Nanoseconds = Frame.Nanoseconds;
|
||||
|
||||
const Math = @import("./math.zig");
|
||||
const Vec2 = Math.Vec2;
|
||||
|
||||
const Input = @This();
|
||||
|
||||
const SokolKeyCodeInfo = @typeInfo(sokol.app.Keycode);
|
||||
pub const KeyCode = @Type(.{
|
||||
.@"enum" = .{
|
||||
.tag_type = std.math.IntFittingRange(0, sokol.app.max_keycodes-1),
|
||||
.decls = SokolKeyCodeInfo.@"enum".decls,
|
||||
.fields = SokolKeyCodeInfo.@"enum".fields,
|
||||
.is_exhaustive = false
|
||||
}
|
||||
});
|
||||
|
||||
pub const MouseButton = enum(i3) {
|
||||
left = @intFromEnum(sokol.app.Mousebutton.LEFT),
|
||||
right = @intFromEnum(sokol.app.Mousebutton.RIGHT),
|
||||
middle = @intFromEnum(sokol.app.Mousebutton.MIDDLE),
|
||||
_
|
||||
};
|
||||
|
||||
pub fn ButtonStateSet(E: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
down: std.EnumSet(E),
|
||||
pressed: std.EnumSet(E),
|
||||
released: std.EnumSet(E),
|
||||
pressed_at: std.EnumMap(E, Nanoseconds),
|
||||
|
||||
pub const empty = Self{
|
||||
.down = .initEmpty(),
|
||||
.pressed = .initEmpty(),
|
||||
.released = .initEmpty(),
|
||||
.pressed_at = .init(.{}),
|
||||
};
|
||||
|
||||
fn press(self: *Self, button: E, now: Nanoseconds) void {
|
||||
self.pressed_at.put(button, now);
|
||||
self.pressed.insert(button);
|
||||
self.down.insert(button);
|
||||
}
|
||||
|
||||
fn release(self: *Self, button: E) void {
|
||||
self.down.remove(button);
|
||||
self.released.insert(button);
|
||||
self.pressed_at.remove(button);
|
||||
}
|
||||
|
||||
fn releaseAll(self: *Self) void {
|
||||
var iter = self.down.iterator();
|
||||
while (iter.next()) |key_code| {
|
||||
self.released.insert(key_code);
|
||||
}
|
||||
self.down = .initEmpty();
|
||||
self.pressed_at = .init(.{});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Event = union(enum) {
|
||||
mouse_pressed: struct {
|
||||
button: MouseButton,
|
||||
position: Vec2,
|
||||
},
|
||||
mouse_released: struct {
|
||||
button: MouseButton,
|
||||
position: Vec2,
|
||||
},
|
||||
mouse_move: Vec2,
|
||||
mouse_enter: Vec2,
|
||||
mouse_leave,
|
||||
mouse_scroll: Vec2,
|
||||
key_pressed: struct {
|
||||
code: KeyCode,
|
||||
repeat: bool
|
||||
},
|
||||
key_released: Input.KeyCode,
|
||||
window_resize,
|
||||
char: u21,
|
||||
};
|
||||
|
||||
pub fn processEvent(frame: *Frame, event: Event) void {
|
||||
const input = &frame.input;
|
||||
|
||||
switch (event) {
|
||||
.key_pressed => |opts| {
|
||||
if (!opts.repeat) {
|
||||
input.keyboard.press(opts.code, frame.time_ns);
|
||||
}
|
||||
},
|
||||
.key_released => |key_code| {
|
||||
input.keyboard.release(key_code);
|
||||
},
|
||||
.mouse_leave => {
|
||||
input.keyboard.releaseAll();
|
||||
|
||||
input.mouse_position = null;
|
||||
input.mouse_button = .empty;
|
||||
},
|
||||
.mouse_enter => |pos| {
|
||||
input.mouse_position = pos;
|
||||
},
|
||||
.mouse_move => |pos| {
|
||||
input.mouse_position = pos;
|
||||
},
|
||||
.mouse_pressed => |opts| {
|
||||
input.mouse_position = opts.position;
|
||||
input.mouse_button.press(opts.button, frame.time_ns);
|
||||
},
|
||||
.mouse_released => |opts| {
|
||||
input.mouse_position = opts.position;
|
||||
input.mouse_button.release(opts.button);
|
||||
},
|
||||
else => {}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user