This file provides guidance to AI coding agents working with code in this repository.
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.
- 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
- 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(languageVersioninjavaToolchainSpec). - Gradle: use the included wrapper (
./gradlew), do not use a system Gradle. The Gradle version is defined ingradle/wrapper/gradle-wrapper.properties. - Git submodule:
SDL_GameControllerDBmust be initialized before building:The build copiesgit submodule update --init
SDL_GameControllerDB/gamecontrollerdb.txtinto resources automatically. Builds will fail with "SDL_GameControllerDB submodule not checked out" if this step is skipped. - Generated sources:
module-info.javaandConstants.javaare generated by Gradle tasks (generateModuleInfo,generateConstants) and run automatically before compilation. After./gradlew clean, a fresh build regenerates them.
./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- Input Pipeline: Physical gamepad → SDL/LWJGL polling (
Input.java) → Profile-based action mapping → Virtual device output - Profile System:
Profilecontains a list ofModes. EachModemaps controller component IDs to lists ofIActionimplementations. Modes support inheritance/fallback chains.ButtonToModeActionhandles mode switching. Profiles are JSON-serialized via Gson. - Action System: Pluggable actions discovered at runtime via ClassGraph scanning of
@Actionannotation.- 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
@ActionPropertyannotation.
- Base interface
- Key Packages:
input/- Core engine:Input.java(orchestrator),Profile.java,Mode.javainput/action/- All action type implementationsgui/-Main.javaentry point, FlatLaf Swing UIffi/- Foreign function interfaces (vJoy, uinput)runmode/- Local/Client/Server mode implementationsjson/- Gson type adapters for profile serialization
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
finalunless mutability or subclassing is required. - Use
finalon all method parameters, local variables, and catch-block variables. - Use
varfor 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 likeDEFAULT_andINITIAL_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.
- Static Constants (final):
public→protected→package→private - Static Fields (non-final):
public→protected→package→private - Static Initializers
- Instance Fields:
public→protected→package→private - Instance Initializers
- Constructors
- Static Methods
- Instance Methods
- Enums
- Interfaces
- Static Inner Classes
- Inner Classes (non-static)
- Static Constants (final):
- 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.gsondirective. These are defined in thegenerateModuleInfotask inbuild.gradle.kts- not inmodule-info.javadirectly, 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, andprivateclasses, interfaces, records, enums, enum constants, fields, constructors, and methods must have a doc comment. The only exceptions areserialVersionUIDfields andLoggerfields namedLOGGER, 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@throwstags 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
@paramtags 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).
- Syntax: use
- JUnit 6 with
@DisplayNamefor readable names,@Nestedfor grouping - Mockito with
@ExtendWith(MockitoExtension.class)
- 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)-titleanddescriptionare localization keys fromstrings.properties - Annotate configurable fields with
@ActionProperty(title, description, editorBuilder, order)- theorderattribute controls GUI layout - The
editorBuilderparameter specifies whichEditorBuildersubclass renders the GUI editor for the field Use existing builders when possible:NumberEditorBuildersubclasses for numeric rangesArrayEditorBuildersubclasses for enumsBooleanEditorBuilderfor booleans- A custom
EditorBuilderimplementation 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, ensureinit()initializes transient fields to the same defaults as the field declarations
Format: <prefix>: <lowercase description> - no period at end, single line.
feat:- new features and general improvementsfix:- bugfixeschore:- dependency updates, baselines, and other maintenancestyle:- code style and formatting changes with no functional impact
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.
- Never push to
master- pushing tomastertriggers a release workflow and is reserved to humans mastermust always be in a buildable state