Skip to content

Latest commit

 

History

History
159 lines (131 loc) · 11.8 KB

File metadata and controls

159 lines (131 loc) · 11.8 KB

AGENTS.md

This file provides guidance to AI coding agents working with code in this repository.

Project

ControllerBuddy - cross-platform advanced gamepad mapping software. Maps physical gamepad inputs through a profile-based action system to virtual devices (vJoy on Windows, uinput on Linux). See @README for detailed project overview.

Tech Stack

  • Language: Java 26
  • Build Tool: Gradle 9
  • Input Handling: LWJGL 3.4 (SDL3)
  • Native Access: FFM API (Project Panama)
  • GUI: Swing with FlatLaf
  • Serialization: Gson 2.14
  • Testing: JUnit 6, Mockito 5

Environment Setup

  • Java: JDK (17+) must be installed to run the Gradle wrapper. The build then auto-provisions the required JDK distribution via Gradle's toolchain support for compilation and execution. The target Java version is defined in build.gradle.kts (languageVersion in javaToolchainSpec).
  • Gradle: use the included wrapper (./gradlew), do not use a system Gradle. The Gradle version is defined in gradle/wrapper/gradle-wrapper.properties.
  • Git submodule: SDL_GameControllerDB must be initialized before building:
    git submodule update --init
    The build copies SDL_GameControllerDB/gamecontrollerdb.txt into resources automatically. Builds will fail with "SDL_GameControllerDB submodule not checked out" if this step is skipped.
  • Generated sources: module-info.java and Constants.java are generated by Gradle tasks (generateModuleInfo, generateConstants) and run automatically before compilation. After ./gradlew clean, a fresh build regenerates them.

Build Commands

./gradlew build --non-interactive                                                             # Build the project
./gradlew test --non-interactive                                                              # Run all tests
./gradlew test --tests 'de.bwravencl.controllerbuddy.util.VersionUtilsTest --non-interactive' # Single test class
./gradlew test --tests '*.VersionUtilsTest.returnsEmptyForNull' --non-interactive             # Single test method
./gradlew jacocoTestReport                                                                    # Run all tests and generate test coverage report
./gradlew spotlessApply                                                                       # Apply code formatting
./gradlew check                                                                               # Run SpotBugs + Spotless + all tests
./gradlew run                                                                                 # Run the application
./gradlew generateConstants                                                                   # Generate version/license constants
./gradlew generateModuleInfo                                                                  # Generate module-info.java

Architecture

  • Input Pipeline: Physical gamepad → SDL/LWJGL polling (Input.java) → Profile-based action mapping → Virtual device output
  • Profile System: Profile contains a list of Modes. Each Mode maps controller component IDs to lists of IAction implementations. Modes support inheritance/fallback chains. ButtonToModeAction handles mode switching. Profiles are JSON-serialized via Gson.
  • Action System: Pluggable actions discovered at runtime via ClassGraph scanning of @Action annotation.
    • Base interface IAction<V> with specializations: IInitializationAction, IDelayableAction, IActivatableAction, IResetableAction.
    • 40+ concrete implementations in src/main/java/de/bwravencl/controllerbuddy/input/action/.
    • Properties exposed to GUI editors via @ActionProperty annotation.
  • Key Packages:
    • input/ - Core engine: Input.java (orchestrator), Profile.java, Mode.java
    • input/action/ - All action type implementations
    • gui/ - Main.java entry point, FlatLaf Swing UI
    • ffi/ - Foreign function interfaces (vJoy, uinput)
    • runmode/ - Local/Client/Server mode implementations
    • json/ - Gson type adapters for profile serialization

Code Style

Formatting (indentation, import ordering, license headers, whitespace) is enforced by Spotless - run ./gradlew spotlessApply after making changes.
SpotBugs runs via ./gradlew check, whereas Error Prone runs during compilation.
GPL v3 license headers are auto-added to all Java files by Spotless.

The conventions below are not auto-enforced and must be followed manually:

  • Never use the em dash character (). Use a hyphen (-) instead.
  • Mark all classes and fields as final unless mutability or inheritance is explicitly required.
  • In non-final classes, mark all public and protected methods as final unless they are explicitly designed to be overridden (e.g., abstract methods). Do not add final to private methods, as they cannot be overridden anyway.
  • Apply the final keyword to all method parameters, local variables, and catch-block variables without exception.
  • Use var for local variables.
  • Use _ for unused local variables and patterns.
  • Use this. only for disambiguation (e.g. in constructors and setters), not for field reads.
  • Constants are static final fields with deeply immutable contents and side-effect-free methods - if any observable state can change, it's not a constant.
  • Constant naming: UPPER_SNAKE_CASE. Use semantic prefixes like DEFAULT_ and INITIAL_ for related groups of constants.
  • Non-constant naming: lowerCamelCase
  • Class layout and ordering: use the following group sequence. If multiple members exist within the same group, sort them alphabetically by name.
    1. Static Constants (final): publicprotectedpackageprivate
    2. Static Fields (non-final): publicprotectedpackageprivate
    3. Static Initializers
    4. Instance Fields: publicprotectedpackageprivate
    5. Instance Initializers
    6. Constructors
    7. Static Methods
    8. Instance Methods
    9. Enums
    10. Interfaces
    11. Static Inner Classes
    12. Inner Classes (non-static)
  • Access modifiers: always use the most restrictive access level possible. Default to private.
  • Use modern Java features: pattern matching with instanceof, switch expressions, records for immutable data holders.
  • Localization: user-facing strings go in resource bundles (strings.properties, strings_de_DE.properties), not hardcoded.
  • Module system: packages that need Gson serialization require an opens ... to com.google.gson directive. These are defined in the generateModuleInfo task in build.gradle.kts - not in module-info.java directly, as that file is generated.
  • Nullness annotations: use JSpecify annotations from org.jspecify.annotations.* to define nullness contracts.
    • The module is null-marked by default for src/main via package-info.java; never add @NullMarked to individual main classes.
    • Test classes are not covered by that module-level marking, so annotate every test class with @NullMarked individually.
    • Use @Nullable explicitly on any parameter, field, or return type that may hold null.
    • Place @Nullable immediately before the type name, after modifiers like final (e.g. private final @Nullable String name;).
  • Documentation comments: follow the rules below for all Javadoc comments.
    • Language use American English, not British English.
    • Syntax: use /// Markdown Javadoc comments (JEP 467), not /** */ Javadoc.
    • Coverage: all public, protected, package-private, and private classes, interfaces, records, enums, enum constants, fields, constructors, and methods must have a doc comment. The only exceptions are serialVersionUID fields and Logger fields named LOGGER, which must not be documented.
    • Summary line: starts with a third-person verb (e.g. "Returns", "Sets", "Creates"). Use "Returns the/whether..." for getters and "Sets the/whether..." for setters.
    • Structure: doc comments for classes, interfaces, records, and enums must have both a short summary sentence and a separate extended description providing additional detail. Separate the summary from the extended description with a blank /// line.
    • Periods: full sentences end with a period. Short label-style descriptions for enum constants or similar identifiers (e.g. /// 'A' key) may omit the period.
    • Inline code: use backticks (`null`, `true`, `false`), not {@code ...}.
    • Cross-references: use Markdown links ([ClassName], [#methodName]), not {@link ...}.
    • Line wrapping: do not manually wrap lines within continuous text - write each sentence or clause as a single long line and let Spotless re-wrap to the correct width. Only insert explicit line breaks where they serve a structural formatting purpose (e.g. between paragraphs, before tags, or to separate list items). When editing an existing doc comment, first unwrap all lines back into continuous text, re-add only the intentional formatting breaks, and then let Spotless re-wrap to the correct width.
    • Tags: include @param, @return, and @throws tags where applicable. Tag descriptions start lowercase. Separate the summary from tags with a blank /// line. For overridden methods, do not repeat tags that are already documented in the superclass or interface with the same meaning - only include tags whose function differs from the inherited documentation.
    • Records: document @param tags for record components on the record's own doc comment, not on the accessor methods.
    • @param <V>: include a type parameter tag on generic classes and interfaces (e.g. /// @param <V> the input value type).

Test Conventions

  • JUnit 6 with @DisplayName for readable names, @Nested for grouping
  • Mockito with @ExtendWith(MockitoExtension.class)

Adding a New Action

  • Extend an appropriate base class (ToAxisAction, ToButtonAction, ToKeyAction, etc.) and implement the input type interface (IAxisToAction, IButtonToDelayableAction, etc.)
  • Annotate the class with @Action(category, description, order, title) - title and description are localization keys from strings.properties
  • Annotate configurable fields with @ActionProperty(title, description, editorBuilder, order) - the order attribute controls GUI layout
  • The editorBuilder parameter specifies which EditorBuilder subclass renders the GUI editor for the field Use existing builders when possible:
    • NumberEditorBuilder subclasses for numeric ranges
    • ArrayEditorBuilder subclasses for enums
    • BooleanEditorBuilder for booleans
    • A custom EditorBuilder implementation is needed when the property requires non-standard UI
  • Override clone() - use deep cloning for mutable fields (e.g. KeyStroke)
  • If the action implements IInitializationAction, ensure init() initializes transient fields to the same defaults as the field declarations

Commit Messages

Format: <prefix>: <lowercase description> - no period at end, single line.

  • feat: - new features and general improvements
  • fix: - bugfixes
  • chore: - dependency updates, baselines, and other maintenance
  • style: - code style and formatting changes with no functional impact

Versioning

Version is derived automatically from git tags at build time (see versionProvider in build.gradle.kts).
If a change affects the profile serialization format (i.e. what gets serialized/deserialized via Gson) the minor version must be bumped by creating an annotated tag:

git tag -a "X.Y" -m "X.Y"

The tag must be pushed before the master branch is pushed, so that CI derives the correct version number.

CI

  • Never push to master - pushing to master triggers a release workflow and is reserved to humans
  • master must always be in a buildable state