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

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

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/

  • Purpose: datapack importer and Kotlin binding generator.
  • Typical flow: explorer -> normalized entities -> writer output.
  • Common edit surface: explorer.kt, entities.kt, writer.kt, then tests under bindings/src/commonTest.
  • Pattern to preserve: single-namespace packs stay compact, multi-namespace packs become namespace-nested objects, and worldgen content is grouped under Worldgen.

build-logic/

  • Purpose: shared Gradle conventions, publishing logic, and project metadata.
  • Common edit surface: convention plugins and Project.kt.
  • Edit here when changing build behavior, publication rules, or project versioning.

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/main/generated or build/generated/... directly.
  • Full walkthrough: Contributing: The Generation Pipeline.

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/

  • 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.
  • A typical change in this module touches one feature family end to end: model, registration, builder entry point, tests, and docs.

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/

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.
  2. The class defines its resourceFolder, transient fileName, and generateJson(dataPack) implementation.
  3. DataPack 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:

For the procedural version of this pattern, use Contributing: Creating a New Generator.

Serializer strategy

Prefer an existing serializer before creating a new one.

Frequently reused serializers, in alphabetical order:

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. See SlotSource.kt for the exact shape, and CLAUDE.md / multiplatform.md for the collision pitfalls (distinct serialNames required when subtypes share a descriptor, e.g. two InlineAutoSerializer<_, List<X>> wrappers).

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). 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

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 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 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