Skip to content

Latest commit

 

History

History
150 lines (122 loc) · 10.5 KB

File metadata and controls

150 lines (122 loc) · 10.5 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.13
  • 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                                                             # Build the project
./gradlew test                                                              # Run all tests
./gradlew test --tests 'de.bwravencl.controllerbuddy.util.VersionUtilsTest' # Single test class
./gradlew test --tests '*.VersionUtilsTest.returnsEmptyForNull'             # 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.
  • Classes and fields should be final unless mutability or subclassing is required.
  • Use final on all method parameters, local variables, and catch-block variables.
  • 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.
  • Constant naming: SCREAMING_SNAKE_CASE. Use semantic prefixes like DEFAULT_ and INITIAL_ for related groups of constants.
  • 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.
  • Documentation comments: follow the rules below for all Javadoc comments.
    • 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