Skip to content

§3. Chameleon

Arno Dölinger edited this page Jun 30, 2026 · 1 revision

Stripping is great when a declaration belongs to one platform. But a lot of shared code is the same logic written twice only because the platforms spell a type or an accessor differently:

// The duplication @Chameleon removes — identical intent, two platform dialects:
@PaperOnly fun resetSelection(player: Player) = resetSelection(player.uniqueId)
@FabricOnly fun resetSelection(player: ServerPlayer) = resetSelection(player.uuid)

A @Chameleon carrier is a platform-annotated declaration that codegen resolves to a single real declaration for the active target. Put the carriers in src/main/chameleons (they are read by codegen, never compiled directly):

// src/main/chameleons/Platform.kt
import io.github.arnodoelinger.platformweaver.*

// Type alias — erases type-name divergence:
@PaperOnly @Chameleon val PlatPlayer: org.bukkit.entity.Player
@FabricOnly @Chameleon val PlatPlayer: net.minecraft.server.level.ServerPlayer

// Extension property — erases accessor divergence (a type alias can't do this):
@PaperOnly @Chameleon val PlatPlayer.platUuid: java.util.UUID get() = uniqueId
@FabricOnly @Chameleon val PlatPlayer.platUuid: java.util.UUID get() = uuid

Now the two functions above collapse into one, written once for every platform:

fun resetSelection(player: PlatPlayer) = resetSelection(player.platUuid)

Carrier shapes

A carrier is a single, expression-bodied line. Three shapes are supported:

Carrier Generated for the target
val Name: Type (bare type) typealias Name = Type
val [Recv.]name: T get() = e (has a body) the property, verbatim
fun [Recv.]name(p): T = e the function, verbatim

A bare val Name: Type becomes a typealias; anything with a receiver or a body is emitted verbatim for the active platform.

Imports in carrier bodies

Member bodies can use anything imported in the carrier file. Codegen carries an import into the generated file only when the emitted declaration references it, so:

  • The type-alias form keeps its guarantee that a non-target platform's API need not be on the classpath, and
  • A member body can naturally reference helpers (e.g. Main.config) without fully-qualifying them.

Configuration

The carrier directory defaults to src/main/chameleons. Change or disable it via the Gradle DSL:

platformweaver {
    target = "paper"
    chameleonsDir = "src/main/chameleons"    // Set to null to disable chameleon codegen
}

When not to reach for @Chameleon

@Chameleon handles type and accessor divergence. When two platforms hand you structurally different data — Paper's Location bundles the world, while Fabric splits it into BlockPos + a world key — that's a real API difference, not a naming one. Normalize it once behind a small adapter type (see Same-name declarations), then chameleon that.

Clone this wiki locally