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:
- Which module should I edit?
- Which project pattern should I follow instead of inventing a new one?
Project tree at a glance
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 bykore/.helpers/andoop/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 underbindings/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/commonMain/kotlin/io/github/ayfri/kore/generatedorbuild/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,
DataPackregistration,Functionextensions, serializers, and tests underkore/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/
- Purpose: documentation markdown, docs navigation, and frontend rendering.
- Docs live in
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.
Argumentis the base abstraction for command-safe value types.- Generated argument wrappers are the preferred representation for registry references.
- Tag variants encode
#namespace:nameconventions. - Literal helpers such as
literal(),int(), andfloat()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:
- A serializable feature class extends
Generator. - The class defines its
resourceFolder, transientfileName, andgenerateJson(dataPack)implementation. DataPackregisters a typed generator list throughregisterGenerator<T>().- A
DataPackextension function instantiates and registers the feature. - The extension returns a typed
*Argumentfor 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:
EitherInlineSerializerIdOrTagReferenceSerializer, a reference wrapping a registry ID or#tagargument, written and read as a bare stringInlineAutoSerializerInlinableListSerializerLowercaseSerializerNamespacedPolymorphicSerializerNbtAsJsonSerializerSinglePropertySimplifierSerializerToStringSerializer
Decision rule:
- Check whether an existing serializer already matches the target JSON shape.
- If not, see whether the model can be expressed with an existing pattern.
- 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:
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) 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.
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 updatekore/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 { }incommonTestand never callgenerate(), so they run on the JVM, Node.js, and a headless browser alike (see Multiplatform Support). ThetestDataPack(...)helper wraps real file/ZIP/JAR generation and is JVM/Node-only - keep specs that call it injvmTest. - 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-createddate-modifieddescriptionkeywordsnav-titlerootrouteOverridetitle
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. . 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 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, 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 the first time you mention scoreboards). Prefer one good inline link over a long trailing list. Keep any
See alsosection 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.
