diff --git a/.gitignore b/.gitignore index db60f7a55..fe4f6d74b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ settings.json gui_layout.json settings.zig.zon gui_layout.zig.zon +gamecontrollerdb.txt +gamecontrollerdb.stamp test.png diff --git a/assets/cubyz/ui/gamepad_cursor.png b/assets/cubyz/ui/gamepad_cursor.png new file mode 100644 index 000000000..9b045b183 Binary files /dev/null and b/assets/cubyz/ui/gamepad_cursor.png differ diff --git a/src/game.zig b/src/game.zig index 110521520..af34766c6 100644 --- a/src/game.zig +++ b/src/game.zig @@ -14,6 +14,7 @@ const network = @import("network.zig"); const Connection = network.Connection; const ConnectionManager = network.ConnectionManager; const vec = @import("vec.zig"); +const Vec2f = vec.Vec2f; const Vec3f = vec.Vec3f; const Vec4f = vec.Vec4f; const Vec3d = vec.Vec3d; @@ -681,7 +682,7 @@ pub fn update(deltaTime: f64) void { // MARK: update() const terminalVelocity = 90.0; const airFrictionCoefficient = gravity/terminalVelocity; // λ = a/v in equillibrium var move: Vec3d = .{0, 0, 0}; - if (main.renderer.mesh_storage.getBlock(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) { + if (main.renderer.mesh_storage.getBlock(@intFromFloat(@floor(Player.super.pos[0])), @intFromFloat(@floor(Player.super.pos[1])), @intFromFloat(@floor(Player.super.pos[2]))) != null) { var acc = Vec3d{0, 0, 0}; if (!Player.isFlying.load(.monotonic)) { acc[2] = -gravity; @@ -705,34 +706,34 @@ pub fn update(deltaTime: f64) void { // MARK: update() var movementDir: Vec3d = .{0, 0, 0}; var movementSpeed: f64 = 0; if(main.Window.grabbed) { - if(KeyBoard.key("forward").pressed) { + if(KeyBoard.key("forward").value > 0.0) { if(KeyBoard.key("sprint").pressed) { if(Player.isGhost.load(.monotonic)) { - movementSpeed = @max(movementSpeed, 128); - movementDir += forward*@as(Vec3d, @splat(128)); + movementSpeed = @max(movementSpeed, 128)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(128*KeyBoard.key("forward").value)); } else if(Player.isFlying.load(.monotonic)) { - movementSpeed = @max(movementSpeed, 32); - movementDir += forward*@as(Vec3d, @splat(32)); + movementSpeed = @max(movementSpeed, 32)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(32*KeyBoard.key("forward").value)); } else { - movementSpeed = @max(movementSpeed, 8); - movementDir += forward*@as(Vec3d, @splat(8)); + movementSpeed = @max(movementSpeed, 8)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(8*KeyBoard.key("forward").value)); } } else { - movementSpeed = @max(movementSpeed, 4); - movementDir += forward*@as(Vec3d, @splat(4)); + movementSpeed = @max(movementSpeed, 4)*KeyBoard.key("forward").value; + movementDir += forward*@as(Vec3d, @splat(4*KeyBoard.key("forward").value)); } } - if(KeyBoard.key("backward").pressed) { - movementSpeed = @max(movementSpeed, 4); - movementDir += forward*@as(Vec3d, @splat(-4)); + if(KeyBoard.key("backward").value > 0.0) { + movementSpeed = @max(movementSpeed, 4)*KeyBoard.key("backward").value; + movementDir += forward*@as(Vec3d, @splat(-4*KeyBoard.key("backward").value)); } - if(KeyBoard.key("left").pressed) { - movementSpeed = @max(movementSpeed, 4); - movementDir += right*@as(Vec3d, @splat(4)); + if(KeyBoard.key("left").value > 0.0) { + movementSpeed = @max(movementSpeed, 4*KeyBoard.key("left").value); + movementDir += right*@as(Vec3d, @splat(4*KeyBoard.key("left").value)); } - if(KeyBoard.key("right").pressed) { - movementSpeed = @max(movementSpeed, 4); - movementDir += right*@as(Vec3d, @splat(-4)); + if(KeyBoard.key("right").value > 0.0) { + movementSpeed = @max(movementSpeed, 4*KeyBoard.key("right").value); + movementDir += right*@as(Vec3d, @splat(-4*KeyBoard.key("right").value)); } if(KeyBoard.key("jump").pressed) { if(Player.isFlying.load(.monotonic)) { @@ -779,6 +780,11 @@ pub fn update(deltaTime: f64) void { // MARK: update() const newSlot: i32 = @as(i32, @intCast(Player.selectedSlot)) -% @as(i32, @intFromFloat(main.Window.scrollOffset)); Player.selectedSlot = @intCast(@mod(newSlot, 12)); main.Window.scrollOffset = 0; + const newPos = Vec2f { + @floatCast(main.KeyBoard.key("cameraRight").value - main.KeyBoard.key("cameraLeft").value), + @floatCast(main.KeyBoard.key("cameraDown").value - main.KeyBoard.key("cameraUp").value), + } * @as(Vec2f, @splat(3.14 * settings.controllerSensitivity)); + main.game.camera.moveRotation(newPos[0] / 64.0, newPos[1] / 64.0); } // This our model for movement on a single frame: diff --git a/src/graphics/Window.zig b/src/graphics/Window.zig index 8cd73b508..d51f6b9f7 100644 --- a/src/graphics/Window.zig +++ b/src/graphics/Window.zig @@ -1,6 +1,8 @@ const std = @import("std"); const main = @import("root"); +const settings = main.settings; +const files = main.files; const vec = main.vec; const Vec2f = vec.Vec2f; @@ -10,16 +12,282 @@ pub const c = @cImport ({ }); var isFullscreen: bool = false; +pub var lastUsedMouse: bool = true; pub var width: u31 = 1280; pub var height: u31 = 720; pub var window: *c.GLFWwindow = undefined; pub var grabbed: bool = false; pub var scrollOffset: f32 = 0; +pub const Gamepad = struct { + pub var gamepadState: std.AutoHashMap(c_int, *c.GLFWgamepadstate) = undefined; + pub var controllerMappingsDownloaded: std.atomic.Value(bool) = std.atomic.Value(bool).init(false); + var controllerConnectedPreviously: bool = false; + fn applyDeadzone(value: f32) f32 { + const minValue = settings.controllerAxisDeadzone; + const maxRange = 1.0 - minValue; + return (value * maxRange) + minValue; + } + pub fn update(delta: f64) void { + if (!controllerConnectedPreviously and isControllerConnected()) { + controllerConnectedPreviously = true; + downloadControllerMappings(); + } + var jid: c_int = 0; + while (jid < c.GLFW_JOYSTICK_LAST) : (jid += 1) { + // Can't initialize with the state, or it will become a reference. + var oldState: c.GLFWgamepadstate = std.mem.zeroes(c.GLFWgamepadstate); + if (gamepadState.get(jid)) |v| { + oldState = v.*; + } + const joystickFound = c.glfwJoystickPresent(jid) != 0 and c.glfwJoystickIsGamepad(jid) != 0; + if (joystickFound) { + if (!gamepadState.contains(jid)) { + gamepadState.put(jid, main.globalAllocator.create(c.GLFWgamepadstate)) catch unreachable; + } + _ = c.glfwGetGamepadState(jid, gamepadState.get(jid).?); + } else { + if (gamepadState.contains(jid)) { + main.globalAllocator.destroy(gamepadState.get(jid).?); + _ = gamepadState.remove(jid); + } + } + const newState: c.GLFWgamepadstate = if (gamepadState.get(jid)) |v| v.* else std.mem.zeroes(c.GLFWgamepadstate); + if (nextGamepadListener != null) { + for (0..c.GLFW_GAMEPAD_BUTTON_LAST) |btn| { + if ((newState.buttons[btn] == 0) and (oldState.buttons[btn] != 0)) { + nextGamepadListener.?(null, @intCast(btn)); + nextGamepadListener = null; + break; + } + } + } + if (nextGamepadListener != null) { + for (0..c.GLFW_GAMEPAD_AXIS_LAST) |axis| { + const newAxis = applyDeadzone(newState.axes[axis]); + const oldAxis = applyDeadzone(oldState.axes[axis]); + if (newAxis != 0 and oldAxis == 0) { + nextGamepadListener.?(.{.axis = @intCast(axis), .positive = newState.axes[axis] > 0}, -1); + nextGamepadListener = null; + break; + } + } + } + for(&main.KeyBoard.keys) |*key| { + if(key.gamepadAxis == null) { + if(key.gamepadButton >= 0) { + const oldPressed = oldState.buttons[@intCast(key.gamepadButton)] != 0; + const newPressed = newState.buttons[@intCast(key.gamepadButton)] != 0; + if(oldPressed != newPressed) { + key.pressed = newPressed; + key.value = if(newPressed) 1.0 else 0.0; + if(key.pressed) { + if(key.pressAction) |pressAction| { + pressAction(); + } + } else { + if(key.releaseAction) |releaseAction| { + releaseAction(); + } + } + } + } + } else { + const axis = key.gamepadAxis.?.axis; + const positive = key.gamepadAxis.?.positive; + var newAxis = applyDeadzone(newState.axes[@intCast(axis)]); + var oldAxis = applyDeadzone(oldState.axes[@intCast(axis)]); + if(!positive) { + newAxis *= -1.0; + oldAxis *= -1.0; + } + newAxis = @max(newAxis, 0.0); + oldAxis = @max(oldAxis, 0.0); + const oldPressed = oldAxis > 0.5; + const newPressed = newAxis > 0.5; + if (oldPressed != newPressed) { + key.pressed = newPressed; + if (newPressed) { + if (key.pressAction) |pressAction| { + pressAction(); + } + } else { + if (key.releaseAction) |releaseAction| { + releaseAction(); + } + } + } + if (newAxis != oldAxis) { + key.value = newAxis; + } + } + } + } + if (!grabbed) { + const x = main.KeyBoard.key("uiRight").value - main.KeyBoard.key("uiLeft").value; + const y = main.KeyBoard.key("uiDown").value - main.KeyBoard.key("uiUp").value; + if (x != 0 or y != 0) { + lastUsedMouse = false; + GLFWCallbacks.currentPos[0] += @floatCast(x * delta * 256); + GLFWCallbacks.currentPos[1] += @floatCast(y * delta * 256); + const winSize = getWindowSize(); + GLFWCallbacks.currentPos[0] = std.math.clamp(GLFWCallbacks.currentPos[0], 0, winSize[0]); + GLFWCallbacks.currentPos[1] = std.math.clamp(GLFWCallbacks.currentPos[1], 0, winSize[1]); + } + } + scrollOffset += @floatCast((main.KeyBoard.key("scrollUp").value - main.KeyBoard.key("scrollDown").value) * delta * 4); + setCursorVisible(!grabbed and lastUsedMouse); + } + pub fn isControllerConnected() bool { + return gamepadState.count() > 0; + } + pub fn wereControllerMappingsDownloaded() bool { + return controllerMappingsDownloaded.load(std.builtin.AtomicOrder.acquire); + } + const ControllerMappingDownloadTask = struct { // MARK: ControllerMappingDownloadTask + curTimestamp: i128, + var running = std.atomic.Value(bool).init(false); + const vtable = main.utils.ThreadPool.VTable{ + .getPriority = @ptrCast(&getPriority), + .isStillNeeded = @ptrCast(&isStillNeeded), + .run = @ptrCast(&run), + .clean = @ptrCast(&clean), + }; + + pub fn schedule(curTimestamp: i128) void { + + if (running.swap(true, .monotonic)) { + std.log.warn("Attempt to schedule a duplicate controller mapping download task!", .{}); + return; // Controller mappings are already downloading. + } + controllerMappingsDownloaded.store(false, .monotonic); + const task = main.globalAllocator.create(ControllerMappingDownloadTask); + task.* = ControllerMappingDownloadTask { + .curTimestamp = curTimestamp, + }; + main.threadPool.addTask(task, &vtable); + // Don't attempt to open the window before the GUI is initialized. + main.gui.openWindow("download_controller_mappings"); + } + + pub fn getPriority(_: *ControllerMappingDownloadTask) f32 { + return std.math.inf(f32); + } + + pub fn isStillNeeded(_: *ControllerMappingDownloadTask) bool { + return true; + } + + pub fn run(self: *ControllerMappingDownloadTask) void { + std.log.info("Starting controller mapping download...", .{}); + defer self.clean(); + var client: std.http.Client = .{.allocator = main.stackAllocator.allocator}; + defer client.deinit(); + var list = std.ArrayList(u8).init(main.stackAllocator.allocator); + defer list.deinit(); + defer controllerMappingsDownloaded.store(true, std.builtin.AtomicOrder.release); + const fetchResult = client.fetch(.{ + .method = .GET, + .location = .{.url = "https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt"}, + .response_storage = .{ .dynamic = &list } + }) catch |err| { + std.log.err("Failed to download controller mappings: {s}", .{@errorName(err)}); + return; + }; + if (fetchResult.status != .ok) { + std.log.err("Failed to download controller mappings: HTTP error {d}", .{@intFromEnum(fetchResult.status)}); + return; + } + files.write("./gamecontrollerdb.txt", list.items) catch |err| { + std.log.err("Failed to write controller mappings: {s}", .{@errorName(err)}); + return; + }; + const timeStampStr = std.fmt.allocPrint(main.stackAllocator.allocator, "{x}", .{self.*.curTimestamp}) catch unreachable; + defer main.stackAllocator.free(timeStampStr); + files.write("gamecontrollerdb.stamp", timeStampStr) catch |err| { + std.log.err("Failed to write controller mappings: {s}", .{@errorName(err)}); + return; + }; + std.log.info("Controller mappings downloaded succesfully!", .{}); + } + + pub fn clean(self: *ControllerMappingDownloadTask) void { + main.globalAllocator.destroy(self); + updateControllerMappings(); + running.store(false, .monotonic); + } + }; + pub fn downloadControllerMappings() void { + var needsDownload: bool = false; + const curTimestamp = std.time.nanoTimestamp(); + const timestamp: i128 = blk: { + const stamp = files.read(main.stackAllocator, "./gamecontrollerdb.stamp") catch break :blk 0; + defer main.stackAllocator.free(stamp); + break :blk std.fmt.parseInt(i128, stamp, 16) catch 0; + }; + const delta = curTimestamp-%timestamp; + needsDownload = delta >= 7*std.time.ns_per_day; + + for (0..c.GLFW_JOYSTICK_LAST) |jsid| { + if ((c.glfwJoystickPresent(@intCast(jsid)) != 0) and (c.glfwJoystickIsGamepad(@intCast(jsid)) == 0)) { + needsDownload = true; + break; + } + } + std.log.info("Game controller mappings {s}need downloading.", .{if (needsDownload) "" else "do not "}); + if (needsDownload) { + ControllerMappingDownloadTask.schedule(curTimestamp); + } else { + controllerMappingsDownloaded.store(true, .monotonic); + updateControllerMappings(); + } + } + pub fn updateControllerMappings() void { + std.log.info("Updating controller mappings in-memory...", .{}); + var _envMap = std.process.getEnvMap(main.stackAllocator.allocator) catch null; + if (_envMap) |*envMap| { + defer envMap.deinit(); + if (envMap.get("SDL_GAMECONTROLLERCONFIG")) |controller_config_env| { + _ = c.glfwUpdateGamepadMappings(@ptrCast(controller_config_env)); + return; + } + } + const data = main.files.read(main.stackAllocator, "./gamecontrollerdb.txt") catch |err| { + if (@TypeOf(err) == std.fs.File.OpenError and err == std.fs.File.OpenError.FileNotFound) { + return; // Ignore not finding mappings. + } + std.log.err("Error opening gamepad mappings file: {s}", .{@errorName(err)}); + return; + }; + var newData = main.stackAllocator.realloc(data, data.len + 1); + defer main.stackAllocator.free(newData); + newData[data.len - 1] = 0; + _ = c.glfwUpdateGamepadMappings(newData.ptr); + std.log.info("Controller mappings updated!", .{}); + } + + pub fn init() void { + gamepadState = .init(main.globalAllocator.allocator); + } + pub fn deinit() void { + var iter = gamepadState.valueIterator(); + while (iter.next()) |value| { + main.globalAllocator.destroy(value.*); + } + gamepadState.deinit(); + } +}; +pub const GamepadAxis = struct { + axis: c_int, + positive: bool = true +}; pub const Key = struct { // MARK: Key name: []const u8, pressed: bool = false, + value: f32 = 0.0, key: c_int = c.GLFW_KEY_UNKNOWN, + gamepadAxis: ?GamepadAxis = null, + gamepadButton: c_int = -1, mouseButton: c_int = -1, scancode: c_int = 0, releaseAction: ?*const fn() void = null, @@ -34,6 +302,40 @@ pub const Key = struct { // MARK: Key capsLock: bool = false, numLock: bool = false, }; + pub fn getGamepadName(self: Key) []const u8 { + if(self.gamepadAxis != null) { + const positive = self.gamepadAxis.?.positive; + return switch(self.gamepadAxis.?.axis) { + c.GLFW_GAMEPAD_AXIS_LEFT_X => if(positive) "Left stick right" else "Left stick left", + c.GLFW_GAMEPAD_AXIS_RIGHT_X => if(positive) "Right stick right" else "Right stick left", + c.GLFW_GAMEPAD_AXIS_LEFT_Y => if(positive) "Left stick down" else "Left stick up", + c.GLFW_GAMEPAD_AXIS_RIGHT_Y => if(positive) "Right stick down" else "Right stick up", + c.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER => if(positive) "Left trigger" else "Left trigger (Negative)", + c.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER => if(positive) "Right trigger" else "Right trigger (Negative)", + else => "(Invalid axis)" + }; + } else { + return switch(self.gamepadButton) { + c.GLFW_GAMEPAD_BUTTON_A => "A", + c.GLFW_GAMEPAD_BUTTON_B => "B", + c.GLFW_GAMEPAD_BUTTON_X => "X", + c.GLFW_GAMEPAD_BUTTON_Y => "Y", + c.GLFW_GAMEPAD_BUTTON_BACK => "Back", + c.GLFW_GAMEPAD_BUTTON_DPAD_DOWN => "Down", + c.GLFW_GAMEPAD_BUTTON_DPAD_LEFT => "Left", + c.GLFW_GAMEPAD_BUTTON_DPAD_RIGHT => "Right", + c.GLFW_GAMEPAD_BUTTON_DPAD_UP => "Up", + c.GLFW_GAMEPAD_BUTTON_GUIDE => "Guide", + c.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER => "Left bumper", + c.GLFW_GAMEPAD_BUTTON_LEFT_THUMB => "Left stick press", + c.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER => "Right bumper", + c.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB => "Right stick press", + c.GLFW_GAMEPAD_BUTTON_START => "Start", + -1 => "(Unbound)", + else => "(Unrecognized button)" + }; + } + } pub fn getName(self: Key) []const u8 { if(self.mouseButton == -1) { @@ -96,6 +398,7 @@ pub const Key = struct { // MARK: Key c.GLFW_KEY_RIGHT_ALT => "Right Alt", c.GLFW_KEY_RIGHT_SUPER => "Right Super", c.GLFW_KEY_MENU => "Menu", + c.GLFW_KEY_UNKNOWN => "(Unbound)", else => "Unknown Key", }; } else { @@ -121,6 +424,7 @@ pub const GLFWCallbacks = struct { // MARK: GLFWCallbacks if(glfw_key == key.key) { if(glfw_key != c.GLFW_KEY_UNKNOWN or scancode == key.scancode) { key.pressed = true; + key.value = 1.0; if(key.pressAction) |pressAction| { pressAction(); } @@ -139,6 +443,7 @@ pub const GLFWCallbacks = struct { // MARK: GLFWCallbacks if(glfw_key == key.key) { if(glfw_key != c.GLFW_KEY_UNKNOWN or scancode == key.scancode) { key.pressed = false; + key.value = 0.0; if(key.releaseAction) |releaseAction| { releaseAction(); } @@ -195,6 +500,7 @@ pub const GLFWCallbacks = struct { // MARK: GLFWCallbacks } ignoreDataAfterRecentGrab = false; currentPos = newPos; + lastUsedMouse = true; } fn mouseButton(_: ?*c.GLFWwindow, button: c_int, action: c_int, mods: c_int) callconv(.C) void { _ = mods; @@ -202,6 +508,7 @@ pub const GLFWCallbacks = struct { // MARK: GLFWCallbacks for(&main.KeyBoard.keys) |*key| { if(button == key.mouseButton) { key.pressed = true; + key.value = 1.0; if(key.pressAction) |pressAction| { pressAction(); } @@ -215,6 +522,7 @@ pub const GLFWCallbacks = struct { // MARK: GLFWCallbacks for(&main.KeyBoard.keys) |*key| { if(button == key.mouseButton) { key.pressed = false; + key.value = 0.0; if(key.releaseAction) |releaseAction| { releaseAction(); } @@ -264,22 +572,35 @@ pub fn setNextKeypressListener(listener: ?*const fn(c_int, c_int, c_int) void) ! if(nextKeypressListener != null) return error.AlreadyUsed; nextKeypressListener = listener; } +var nextGamepadListener: ?*const fn(?GamepadAxis, c_int) void = null; +pub fn setNextGamepadListener(listener: ?*const fn(?GamepadAxis, c_int) void) !void { + if (nextGamepadListener != null) return error.AlreadyUsed; + nextGamepadListener = listener; +} + +fn updateCursor() void { + if(grabbed) { + c.glfwSetInputMode(window, c.GLFW_CURSOR, c.GLFW_CURSOR_DISABLED); + // Behavior seems much more intended without this line on MacOS. + // Perhaps this is an inconsistency in GLFW due to its fresh XQuartz support? + if(@import("builtin").target.os.tag != .macos) { + if (c.glfwRawMouseMotionSupported() != 0) + c.glfwSetInputMode(window, c.GLFW_RAW_MOUSE_MOTION, c.GLFW_TRUE); + } + GLFWCallbacks.ignoreDataAfterRecentGrab = true; + } else { + if (cursorVisible) { + c.glfwSetInputMode(window, c.GLFW_CURSOR, c.GLFW_CURSOR_NORMAL); + } else { + c.glfwSetInputMode(window, c.GLFW_CURSOR, c.GLFW_CURSOR_HIDDEN); + } + } +} pub fn setMouseGrabbed(grab: bool) void { if(grabbed != grab) { - if(grab) { - c.glfwSetInputMode(window, c.GLFW_CURSOR, c.GLFW_CURSOR_DISABLED); - // Behavior seems much more intended without this line on MacOS. - // Perhaps this is an inconsistency in GLFW due to its fresh XQuartz support? - if(@import("builtin").target.os.tag != .macos) { - if (c.glfwRawMouseMotionSupported() != 0) - c.glfwSetInputMode(window, c.GLFW_RAW_MOUSE_MOTION, c.GLFW_TRUE); - } - GLFWCallbacks.ignoreDataAfterRecentGrab = true; - } else { - c.glfwSetInputMode(window, c.GLFW_CURSOR, c.GLFW_CURSOR_NORMAL); - } grabbed = grab; + updateCursor(); } } @@ -349,16 +670,26 @@ pub fn init() void { // MARK: init() c.glEnable(c.GL_DEBUG_OUTPUT_SYNCHRONOUS); c.glDebugMessageCallback(GLFWCallbacks.glDebugOutput, null); c.glDebugMessageControl(c.GL_DONT_CARE, c.GL_DONT_CARE, c.GL_DONT_CARE, 0, null, c.GL_TRUE); + Gamepad.init(); } pub fn deinit() void { + Gamepad.deinit(); c.glfwDestroyWindow(window); c.glfwTerminate(); } +var cursorVisible: bool = true; +fn setCursorVisible(visible: bool) void { + if (cursorVisible != visible) { + cursorVisible = visible; + updateCursor(); + } +} -pub fn handleEvents() void { +pub fn handleEvents(deltaTime: f64) void { scrollOffset = 0; c.glfwPollEvents(); + Gamepad.update(deltaTime); } var oldX: c_int = 0; diff --git a/src/gui/gamepad_cursor.zig b/src/gui/gamepad_cursor.zig new file mode 100644 index 000000000..0b1a67307 --- /dev/null +++ b/src/gui/gamepad_cursor.zig @@ -0,0 +1,28 @@ +const std = @import("std"); + +const main = @import("root"); +const graphics = main.graphics; +const Texture = graphics.Texture; +const Vec2f = main.vec.Vec2f; + +const gui = @import("gui.zig"); + +const size: f32 = 16; + +var texture: Texture = undefined; + +pub fn init() void { + texture = Texture.initFromFile("assets/cubyz/ui/gamepad_cursor.png"); +} + +pub fn deinit() void { + texture.deinit(); +} + +pub fn render() void { + if (main.Window.lastUsedMouse or main.Window.grabbed) return; + texture.bindTo(0); + graphics.draw.setColor(0xffffffff); + const mousePos = main.Window.getMousePosition(); + graphics.draw.boundImage(@as(Vec2f, @splat(-size/2.0)) + (mousePos/@as(Vec2f, @splat(gui.scale))), .{size, size}); +} diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 11c1a70ca..a4f9a96d8 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -22,6 +22,7 @@ pub const GuiComponent = @import("gui_component.zig").GuiComponent; pub const GuiWindow = @import("GuiWindow.zig"); pub const windowlist = @import("windows/_windowlist.zig"); +const GamepadCursor = @import("gamepad_cursor.zig"); var windowList: List(*GuiWindow) = undefined; var hudWindows: List(*GuiWindow) = undefined; @@ -158,10 +159,12 @@ pub fn init() void { // MARK: init() TextInput.__init(); load(); inventory.init(); + GamepadCursor.init(); } pub fn deinit() void { save(); + GamepadCursor.deinit(); windowList.deinit(); hudWindows.deinit(); for(openWindows.items) |window| { @@ -220,7 +223,7 @@ pub fn save() void { // MARK: save() windowZon.put("scale", window.scale); guiZon.put(window.id, windowZon); } - + main.files.writeZon("gui_layout.zig.zon", guiZon) catch |err| { std.log.err("Could not write gui_layout.zig.zon: {s}", .{@errorName(err)}); }; @@ -561,6 +564,9 @@ pub fn updateAndRenderGui() void { } inventory.render(mousePos); } + const oldScale = draw.setScale(scale); + defer draw.restoreScale(oldScale); + GamepadCursor.render(); } pub fn toggleGameMenu() void { @@ -691,4 +697,4 @@ pub const inventory = struct { // MARK: inventory } }; } -}; \ No newline at end of file +}; diff --git a/src/gui/windows/_windowlist.zig b/src/gui/windows/_windowlist.zig index f6f434ad5..b159c4399 100644 --- a/src/gui/windows/_windowlist.zig +++ b/src/gui/windows/_windowlist.zig @@ -9,6 +9,7 @@ pub const debug_network = @import("debug_network.zig"); pub const debug_network_advanced = @import("debug_network_advanced.zig"); pub const debug = @import("debug.zig"); pub const delete_world_confirmation = @import("delete_world_confirmation.zig"); +pub const download_controller_mappings = @import("download_controller_mappings.zig"); pub const gpu_performance_measuring = @import("gpu_performance_measuring.zig"); pub const graphics = @import("graphics.zig"); pub const healthbar = @import("healthbar.zig"); diff --git a/src/gui/windows/controls.zig b/src/gui/windows/controls.zig index 3eae1c762..096c03f8b 100644 --- a/src/gui/windows/controls.zig +++ b/src/gui/windows/controls.zig @@ -2,7 +2,7 @@ const std = @import("std"); const main = @import("root"); const Vec2f = main.vec.Vec2f; - +const c = main.Window.c; const gui = @import("../gui.zig"); const GuiComponent = gui.GuiComponent; const GuiWindow = gui.GuiWindow; @@ -13,19 +13,18 @@ const VerticalList = @import("../components/VerticalList.zig"); const ContinuousSlider = @import("../components/ContinuousSlider.zig"); pub var window = GuiWindow { - .contentSize = Vec2f{128, 256}, + .contentSize = Vec2f{128, 192}, }; const padding: f32 = 8; var selectedKey: ?*main.Window.Key = null; +var editingKeyboard: bool = true; var needsUpdate: bool = false; - -fn function(keyPtr: usize) void { +fn keyFunction(keyPtr: usize) void { main.Window.setNextKeypressListener(&keypressListener) catch return; selectedKey = @ptrFromInt(keyPtr); needsUpdate = true; } - fn keypressListener(key: c_int, mouseButton: c_int, scancode: c_int) void { selectedKey.?.key = key; selectedKey.?.mouseButton = mouseButton; @@ -35,28 +34,75 @@ fn keypressListener(key: c_int, mouseButton: c_int, scancode: c_int) void { main.settings.save(); } +fn gamepadFunction(keyPtr: usize) void { + main.Window.setNextGamepadListener(&gamepadListener) catch return; + selectedKey = @ptrFromInt(keyPtr); + needsUpdate = true; +} +fn gamepadListener(axis: ?main.Window.GamepadAxis, btn: c_int) void { + selectedKey.?.gamepadAxis = axis; + selectedKey.?.gamepadButton = btn; + selectedKey = null; + needsUpdate = true; + main.settings.save(); +} fn updateSensitivity(sensitivity: f32) void { - main.settings.mouseSensitivity = sensitivity; + if (editingKeyboard) { + main.settings.mouseSensitivity = sensitivity; + } else { + main.settings.controllerSensitivity = sensitivity; + } main.settings.save(); } +fn updateDeadzone(deadzone: f32) void { + main.settings.controllerAxisDeadzone = deadzone; +} + +fn deadzoneFormatter(allocator: main.utils.NeverFailingAllocator, value: f32) []const u8 { + return std.fmt.allocPrint(allocator.allocator, "Deadzone: {d:.0}%", .{value*100}) catch unreachable; +} + fn sensitivityFormatter(allocator: main.utils.NeverFailingAllocator, value: f32) []const u8 { - return std.fmt.allocPrint(allocator.allocator, "Mouse Sensitivity: {d:.0}%", .{value*100}) catch unreachable; + return std.fmt.allocPrint(allocator.allocator, "{s} Sensitivity: {d:.0}%", .{if (editingKeyboard) "Mouse" else "Controller", value*100}) catch unreachable; +} + +fn toggleKeyboard(_: usize) void { + editingKeyboard = !editingKeyboard; + needsUpdate = true; +} +fn unbindKey(keyPtr: usize) void { + var key: ?*main.Window.Key = @ptrFromInt(keyPtr); + if (editingKeyboard) { + key.?.key = c.GLFW_KEY_UNKNOWN; + key.?.mouseButton = -1; + key.?.scancode = 0; + } else { + key.?.gamepadAxis = null; + key.?.gamepadButton = -1; + } + needsUpdate = true; } pub fn onOpen() void { - const list = VerticalList.init(.{padding, 16 + padding}, 300, 8); - list.add(ContinuousSlider.init(.{0, 0}, 256, 0, 5, main.settings.mouseSensitivity, &updateSensitivity, &sensitivityFormatter)); + const list = VerticalList.init(.{padding, 16 + padding}, 364, 8); + list.add(Button.initText(.{0, 0}, 128, if (editingKeyboard) "Gamepad" else "Keyboard", .{.callback = &toggleKeyboard})); + list.add(ContinuousSlider.init(.{0, 0}, 256, 0, 5, if (editingKeyboard) main.settings.mouseSensitivity else main.settings.controllerSensitivity, &updateSensitivity, &sensitivityFormatter)); + if (!editingKeyboard) { + list.add(ContinuousSlider.init(.{0, 0}, 256, 0, 5, main.settings.controllerAxisDeadzone, &updateDeadzone, &deadzoneFormatter)); + } for(&main.KeyBoard.keys) |*key| { const label = Label.init(.{0, 0}, 128, key.name, .left); const button = if(key == selectedKey) ( Button.initText(.{16, 0}, 128, "...", .{}) ) else ( - Button.initText(.{16, 0}, 128, key.getName(), .{.callback = &function, .arg = @intFromPtr(key)}) + Button.initText(.{16, 0}, 128, if (editingKeyboard) key.getName() else key.getGamepadName(), .{.callback = if (editingKeyboard) &keyFunction else &gamepadFunction, .arg = @intFromPtr(key)}) ); + const unbindBtn = Button.initText(.{16, 0}, 64, "Unbind", .{.callback = &unbindKey, .arg = @intFromPtr(key)}); const row = HorizontalList.init(); row.add(label); row.add(button); + row.add(unbindBtn); row.finish(.{0, 0}, .center); list.add(row); } @@ -80,4 +126,4 @@ pub fn render() void { onOpen(); window.rootComponent.?.verticalList.scrollBar.currentState = oldScroll; } -} \ No newline at end of file +} diff --git a/src/gui/windows/download_controller_mappings.zig b/src/gui/windows/download_controller_mappings.zig new file mode 100644 index 000000000..111b9c95f --- /dev/null +++ b/src/gui/windows/download_controller_mappings.zig @@ -0,0 +1,44 @@ +const std = @import("std"); + +const main = @import("root"); +const files = main.files; +const settings = main.settings; +const Vec2f = main.vec.Vec2f; + +const gui = @import("../gui.zig"); +const GuiComponent = gui.GuiComponent; +const GuiWindow = gui.GuiWindow; +const Button = @import("../components/Button.zig"); +const CheckBox = @import("../components/CheckBox.zig"); +const Label = @import("../components/Label.zig"); +const VerticalList = @import("../components/VerticalList.zig"); +const HorizontalList = @import("../components/HorizontalList.zig"); + +pub var window = GuiWindow { + .contentSize = Vec2f{128, 64}, + .hasBackground = true, + .closeable = false, + .relativePosition = .{ + .{ .attachedToFrame = .{.selfAttachmentPoint = .upper, .otherAttachmentPoint = .upper} }, + .{ .attachedToFrame = .{.selfAttachmentPoint = .upper, .otherAttachmentPoint = .upper} }, + }, +}; + +const padding: f32 = 8; +pub fn update() void { + if (main.Window.Gamepad.wereControllerMappingsDownloaded()) { + gui.closeWindowFromRef(&window); + } +} +pub fn onOpen() void { + const label = Label.init(.{padding, 16 + padding}, 128, "Downloading controller mappings...", .center); + window.rootComponent = label.toComponent(); + window.contentSize = window.rootComponent.?.pos() + window.rootComponent.?.size() + @as(Vec2f, @splat(padding)); + gui.updateWindowPositions(); +} + +pub fn onClose() void { + if(window.rootComponent) |*comp| { + comp.deinit(); + } +} diff --git a/src/gui/windows/settings.zig b/src/gui/windows/settings.zig index c5f07fe86..c512dbae0 100644 --- a/src/gui/windows/settings.zig +++ b/src/gui/windows/settings.zig @@ -32,4 +32,4 @@ pub fn onClose() void { if(window.rootComponent) |*comp| { comp.deinit(); } -} \ No newline at end of file +} diff --git a/src/main.zig b/src/main.zig index 98ca4201b..3253d4d8c 100644 --- a/src/main.zig +++ b/src/main.zig @@ -298,6 +298,13 @@ fn toggleNetworkDebugOverlay() void { fn toggleAdvancedNetworkDebugOverlay() void { gui.toggleWindow("debug_network_advanced"); } +fn cycleHotbarSlot(i: comptime_int) *const fn() void { + return &struct { + fn set() void { + game.Player.selectedSlot = @intCast(@mod(@as(i33, game.Player.selectedSlot) + i, 12)); + } + }.set; +} fn setHotbarSlot(i: comptime_int) *const fn() void { return &struct { fn set() void { @@ -310,32 +317,39 @@ pub const KeyBoard = struct { // MARK: KeyBoard const c = Window.c; pub var keys = [_]Window.Key { // Gameplay: - .{.name = "forward", .key = c.GLFW_KEY_W}, - .{.name = "left", .key = c.GLFW_KEY_A}, - .{.name = "backward", .key = c.GLFW_KEY_S}, - .{.name = "right", .key = c.GLFW_KEY_D}, - .{.name = "sprint", .key = c.GLFW_KEY_LEFT_CONTROL}, - .{.name = "jump", .key = c.GLFW_KEY_SPACE}, - .{.name = "fly", .key = c.GLFW_KEY_F, .pressAction = &game.flyToggle}, + .{.name = "forward", .key = c.GLFW_KEY_W, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, + .{.name = "left", .key = c.GLFW_KEY_A, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = false}}, + .{.name = "backward", .key = c.GLFW_KEY_S, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = true}}, + .{.name = "right", .key = c.GLFW_KEY_D, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = true}}, + .{.name = "sprint", .key = c.GLFW_KEY_LEFT_CONTROL, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_LEFT_THUMB}, + .{.name = "jump", .key = c.GLFW_KEY_SPACE, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_A}, + .{.name = "fly", .key = c.GLFW_KEY_F, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_DPAD_DOWN, .pressAction = &game.flyToggle}, .{.name = "ghost", .key = c.GLFW_KEY_G, .pressAction = &game.ghostToggle}, .{.name = "hyperSpeed", .key = c.GLFW_KEY_H, .pressAction = &game.hyperSpeedToggle}, .{.name = "gamemode", .key = c.GLFW_KEY_M, .releaseAction = &game.gamemodeToggle}, - .{.name = "fall", .key = c.GLFW_KEY_LEFT_SHIFT}, - .{.name = "shift", .key = c.GLFW_KEY_LEFT_SHIFT}, + .{.name = "fall", .key = c.GLFW_KEY_LEFT_SHIFT, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB}, + .{.name = "shift", .key = c.GLFW_KEY_LEFT_SHIFT, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_THUMB}, .{.name = "fullscreen", .key = c.GLFW_KEY_F11, .releaseAction = &Window.toggleFullscreen}, - .{.name = "placeBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .pressAction = &game.pressPlace, .releaseAction = &game.releasePlace}, - .{.name = "breakBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .pressAction = &game.pressBreak, .releaseAction = &game.releaseBreak}, - .{.name = "acquireSelectedBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_MIDDLE, .pressAction = &game.pressAcquireSelectedBlock}, + .{.name = "placeBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_TRIGGER}, .pressAction = &game.pressPlace, .releaseAction = &game.releasePlace}, + .{.name = "breakBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER}, .pressAction = &game.pressBreak, .releaseAction = &game.releaseBreak}, + .{.name = "acquireSelectedBlock", .mouseButton = c.GLFW_MOUSE_BUTTON_MIDDLE, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_DPAD_LEFT, .pressAction = &game.pressAcquireSelectedBlock}, .{.name = "takeBackgroundImage", .key = c.GLFW_KEY_PRINT_SCREEN, .releaseAction = &takeBackgroundImageFn}, // Gui: - .{.name = "escape", .key = c.GLFW_KEY_ESCAPE, .releaseAction = &escape}, - .{.name = "openInventory", .key = c.GLFW_KEY_E, .releaseAction = &openInventory}, - .{.name = "openCreativeInventory(aka cheat inventory)", .key = c.GLFW_KEY_C, .releaseAction = &openCreativeInventory}, + .{.name = "escape", .key = c.GLFW_KEY_ESCAPE, .releaseAction = &escape, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_B}, + .{.name = "openInventory", .key = c.GLFW_KEY_E, .releaseAction = &openInventory, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_X}, + .{.name = "openCreativeInventory(aka cheat inventory)", .key = c.GLFW_KEY_C, .releaseAction = &openCreativeInventory, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_Y}, .{.name = "openChat", .key = c.GLFW_KEY_T, .releaseAction = &openChat}, - .{.name = "mainGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .pressAction = &gui.mainButtonPressed, .releaseAction = &gui.mainButtonReleased}, - .{.name = "secondaryGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .pressAction = &gui.secondaryButtonPressed, .releaseAction = &gui.secondaryButtonReleased}, + .{.name = "mainGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_LEFT, .pressAction = &gui.mainButtonPressed, .releaseAction = &gui.mainButtonReleased, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_A}, + .{.name = "secondaryGuiButton", .mouseButton = c.GLFW_MOUSE_BUTTON_RIGHT, .pressAction = &gui.secondaryButtonPressed, .releaseAction = &gui.secondaryButtonReleased, .gamepadButton = c.GLFW_GAMEPAD_BUTTON_Y}, + // gamepad gui. + .{.name = "scrollUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = false}}, + .{.name = "scrollDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = true}}, + .{.name = "uiUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = false}}, + .{.name = "uiLeft", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = false}}, + .{.name = "uiDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_Y, .positive = true}}, + .{.name = "uiRight", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_LEFT_X, .positive = true}}, // text: .{.name = "textCursorLeft", .key = c.GLFW_KEY_LEFT, .repeatAction = &gui.textCallbacks.left}, .{.name = "textCursorRight", .key = c.GLFW_KEY_RIGHT, .repeatAction = &gui.textCallbacks.right}, @@ -364,7 +378,12 @@ pub const KeyBoard = struct { // MARK: KeyBoard .{.name = "Hotbar 10", .key = c.GLFW_KEY_0, .releaseAction = setHotbarSlot(10)}, .{.name = "Hotbar 11", .key = c.GLFW_KEY_MINUS, .releaseAction = setHotbarSlot(11)}, .{.name = "Hotbar 12", .key = c.GLFW_KEY_EQUAL, .releaseAction = setHotbarSlot(12)}, - + .{.name = "Hotbar left", .gamepadButton = c.GLFW_GAMEPAD_BUTTON_LEFT_BUMPER, .releaseAction = cycleHotbarSlot(-1)}, + .{.name = "Hotbar right", .gamepadButton = c.GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER, .releaseAction = cycleHotbarSlot(1)}, + .{.name = "cameraLeft", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_X, .positive = false}}, + .{.name = "cameraRight", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_X, .positive = true}}, + .{.name = "cameraUp", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = false}}, + .{.name = "cameraDown", .gamepadAxis = .{.axis = c.GLFW_GAMEPAD_AXIS_RIGHT_Y, .positive = true}}, // debug: .{.name = "hideMenu", .key = c.GLFW_KEY_F1, .releaseAction = &toggleHideGui}, .{.name = "debugOverlay", .key = c.GLFW_KEY_F3, .releaseAction = &toggleDebugOverlay}, @@ -434,7 +453,7 @@ pub fn convertJsonToZon(jsonPath: []const u8) void { // TODO: Remove after #480 var zonString = List(u8).init(stackAllocator); defer zonString.deinit(); std.log.debug("{s}", .{jsonString}); - + var i: usize = 0; while(i < jsonString.len) : (i += 1) { switch(jsonString[i]) { @@ -622,7 +641,8 @@ pub fn main() void { // MARK: main() lastDeltaTime.store(deltaTime, .monotonic); lastBeginRendering = begin; - Window.handleEvents(); + Window.handleEvents(deltaTime); + file_monitor.handleEvents(); if(game.world != null) { // Update the game diff --git a/src/settings.zig b/src/settings.zig index 1bf0abcd4..64e9509c7 100644 --- a/src/settings.zig +++ b/src/settings.zig @@ -25,6 +25,7 @@ pub var fpsCap: ?u32 = null; pub var fov: f32 = 70; pub var mouseSensitivity: f32 = 1; +pub var controllerSensitivity: f32 = 1; pub var renderDistance: u16 = 7; @@ -56,6 +57,7 @@ pub var developerAutoEnterWorld: []const u8 = ""; pub var developerGPUInfiniteLoopDetection: bool = false; +pub var controllerAxisDeadzone: f32 = 0.0; pub fn init() void { const zon: ZonElement = main.files.readToZon(main.stackAllocator, "settings.zig.zon") catch |err| blk: {