aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitmodules3
-rw-r--r--build.zig61
m---------lib/spoon0
-rw-r--r--src/ball.zig28
-rw-r--r--src/brothers.zig131
-rw-r--r--src/game.zig47
-rw-r--r--src/main.zig102
-rw-r--r--src/playfield.zig38
-rw-r--r--src/utils.zig12
-rw-r--r--src/wasm4.zig136
10 files changed, 224 insertions, 334 deletions
diff --git a/.gitmodules b/.gitmodules
index 254eeba..e69de29 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +0,0 @@
-[submodule "lib/spoon"]
- path = lib/spoon
- url = https://git.sr.ht/~leon_plickat/zig-spoon
diff --git a/build.zig b/build.zig
index f350b17..7431b33 100644
--- a/build.zig
+++ b/build.zig
@@ -1,57 +1,18 @@
const std = @import("std");
-pub fn build(b: *std.build.Builder) void {
- // Standard target options allows the person running `zig build` to choose
- // what target to build for. Here we do not override the defaults, which
- // means any target is allowed, and the default is native. Other options
- // for restricting supported target set are available.
- const target = b.standardTargetOptions(.{});
-
- // Standard release options allow the person running `zig build` to select
- // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
+pub fn build(b: *std.build.Builder) !void {
const mode = b.standardReleaseOptions();
+ const lib = b.addSharedLibrary("cart", "src/main.zig", .unversioned);
- const exe = b.addExecutable("grenade-brothers", "src/main.zig");
- exe.addPackagePath("spoon", "lib/spoon/import.zig");
- exe.setTarget(target);
- exe.setBuildMode(mode);
- exe.install();
-
- const run_cmd = exe.run();
- run_cmd.step.dependOn(b.getInstallStep());
- if (b.args) |args| {
- run_cmd.addArgs(args);
- }
-
- const coverage = b.option(bool, "test-coverage", "Generate test coverage") orelse false;
-
- const run_step = b.step("run", "Run the app");
- run_step.dependOn(&run_cmd.step);
-
- const exe_tests = b.addTest("src/main.zig");
- exe_tests.addPackagePath("spoon", "lib/spoon/import.zig");
- exe_tests.setTarget(target);
- exe_tests.setBuildMode(mode);
+ lib.setBuildMode(mode);
+ lib.setTarget(.{ .cpu_arch = .wasm32, .os_tag = .freestanding });
+ lib.import_memory = true;
+ lib.initial_memory = 65536;
+ lib.max_memory = 65536;
+ lib.stack_size = 14752;
- // Code coverage with kcov, we need an allocator for the setup
- var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
- defer _ = general_purpose_allocator.deinit();
- const gpa = general_purpose_allocator.allocator();
- // We want to exclude the $HOME/.zig path
- const home = std.process.getEnvVarOwned(gpa, "HOME") catch "";
- defer gpa.free(home);
- const exclude = std.fmt.allocPrint(gpa, "--exclude-path={s}/.zig/,/usr", .{home}) catch "";
- defer gpa.free(exclude);
- if (coverage) {
- exe_tests.setExecCmd(&[_]?[]const u8{
- "kcov",
- exclude,
- //"--path-strip-level=3", // any kcov flags can be specified here
- "kcov-output", // output dir for kcov
- null, // to get zig to use the --test-cmd-bin flag
- });
- }
+ // Export WASM-4 symbols
+ lib.export_symbol_names = &[_][]const u8{ "start", "update" };
- const test_step = b.step("test", "Run unit tests");
- test_step.dependOn(&exe_tests.step);
+ lib.install();
}
diff --git a/lib/spoon b/lib/spoon
deleted file mode 160000
-Subproject c2be37a8087ccb6b01981cb617fef7755dc72b6
diff --git a/src/ball.zig b/src/ball.zig
new file mode 100644
index 0000000..11737bb
--- /dev/null
+++ b/src/ball.zig
@@ -0,0 +1,28 @@
+const std = @import("std");
+const utils = @import("utils.zig");
+const w4 = @import("wasm4.zig");
+
+pub const Ball = struct {
+ x: f64,
+ y: f64,
+ dx: f64,
+ dy: f64,
+ pub fn draw(self: Ball) void {
+ var y = @floatToInt(u8, std.math.round(self.y));
+ var x = @floatToInt(u8, std.math.round(self.x));
+ w4.DRAW_COLORS.* = 0x4321;
+ w4.blit(&ball, x, y, ball_width, ball_height, ball_flags);
+ }
+ pub fn resetRound(self: *Ball, side: utils.side) void {
+ self.x = @intToFloat(f64, utils.startingX[@enumToInt(side)] + 4);
+ self.y = 160 - 32 - 8;
+ self.dx = 0;
+ self.dy = 0;
+ }
+};
+
+//----- Sprite ----------------------------------------------------------------
+const ball_width = 8;
+const ball_height = 8;
+const ball_flags = 1; // BLIT_2BPP
+const ball = [16]u8{ 0x1a, 0xa4, 0x6f, 0xf9, 0xbf, 0xae, 0xbf, 0xae, 0xbf, 0xfe, 0xbf, 0xfe, 0x6f, 0xf9, 0x1a, 0xa4 };
diff --git a/src/brothers.zig b/src/brothers.zig
index 6f5fdce..3b92de4 100644
--- a/src/brothers.zig
+++ b/src/brothers.zig
@@ -1,128 +1,29 @@
const std = @import("std");
-const spoon = @import("spoon");
-
-const ball = @import("ball.zig");
-
-pub const Side = enum(u1) {
- left,
- right,
-};
-
-const startingX = [2]f64{ 15, 60 };
-const colors = [2]spoon.Attribute.Colour{ .blue, .red };
-const leftLimit = [2]f64{ 1, 41 };
-const rightLimit = [2]f64{ 34, 74 }; // (38, 79) minus a brother's width
+const utils = @import("utils.zig");
+const w4 = @import("wasm4.zig");
pub const Brother = struct {
- side: Side,
+ side: utils.side,
score: u8,
- x: f64,
+ x: u8,
y: f64,
- dx: f64,
- dy: f64,
- moveDuration: u64,
- pub fn draw(self: Brother, rc: *spoon.Term.RenderContext) !void {
- try rc.setAttribute(.{ .fg = colors[@enumToInt(self.side)] });
- var iter = std.mem.split(u8, brother, "\n");
- var y = @floatToInt(usize, std.math.round(self.y));
- var x = @floatToInt(usize, std.math.round(self.x));
- while (iter.next()) |line| : (y += 1) {
- try rc.moveCursorTo(y, x);
- _ = try rc.buffer.writer().write(line);
- }
+ pub fn draw(self: Brother) void {
+ var y = @floatToInt(u8, std.math.round(self.y));
+ w4.DRAW_COLORS.* = 0x30;
+ w4.blit(&brother, self.x, y - brother_height, brother_width, brother_height, brother_flags);
}
- pub fn moveJump(self: *Brother) void {
- if (self.dy == 0) { // no double jumps! TODO allow kicks off the wall
- self.dy -= 4 / (1000 / 60.0);
- }
- }
- pub fn moveLeft(self: *Brother) void {
- self.dx -= 5 / (1000 / 60.0);
- self.moveDuration = 24;
- }
- pub fn moveRight(self: *Brother) void {
- self.dx += 5 / (1000 / 60.0);
- self.moveDuration = 24;
- }
- pub fn reset(self: *Brother, side: Side) void {
+ pub fn reset(self: *Brother, side: utils.side) void {
self.side = side;
self.score = 0;
}
pub fn resetRound(self: *Brother) void {
- self.x = startingX[@enumToInt(self.side)];
- self.y = 17;
- self.dx = 0;
- self.dy = 0;
- self.moveDuration = 0;
- }
- pub fn step(self: *Brother, b: *ball.Ball) void {
- // Horizontal movement
- const x = self.x + self.dx;
- const ll = leftLimit[@enumToInt(self.side)];
- const rl = rightLimit[@enumToInt(self.side)];
- if (x < ll) {
- self.x = ll;
- self.dx = 0;
- self.moveDuration = 0;
- } else if (x > rl) {
- self.x = rl;
- self.dx = 0;
- self.moveDuration = 0;
- } else {
- self.x = x;
- if (self.moveDuration > 0) {
- self.moveDuration -= 1;
- if (self.moveDuration == 0) {
- self.dx = 0;
- }
- }
- }
- // Vertical movement
- const y = self.y + self.dy;
- if (y < 12) { // jumping
- self.y = 12;
- self.dy = -self.dy;
- } else if (y > 17) { // falling
- self.y = 17;
- self.dy = 0;
- } else {
- self.y = y;
- }
- // Check for ball collisions
- if (b.y >= y and b.y <= y + 2 and b.x >= x and b.x < x + 5) {
- if (b.dy > 0) {
- b.dy = -b.dy / 1.5;
- }
- b.dx = b.dx / 2.0;
- var strength: f64 = 1;
- if (b.dx > 0 and self.dx < 0)
- strength *= 2; // moving in opposite directions
- if (y < 12) { // jumping
- strength *= 2;
- }
- if (b.x < x + 1) {
- b.dx -= strength * 4 / (1000 / 60.0);
- } else if (b.x < x + 2) {
- b.dx -= strength * 2 / (1000 / 60.0);
- } else if (b.x < x + 3) {
- var modifier: f64 = 1;
- if (self.side == .left) modifier = -1;
- b.dx += modifier * strength * 2 / (1000 / 60.0);
- } else if (b.x < x + 4) {
- b.dx += strength * 2 / (1000 / 60.0);
- } else {
- b.dx += strength * 4 / (1000 / 60.0);
- }
- b.dy = b.dy * strength - 0.04;
- }
+ self.x = utils.startingX[@enumToInt(self.side)];
+ self.y = 160;
}
};
-const brother =
- \\█ █
- \\█ █ █
- \\█████
- \\█████
- \\█ █
- \\█ █
-;
+//----- Sprite ----------------------------------------------------------------
+const brother_width = 16;
+const brother_height = 32;
+const brother_flags = 0; // BLIT_1BPP
+const brother = [64]u8{ 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xfb, 0xdf, 0xfb, 0xdf, 0xfb, 0xdf, 0xfb, 0xdf, 0xf9, 0x9f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f, 0xf8, 0x1f };
diff --git a/src/game.zig b/src/game.zig
index 7f44856..7dbba66 100644
--- a/src/game.zig
+++ b/src/game.zig
@@ -1,50 +1,27 @@
const std = @import("std");
-const spoon = @import("spoon");
+const utils = @import("utils.zig");
+const w4 = @import("wasm4.zig");
const ball = @import("ball.zig");
const brothers = @import("brothers.zig");
-const playfield = @import("playfield.zig");
pub const Game = struct {
ball: ball.Ball = undefined,
brothers: [2]brothers.Brother = undefined,
- side: brothers.Side = undefined,
- pub fn draw(self: Game, rc: *spoon.Term.RenderContext) !void {
- try playfield.draw(rc);
- try self.brothers[0].draw(rc);
- try self.brothers[1].draw(rc);
- try self.ball.draw(rc);
- var score: [1]u8 = undefined;
- try rc.moveCursorTo(1, 3);
- score[0] = '0' + self.brothers[0].score;
- _ = try rc.buffer.writer().write(score[0..]);
- try rc.moveCursorTo(1, 76);
- score[0] = '0' + self.brothers[1].score;
- _ = try rc.buffer.writer().write(score[0..]);
+ playerSide: utils.side = undefined,
+ pub fn draw(self: *Game) void {
+ self.ball.draw();
+ self.brothers[0].draw();
+ self.brothers[1].draw();
+ // draw the net
+ w4.DRAW_COLORS.* = 0x42;
+ w4.rect(78, 100, 4, 61);
}
- pub fn moveJump(self: *Game, side: brothers.Side) void {
- self.brothers[@enumToInt(side)].moveJump();
- }
- pub fn moveLeft(self: *Game, side: brothers.Side) void {
- self.brothers[@enumToInt(side)].moveLeft();
- }
- pub fn moveRight(self: *Game, side: brothers.Side) void {
- self.brothers[@enumToInt(side)].moveRight();
- }
- pub fn reset(self: *Game, side: brothers.Side) void {
- self.side = side;
+ pub fn reset(self: *Game) void {
self.brothers[0].reset(.left);
self.brothers[1].reset(.right);
- self.resetRound();
- }
- pub fn resetRound(self: *Game) void {
- self.ball.reset(.left);
+ self.ball.resetRound(.left);
self.brothers[0].resetRound();
self.brothers[1].resetRound();
}
- pub fn step(self: *Game) void {
- self.ball.step();
- self.brothers[0].step(&self.ball);
- self.brothers[1].step(&self.ball);
- }
};
diff --git a/src/main.zig b/src/main.zig
index 875b02c..29b7db5 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,99 +1,15 @@
-const std = @import("std");
-const spoon = @import("spoon");
-
const game = @import("game.zig");
+const std = @import("std");
+const utils = @import("utils.zig");
+const w4 = @import("wasm4.zig");
-var term: spoon.Term = undefined;
-var done: bool = false;
-
-//----- Game State -----------------------------------------------------------
-var gs: game.Game = undefined;
-
-//----- Main -----------------------------------------------------------------
-pub fn main() !void {
- try term.init();
- defer term.deinit();
-
- std.os.sigaction(std.os.SIG.WINCH, &std.os.Sigaction{
- .handler = .{ .handler = handleSigWinch },
- .mask = std.os.empty_sigset,
- .flags = 0,
- }, null);
-
- var fds: [1]std.os.pollfd = undefined;
- fds[0] = .{
- .fd = term.tty.handle,
- .events = std.os.POLL.IN,
- .revents = undefined,
- };
-
- try term.uncook(.{});
- defer term.cook() catch {};
-
- try term.fetchSize();
- try term.setWindowTitle("Grenade Brothers", .{});
-
- gs.reset(.left);
- try renderAll();
-
- var buf: [16]u8 = undefined;
- while (!done) {
- // TODO We need to measure how long it took before a key was hit so that we can adjust the timeout on the next loop
- // otherwise we will get inconsistent ticks for movement steps
- const timeout = try std.os.poll(&fds, @floatToInt(u64, 1000 / 60.0));
-
- if (timeout > 0) { // if timeout if not 0 then some fds we are polling have events for us
- const read = try term.readInput(&buf);
- var it = spoon.inputParser(buf[0..read]);
- while (it.next()) |in| {
- if (in.eqlDescription("escape") or in.eqlDescription("q")) {
- done = true;
- break;
- } else if (in.eqlDescription("arrow-left")) {
- gs.moveLeft(.right);
- } else if (in.eqlDescription("arrow-right")) {
- gs.moveRight(.right);
- } else if (in.eqlDescription("arrow-up")) {
- gs.moveJump(.right);
- } else if (in.eqlDescription("a")) {
- gs.moveLeft(.left);
- } else if (in.eqlDescription("d")) {
- gs.moveRight(.left);
- } else if (in.eqlDescription("space")) {
- gs.moveJump(.left);
- }
- }
- } else {
- gs.step();
- }
- try renderAll();
- }
-}
-
-fn renderAll() !void {
- var rc = try term.getRenderContext();
- defer rc.done() catch {};
-
- try rc.clear();
-
- if (term.width < 80 or term.width < 24) {
- try rc.setAttribute(.{ .fg = .red, .bold = true });
- try rc.writeAllWrapping("Terminal too small!");
- return;
- }
-
- try gs.draw(&rc);
-}
+//----- Globals ---------------------------------------------------------------
+var Game: game.Game = undefined;
-fn handleSigWinch(_: c_int) callconv(.C) void {
- term.fetchSize() catch {};
- renderAll() catch {};
+export fn start() void {
+ Game.reset();
}
-/// Custom panic handler, so that we can try to cook the terminal on a crash,
-/// as otherwise all messages will be mangled.
-pub fn panic(msg: []const u8, trace: ?*std.builtin.StackTrace) noreturn {
- @setCold(true);
- term.cook() catch {};
- std.builtin.default_panic(msg, trace);
+export fn update() void {
+ Game.draw();
}
diff --git a/src/playfield.zig b/src/playfield.zig
deleted file mode 100644
index 757ec60..0000000
--- a/src/playfield.zig
+++ /dev/null
@@ -1,38 +0,0 @@
-const std = @import("std");
-const spoon = @import("spoon");
-
-pub fn draw(rc: *spoon.Term.RenderContext) !void {
- var iter = std.mem.split(u8, field, "\n");
- var y: usize = 0;
- while (iter.next()) |line| : (y += 1) {
- try rc.moveCursorTo(y, 0);
- _ = try rc.buffer.writer().write(line);
- }
-}
-
-const field =
- \\████████████████████████████████████████████████████████████████████████████████
- \\█ ██ █
- \\████████████████████████████████████████████████████████████████████████████████
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\█ ██ █
- \\████████████████████████████████████████████████████████████████████████████████
-;
diff --git a/src/utils.zig b/src/utils.zig
new file mode 100644
index 0000000..5fa40f9
--- /dev/null
+++ b/src/utils.zig
@@ -0,0 +1,12 @@
+//----- Physics ---------------------------------------------------------------
+pub const gravity: f64 = 9.807; // m/s²
+pub const scale: f64 = 1.0 / 30; // 30 pixels == 1m
+
+//----- Playground ------------------------------------------------------------
+pub const side = enum(u1) {
+ left,
+ right,
+};
+pub const startingX = [2]u8{ 23, 160 - 23 - 16 };
+pub const leftLimit = [2]u8{ 0, 81 };
+pub const rightLimit = [2]u8{ 77, 159 };
diff --git a/src/wasm4.zig b/src/wasm4.zig
new file mode 100644
index 0000000..c46a261
--- /dev/null
+++ b/src/wasm4.zig
@@ -0,0 +1,136 @@
+//
+// WASM-4: https://wasm4.org/docs
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Platform Constants │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+pub const SCREEN_SIZE: u32 = 160;
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Memory Addresses │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+pub const PALETTE: *[4]u32 = @intToPtr(*[4]u32, 0x04);
+pub const DRAW_COLORS: *u16 = @intToPtr(*u16, 0x14);
+pub const GAMEPAD1: *const u8 = @intToPtr(*const u8, 0x16);
+pub const GAMEPAD2: *const u8 = @intToPtr(*const u8, 0x17);
+pub const GAMEPAD3: *const u8 = @intToPtr(*const u8, 0x18);
+pub const GAMEPAD4: *const u8 = @intToPtr(*const u8, 0x19);
+pub const MOUSE_X: *const i16 = @intToPtr(*const i16, 0x1a);
+pub const MOUSE_Y: *const i16 = @intToPtr(*const i16, 0x1c);
+pub const MOUSE_BUTTONS: *const u8 = @intToPtr(*const u8, 0x1e);
+pub const SYSTEM_FLAGS: *u8 = @intToPtr(*u8, 0x1f);
+pub const NETPLAY: *const u8 = @intToPtr(*const u8, 0x20);
+pub const FRAMEBUFFER: *[6400]u8 = @intToPtr(*[6400]u8, 0xA0);
+
+pub const BUTTON_1: u8 = 1;
+pub const BUTTON_2: u8 = 2;
+pub const BUTTON_LEFT: u8 = 16;
+pub const BUTTON_RIGHT: u8 = 32;
+pub const BUTTON_UP: u8 = 64;
+pub const BUTTON_DOWN: u8 = 128;
+
+pub const MOUSE_LEFT: u8 = 1;
+pub const MOUSE_RIGHT: u8 = 2;
+pub const MOUSE_MIDDLE: u8 = 4;
+
+pub const SYSTEM_PRESERVE_FRAMEBUFFER: u8 = 1;
+pub const SYSTEM_HIDE_GAMEPAD_OVERLAY: u8 = 2;
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Drawing Functions │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+/// Copies pixels to the framebuffer.
+pub extern fn blit(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, flags: u32) void;
+
+/// Copies a subregion within a larger sprite atlas to the framebuffer.
+pub extern fn blitSub(sprite: [*]const u8, x: i32, y: i32, width: u32, height: u32, src_x: u32, src_y: u32, stride: u32, flags: u32) void;
+
+pub const BLIT_2BPP: u32 = 1;
+pub const BLIT_1BPP: u32 = 0;
+pub const BLIT_FLIP_X: u32 = 2;
+pub const BLIT_FLIP_Y: u32 = 4;
+pub const BLIT_ROTATE: u32 = 8;
+
+/// Draws a line between two points.
+pub extern fn line(x1: i32, y1: i32, x2: i32, y2: i32) void;
+
+/// Draws an oval (or circle).
+pub extern fn oval(x: i32, y: i32, width: u32, height: u32) void;
+
+/// Draws a rectangle.
+pub extern fn rect(x: i32, y: i32, width: u32, height: u32) void;
+
+/// Draws text using the built-in system font.
+pub fn text(str: []const u8, x: i32, y: i32) void {
+ textUtf8(str.ptr, str.len, x, y);
+}
+extern fn textUtf8(strPtr: [*]const u8, strLen: usize, x: i32, y: i32) void;
+
+/// Draws a vertical line
+pub extern fn vline(x: i32, y: i32, len: u32) void;
+
+/// Draws a horizontal line
+pub extern fn hline(x: i32, y: i32, len: u32) void;
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Sound Functions │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+/// Plays a sound tone.
+pub extern fn tone(frequency: u32, duration: u32, volume: u32, flags: u32) void;
+
+pub const TONE_PULSE1: u32 = 0;
+pub const TONE_PULSE2: u32 = 1;
+pub const TONE_TRIANGLE: u32 = 2;
+pub const TONE_NOISE: u32 = 3;
+pub const TONE_MODE1: u32 = 0;
+pub const TONE_MODE2: u32 = 4;
+pub const TONE_MODE3: u32 = 8;
+pub const TONE_MODE4: u32 = 12;
+pub const TONE_PAN_LEFT: u32 = 16;
+pub const TONE_PAN_RIGHT: u32 = 32;
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Storage Functions │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+/// Reads up to `size` bytes from persistent storage into the pointer `dest`.
+pub extern fn diskr(dest: [*]u8, size: u32) u32;
+
+/// Writes up to `size` bytes from the pointer `src` into persistent storage.
+pub extern fn diskw(src: [*]const u8, size: u32) u32;
+
+// ┌───────────────────────────────────────────────────────────────────────────┐
+// │ │
+// │ Other Functions │
+// │ │
+// └───────────────────────────────────────────────────────────────────────────┘
+
+/// Prints a message to the debug console.
+pub fn trace(x: []const u8) void {
+ traceUtf8(x.ptr, x.len);
+}
+extern fn traceUtf8(strPtr: [*]const u8, strLen: usize) void;
+
+/// Use with caution, as there's no compile-time type checking.
+///
+/// * %c, %d, and %x expect 32-bit integers.
+/// * %f expects 64-bit floats.
+/// * %s expects a *zero-terminated* string pointer.
+///
+/// See https://github.com/aduros/wasm4/issues/244 for discussion and type-safe
+/// alternatives.
+pub extern fn tracef(x: [*:0]const u8, ...) void;