A JMonkeyEngine 3 multiplayer game using Entity-Component-System architecture.
| Task | Location | Base Class |
|---|---|---|
| Component | api/src/main/java/infinity/es/ |
EntityComponent |
| System | infinity-server/src/main/java/infinity/systems/ |
AbstractGameSystem |
| App State | infinity-client/src/main/java/infinity/ |
BaseAppState |
| Arena Module | infinity-server/src/main/java/infinity/modules/<category>/ |
ArenaModule (per ADR-0008) |
-
Use
finalfor method parameters -
SPDX-only
BSD-3-Clauselicense header on all source files (*.java,*.groovy). Format:// SPDX-License-Identifier: BSD-3-Clausefollowed by// Copyright (c) 2018-2026 Asser Fahrenholz. Full license text lives inLICENSE.md; third-party material (Subspace/Continuum game files, community maps, MillionthVector textures) is documented inTHIRD-PARTY-NOTICES.mdand is not covered byLICENSE.md. -
Tuning knobs go in Groovy, not Java. Whenever you encounter a literal numeric/string constant that smells like a tuning knob (gameplay balance, physics feel, timing budget, threshold) or when adding a new tuning knob, put it in a
.groovyfile under one of the existing config tiers and read it via the existing config layer (typed*Configrecords → ECS components per Config-Component Projection inconfig-pattern.md/ ADR-0006, orSettingsSystem):- Preset scope —
zone/conf/<preset>/(e.g.trench-04-2026/ships.groovy). Use for stat templates, balance numbers, anything keyed on a preset / arena style. This is the canonical first stop today. - Arena scope —
zone/arenas/<name>/arena.groovy. Use for per-arena overrides. - Zone scope —
zone/zone.groovy. Use for zone-wide defaults / ops knobs.
True magic numbers (loop bounds, math identities like
2π, well-known protocol constants) stay in Java. When unsure, lean toward Groovy — it's easier to demote a knob back to a constant than to flush a magic number out of compiled code. - Preset scope —
-
Keep
ship-config-dictionary.mdin sync. Tracks which per-ship INI keys are ported to typed GroovyShipConfigfields and which still live in INI (or aren't read at all). When you add, move, or delete a typed ship-config field, update the matching row in the same change. Don't let the ledger drift — a stale dictionary is worse than no dictionary because it nudges future edits toward duplicate fields. -
Keep
settings-pipeline.mdin sync. Master tracker for every Subspace fragment key as it flows through the five gates: groovy file →Groovy*Loader→*Configrecord → prize applier → consuming subsystem. Whenever you author a new key in a.groovypreset, extend a loader to read a key, add a*Configfield, implement a prize applier, or wire a runtime consumer, flip the matching cell in the same change. The table is the canonical "what's wired vs what's a TODO" view — drift makes it lie about gameplay status. -
Tracker hygiene. When a row/item lands, delete it from the tracker — never strikethrough or leave commented-out remnants. Crossed-out content is context-window clutter for future sessions and inflates trackers without adding signal. Applies to all
.scratch/*.mdfiles (pipeline tracker, dictionary, BACKLOG).
Path-scoped rules live in .claude/rules/ and load automatically when relevant files are read. The full set of ADRs they enforce is indexed in docs/adr/README.md.
components.md— immutability + no-arg constructor (api/src/main/java/infinity/es/**) — see ADR-0001.entity-sets.md— release interminate()(infinity-server/+infinity-client/Java).entity-containers.md— useEntityContainer<T>for "component-set → per-entity object" maps (drivers, AI sidecars, RMI handles); don't hand-rollMap<EntityId, T>+ manual add/remove loops (infinity-server/+infinity-client/Java).systems.md— logic-in-systems; component-write discipline pointer (infinity-server/src/main/java/infinity/systems/**) — see ADR-0001.replacement-as-mutation.md— RaM: one canonical writer per component; other systems emit intents drained by the writer; phased tick (infinity-server/src/main/java/infinity/systems/**,api/src/main/java/infinity/sim/**) — formalised by ADR-0001.world-coordinates.md—TileIdAPIs,InfinityConstants.GRID_CELL_SIZEsource of truth (infinity-server/+infinity-client/Java).api-contracts.md— api/ is data + interfaces only; no deps on server/client (api/src/**) — see ADR-0005.client-read-only.md— client observes, server owns; writes via RMI;BodyPositionnot polling (infinity-client/src/main/java/infinity/client/**+ loose*AppState) — see ADR-0005.config-pattern.md— Config-Component Projection: template (*Configrecords) vs instance (components); spawn systems project template → component; hot-path consumers read components only (api/src/main/java/infinity/config/**+api/src/main/java/infinity/es/ship/**) — formalised by ADR-0002.decay-ttl.md—Decayis the only TTL mechanism; templates carry duration, spawn systems project toDecay; no parallel*Decay/*Ttlcomponents (api/src/main/java/infinity/es/**,infinity-server/src/main/java/infinity/systems/**) — formalised by ADR-0007.player-scaling.md— consider whether spawn / balance / threshold knobs should scale with active player count; default to additivebase + perPlayer × N; per-arena, not global (infinity-server/Java systems).prize-applier.md— look up prize logic + canonical Subspace tunables inREFERENCE.mdbefore implementing or modifying a prize applier (infinity-server/src/main/java/infinity/systems/ship/applier/**).settings-pipeline.md— look up Subspace canon inREFERENCE.mdbefore authoring/modifying any typed adapter,*Config, or settings consumer; section headers (e.g.[Misc]) don't tell you which mechanic owns a key (infinity-server/src/main/java/infinity/settings/**+api/src/main/java/infinity/config/**) — see ADR-0004.pmd-on-touched-files.md— after editing any*.java, run:<module>:pmdPath -PpmdPath=<paths>on touched files, surface violations, and fix one lowest-effort violation per batch as a ratchet (api/src/**,infinity-server/src/main/java/**,infinity-client/src/main/java/**).lsp-for-java-navigation.md— prefermcp__java-lsp__*(and the genericLSPtool for hover / impls / call hierarchy) overgrepfor Java navigation; type-aware lookups beat text search on a ~100k-LOC codebase (api/src/**/*.java,infinity-server/src/**/*.java,infinity-client/src/**/*.java).javadoc-discipline.md— javadoc is a comment; one short line max for class/method summaries; cross-link rules + ADRs with@see, don't restate the recipe; WHY-comments at the site, not in the type's javadoc (api/src/**/*.java,infinity-server/src/**/*.java,infinity-client/src/**/*.java).web-research-before-design.md— before non-trivial design/implementation involving a library, framework, external API, or evolving spec, do a 60-secondWebSearch/WebFetchpass on current docs; training data and memory age fast (always-on, behavioural).
Layer boundaries are also enforced as tests — see LayerDependencyTest per ADR-0005.
./gradlew build # Build all
./gradlew :infinity-client:run # Run game
./gradlew :infinity-client:runX11 # Run on Linux/Wayland (X11 backend)
./gradlew :infinity-client:runMac # Run on macOS (-XstartOnFirstThread)Single source of truth — version lives only in the root build.gradle subprojects block. No -SNAPSHOT suffix; the git tag is the source of truth for what's released.
- Bump
build.gradle→version='X.Y.Z' - Add a
vX.Y.Zsection toRELEASE-NOTES.mdat the top, under New Features / Bug Fixes / Breaking Changes / Other. Draw fromgit log <prev-tag>..HEAD --no-merges. Land it in the same commit as the version bump. - Commit, tag (
git tag -a vX.Y.Z -m "msg"), push both - Bump
build.gradle→version='X.Y.(Z+1)'for ongoing dev
See .claude/skills/ for detailed patterns. Library-prefixed where applicable:
jMonkeyEngine 3:
jme-appstate/- Client-sideBaseAppState(UI, rendering, input)jme-effects/- Particle emitters, post-processing filters, bloom/glowjme-materials/- Materials,.j3m,.j3mdmaterial definitionsjme-shaders/- Shaders, GLSL, shader node system
Moss (physics / world):
moss-physics/- Collision detection, physics bodies, shapesmoss-world-grid/- Cell/leaf/column/tile grid; useTileIdfor map placement (not* 1024)
Simsilica (Lemur / SimEthereal / SiO2 / Zay-ES):
lemur-ui/- Lemur UI framework: menus, HUD, buttons, labelssim-ethereal/- SimEthereal networking & state syncsio2-system/- Server-side game systems (BaseInfinitySystem)zay-es-component/- Zay-ESEntityComponentclasseszay-es-debug/- ECS debugging (EntitySetleaks, component queries)
Subspace Infinity (project-specific):
project-overview/- Project structure, tech stack, conventionsinfinity-architecture/- api ↔ server ↔ client layering, data flow, "where does X go?"create-module/-ArenaModulearena-composition contract (ADR-0008)arena-settings/- Per-arenaarena.confINI settings,SettingsSystemlvl-format/- Subspace .lvl binary format: BMP tileset, eLVL metadata
Meta:
dependency-sources/- Where to find Moss/Simsilica library source codesubspace-moss-terminology/- Disambiguate overloaded terms (cell, tile, region, arena) across Subspace, MOSS, and Infinity
Issues live as markdown files under .scratch/<feature>/. See docs/agents/issue-tracker.md.
Five canonical roles, default strings (needs-triage, needs-info, ready-for-agent, ready-for-human, wontfix). See docs/agents/triage-labels.md.
Single-context: CONTEXT.md and docs/adr/ at the repo root (created lazily by skills). See docs/agents/domain.md.