Initial commit

This commit is contained in:
Rokas Puzonas 2026-01-30 23:28:18 +02:00
commit 052c3ad624
109 changed files with 7759 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
zig-out
.zig-cache
*.tiled-session

28
README.md Normal file
View 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
View 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
View 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
View 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
View 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",
},
}

View 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);
}

View File

@ -0,0 +1,3 @@
#define STB_IMAGE_IMPLEMENTATION
#define STBI_NO_STDIO
#include "stb_image.h"

View 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;
}

View File

@ -0,0 +1,2 @@
#define STB_RECT_PACK_IMPLEMENTATION
#include "stb_rect_pack.h"

170
libs/stb/src/stb_vorbis.zig Normal file
View 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),
};
}

View 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
View 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);
}
}

View 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
View 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;
}

View 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
View 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
View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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();
}

View File

@ -0,0 +1,14 @@
{
"automappingRulesFile": "",
"commands": [
],
"compatibilityVersion": 1100,
"extensionsPath": "extensions",
"folders": [
"."
],
"properties": [
],
"propertyTypes": [
]
}

BIN
src/assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

View 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

225
src/assets/map.tmx Normal file
View 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>

View 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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

4
src/assets/tilemap.tsx Normal file
View 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
View 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

Binary file not shown.

BIN
src/assets/wood02.ogg Normal file

Binary file not shown.

BIN
src/assets/wood03.ogg Normal file

Binary file not shown.

74
src/engine/audio/data.zig Normal file
View 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
View 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
View 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
View 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)];
}

View 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);
}

View 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,
};

View File

@ -0,0 +1,3 @@
pub const Font = @import("./font.zig");
pub const Context = @import("./context.zig");

View 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
View 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
View 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
View 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
View 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