# Contributing: Architecture and Patterns

> Detailed internal architecture, project layout, module responsibilities, and recurring implementation patterns for Kore contributors.

Updated 2026-10-24. Canonical: https://kore.ayfri.com/docs/contributing/architecture-and-patterns

This page documents the contributor-facing architecture of Kore and the implementation patterns worth reusing.

It is intentionally focused on the parts that help you answer two questions quickly:

1. *Which module should I edit?*
2. *Which project pattern should I follow instead of inventing a new one?*

## Project tree at a glance

```text
Kore/
├─ bindings/
├─ build-logic/
├─ generation/
├─ helpers/
├─ kore/
├─ oop/
└─ website/
```

This tree is intentionally small. It reflects the modules contributors should reason about first; local-only or
git-excluded sandboxes are omitted on purpose.

## How the main modules relate

- `bindings/` imports datapacks and emits Kotlin bindings for resources and registries.
- `build-logic/` centralizes shared Gradle conventions and project metadata.
- `generation/` transforms upstream Minecraft data into generated Kotlin/resources consumed by `kore/`.
- `helpers/` and `oop/` layer higher-level APIs on top of the core DSL.
- `kore/` is the main DSL and runtime surface most contributors touch first.
- `website/` documents both the public API and contributor workflows.

## Module boundaries and edit surfaces

### [`bindings/`](https://github.com/ayfri/kore/tree/master/bindings)

- Purpose: datapack importer and Kotlin binding generator.
- Typical flow: explorer -> normalized entities -> writer output.
- Common edit surface: [`explorer.kt`](https://github.com/ayfri/kore/blob/master/bindings/src/commonMain/kotlin/io/github/ayfri/kore/bindings/explorer.kt), [`entities.kt`](https://github.com/ayfri/kore/blob/master/bindings/src/commonMain/kotlin/io/github/ayfri/kore/bindings/entities.kt), [
  `writer.kt`](https://github.com/ayfri/kore/blob/master/bindings/src/commonMain/kotlin/io/github/ayfri/kore/bindings/writer.kt), then tests under [`bindings/src/commonTest`](https://github.com/ayfri/kore/tree/master/bindings/src/commonTest/kotlin/io/github/ayfri/kore/bindings).
- Pattern to preserve: single-namespace packs stay compact, multi-namespace packs become namespace-nested objects, and
  worldgen content is grouped under `Worldgen`.

### [`build-logic/`](https://github.com/ayfri/kore/tree/master/build-logic)

- Purpose: shared Gradle conventions, publishing logic, and project metadata.
- Common edit surface: convention plugins and [`Project.kt`](https://github.com/ayfri/kore/blob/master/build-logic/convention/src/main/kotlin/Project.kt).
- Edit here when changing build behavior, publication rules, or project versioning.

### [`generation/`](https://github.com/ayfri/kore/tree/master/generation)

- Purpose: source-data processing and generated Kotlin/resource output.
- Edit here when a generated enum, registry wrapper, or source-derived structure is wrong.
- **Never** fix a generation issue by editing `kore/src/commonMain/kotlin/io/github/ayfri/kore/generated` or
  `build/generated/...` directly.
- Full walkthrough: [Contributing: The Generation Pipeline](https://kore.ayfri.com/docs/contributing/generation-pipeline.md).

### [`helpers/`](https://github.com/ayfri/kore/tree/master/helpers)

- Purpose: optional higher-level helpers built on the core DSL.
- Edit here only when the issue explicitly targets helper abstractions or reusable convenience APIs.
- Mirror core DSL patterns instead of creating a parallel architecture.

### [`kore/`](https://github.com/ayfri/kore/tree/master/kore)

- Purpose: core DSL, typed arguments, command wrappers, serializers, worldgen builders, and data-driven resources.
- Common edit surface: feature classes, `DataPack` registration, `Function` extensions, serializers, and tests under [
  `kore/src/commonTest`](https://github.com/ayfri/kore/tree/master/kore/src/commonTest/kotlin/io/github/ayfri/kore).
- A typical change in this module touches one feature family end to end: model, registration, builder entry point,
  tests, and docs.

### [`oop/`](https://github.com/ayfri/kore/tree/master/oop)

- Purpose: object-oriented abstractions layered on top of core Kore primitives.
- Edit here when the issue is specifically about that façade, not when the underlying DSL itself is wrong.
- Keep naming and behavior aligned with `kore/` to avoid divergent APIs.

### [`website/`](https://github.com/ayfri/kore/tree/master/website)

- Purpose: documentation markdown, docs navigation, and frontend rendering.
- Docs live in [`website/src/jsMain/resources/markdown/doc`](https://github.com/ayfri/kore/tree/master/website/src/jsMain/resources/markdown/doc).
- Edit this module in the same PR as any user-visible behavior change.

## Core patterns to reuse

### Argument wrappers and literals

Kore prefers typed wrappers over raw strings.

- `Argument` is the base abstraction for command-safe value types.
- Generated argument wrappers are the preferred representation for registry references.
- Tag variants encode `#namespace:name` conventions.
- Literal helpers such as `literal()`, `int()`, and `float()` keep command assembly explicit and safe.

This improves autocomplete quality, reduces malformed command strings, and keeps serialization predictable.

### Command wrappers

Command APIs generally live as `Function` extension functions.

- Build lines with `addLine(command("name", args...))`.
- Accept typed arguments directly when possible.
- Keep wrappers thin: syntax composition belongs here, domain state belongs in arguments or data classes.

This keeps generated commands deterministic and test-friendly.

### Generator pattern

Most data-driven resources in `kore` follow one consistent model:

1. A serializable feature class extends [`Generator`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/Generator.kt).
2. The class defines its `resourceFolder`, transient `fileName`, and `generateJson(dataPack)` implementation.
3. [`DataPack`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/DataPack.kt) registers a typed generator list through `registerGenerator<T>()`.
4. A `DataPack` extension function instantiates and registers the feature.
5. The extension returns a typed `*Argument` for later references.

Concrete references:

- [`Generator.kt`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/Generator.kt)
- [`DataPack.kt`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/DataPack.kt)
- [`Instrument.kt`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/features/instruments/Instrument.kt)
- [`InstrumentTests.kt`](https://github.com/ayfri/kore/blob/master/kore/src/commonTest/kotlin/io/github/ayfri/kore/features/InstrumentTests.kt)

For the procedural version of this pattern, use [Contributing: Creating a New Generator](https://kore.ayfri.com/docs/contributing/creating-a-new-generator.md).

### Serializer strategy

Prefer an existing serializer before creating a new one.

Frequently reused serializers, in alphabetical order:

- [`EitherInlineSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/EitherInlineSerializer.kt)
- [`IdOrTagReferenceSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/IdOrTagReferenceSerializer.kt), a reference wrapping a registry ID or `#tag` argument, written and read as a bare string
- [`InlineAutoSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/InlineAutoSerializer.kt)
- [`InlinableListSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/InlinableListSerializer.kt)
- [`LowercaseSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/LowercaseSerializer.kt)
- [`NamespacedPolymorphicSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/NamespacedPolymorphicSerializer.kt)
- [`NbtAsJsonSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/NbtAsJsonSerializer.kt)
- [`SinglePropertySimplifierSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/SinglePropertySimplifierSerializer.kt)
- [`ToStringSerializer`](https://github.com/ayfri/kore/blob/master/kore/src/commonMain/kotlin/io/github/ayfri/kore/serializers/ToStringSerializer.kt)

Decision rule:

1. Check whether an existing serializer already matches the target JSON shape.
2. If not, see whether the model can be expressed with an existing pattern.
3. Only then add a new serializer, with focused tests.

New sealed hierarchies serialized with `NamespacedPolymorphicSerializer` (a `type`-discriminated Minecraft polymorphic
shape) must not enumerate subtypes by hand or via reflection.
Annotate the sealed base with `@GeneratedSealedSerializer`; the `kore-ksp` module's `SealedSerializerProcessor`
generates a reflection-free `fooSealedSerializer()` factory at compile time, which the family's serializer object passes
to `NamespacedPolymorphicSerializer`'s constructor. The factory returns a `SealedDispatcher` that switches on the
subtype's simple name, so serializing one subtype only loads that subtype, not the whole family.

### Subtype names

A subtype is written under the snake_case of its class name: `LinearPos` becomes `linear_pos`. When the subtypes of a
family end with a word the game name leaves out, the family's serializer sets it once as `suffix`, and the name drops it
when present:

```kotlin
data object IntProviderSerializer : NamespacedPolymorphicSerializer<IntProvider>(
	intProviderSealedSerializer(),
	suffix = "IntProvider",
)
```

`UniformIntProvider` is then written as `uniform`. `@SerialName` stays only on a subtype whose game name isn't that name,
such as `@SerialName("type_specific/sheep") class SheepSubPredicate`, written as its exact value without the namespace.
`namespace` is the prefix of the written names, `minecraft` by default, `null` for names written bare and `fabric` for the
Fabric resource conditions. Two subtypes of a family written under the same name make its serializer fail.

### Property names

Properties are written in snake_case in both JSON and SNBT, so `maxDamage` becomes `max_damage` with no annotation.
`@JsonSerialName("offsetX")` overrides the key in both formats, as `LocationCheck` does for its camel-case offsets.
A `@SerialName` is only needed on a sealed subtype whose name isn't its class name in snake_case (see
[Subtype names](#subtype-names)) or when the key isn't the snake_case of the property, and in SNBT it is snake_cased
as well, so a key such as `offsetX` needs `@JsonSerialName`.
Map keys and raw `NbtCompound` content are never renamed.

SNBT is read the same way: `decodeFromSnbt` and `decodeFromNbtTag` look the keys of a class up under their snake_case
names. A serializer that reads NBT by hand goes through `Decoder.nbtDecoder` instead of an `is NbtDecoder` check, and
decodes nested values with `decodeFromNbtTag`.

### Number text

A finite `Float` or `Double` is written with Java's `toString()` layout on the JVM and on JS, in JSON and in SNBT: plain with
at least one decimal from `0.001` to below `10000000` (`1f` is `1.0`, `0.1` is `0.1`), else a mantissa with at least one
decimal and an exponent (`1e-5` is `1.0E-5`, `3e7` is `3.0E7`, `16777216f` is `1.6777216E7`). Kotlin/JS prints `1f` as `1`
and `1e-5` as `0.00001`, so it needs the rewrite, and the JVM does nothing: `toCanonicalString()` is `toString()` there.

`DataPack.jsonEncoder` applies it to every data pack file. On the JVM it encodes like a plain `Json`, on JS it wraps the
encoder to write `toCanonicalString()` for each float and double. A serializer that builds a nested JSON tree from
`encoder.json` calls `encodeToCanonicalJsonElement` instead of `encodeToJsonElement`, and SNBT goes through `toSnbt()`.
A plain `Json` instance prints the platform text. When the shortest digits are a single one, Java prints the closest
two-digit form, and so does `toCanonicalString()` on JS (`Double.MIN_VALUE` is `4.9E-324`, `Float.MIN_VALUE` is `1.4E-45`).

### Per-pack state

Helpers that must set something up once per datapack (a `load` function, a shared objective, a runtime object) keep
that bookkeeping on the pack with `DataPack.state`, never in a top-level map or set. The state is released with the pack,
and two packs sharing a name never see each other's state.

```kotlin
private val initializedTimers = DataPackStateKey<MutableSet<String>>("oop.timers")

fun DataPack.registerTimer(timer: Timer) {
	if (state(initializedTimers) { mutableSetOf() }.add(timer.name)) load("timer_${timer.name}_init") { /* ... */ }
}
```

`stateOrNull(key)` reads the state without creating it.

## Fast heuristics when you are unsure where a change belongs

- **Build, publishing, or versioning behavior** -> `build-logic/` and root Gradle metadata.
- **Contributor or user-facing explanation** -> `website/` in the same PR.
- **Datapack import output shape** -> `bindings/` pipeline and tests.
- **Generated enum or registry wrapper shape** -> `generation/`, then regenerate and update `kore/` consumers.
- **New registry-backed DSL resource** -> usually `kore/`, plus generation if a new argument wrapper is required.

## Testing patterns that speed up contribution

- Feature tests usually assert emitted JSON payloads and generated command/resource lines.
- Most specs build a `dataPack { }` in `commonTest` and never call `generate()`, so they run on the JVM, Node.js, and
  a headless browser alike (see [Multiplatform Support](https://kore.ayfri.com/docs/advanced/multiplatform.md)). The `testDataPack(...)` helper wraps real
  file/ZIP/JAR generation and is JVM/Node-only - keep specs that call it in `jvmTest`.
- Serializer tests should focus on roundtrip behavior and JSON or SNBT shape checks.

When you touch generators, the usual path is: model -> `DataPack` registration -> builder entry point -> tests -> docs.

## Documentation pages

Contributor pages are easier to maintain when they link to a single source of truth instead of restating the same rules.

Required frontmatter keys, in alphabetical order:

- `date-created`
- `date-modified`
- `description`
- `keywords`
- `nav-title`
- `root`
- `routeOverride`
- `title`

Versions that change with each release are placeholders, a key between double curly braces, expanded at build time
in prose, links and code blocks, the `.md` copies and `llms-full.txt` included:

| Key                  | Value                                                                                |
|----------------------|--------------------------------------------------------------------------------------|
| `java.version`       | Java version required by `mc.version`                                                |
| `kore.mavenSnapshot` | Maven snapshot coordinate of `master` (`<Kore version>-<master Minecraft>-SNAPSHOT`) |
| `kore.release`       | Latest stable release, e.g. `2.16.0-26.2`                                            |
| `kore.snapshot`      | Latest release for a Minecraft snapshot, or `kore.release` when there is none newer  |
| `kore.version`       | Kore part of `kore.release`                                                          |
| `mc.packFormatMajor` | Data pack format of `mc.version`, major part                                         |
| `mc.packFormatMinor` | Data pack format of `mc.version`, minor part                                         |
| `mc.version`         | Minecraft version of `kore.release`                                                  |

The values come from the GitHub releases and PixiGeko's `versions_data.json`, fetched once per build directory:
rerun `fetchGitHubReleases` and `fetchMinecraftVersions` with `--rerun` to refresh a local copy. An unknown key, a
placeholder in the frontmatter or a heading, or an export without any stable release fails the build.

An image whose destination starts with `mc:` renders a vanilla texture inline, the path being relative to `textures/`
without extension, e.g. `![Furnace](mc:block/furnace_front)`. Use it only where seeing the in-game block, item or GUI
sprite tells the reader something (a workstation column, frame styles), never as decoration, and check the path
exists in [misode/mcmeta](https://github.com/misode/mcmeta/tree/assets/assets/minecraft/textures) first. Animated
textures show their first frame.

Bigger in-game renders (dialogs, containers, HUD elements, toasts, trades) live in `components/mc/DocMockups.kt`, built
from the vanilla GUI textures in `components/mc/McUi.kt`. Embed one right after the example it renders with
`{{{ .components.mc.KitsMenuMockup }}}`, and update it whenever that example changes. Mockups use vanilla GUI
coordinates through `gui(pixels)` (GUI scale 2) and `McAt`, so offsets copied from the game's screen classes land on
the same pixels. Text is drawn as SVG pixel squares by `McFont.kt`, from glyph bitmaps of the OFL
[Minecraft font](https://github.com/IdreesInc/Minecraft-Font), so it stays sharp at any zoom where browser font
rendering blurs pixel fonts. `McText` takes a list of `McSpan` runs (color, bold, italic, underlined, strikethrough)
for lines mixing styles, like chat messages built from several text components.

Interactive explainers (sliders that redraw a distribution, for example) live in `components/doc/`, like `ProviderExplorer`.
Embed one the same way, right after the table it illustrates, and write the sentence introducing it so the page reads
fine without it: the `.md` copies and `llms.txt` drop the widget line. The pills, toggle groups, slider and text fields these
widgets share live in `components/doc/DocControls.kt`: reuse them instead of styling new controls.

Keep routes stable, keep navigation intentional, and update entry pages when a new doc should become discoverable.

### How to write a documentation page

Content rules that keep the docs consistent and trustworthy:

- **Example-first.** Every public feature needs at least one copy-pastable Kotlin snippet. Verify each snippet against
  the real API (the source or a test) before committing - never invent method names or argument shapes. Use tabs for
  indentation, matching the Kotlin code style.
- **State capabilities, not changes.** Write what the API *does*, not what changed between versions. Avoid changelog
  phrasing like "now possible" or "new in this release" in body copy; that information belongs in commit messages and
  release notes, not in a reference page that must read correctly a year later.
- **Cross-link inline, not in a pile.** Link related pages from the sentence that mentions the concept (for example,
  link [Scoreboards](https://kore.ayfri.com/docs/concepts/scoreboards.md) the first time you mention scoreboards). Prefer one good inline link over a long
  trailing list. Keep any `See also` section short - three or four of the most relevant links, not a dump of everything
  related.
- **Link to the Minecraft Wiki for vanilla concepts.** When a page touches a vanilla system (a command, a registry, an
  NBT structure, a pack format), link the relevant [Minecraft Wiki](https://minecraft.wiki) page. It is high quality and
  saves Kore from restating vanilla behavior - Kore docs should explain the *Kore* DSL and defer vanilla semantics to
  the
  wiki.
- **Do not duplicate.** If a concept already has a home page, link it instead of re-explaining it. One source of truth
  per topic; everything else points at it.

## Why Kore uses these technical choices

- **Centralized serializers** keep JSON and NBT output stable across modules and make diffs easier to review.
- **Generator-first data-driven features** keep feature additions mechanical instead of bespoke.
- **Tests close to feature families** help catch schema drift when Minecraft updates resource definitions.
- **Thin command wrappers** preserve command predictability while still covering vanilla syntax broadly.
- **Type-safe arguments over raw strings** reduce command regressions and improve IDE discoverability.

## See also

- [Contributing: Contributing](https://kore.ayfri.com/docs/contributing/contributing.md)
- [Contributing: Creating a New Generator](https://kore.ayfri.com/docs/contributing/creating-a-new-generator.md)
- [Contributing: The Generation Pipeline](https://kore.ayfri.com/docs/contributing/generation-pipeline.md)
- [Contributing: Workflow](https://kore.ayfri.com/docs/contributing/contributing-workflow.md)
- [Multiplatform Support](https://kore.ayfri.com/docs/advanced/multiplatform.md)
