Skip to content

Refactor: Implement Input Action Mapping System - #156

Merged
MichaelFisher1997 merged 8 commits into
mainfrom
feature/input-action-mapping
Jan 13, 2026
Merged

Refactor: Implement Input Action Mapping System#156
MichaelFisher1997 merged 8 commits into
mainfrom
feature/input-action-mapping

Conversation

@MichaelFisher1997

@MichaelFisher1997 MichaelFisher1997 commented Jan 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR implements a comprehensive Input Action Mapping system as requested in #148. It has been thoroughly refactored to address all code review feedback, resolve integration test failures, and improve system robustness.

Key Changes:

  1. Logical Action Abstraction: Introduced GameAction enum to represent player intents (e.g., Jump, InteractPrimary) instead of hardcoded keys. Default keys are documented in the enum.
  2. Configurable Bindings: InputMapper now supports runtime rebinding with support for both primary and alternate bindings (Keyboard & Mouse).
  3. Robust Serialization: Refactored to use std.json for all settings persistence, utilizing the latest Zig 0.16.0-dev APIs.
  4. Settings Persistence: Implemented InputSettings in src/game/input_settings.zig with robust platform path detection (including BSD support) and comprehensive error logging.
  5. Decoupled Input: Refactored Player, App, Session, MapController, and Camera to depend on logical actions.
  6. Efficiency & Safety Improvements:
    • Error Propagation: Enhanced load and parseJson to propagate errors correctly and provide detailed warnings.
    • Version Migration: Added schema versioning with safe migration logic to handle evolving settings formats.
    • Initialization Optimization: Added loadAndReturnMapper to streamline system startup.
    • Static Defaults: Replaced redundant InputMapper allocations with a static DEFAULT_BINDINGS array.
    • Bidirectional Equality: Improved InputBinding.eql to correctly handle .key and .key_alt representing the same physical key symmetrically.
    • Robust Testing: Updated tests to handle JSON parser variations across Zig versions.

Verification:

  • All 134 unit and integration tests passed.
  • Added new tests for version migration and corrupt settings file handling.
  • Verified robust deserialization and equality logic.
  • Verified full build with zig build.

Resolves #148

…dings and Persistence

- Introduced GameAction enum and InputMapper for hardware-agnostic input
- Added support for configurable key and mouse bindings
- Implemented settings persistence in src/game/settings.zig
- Refactored Player, App, Session, and MapController to use the action system
- Resolves #148
@github-actions github-actions Bot added documentation Improvements or additions to documentation engine game labels Jan 13, 2026
@MichaelFisher1997

Copy link
Copy Markdown
Collaborator Author

/oc
Review this pull request:

Check for code quality issues
Look for potential bugs
Suggest improvements

@opencode-agent

Copy link
Copy Markdown
Contributor

Failed to parse response. Part types found: [step-start, step-finish]

opencode session  |  github run

@opencode-agent

Copy link
Copy Markdown
Contributor

Code Review: Input Action Mapping System

Critical Issues

1. Memory Leak in menus.zig:82-86

var input_settings = InputSettings.init(ctx.allocator);
input_settings.input_mapper = ctx.input_mapper.*;
input_settings.save() catch |err| { ... };

InputSettings allocates memory internally but is never deinitialized. Should call input_settings.deinit() or redesign to avoid the intermediate struct.

2. Duplicate Settings Loading in app.zig:154-157

var input_mapper = InputMapper.init();  // Default init
const input_settings = InputSettings.load(allocator);
input_mapper = input_settings.input_mapper;  // Immediately overwritten

The default InputMapper.init() allocation is wasted. Initialize input_mapper directly from loaded settings.

3. Missing Allocator in settings.zig:36

pub fn load(allocator: std.mem.Allocator) Settings {
    var settings = Settings.init(allocator);

The Settings.init() call doesn't pass the allocator - this will cause a null pointer or undefined behavior.


Potential Bugs

4. JSON Parsing is Fragile in input_mapper.zig:436-464
Manual JSON parsing with indexOfPos and brace counting will break on:

  • Nested objects in future extensions
  • Whitespace variations
  • JSON formatting changes

5. Missing Error Handling in camera.zig:87

const move_vec = mapper.getMovementVector(input);
if (move_vec.z > 0) move_dir = move_dir.add(self.forward);

No fallback if mapper is null or improperly initialized.

6. Inconsistent Error Propagation in app.zig:156-157

const input_settings = InputSettings.load(allocator);
input_mapper = input_settings.input_mapper;

If load() returns default settings on error, the original custom bindings are silently discarded.


Code Quality Issues

7. Dead Code in input_mapper.zig:399

_ = action;  // Unused variable - should be removed

8. Overly Complex State Machine in app.zig:280-317
The ui_back handling with nested state transitions is difficult to maintain. Consider extracting to a dedicated handleUiBack() function.

9. Magic Numbers in settings.zig:25-26

const SETTINGS_FILENAME = "settings.json";
const APP_NAME = "zigcraft";

These should be documented or in a dedicated constants section.

10. Missing deinit for InputSettings in Multiple Locations

  • app.zig:236 - input_settings never deinitialized
  • menus.zig:82-86 - Same issue

Improvements

11. Use std.json Instead of Manual JSON
Replace manual serialization in input_mapper.zig with std.json for maintainability and correctness.

12. Add Validation to InputBinding.eql()
Consider adding bidirectional equality checking (A.eql(B) should equal B.eql(A)).

13. Document the GameAction Enum
Add documentation explaining each action's purpose and default behavior.

14. Consider Adding Reset-to-Defaults for Individual Bindings
Currently only resetToDefaults() exists; individual action reset would be useful.


Summary

Severity Count
Critical 3
High 3
Medium 4
Low 4

The most pressing issues are the memory leak in menus.zig and the missing allocator in settings.zig. The manual JSON parsing approach will become problematic as the settings schema evolves.

New%20session%20-%202026-01-13T22%3A06%3A33.442Z
opencode session  |  github run

…ON parsing

- Fixed memory leaks in menus.zig and app.zig by adding missing deinit calls
- Optimized InputMapper initialization in app.zig to avoid redundant default init
- Renamed settings.zig to input_settings.zig for clarity and added deinit()
- Refactored InputMapper and InputSettings to use std.json for robust serialization
- Extracted handleUiBack() in app.zig to simplify the main update loop
- Updated code to use new Zig 0.16.0-dev APIs (std.Io.Writer.Allocating, ArrayList.empty)
- Added documentation for all GameAction entries
@MichaelFisher1997

Copy link
Copy Markdown
Collaborator Author

/oc
Review this pull request:

Check for code quality issues
Look for potential bugs
Suggest improvements

@opencode-agent

Copy link
Copy Markdown
Contributor

Code Review: Input Action Mapping System

Critical Issues

1. Memory Leak in input_mapper.zig:306-307

pub fn resetAction(self: *InputMapper, action: GameAction) void {
    const defaults = InputMapper.init();  // Allocates new mapper
    self.bindings[@intFromEnum(action)] = defaults.bindings[@intFromEnum(action)];
    // defaults is never deinitialized - memory leak
}

InputMapper.init() allocates an array internally but is immediately discarded. This should be refactored to store defaults as a static constant to avoid allocation.

2. Inconsistent Error Handling in app.zig:154-157

var input_settings = InputSettings.load(allocator);
defer input_settings.deinit();
const input_mapper = input_settings.input_mapper;

The comment "Load custom bindings" is misleading. If load() fails (file missing/corrupt), it returns default bindings - the original bindings are silently discarded without any logging.

3. Fragile JSON Parsing in input_mapper.zig:399

pub fn deserialize(self: *InputMapper, allocator: std.mem.Allocator, data: []const u8) !void {
    var parsed = try std.json.parseFromSlice([GameAction.count]ActionBinding, allocator, data, .{
        .ignore_unknown_fields = true,
    });
    defer parsed.deinit();
    self.bindings = parsed.value;
}

The test at lines 413-428 manually parses JSON instead of using deserialize(). This creates a discrepancy - the test doesn't verify the actual deserialization method works correctly.

Potential Bugs

4. Dead Code in input_mapper.zig:399

_ = action;  // Unused variable - should be removed

This appears in the deserialize function but action parameter doesn't exist there anymore.

5. Missing Validation in InputBinding.eql()
The equality function isn't symmetric. A .key binding won't equal a .key_alt binding even if they represent the same physical key. This could cause issues in UI binding conflict detection.

6. Platform Path Handling in input_settings.zig:73-92

fn getSettingsPath(allocator: std.mem.Allocator) ![]u8 {
    if (builtin.os.tag == .linux) {

Missing cases for Android, FreeBSD, and other supported Zig targets will cause error.UnsupportedPlatform at runtime.

Code Quality Issues

7. Unused Variable in app.zig:155

var input_settings = InputSettings.load(allocator);
defer input_settings.deinit();
const input_mapper = input_settings.input_mapper;

input_settings is declared but immediately copied and never used directly. Consider:

const input_mapper = InputSettings.load(allocator).input_mapper;
defer { _ = input_mapper; }  // Keep deinit

8. Missing Allocator Documentation
InputSettings.load() signature doesn't indicate it can return default settings on error. Callers might not realize their custom bindings could be lost silently.

9. Inconsistent State Machine Complexity
The handleUiBack() function is well-extracted (good!), but the main state machine in app.zig:254-278 still has complex nested switch logic that could benefit from similar extraction.

Improvements

10. Add Reset-to-Defaults for Individual Bindings
Currently only resetToDefaults() exists. Consider adding:

pub fn resetActionToDefault(self: *InputMapper, action: GameAction) void

(With static defaults array to avoid allocation - see issue #1)

11. Document Default Bindings
The enum documentation could include the default key for each action for user reference:

/// Move player forward (Default: W)
move_forward,

12. Consider Bidirectional Equality

pub fn eql(self: InputBinding, other: InputBinding) bool {
    // Also check if key == key_alt for same physical key
}

Summary

Severity Count Key Issues
Critical 2 Memory leak in resetAction, silent data loss on settings load
High 2 Dead code, fragile test coverage
Medium 3 Unused variable, missing platform support, documentation gaps
Low 3 State machine complexity, equality asymmetry, API clarity

The implementation is mostly solid but needs fixes for the memory leak and error handling visibility before merge. The test coverage for deserialization should be updated to actually test the deserialize() method.

New%20session%20-%202026-01-13T22%3A17%3A53.555Z
opencode session  |  github run

- Fixed serialize/toJson to correctly transfer ownership from Allocating writer
- Fixed memory leak in InputMapper.serialize and InputSettings.toJson
- Updated InputMapper unit test to use refactored deserialize method
- Resolved UnexpectedEndOfInput error caused by returning empty buffers
…nd robust testing

- Replaced heap-allocating InputMapper.init() in resetAction with static DEFAULT_BINDINGS to fix memory leak
- Renamed resetAction to resetActionToDefault for clarity
- Fixed silent data loss on settings load by adding comprehensive logging in InputSettings.load
- Updated platform path handling with robust fallbacks in getSettingsPath
- Refactored InputBinding.eql to handle symmetric equality between .key and .key_alt
- Updated InputMapper tests to verify actual deserialize() method and equality logic
- Documented default keys for all GameAction entries
- Optimized App.init to avoid redundant InputMapper copies
@MichaelFisher1997

Copy link
Copy Markdown
Collaborator Author

/oc
Review this pull request:

Check for code quality issues
Look for potential bugs
Suggest improvements

@opencode-agent

Copy link
Copy Markdown
Contributor

Code Review Summary

Critical Issues

1. Missing *mut qualifier in input_settings.zig:124

fn parseJson(self: *InputSettings, data: []const u8) !void {

Should be *mut InputSettings since it modifies self.input_mapper.bindings.

2. Potential undefined behavior in input_mapper.zig:417

self.bindings = parsed.value;

The deserialize function takes *InputMapper (mutable pointer) but line 411 shows *const InputMapper in the comment. Verify the signature is correct.

High Priority Issues

3. Duplicate settings initialization pattern in app.zig:234-236

var input_settings = InputSettings.init(self.allocator);
defer input_settings.deinit();
input_settings.input_mapper = self.input_mapper;

This creates a new InputSettings with default bindings, then overwrites them. The default initialization is wasted. Consider extracting just the save logic.

4. Same pattern in menus.zig:82-84

var input_settings = InputSettings.init(ctx.allocator);
defer input_settings.deinit();
input_settings.input_mapper = ctx.input_mapper.*;

Duplicated code pattern - consider extracting a shared InputSettings.saveFromMapper helper.

Medium Priority Issues

5. Incomplete platform path detection in input_settings.zig:104-108

} else {
    // Generic fallback for other platforms
    return std.fmt.allocPrint(allocator, "./{s}", .{SETTINGS_FILENAME});
}

Android and other mobile platforms may not have a writable current directory. Consider using app-specific storage paths where available.

6. No version migration in serialization

{"version":2,"bindings":[...]}

If settings schema changes in the future, there's no migration path from v1 to v2. Consider adding migration logic or at minimum a comment about backward compatibility expectations.

7. Silent data loss on settings load error in input_settings.zig:47-51

const data = std.fs.cwd().readFileAlloc(...) catch |err| {
    if (err != error.FileNotFound) {
        log.log.warn("...Using default bindings.", .{ path, err });
    }
    return settings;
};

When file is corrupt, user loses custom bindings without explicit warning about what specifically failed. The current warning is good but consider adding a more specific error message.

Low Priority Issues

8. Magic number for max file size in input_settings.zig:47

const data = std.fs.cwd().readFileAlloc(path, allocator, @enumFromInt(1024 * 1024)) catch ...

Consider defining const MAX_SETTINGS_SIZE = 1024 * 1024; for clarity.

9. Test coverage gap for InputSettings.load
No tests verify behavior when settings file is corrupt or has invalid JSON. Consider adding:

test "InputSettings.load handles corrupt file" {
    // Write corrupt JSON, verify default bindings returned
}

Suggested Improvements

10. Add resetActionToDefault tests
The function is well-implemented using static DEFAULT_BINDINGS, but no explicit tests exist for it.

11. Consider adding InputSettings.createFromMapper constructor

pub fn createFromMapper(allocator: std.mem.Allocator, mapper: InputMapper) InputSettings {
    return .{ .allocator = allocator, .input_mapper = mapper };
}

Would eliminate the initialization-then-overwrite pattern.

Summary

Severity Count Key Concerns
Critical 1 Missing *mut qualifier in parseJson
High 2 Duplicate settings save patterns
Medium 3 Platform coverage, version migration, error messaging
Low 3 Magic numbers, test coverage gaps

The implementation is solid overall. The previous memory leak from resetAction has been properly fixed using static DEFAULT_BINDINGS. The bidirectional equality in InputBinding.eql is correctly implemented. The main improvements would be extracting the shared save pattern and adding the *mut qualifier.

New%20session%20-%202026-01-13T22%3A28%3A19.849Z
opencode session  |  github run

…nt allocations, and improve error handling

- Refactored InputMapper to use a static DEFAULT_BINDINGS array, fixing memory leaks and improving performance
- Added initFromMapper and saveFromMapper helpers to InputSettings to avoid redundant default allocations
- Improved platform detection in getSettingsPath (added BSD support and better fallbacks)
- Added version check and migration log to InputSettings
- Enhanced InputBinding.eql to be bidirectional and correctly handle key equivalents
- Documented default keys for all GameActions
- Added unit tests for resetActionToDefault and corrupt settings file handling
@MichaelFisher1997

Copy link
Copy Markdown
Collaborator Author

/oc
Review this pull request:

Check for code quality issues
Look for potential bugs
Suggest improvements

@opencode-agent

Copy link
Copy Markdown
Contributor

Based on my review of the code, here are the issues I found:

Critical Issues

1. Missing error propagation in input_settings.zig:142-160
The parseJson function catches errors but doesn't return them, silently continuing with default bindings on parse failure:

settings.parseJson(data) catch |err| {
    log.log.warn("Failed to parse settings file...", .{});
};  // Error is swallowed - parseJson returns void on error

This causes user-customized bindings to be lost without the caller knowing.

2. Silent data loss on settings load failure (input_settings.zig:66-68)
When parseJson fails (corrupt file, wrong version), the function returns default settings without any indication that the user's custom bindings were discarded. Consider returning an error or adding a flag.

High Priority Issues

3. Version migration is incomplete (input_settings.zig:154-158)

if (parsed.value.version < 2) {
    log.log.info("Migrating input settings...");
    // No actual migration logic - just accepts potentially incompatible data
}

A v1 file with fewer bindings could corrupt v2 settings by overwriting newer action bindings with old data.

4. Duplicate initialization pattern in app.zig:155 and input_settings.zig:31-36

var input_settings = InputSettings.load(allocator);
// ... 
const input_mapper = input_settings.input_mapper;

InputSettings.init() allocates an InputMapper with defaults that's immediately overwritten. Consider adding a constructor:

pub fn loadAndReturnMapper(allocator: std.mem.Allocator) InputMapper {
    return InputSettings.load(allocator).input_mapper;
}

Medium Priority Issues

5. No validation of bindings array length (input_settings.zig:148-151)

var parsed = try std.json.parseFromSlice(Schema, ...);

If the saved JSON has wrong array size, this could cause issues. Consider adding validation after parsing.

6. Test coverage gap for InputSettings.load with version migration
The test at line 164-183 only tests parseJson with invalid JSON, not version migration scenarios.

Code Quality Observations

Positive patterns observed:

  • Static DEFAULT_BINDINGS array avoids memory leak (good fix from previous review)
  • InputBinding.eql() correctly handles bidirectional key/alt equality
  • Platform path detection covers Linux, macOS, Windows, BSD
  • Well-documented GameAction enum with default key comments

Minor suggestions:

  • Consider adding @memcpy for binding assignment to be explicit about copy semantics
  • The CURRENT_VERSION constant should be documented with expected migration behavior

New%20session%20-%202026-01-13T22%3A33%3A00.439Z
opencode session  |  github run

…ings.load

- Swapped allocator and path arguments
- Wrapped MAX_SETTINGS_SIZE in Io.Limit.limited()
- Verified fix via successful unit tests
…nitialization efficiency

- Improved error propagation and logging in InputSettings.load
- Implemented basic version migration and validation in parseJson
- Added InputSettings.loadAndReturnMapper to avoid redundant default initialization
- Used @memcpy for explicit copy semantics in binding assignments
- Added unit tests for version migration and corrupt settings handling
- Documented CURRENT_VERSION and MAX_SETTINGS_SIZE
…tedToken

- JSON parser in different Zig versions might return either error for garbage input
- Verified fix passes locally and addresses integration test failure
@MichaelFisher1997

Copy link
Copy Markdown
Collaborator Author

/oc
Review this pull request:

Check for code quality issues
Look for potential bugs
Suggest improvements

@opencode-agent

Copy link
Copy Markdown
Contributor

Code Review: Input Action Mapping System

Based on my review of the code, here's my analysis:

✅ Issues Fixed

  1. Memory Leak Resolved - input_mapper.zig:321-323 now uses static DEFAULT_BINDINGS array instead of allocating a new InputMapper
  2. Missing Mutability Fixed - parseJson correctly uses *InputSettings (line 151)
  3. Duplicate Initialization Pattern - loadAndReturnMapper helper added (input_settings.zig:78-82)
  4. Bidirectional Equality - InputBinding.eql properly symmetric (input_mapper.zig:117-143)
  5. Error Propagation - parseJson now returns !void with proper error handling
  6. Version Migration Safety - Uses @min() to prevent array bounds issues (input_settings.zig:172-173)
  7. Clean API Patterns - saveFromMapper helper avoids redundant initialization
  8. Test Coverage - Tests exist for version migration and corrupt files (input_settings.zig:177-214)
  9. Dead Code Removed - No leftover _ = action patterns
  10. App Integration - Uses loadAndReturnMapper (app.zig:155) and saveFromMapper (menus.zig:82)

⚠️ Minor Observations

  1. Version migration is minimal (input_settings.zig:163-166) - Currently just logs and accepts v1 data. Future schema changes may need actual migration logic.

  2. Platform fallback (input_settings.zig:132-135) - Android and other untested platforms fall back to ./settings.json. Acceptable for now.

  3. No tests for loadAndReturnMapper - The helper exists but isn't directly tested in isolation.

✅ Code Quality

  • Static DEFAULT_BINDINGS avoids heap allocation
  • @memcpy used for binding copying (explicit, safe)
  • Well-documented GameAction enum with default key comments
  • Proper error logging with context
  • Clean separation between InputMapper (runtime bindings) and InputSettings (persistence)

Summary

Category Status
Memory Safety ✅ No leaks
Error Handling ✅ Proper propagation
API Design ✅ Clean helpers
Test Coverage ✅ Migration & corruption tests
Code Quality ✅ Well-structured

Approval: The implementation is solid. All previously identified critical issues have been resolved. The code is ready for merge.

New%20session%20-%202026-01-13T22%3A47%3A40.801Z
opencode session  |  github run

@MichaelFisher1997
MichaelFisher1997 merged commit bec000c into main Jan 13, 2026
5 checks passed
@github-actions
github-actions Bot deleted the feature/input-action-mapping branch April 29, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation engine game

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor: Implement Input Action Mapping System

1 participant