-
Notifications
You must be signed in to change notification settings - Fork 0
Java API
A resource pack entry matches on the block and its blockstate, so a colour held anywhere else cannot be expressed: a lamp holding a dyed bulb, a colour stored in a block entity, a colour read from an attached machine. A mod supplies those from Java.
The API is the package xyz.atmerek.contraptionlights.api in its entirety. Nothing else in the mod is API, and everything else may move without notice.
It is client side in full. Nothing in the package may be reached from a dedicated server, where the classes it leads to do not exist.
Coloured light is also off by default, and a provider is consulted only while it is on. Testing one starts with enableColoredLight, in config/contraptionlights-client.toml or in Sodium's Video Settings.
The jar comes from the Modrinth maven, and is required at compile time only.
repositories {
exclusiveContent {
forRepository {
maven {
name = "Modrinth"
url = "https://api.modrinth.com/maven"
}
}
filter {
includeGroup("maven.modrinth")
}
}
}
dependencies {
compileOnly "maven.modrinth:contraption-lights:1.5.0"
}The coordinate's version is the version number as it appears on the versions page.
The dependency is declared optional, so the dependent mod still loads without it:
[[dependencies.yourmod]]
modId = "contraptionlights"
type = "optional"
ordering = "NONE"
side = "CLIENT"Every reference to the API must be confined to a single class in the dependent mod, reached only after a presence check. Loading a class links the types it names, so a check written inside the class that uses the API executes too late and raises NoClassDefFoundError first.
The examples on this page use two classes belonging to the dependent mod, neither of them supplied by Contraption Lights. MyLightColors is the isolated class, the only one that names an API type. ModCompat holds the presence check, and must not mention the API at all, since it is read from places that run whether Contraption Lights is installed or not.
public final class ModCompat {
public static final boolean CONTRAPTION_LIGHTS = ModList.get().isLoaded("contraptionlights");
}if (ModCompat.CONTRAPTION_LIGHTS) {
MyLightColors.register();
}public final class MyLightColors {
public static void register() {
ContraptionLightsApi.registerLightColor(MY_LAMP.get(), (level, pos, state) -> {
if (level.getBlockEntity(pos) instanceof MyLampBlockEntity lamp) {
return lamp.getGlowColor();
}
return LightColorProvider.PASS;
});
}
public static void colorChanged(Level level, BlockPos pos) {
ContraptionLightsApi.lightColorChanged(level, pos);
}
}That is the whole isolated class: one method called once at startup, and one called whenever a colour changes. register() belongs in FMLClientSetupEvent, by which point the block registry is populated, and it is the guarded call shown above that goes there rather than the class itself.
| Call | Effect |
|---|---|
registerLightColor(Block, LightColorProvider) |
Colours one block. |
registerLightColor(LightColorProvider) |
Consulted for every light-emitting block, after any provider registered for that block in particular. |
lightColorChanged(Level, BlockPos) |
Reports that the colour at a position has changed. Required whenever a colour can change without the block changing; see colour changes. |
int lightColor(BlockGetter level, BlockPos pos, BlockState state);
default int lightColor(BlockState state, @Nullable CompoundTag nbt) {
return PASS;
}The return is 0xRRGGBB, or PASS to decline and defer to the ordinary lookup. PASS is -1, a value no colour computed from block data can land on, so every value in the 0xRRGGBB range stays available. PASS was 0 in the 1.5.0 betas, where it collided with pure black; a mod compiled against one of those must be rebuilt, since the constant is inlined at compile time.
Only the hue of the return is used. It is normalised so its brightest channel is full, and the block's light level determines range. The value does not pass through the saturation boost applied during texture derivation, a returned colour being authoritative.
The second method is consulted in place of the first when the block is riding a Create contraption, where no live block entity exists and only saved data is available. It defaults to PASS, so a block on a moving contraption falls back to its palette colour unless the method is implemented.
Providers are consulted in reverse registration order, most recent first, with per-block providers before catch-all ones. The first to return anything other than PASS wins.
A cycling entry for the same block outranks a provider, the cycle being resolved before any provider is consulted. Every other palette entry ranks below. See resolution order.
Called from the chunk building threads. A provider must be thread safe, must not modify the world, and must return quickly. It is invoked once per light-emitting block per section rebuild, which makes it a hot path: a map lookup and a field read is the appropriate shape, traversing a structure is not.
The position may not be loaded. Reading a block entity being replaced on another thread can throw. A provider that throws is logged once and then treated as PASS for that block, so a fault degrades to a wrong colour rather than a crash. That is a backstop, not a substitute for handling the case.
The colour must be stable. A colour that moves of its own accord has to be reported at every step, and each report rebuilds the surrounding sections. Moving colours belong in a JSON cycle, which changes colour with no rebuild at all.
Colour only. A provider determines the colour of a light, never its brightness or its existence. A light level held in block entity data is a separate mechanism, served by the auxiliary light manager rather than by this API.
A change to block entity data is not a block change. Nothing marks the surrounding sections for a redraw, so a light whose colour lives in block data keeps its previous colour until something unrelated happens to rebuild the section, which on a settled base can be a very long time.
Reporting the change is the responsibility of the mod that owns the block. Nothing is polled, and a provider that returns a new colour without saying so is simply not consulted again.
lightColorChanged(level, pos) marks the surrounding sections for a redraw. The reach comes from the block's own light level, and a block riding a Sable sub-level is covered as well. Repeat calls where the colour turns out to be unchanged return without doing anything, so the call belongs at every site that might have altered the colour rather than only at the one that certainly did. onDataPacket is the usual site, a colour arriving from the server being the usual case.
@Override
public void onDataPacket(Connection connection, ClientboundBlockEntityDataPacket packet, HolderLookup.Provider registries) {
super.onDataPacket(connection, packet, registries);
if (ModCompat.CONTRAPTION_LIGHTS) {
MyLightColors.colorChanged(level, worldPosition);
}
}The block entity is a class of the dependent mod that names no API type, so it calls MyLightColors rather than the API itself, behind the same cached presence check. Both are that mod's own classes, defined under depending on the API.
A colour that depends only on the blockstate needs none of this. A blockstate change is a block change and redraws on its own, which covers a light level driven by Properties.lightLevel and any colour written as a blockstate property.
A provider is consulted for a placed block, and for a block riding a Create contraption through the second method. Light originating from an item never reaches a provider: an item held in hand, in an item frame, or lying on the ground resolves from the block form of that item, with no position and no block entity to read. Such a light takes its colour from the palette, so a block whose colour lives in block data glows its palette colour while carried and its provider colour once placed. An item entry is what colours the carried form.
There is no means of unregistering. A provider registered during client setup persists for the session, which is the only lifetime intended for it.