From 83ce69a794196ad5919deaf97178c42d88025ca7 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 9 Feb 2026 00:53:32 +0200 Subject: [PATCH] add basic replay --- engine/build.zig | 2 +- engine/src/lib/root.zig | 7 +- engine/src/runtime/engine.zig | 496 +++++++++++++++++++++++--------- engine/src/runtime/graphics.zig | 6 - engine/src/runtime/input.zig | 52 +--- 5 files changed, 367 insertions(+), 196 deletions(-) diff --git a/engine/build.zig b/engine/build.zig index ee5d44e..c523147 100644 --- a/engine/build.zig +++ b/engine/build.zig @@ -35,7 +35,7 @@ pub fn init(outer_builder: *std.Build, opts: InitOptions) void { } const has_imgui = opts.has_imgui orelse (opts.optimize == .Debug); - const statically_linked = opts.statically_linked orelse !hot_reload; + const statically_linked = opts.statically_linked orelse true; var build_options = b.addOptions(); build_options.addOption(bool, "has_imgui", has_imgui); diff --git a/engine/src/lib/root.zig b/engine/src/lib/root.zig index caaeb94..8f07b84 100644 --- a/engine/src/lib/root.zig +++ b/engine/src/lib/root.zig @@ -286,7 +286,7 @@ pub const Frame = struct { screen_size: Vec2, clear_color: Vec4, - pub fn init(gpa: std.mem.Allocator) Frame { + pub fn init(gpa: std.mem.Allocator, screen_size: Vec2) Frame { return Frame{ .arena = std.heap.ArenaAllocator.init(gpa), .time_ns = 0, @@ -300,7 +300,7 @@ pub const Frame = struct { .clear_color = rgb(0, 0, 0), .audio_commands = .empty, .hide_cursor = false, - .screen_size = .init(0, 0), + .screen_size = screen_size, }; } @@ -490,7 +490,8 @@ pub fn exportCallbacksIfNeeded( comptime deinit: Callbacks.DeinitFn, comptime debug: Callbacks.DebugFn ) void { - if (build_options.statically_linked) { + const builtin = @import("builtin"); + if (builtin.output_mode != .Lib) { return; } diff --git a/engine/src/runtime/engine.zig b/engine/src/runtime/engine.zig index ab3a237..6614861 100644 --- a/engine/src/runtime/engine.zig +++ b/engine/src/runtime/engine.zig @@ -24,6 +24,7 @@ const GameCallbacks = Lib.Callbacks; pub const Math = Lib.Math; pub const Vec2 = Math.Vec2; const rgb = Math.rgb; +const Vec4 = Math.Vec4; const GameLinking = struct { kind: union(enum) { @@ -246,37 +247,219 @@ const GameLinking = struct { }; const GameState = struct { - state: GameCallbacks.State, + gpa: std.mem.Allocator, - input: Input, + state: ?GameCallbacks.State, + + mouse_position: ?Vec2, frame: Lib.Frame, started_at: std.time.Instant, last_frame_at: Lib.Nanoseconds, - previous_tick_canvas_size: ?Vec2, seed: u64, - const Options = struct { - gpa: std.mem.Allocator, + const Initial = struct { + screen_size: Vec2, seed: u64 }; - pub fn init(opts: Options) GameState { + const Tick = struct { + dt_ns: u64, + events: []Input.Event, + }; + + const TickResult = struct { + audio: []Lib.AudioCommand, + graphics: []Lib.GraphicsCommand, + clear_color: Vec4, + cursor_hidden: bool + }; + + pub fn init(gpa: std.mem.Allocator, initial: Initial) GameState { return GameState{ - .input = .initial, - .frame = .init(opts.gpa), + .gpa = gpa, + .mouse_position = null, + .frame = .init(gpa, initial.screen_size), .started_at = std.time.Instant.now() catch @panic("Instant.now() unsupported"), .last_frame_at = 0, - .previous_tick_canvas_size = null, - .seed = opts.seed, - .state = undefined + .seed = initial.seed, + .state = null }; } + fn processEvent(self: *GameState, event: Input.Event) void { + const frame = &self.frame; + switch (event) { + .key_pressed => |opts| { + if (!opts.repeat) { + frame.keyboard.press(opts.code, self.frame.time_ns); + } + }, + .key_released => |key_code| { + frame.keyboard.release(key_code); + }, + .mouse_leave => { + frame.keyboard.releaseAll(); + + assert(self.mouse_position != null); + self.mouse_position = null; + frame.mouse_button = .empty; + }, + .mouse_enter => |pos| { + assert(self.mouse_position == null); + self.mouse_position = pos; + }, + .mouse_move => |pos| { + assert(self.mouse_position != null); + self.mouse_position = pos; + }, + .mouse_pressed => |opts| { + assert(self.mouse_position != null); + self.mouse_position = opts.position; + frame.mouse_button.press(opts.button, frame.time_ns); + }, + .mouse_released => |opts| { + assert(self.mouse_position != null); + self.mouse_position = opts.position; + frame.mouse_button.release(opts.button); + }, + .window_resize => |opts| { + frame.screen_size = .initFromInt(u32, opts.width, opts.height); + }, + else => {} + } + } + + fn runGameInit(self: *GameState, init_fn: GameCallbacks.InitFn) void { + const opts = Lib.Init{ + .gpa = self.gpa, + .seed = self.seed, + }; + self.state = init_fn(&opts); + } + + fn runGameDeinit(self: *GameState, deinit_fn: GameCallbacks.DeinitFn) void { + assert(self.state != null); + deinit_fn(self.state.?); + self.state = null; + } + + fn runGameTick(self: *GameState, tick_fn: GameCallbacks.TickFn) void { + assert(self.state != null); + tick_fn(self.state.?, &self.frame); + } + + fn tick(self: *GameState, tick_fn: GameCallbacks.TickFn, opts: GameState.Tick) !TickResult { + const frame = &self.frame; + + _ = frame.arena.reset(.retain_capacity); + const arena = frame.arena.allocator(); + + const audio_commands_capacity = frame.audio_commands.capacity; + frame.audio_commands = .empty; + try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity); + + const graphics_commands_capacity = frame.graphics_commands.capacity; + frame.graphics_commands = .empty; + try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity); + + frame.dt_ns = opts.dt_ns; + frame.time_ns += opts.dt_ns; + + for (opts.events) |event| { + if (self.mouse_position == null) { + if (event == .mouse_leave) { + continue; + } + + const enter_position = switch(event) { + .mouse_pressed => |e_opts| e_opts.position, + .mouse_released => |e_opts| e_opts.position, + .mouse_move => |pos| pos, + else => null + }; + + if (enter_position) |pos| { + self.processEvent(.{ .mouse_enter = pos }); + } + } else { + if (event == .mouse_enter) { + continue; + } + } + + self.processEvent(event); + } + + frame.mouse_position = self.mouse_position; + + var maybe_screen_scaler: ?ScreenScalar = null; + if (frame.canvas_size) |canvas_size| { + const screen_scaler = ScreenScalar.init(frame.screen_size, canvas_size); + maybe_screen_scaler = screen_scaler; + screen_scaler.push(frame); + + if (frame.mouse_position) |mouse_position| { + frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale); + } + } + + self.runGameTick(tick_fn); + + if (maybe_screen_scaler) |screen_scaler| { + screen_scaler.pop(frame, frame.clear_color); + } + + frame.keyboard.pressed = .initEmpty(); + frame.keyboard.released = .initEmpty(); + frame.mouse_button.pressed = .initEmpty(); + frame.mouse_button.released = .initEmpty(); + + return TickResult{ + .graphics = self.frame.graphics_commands.items, + .audio = self.frame.audio_commands.items, + .clear_color = self.frame.clear_color, + .cursor_hidden = self.frame.hide_cursor + }; + } + + fn runGameDebug(self: *GameState, debug_fn: GameCallbacks.DebugFn, imgui_ctx: *Lib.ImGui) void { + assert(self.state != null); + debug_fn(self.state.?, imgui_ctx); + } + pub fn deinit(self: *GameState) void { self.frame.deinit(); } }; +const Recording = struct { + arena: std.heap.ArenaAllocator, + + initial: GameState.Initial, + ticks: std.ArrayList(GameState.Tick), + + pub fn init(gpa: std.mem.Allocator, initial: GameState.Initial) Recording { + return Recording{ + .arena = .init(gpa), + .ticks = .empty, + .initial = initial + }; + } + + pub fn appendTick(self: *Recording, tick: GameState.Tick) !void { + const arena = self.arena.allocator(); + + try self.ticks.append(arena, .{ + .dt_ns = tick.dt_ns, + .events = try arena.dupe(Input.Event, tick.events) + }); + } + + pub fn deinit(self: Recording) void { + self.arena.deinit(); + } +}; + const Engine = @This(); allocator: std.mem.Allocator, @@ -285,31 +468,58 @@ graphics: Graphics, game_linking: GameLinking, game_state: GameState, +queued_events: std.ArrayList(Input.Event), +is_mouse_inside: bool, + seed: u64, show_debug: bool, restart_game: bool, +recording: ?Recording, + +play_recording: bool, +recording_tick: u64, const RunOptions = struct { allocator: std.mem.Allocator, - game_library_path: ?[]const u8 = null + game_library_path: ?[]const u8 = null, + do_recording: bool = (builtin.mode == .Debug), + screen_width: u32 = 640, + screen_height: u32 = 480, }; pub fn run(self: *Engine, opts: RunOptions) !void { + var game_linking: GameLinking = undefined; + if (opts.game_library_path) |game_library_path| { + game_linking = try .initDynamic(opts.allocator, game_library_path); + } else { + game_linking = try .initStatic(); + } + + const initial = GameState.Initial{ + .seed = @bitCast(std.time.milliTimestamp()), + .screen_size = .initFromInt(u32, opts.screen_width, opts.screen_height) + }; + var game_state: GameState = .init(opts.allocator, initial); + game_state.runGameInit(game_linking.callbacks.init); + self.* = Engine{ .allocator = opts.allocator, .graphics = undefined, - .game_state = undefined, - .game_linking = undefined, + .game_state = game_state, + .game_linking = game_linking, .show_debug = false, .restart_game = false, - .seed = @bitCast(std.time.milliTimestamp()), + .recording = null, + .queued_events = .empty, + .is_mouse_inside = false, + .play_recording = false, + .recording_tick = 0, + .seed = initial.seed, }; - if (opts.game_library_path) |game_library_path| { - self.game_linking = try .initDynamic(self.allocator, game_library_path); - } else { - self.game_linking = try .initStatic(); + if (opts.do_recording) { + self.recording = .init(opts.allocator, initial); } tracy.setThreadName("Main"); @@ -350,8 +560,8 @@ pub fn run(self: *Engine, opts: RunOptions) !void { .cleanup_userdata_cb = sokolCleanupCallback, .event_userdata_cb = sokolEventCallback, .user_data = self, - .width = 640, - .height = 480, + .width = @intCast(opts.screen_width), + .height = @intCast(opts.screen_height), .icon = icon, .window_title = "Game", .logger = .{ .func = sokolLogCallback }, @@ -378,24 +588,18 @@ fn sokolInit(self: *Engine) !void { .allocator = self.allocator, .logger = .{ .func = sokolLogCallback }, }); - - self.game_state = .init(.{ - .gpa = self.allocator, - .seed = self.seed - }); - - const opts = Lib.Init{ - .gpa = self.allocator, - .seed = self.game_state.seed, - }; - self.game_state.state = self.game_linking.callbacks.init(&opts); } fn sokolCleanup(self: *Engine) void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - self.game_linking.callbacks.deinit(self.game_state.state); + if (self.recording) |recording| { + recording.deinit(); + } + + self.queued_events.deinit(self.allocator); + self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); self.game_state.deinit(); Audio.deinit(); self.graphics.deinit(self.allocator); @@ -408,105 +612,79 @@ fn sokolFrame(self: *Engine) !void { const zone = tracy.initZone(@src(), .{ }); defer zone.deinit(); - const frame = &self.game_state.frame; - try self.game_linking.check(); - const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); + const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); + const time_passed = now.since(self.game_state.started_at); + const dt_ns = time_passed - self.game_state.last_frame_at; + self.game_state.last_frame_at = time_passed; - var maybe_screen_scaler: ?ScreenScalar = null; - if (self.game_state.previous_tick_canvas_size) |canvas_size| { - maybe_screen_scaler = ScreenScalar.init(screen_size, canvas_size); + if (self.play_recording) { + assert(self.recording != null); + if (self.recording_tick >= self.recording.?.ticks.items.len) { + self.play_recording = false; + } } + var tick: GameState.Tick = undefined; + if (self.play_recording) { + assert(self.recording != null); + const ticks = self.recording.?.ticks.items; + tick = ticks[self.recording_tick]; + self.recording_tick += 1; + } else { + tick = GameState.Tick{ + .dt_ns = dt_ns, + .events = self.queued_events.items + }; + + if (self.recording) |*recording| { + try recording.appendTick(tick); + } + } + + const tick_result = try self.game_state.tick(self.game_linking.callbacks.tick, tick); + + self.queued_events.clearRetainingCapacity(); + { - const now = std.time.Instant.now() catch @panic("Instant.now() unsupported"); - const time_passed = now.since(self.game_state.started_at); - defer self.game_state.last_frame_at = time_passed; + sapp.showMouse(!tick_result.cursor_hidden); - _ = frame.arena.reset(.retain_capacity); - const arena = frame.arena.allocator(); + { + self.graphics.beginFrame(); + defer self.graphics.endFrame(tick_result.clear_color); - const audio_commands_capacity = frame.audio_commands.capacity; - frame.audio_commands = .empty; - try frame.audio_commands.ensureTotalCapacity(arena, audio_commands_capacity); + self.graphics.drawCommands(tick_result.graphics); - const graphics_commands_capacity = frame.graphics_commands.capacity; - frame.graphics_commands = .empty; - try frame.graphics_commands.ensureTotalCapacity(arena, graphics_commands_capacity); - - frame.screen_size = screen_size; - frame.time_ns = time_passed; - frame.dt_ns = time_passed - self.game_state.last_frame_at; - - frame.mouse_position = self.game_state.input.mouse_position; - if (maybe_screen_scaler) |screen_scaler| { - screen_scaler.push(frame); - - if (frame.mouse_position) |mouse_position| { - frame.mouse_position = mouse_position.sub(screen_scaler.translation).divideScalar(screen_scaler.scale); + if (self.show_debug and build_options.has_imgui) { + try self.showDebugWindow(&self.game_state.frame); } } - if (debug_keybinds) { - if (frame.isKeyPressed(.F3)) { - self.show_debug = !self.show_debug; - } - - if (frame.isKeyPressed(.F5)) { - self.restart_game = true; - } + for (tick_result.audio) |command| { + try Audio.mixer.commands.push(command); } - - self.game_linking.callbacks.tick(self.game_state.state, frame); - - if (maybe_screen_scaler) |screen_scaler| { - screen_scaler.pop(frame, frame.clear_color); - } - - frame.keyboard.pressed = .initEmpty(); - frame.keyboard.released = .initEmpty(); - frame.mouse_button.pressed = .initEmpty(); - frame.mouse_button.released = .initEmpty(); - } - - // Canvas size modification must always be applied a frame later. - // So that mouse coordinate transformations are consistent. - self.game_state.previous_tick_canvas_size = frame.canvas_size; - - sapp.showMouse(!frame.hide_cursor); - - { - self.graphics.beginFrame(); - defer self.graphics.endFrame(frame.clear_color); - - self.graphics.drawCommands(frame.graphics_commands.items); - - if (self.show_debug and build_options.has_imgui) { - try self.showDebugWindow(frame); - } - } - - for (frame.audio_commands.items) |command| { - try Audio.mixer.commands.push(command); } if (self.restart_game) { self.restart_game = false; - self.game_linking.callbacks.deinit(self.game_state.state); - self.game_state.deinit(); - self.game_state = .init(.{ - .gpa = self.allocator, - .seed = self.seed - }); - - const opts = Lib.Init{ - .gpa = self.allocator, - .seed = self.game_state.seed, + const initial = GameState.Initial{ + .seed = self.seed, + .screen_size = .init(sapp.widthf(), sapp.heightf()) }; - self.game_state.state = self.game_linking.callbacks.init(&opts); + + self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); + self.game_state.deinit(); + + self.game_state = .init(self.allocator, initial); + self.game_state.runGameInit(self.game_linking.callbacks.init); + + if (self.recording) |recording| { + recording.deinit(); + self.recording = .init(self.allocator, initial); + } } } @@ -528,7 +706,8 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { var imgui_ctx = Lib.ImGui.init(self.allocator); defer imgui_ctx.deinit(); - self.game_linking.callbacks.debug(self.game_state.state, &imgui_ctx); + + self.game_state.runGameDebug(self.game_linking.callbacks.debug, &imgui_ctx); for (imgui_ctx.commands.items) |cmd| { switch (cmd) { .text => |str| imgui.text(str) @@ -543,6 +722,22 @@ fn showDebugWindow(self: *Engine, frame: *Lib.Frame) !void { self.restart_game = true; } + if (self.recording) |recording| { + imgui.beginDisabled(self.play_recording); + defer imgui.endDisabled(); + + if (imgui.button("Replay")) { + self.game_state.runGameDeinit(self.game_linking.callbacks.deinit); + self.game_state.deinit(); + + self.game_state = .init(self.allocator, recording.initial); + self.game_state.runGameInit(self.game_linking.callbacks.init); + + self.play_recording = true; + self.recording_tick = 0; + } + } + const linking_label = if (self.game_linking.kind == .static) "static" else "dynamic"; imgui.textFmt("Linking: {s}", .{ linking_label }); @@ -567,24 +762,38 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { const e = e_ptr.*; - const input = &self.game_state.input; - const frame = &self.game_state.frame; - if (imgui.handleEvent(e)) { - if (input.mouse_position != null) { - input.processEvent(frame, .{ + if (self.is_mouse_inside) { + try self.queued_events.append(self.allocator, .{ .mouse_leave = {} }); + self.is_mouse_inside = false; } - input.mouse_position = null; + return true; } + if (debug_keybinds and e.type == .KEY_DOWN) { + if (e.key_code == .F4) { + self.show_debug = !self.show_debug; + return true; + } + + if (e.key_code == .F5) { + self.restart_game = true; + return true; + } + } + + if (self.play_recording) { + return false; + } + blk: switch (e.type) { .MOUSE_DOWN => { const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .mouse_pressed = .{ .button = mouse_button, .position = Vec2.init(e.mouse_x, e.mouse_y) @@ -596,7 +805,7 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { .MOUSE_UP => { const mouse_button = Input.getMouseButtonFromSokol(e.mouse_button) orelse break :blk; - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .mouse_released = .{ .button = mouse_button, .position = Vec2.init(e.mouse_x, e.mouse_y) @@ -606,48 +815,57 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { return true; }, .MOUSE_MOVE => { - if (input.mouse_position == null) { - input.processEvent(frame, .{ - .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) - }); - } else { - input.processEvent(frame, .{ + if (self.is_mouse_inside) { + try self.queued_events.append(self.allocator, .{ .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) }); + } else { + try self.queued_events.append(self.allocator, .{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + self.is_mouse_inside = true; } return true; }, .MOUSE_ENTER => { - if (input.mouse_position == null) { - input.processEvent(frame, .{ + if (!self.is_mouse_inside) { + try self.queued_events.append(self.allocator, .{ .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) }); + self.is_mouse_inside = true; } + return true; }, .RESIZED => { - if (input.mouse_position != null) { - input.processEvent(frame, .{ + if (self.is_mouse_inside) { + try self.queued_events.append(self.allocator, .{ .mouse_leave = {} }); + self.is_mouse_inside = false; } - input.processEvent(frame, .{ - .window_resize = {} + try self.queued_events.append(self.allocator, .{ + .window_resize = .{ + .width = @intCast(e.window_width), + .height = @intCast(e.window_height) + } }); return true; }, .MOUSE_LEAVE => { - if (input.mouse_position != null) { - input.processEvent(frame, .{ + if (self.is_mouse_inside) { + try self.queued_events.append(self.allocator, .{ .mouse_leave = {} }); + + self.is_mouse_inside = false; } return true; }, .MOUSE_SCROLL => { - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) }); @@ -656,7 +874,7 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { .KEY_DOWN => { const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .key_pressed = .{ .code = key_code, .repeat = e.key_repeat @@ -668,14 +886,14 @@ fn sokolEvent(self: *Engine, e_ptr: [*c]const sapp.Event) !bool { .KEY_UP => { const key_code = Input.getKeyCodeFromSokol(e.key_code) orelse break :blk; - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .key_released = key_code }); return true; }, .CHAR => { - input.processEvent(frame, .{ + try self.queued_events.append(self.allocator, .{ .char = @intCast(e.char_code) }); diff --git a/engine/src/runtime/graphics.zig b/engine/src/runtime/graphics.zig index d3918a7..5971028 100644 --- a/engine/src/runtime/graphics.zig +++ b/engine/src/runtime/graphics.zig @@ -251,9 +251,6 @@ pub fn drawCommands(self: *Graphics, commands: []const Command) void { } pub fn beginFrame(self: *Graphics) void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - imgui.newFrame(.{ .width = sapp.width(), .height = sapp.height(), @@ -275,9 +272,6 @@ pub fn beginFrame(self: *Graphics) void { } pub fn endFrame(self: *Graphics, clear_color: Vec4) void { - const zone = tracy.initZone(@src(), .{ }); - defer zone.deinit(); - var pass_action: sg.PassAction = .{}; pass_action.colors[0] = sg.ColorAttachmentAction{ diff --git a/engine/src/runtime/input.zig b/engine/src/runtime/input.zig index 6489008..4e14208 100644 --- a/engine/src/runtime/input.zig +++ b/engine/src/runtime/input.zig @@ -7,8 +7,6 @@ const Frame = lib.Frame; const Nanoseconds = lib.Nanoseconds; const Vec2 = lib.Math.Vec2; -const Input = @This(); - pub const Event = union(enum) { mouse_pressed: struct { button: lib.MouseButton, @@ -27,54 +25,14 @@ pub const Event = union(enum) { repeat: bool }, key_released: lib.KeyCode, - window_resize, + window_resize: struct { + width: u32, + height: u32, + }, char: u21, }; -mouse_position: ?Vec2, - -pub const initial = Input{ - .mouse_position = null -}; - -pub fn processEvent(self: *Input, frame: *Frame, event: Event) void { - switch (event) { - .key_pressed => |opts| { - if (!opts.repeat) { - frame.keyboard.press(opts.code, frame.time_ns); - } - }, - .key_released => |key_code| { - frame.keyboard.release(key_code); - }, - .mouse_leave => { - frame.keyboard.releaseAll(); - - assert(self.mouse_position != null); - self.mouse_position = null; - frame.mouse_button = .empty; - }, - .mouse_enter => |pos| { - assert(self.mouse_position == null); - self.mouse_position = pos; - }, - .mouse_move => |pos| { - assert(self.mouse_position != null); - self.mouse_position = pos; - }, - .mouse_pressed => |opts| { - assert(self.mouse_position != null); - self.mouse_position = opts.position; - frame.mouse_button.press(opts.button, frame.time_ns); - }, - .mouse_released => |opts| { - assert(self.mouse_position != null); - self.mouse_position = opts.position; - frame.mouse_button.release(opts.button); - }, - else => {} - } -} +pub const EventEnum = @typeInfo(Event).@"union".tag_type.?; pub fn getKeyCodeFromSokol(key_code: sokol.app.Keycode) ?lib.KeyCode { return switch (key_code) {