Game controller support (#717)

* Refactor getting X positions of buttons in preparation for not moving the window when clicking the buttons

* Initial support for controllers

* Basic controller support

* Allow runtime download of gamecontrollerdb.txt

* Add new gamepad cursor asset, and use it.

* Various improvements
Allow saving and configuring gamepad mappings
Add gamepad sensitivity, for camera only
Adjust multipliers in usage of gamepad axes
Prompt for downloading controller mappings, and allow configuration

* Refactor startup window handling so that controller settings does not duplicate code, or have to detect if startup has finished

* Clamp cursor position to window size when changing via gamepad

* Add deadzone

* Implement changes requested

* Follow more suggestions

* Use atomics

* Use orelse for oldState in Gamepads.update

* Implement requested changes

* Fix accidental removal of whitespace on two empty lines

* Fix incorrect formatting on blank line due to Vim expanding tabs to spaces

* Fix build errors.

* Change unusual orelse usage with better if expression

* Add resettable keys

* Revert "Add resettable keys"

This reverts commit 902f032ec834161dcaac97cbe596788e0110e870.

* Update controller support

* Fix things I forgot about

* Fix more things

* Implement most requested changes and change button text to be more clear

* Update controller support

* Hopefully fix format without breaking it.

* Refactor getting X positions of buttons in preparation for not moving the window when clicking the buttons

* Initial support for controllers

* Basic controller support

* Allow runtime download of gamecontrollerdb.txt

* Add new gamepad cursor asset, and use it.

* Various improvements
Allow saving and configuring gamepad mappings
Add gamepad sensitivity, for camera only
Adjust multipliers in usage of gamepad axes
Prompt for downloading controller mappings, and allow configuration

* Refactor startup window handling so that controller settings does not duplicate code, or have to detect if startup has finished

* Clamp cursor position to window size when changing via gamepad

* Add deadzone

* Implement changes requested

* Follow more suggestions

* Use atomics

* Use orelse for oldState in Gamepads.update

* Implement requested changes

* Fix build errors.

* Change unusual orelse usage with better if expression

* Add resettable keys

* Revert "Add resettable keys"

This reverts commit 902f032ec834161dcaac97cbe596788e0110e870.

* Update controller support

* Fix things I forgot about

* Fix more things

* Implement most requested changes and change button text to be more clear

* Update controller support

* Hopefully fix format without breaking it.

* Implement suggestions

* Update controller mapping downloading to be more automatic

* Fix parameter in Window.Gamepad.ControllerMappingDownloadTask.isStillNeeded

* Revert order of window opening and opening next startup window in openStartupWindow

* Only download controller mappings the first moment a controller is plugged in

* Open controller mapping download UI when actually downloading mappings, or at startup if they have started before GUI initialization but haven't finished yet.

* Don't unnecessarily try to download controller mappings at init when it will be done once the first update happens anyways.

* Implement requested changes

* Remove debugging log output

* Implement changes to download_controller_mappings.zig
This commit is contained in:
Zachary Hall 2024-10-14 08:31:59 -07:00 committed by GitHub
parent 507dc35e20
commit e6fa7498c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 552 additions and 66 deletions

2
.gitignore vendored
View File

@ -9,6 +9,8 @@ settings.json
gui_layout.json
settings.zig.zon
gui_layout.zig.zon
gamecontrollerdb.txt
gamecontrollerdb.stamp
test.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 B

View File

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

View File

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

View File

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

View File

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

View File

@ -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");

View File

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

View File

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

View File

@ -32,4 +32,4 @@ pub fn onClose() void {
if(window.rootComponent) |*comp| {
comp.deinit();
}
}
}

View File

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

View File

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