minor refector and edits

This commit is contained in:
Plastered_Crab
2026-06-20 09:28:40 -07:00
parent 0a7deedeea
commit 024d9ededf
17 changed files with 122 additions and 152 deletions
@@ -36,10 +36,9 @@ public class LambDynamicLightsInitializer implements DynamicLightsInitializer {
ctx.register(EntityType.PLAYER, PlayerLuminance.INSTANCE);
});
// Attach a real monochrome dynamic light to INDIVIDUAL glowing particles. Each light follows
// its particle and reports isRemoved() when the particle dies, so the manager cleans it up. The
// per-emitter cap on how many particles may be lit at once is enforced at particle spawn (see
// BedrockParticle.maxLitParticles / SnowstormParticle), not here.
// Attach a monochrome dynamic light to each glowing particle. The light follows its particle and
// reports isRemoved() when the particle dies, so the manager cleans it up. The cap on how many
// particles per emitter may be lit at once is enforced at particle spawn, not here.
final DynamicLightBehaviorManager behaviorManager = context.dynamicLightBehaviorManager();
MoveDynamicLights.setSink(particle -> behaviorManager.add(new SnowstormParticleLight(particle)));
}
@@ -37,8 +37,6 @@ public final class PokemonLuminance extends CustomLuminance implements EntityLum
ItemStack item = pokemon.getShownItem();
int itemLightLevel = item.isEmpty() ? 0 : itemLightSourceManager.getLuminance(item, underwater);
int formLightLevel = extractFormLightLevel(pokemon.getForm(), underwater).orElse(0);
// Animation based emissives
int animLightLevel = PokemonAnimationLight.luminanceFor(pokemon);
return Math.max(Math.max(itemLightLevel, formLightLevel), animLightLevel);
@@ -14,13 +14,11 @@ import net.minecraft.core.BlockPos;
/**
* A real, monochrome dynamic light attached to a single Cobblemon particle, so each glowing particle
* lights the world around it like its own light source (maximum fidelity). The light level is whatever
* the particle currently emits ({@link SnowstormParticle#currentLightLevel()} — a flat author-set level
* or a per-sprite-frame value), so a particle that emits 0 contributes nothing even though it exists.
* lights the world around it like its own light source. The level is whatever the particle currently
* emits ({@link SnowstormParticle#currentLightLevel()}); a particle emitting 0 contributes nothing.
*
* <p>The number of these per emitter is capped (see {@code BedrockParticle.maxLitParticles}); the cap
* is enforced at particle spawn, not here. This follows the particle as it moves and removes itself
* when the particle dies.</p>
* <p>How many of these exist per emitter is capped (by the maxLitParticlesPerEmitter config, enforced
* at particle spawn, not here). This follows the particle as it moves and removes itself when it dies.</p>
*
* <p>Only instantiated by {@link LambDynamicLightsInitializer} (the LambDynamicLights entrypoint), so
* it is never loaded unless LambDynamicLights is installed.</p>
@@ -33,7 +31,7 @@ public final class SnowstormParticleLight implements DynamicLightBehavior {
private int prevY = Integer.MIN_VALUE;
private int prevZ = Integer.MIN_VALUE;
private int prevLuminance = -1;
/** Level cached each tick in hasChanged() so the per-block lightAtPos stays cheap and consistent. */
/** Level cached each tick in hasChanged() so the per block lightAtPos call stays cheap and stable. */
private double cachedLuminance;
public SnowstormParticleLight(SnowstormParticle particle) {
@@ -49,18 +47,17 @@ public final class SnowstormParticleLight implements DynamicLightBehavior {
double dy = pos.getY() + 0.5 - particle.getY();
double dz = pos.getZ() + 0.5 - particle.getZ();
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// Vanilla-style falloff: exactly 1 light level lost per block, so an authored level of N lights
// a ~N-block radius just like a Minecraft block-light source. We deliberately IGNORE the
// falloffRatio LambDynamicLights passes (15 / MAX_RADIUS ~= 1.94), which would otherwise
// compress the falloff into the mod's 7.75-block cap and make every light reach only ~half its
// level in blocks (i.e. look dimmer than the number implies). Note: the mod still hard-caps
// reach at MAX_RADIUS (~7.75 blocks), so authored levels above ~8 are clipped at that radius.
// Lose exactly 1 level per block so an authored level of N lights about N blocks, like vanilla
// block light. We intentionally ignore the falloffRatio LambDynamicLights passes (15 / MAX_RADIUS,
// about 1.94): applying it would compress the falloff into the mod's 7.75 block cap and make every
// light reach only about half its level in blocks, looking dimmer than the number implies. The mod
// still limits reach to MAX_RADIUS (about 7.75 blocks), so levels above 8 are clipped there.
return Math.max(0.0, lum - dist);
}
@Override
public DynamicLightBehavior.BoundingBox getBoundingBox() {
// The actively-emitting volume is just the particle's block (the API expands by falloff itself).
// Report only the emitting volume (the particle's own block); the API expands it by falloff.
int x = (int) Math.floor(particle.getX());
int y = (int) Math.floor(particle.getY());
int z = (int) Math.floor(particle.getZ());
@@ -44,22 +44,22 @@ class BedrockParticle(
var collision: ParticleCollision = ParticleCollision(),
var environmentLighting: Boolean = false,
/**
* Block-light level (0..15) each particle of this type emits into the world via LambDynamicLights.
* A particle existing does NOT imply light: only particles whose resolved level is > 0 emit. This
* scalar is the flat "hard" level used when [lightEmissionFrames] is empty.
* Block light level (0..15) each particle of this type emits into the world via LambDynamicLights.
* A particle existing does not imply light: only particles whose resolved level is above 0 emit.
* This is the flat level, used when [lightEmissionFrames] is empty.
*/
var lightEmission: Int = 0,
/**
* Optional per-sprite-frame light levels (0..15), indexed by the particle's current flipbook frame.
* When non-empty it overrides [lightEmission], so a sprite-sheet particle can light differently per
* frame (out-of-range frames clamp to the ends). Empty = use the flat [lightEmission].
* Optional light levels (0..15) indexed by the particle's current flipbook frame. When non empty it
* overrides [lightEmission] so a particle can light differently per frame; frames beyond the array
* clamp to its ends. Empty means use the flat [lightEmission].
*/
var lightEmissionFrames: List<Int> = emptyList(),
var creationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
var expirationEvents: MutableList<SimpleEventTrigger> = mutableListOf(),
var timeline: EventTriggerTimeline = EventTriggerTimeline(mutableMapOf())
) {
/** True when this particle type emits block light at all (flat level or any per-frame level > 0). */
/** True when this particle type emits block light at all, via the flat level or any per frame level. */
val emitsLight: Boolean
get() = lightEmission > 0 || lightEmissionFrames.any { it > 0 }
@@ -45,8 +45,8 @@ abstract class ParticleUVMode : CodecMapped {
abstract fun get(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double, uvDetails: UVDetails): UVDetails
/** The current sprite-sheet frame index for a particle at this age (0 for non-animated modes).
* Used to look up per-frame light levels (see [BedrockParticle.lightEmissionFrames]). */
/** The current flipbook frame index for a particle at this age (0 for static modes). Used to look
* up per frame light levels (see [BedrockParticle.lightEmissionFrames]). */
open fun frameAt(moLangRuntime: MoLangRuntime, age: Double, maxAge: Double): Int = 0
}
@@ -33,6 +33,7 @@ import com.cobblemon.mod.common.client.gui.tmmachine.TMMachineScreen
import com.cobblemon.mod.common.client.particle.BedrockParticleOptionsRepository
import com.cobblemon.mod.common.client.render.ClientPlayerIcon
import com.cobblemon.mod.common.client.render.DeferredRenderer
import com.cobblemon.mod.common.client.render.PokemonAnimationLight
import com.cobblemon.mod.common.client.render.SnowstormShaderBypassRenderer
import com.cobblemon.mod.common.client.render.block.*
import com.cobblemon.mod.common.client.render.boat.CobblemonBoatRenderer
@@ -141,7 +142,7 @@ object CobblemonClient {
PlatformEvents.CLIENT_PLAYER_LOGIN.subscribe { onLogin() }
PlatformEvents.CLIENT_PLAYER_LOGOUT.subscribe { onLogout() }
// Renders snowstorm particles with a vanilla-identical look when a shaderpack is active.
// Redraws snowstorm particles so they match vanilla when a shaderpack is active.
SnowstormShaderBypassRenderer.register()
this.registerBlockEntityRenderers()
@@ -474,8 +475,8 @@ object CobblemonClient {
LOGGER.info("Loading assets...")
// Particles come first because animations need them.
BedrockParticleOptionsRepository.loadEffects(resourceManager)
// Per-animation emissive light windows (LambDynamicLights, no-op without it).
com.cobblemon.mod.common.client.render.PokemonAnimationLight.load(resourceManager)
// Emissive light windows keyed by animation; does nothing unless LambDynamicLights is installed.
PokemonAnimationLight.load(resourceManager)
// Animations come next because models need them.
BedrockAnimationRepository.loadAnimations(
resourceManager = resourceManager,
@@ -51,27 +51,26 @@ object BedrockParticleOptionsRepository {
}
/**
* Loads per-particle dynamic-light levels from a separate Cobblemon data location and merges them
* onto the already-loaded effects. This is kept OUT of the `.particle.json` on purpose: Snowstorm
* (the Bedrock particle editor) rebuilds the components block on export and silently drops unknown
* components, so a light field inside the particle file would be lost on re-save. A sidecar file is
* never touched by Snowstorm.
* Loads particle light levels from a separate Cobblemon data location and merges them onto the
* already loaded effects. These are kept out of the `.particle.json` on purpose: Snowstorm (the
* Bedrock particle editor) rebuilds the components block on export and silently drops unknown
* components, so a light field inside the particle file would be lost the next time it is saved.
* A sidecar file is never touched by Snowstorm.
*
* Files live under `assets/<namespace>/dynamiclights/particles/` (any `.json` file), each a map
* of particle-effect id to its light config, e.g.:
* Files live under `assets/<namespace>/dynamiclights/particles/` (any `.json` file), each a map of
* effect id to its light config, e.g.:
* ```json
* {
* "cobblemon:thunderbolt_actor": { "light_level": 14 },
* "cobblemon:flamethrower_actor": { "light_levels": [0, 8, 14, 14, 8] }
* }
* ```
* `light_level` is a flat 0..15 level every particle emits; the optional `light_levels` array gives
* a level per sprite-sheet frame (overrides the flat level). Entries for unknown effect ids are
* ignored.
* `light_level` is a flat level (0..15) every particle emits; the optional `light_levels` array gives
* a level per flipbook frame and overrides the flat level. Entries for unknown effect ids are ignored.
*/
private fun loadLightConfig(resourceManager: ResourceManager) {
// LambDynamicLights is the only thing that consumes these levels. If it isn't installed, don't
// even read the sidecar files — the light config is never parsed, applied, or considered.
// even read the sidecar files; the light config is never parsed, applied, or considered.
val lambDynamicLightsInstalled = Cobblemon.implementation.isModInstalled("lambdynlights") ||
Cobblemon.implementation.isModInstalled("lambdynamiclights")
if (!lambDynamicLightsInstalled) return
@@ -11,14 +11,14 @@ package com.cobblemon.mod.common.client.particle
import com.cobblemon.mod.common.client.render.SnowstormParticle
/**
* API-free bridge that lets the optional LambDynamicLights integration attach a real dynamic light to
* individual glowing particles without the common particle code referencing the LambDynamicLights API.
* Bridge that lets the optional LambDynamicLights integration attach a dynamic light to individual
* glowing particles without the common particle code referencing the LambDynamicLights API directly.
*
* Each particle whose resolved light level is > 0 (and that wins one of its emitter's limited light
* slots) registers its own light through [sink]. When LambDynamicLights is installed, its entrypoint
* ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets [sink] to a
* handler that adds a per-particle `DynamicLightBehavior`. When it isn't installed [sink] stays null
* and the registration call is a no-op — so there is no hard dependency.
* Each particle whose resolved light level is above 0 (and that claims one of its emitter's limited
* light slots) registers its own light through [sink]. When LambDynamicLights is installed its
* entrypoint ([com.cobblemon.mod.common.compat.lambdynamiclights.LambDynamicLightsInitializer]) sets
* [sink]; when it is absent [sink] stays null and the registration call does nothing, so there is no
* hard dependency.
*/
object MoveDynamicLights {
@JvmStatic
@@ -376,16 +376,14 @@ class ParticleStorm(
particles.forEach { particle -> particle.invisible = true }
}
// --- LambDynamicLights (per-particle dynamic light) integration -----------------------------
/** How many of this emitter's live particles currently hold a real dynamic light (capped by the
* `maxLitParticlesPerEmitter` Minecraft config). A particle existing does NOT imply a light. */
/** How many of this emitter's live particles currently hold a dynamic light, capped by the
* maxLitParticlesPerEmitter config. A particle existing does not imply a light. */
var litParticleCount = 0
private set
/** Tries to claim one of this emitter's limited light slots for a particle. Returns true if a slot
* was available (and reserved); false if the emitter is already at the configured cap. The cap is
* the `maxLitParticlesPerEmitter` config value (0 disables per-particle lights entirely). */
/** Claims one of this emitter's limited light slots for a particle. Returns true if a slot was
* free (and reserved), false if the emitter is at the configured cap. A cap of 0 disables the
* particle lights entirely. */
fun reserveLightSlot(): Boolean {
if (litParticleCount >= Cobblemon.config.maxLitParticlesPerEmitter) return false
litParticleCount++
@@ -21,16 +21,16 @@ import java.nio.charset.StandardCharsets
import net.minecraft.server.packs.resources.ResourceManager
/**
* Per-animation emissive light windows for Pokemon whose emissive parts only appear or flare during
* specific animations (Pikachu cheeks, the Cyndaquil line's flames flaring in battle, Bellibolt's belly,
* etc.). A Pokemon emits the window's block-light level only while a matching animation's elapsed time
* falls inside one of the windows. Consulted by the LambDynamicLights `PokemonLuminance`.
* Light levels emitted by a Pokemon's emissive parts during specific animations, for the optional
* LambDynamicLights integration. Some Pokemon only show their emissive geometry at certain moments
* (Pikachu's cheeks, the Cyndaquil line's flames flaring in battle, Bellibolt's belly), so the level
* applies only while a matching animation's elapsed time falls inside one of its windows.
*
* Matches three kinds of running animation: the current PRIMARY animation (moves like special/physical/
* status), ACTIVE/stateful animations (cry/battle_cry), and the current POSE's idle animations
* (battle_idle/air_idle/air_fly etc., which loop continuously). Data is loaded from JSON under
* `assets/<namespace>/dynamiclights/entity/animations/`: species path -> animation short name ->
* list of `{ start, end, level }` (seconds into the animation; looping/continuous anims wrap).
* Windows are matched against the current primary animation (moves such as special, physical, status),
* active stateful animations (cry, battle_cry), and the current pose's idle animations (battle_idle,
* air_idle, air_fly) which loop continuously. Data lives in JSON under
* `assets/<namespace>/dynamiclights/entity/animations/`, mapping species to animation name to a list
* of `{ start, end, level }` in seconds.
*/
object PokemonAnimationLight {
private val GSON = GsonBuilder().create()
@@ -14,9 +14,9 @@ import java.lang.reflect.Method
* Detects whether a shaderpack is currently active, via Iris (Fabric/NeoForge) or OptiFine.
*
* Every reference here is reflective and fully guarded: Iris/OptiFine are optional and may be
* absent at runtime, so we never hard-reference their classes. If neither is present (or their
* absent at runtime, so we never reference their classes directly. If neither is present (or their
* APIs ever change shape) the lookups fail silently during class init and [shadersActive] simply
* returns `false`. This is the loader-safe contract the rest of the particle code relies on — it
* returns `false`. The rest of the particle code relies on this contract staying safe under any loader — it
* must work identically on Fabric and NeoForge and when no shader mod is installed at all.
*
* The reflective handles are resolved once; only the cheap boolean accessor is invoked per call.
@@ -57,11 +57,11 @@ class SnowstormParticle(
const val MAXIMUM_DISTANCE_CHANGE_PER_TICK_FOR_FRICTION = 0.005
/**
* Every live (non-removed) snowstorm particle, in spawn order. When a shaderpack is active we
* pull these out of the vanilla [net.minecraft.client.particle.ParticleEngine] (see
* [getRenderType] returning [NO_RENDER]) and redraw them post-composite via
* [SnowstormShaderBypassRenderer], so the pack's particle program can't alter their look.
* Insertion-ordered; particles use identity equality so a LinkedHashSet is safe.
* Every live snowstorm particle, in spawn order. When a shaderpack is active we pull these out
* of the vanilla [net.minecraft.client.particle.ParticleEngine] (see [getRenderType] returning
* [NO_RENDER]) and redraw them after composite via [SnowstormShaderBypassRenderer] so the pack's
* particle program can't alter their look. Ordered by insertion; particles use identity equality
* so a LinkedHashSet is safe.
*/
val activeParticles: MutableSet<SnowstormParticle> = LinkedHashSet()
}
@@ -143,10 +143,9 @@ class SnowstormParticle(
storm.effect.particle.creationEvents.forEach { it.trigger(storm, this) }
activeParticles.add(this)
// If LambDynamicLights is installed and this particle type emits light, give THIS particle its
// own real dynamic light — but only if the emitter still has a free light slot (capped by
// BedrockParticle.maxLitParticles). A particle existing does not imply light; the level comes
// from currentLightLevel(). No-op when the mod is absent (sink stays null).
// Give this particle its own dynamic light only when LambDynamicLights is present, the type
// emits light, and the emitter still has a free light slot. The level comes from
// currentLightLevel(); when the mod is absent the sink is null and this does nothing.
val sink = MoveDynamicLights.sink
if (sink != null && storm.effect.particle.emitsLight && storm.reserveLightSlot()) {
litSlot = true
@@ -155,9 +154,9 @@ class SnowstormParticle(
}
/**
* The block-light level (0..15) this particle emits right now. Uses the per-sprite-frame array when
* the type defines one (indexed by the current flipbook frame), else the flat level. Invisible
* (culled) particles emit nothing.
* The block light level (0..15) this particle emits right now. Uses the per frame array when the
* type defines one (indexed by the current flipbook frame), otherwise the flat level. Invisible
* particles emit nothing.
*/
fun currentLightLevel(): Int {
if (invisible) return 0
@@ -535,18 +534,18 @@ class SnowstormParticle(
runtime.environment.variable.setDirectly("particle_lifetime", DoubleValue(lifetime / 20.0))
}
// Under an active shaderpack we suppress the vanilla ParticleEngine draw (it would be routed
// through the pack's translucent particle program and mangled differently per pack) and instead
// redraw post-composite in SnowstormShaderBypassRenderer for a vanilla-identical, pack-agnostic
// look. With no shaders this is the normal material sheet, so the majority path is unchanged.
// Under an active shaderpack we suppress the vanilla ParticleEngine draw (it would be routed through
// the pack's translucent particle program and mangled differently per pack) and instead redraw after
// composite in SnowstormShaderBypassRenderer so the look matches vanilla under any pack. With no
// shaders this is the normal material sheet, so the majority path is unchanged.
override fun getRenderType() = if (Cobblemon.config.shaderParticleBypass && ShaderCompat.shadersActive()) NO_RENDER else particleTextureSheet
override fun remove() {
super.remove()
storm.particles.remove(this)
activeParticles.remove(this)
// Free our dynamic-light slot so another particle of this emitter can light up. The registered
// behavior reports isRemoved() via this particle's isAlive(), so the manager drops the light.
// Free our light slot so another particle of this emitter can light up. The registered behavior
// reports isRemoved() via this particle's isAlive(), so the manager drops the light on its own.
if (litSlot) {
storm.releaseLightSlot()
litSlot = false
@@ -18,22 +18,22 @@ import net.minecraft.client.Minecraft
import org.joml.Matrix4f
/**
* Renders Cobblemon's snowstorm particles with a vanilla-identical, pack-agnostic look while a
* Renders Cobblemon's snowstorm particles so they look like vanilla under any pack while a
* shaderpack (Iris/OptiFine) is active.
*
* ## Why this exists
* Iris/OptiFine replace the vanilla particle shader with the shaderpack's translucent-particle
* Iris/OptiFine replace the vanilla particle shader with the shaderpack's translucent particle
* program for the duration of the world pass, and every pack composites that program differently —
* which made our particles render too solid (Complementary), invisible (Photon), or still-darkening
* which made our particles render too solid (Complementary), invisible (Photon), or still darkening
* (Bliss) no matter how we tuned blend/alpha inside the vanilla [net.minecraft.client.particle.ParticleEngine].
* The pack has final authority there; we cannot win.
*
* The one place a shaderpack does NOT intercept a draw is **after** `LevelRenderer.renderLevel`
* returns: Iris only overrides shaders while `isRenderingWorld` is true, and it flips that off during
* its final composite (injected at the world-render RETURN). So we:
* its final composite (injected at the RETURN of world rendering). So we:
* 1. suppress the vanilla draw of these particles while shaders are active
* ([SnowstormParticle.getRenderType] returns NO_RENDER), and
* 2. redraw them here at [RenderEvent.Stage.POST_LEVEL] (post-composite) with the exact vanilla GL
* 2. redraw them here at [RenderEvent.Stage.POST_LEVEL] (after composite) with the exact vanilla GL
* state from [com.cobblemon.mod.common.api.snowstorm.ParticleMaterials] and the unchanged
* [SnowstormParticle.render] quad math.
*
@@ -43,16 +43,16 @@ import org.joml.Matrix4f
* pack's lighting/fog/bloom, which is exactly the intended "looks like vanilla" result.
*
* When no shaderpack is active this renderer does nothing and particles draw normally through the
* ParticleEngine, so the majority (no-shader) path is completely unchanged.
* ParticleEngine, so the majority path (no shaders) is completely unchanged.
*
* @author Cobblemon
*/
object SnowstormShaderBypassRenderer {
// The world's camera view matrix for the current frame, captured at the late in-level stage
// where Fabric/NeoForge still have the camera transform applied. POST_LEVEL fires after that
// state is unwound, but the bypass draw still needs it because SnowstormParticle.render emits
// camera-relative vertices just like vanilla particles do.
// The world's camera view matrix for the current frame, captured at the late stage within the
// level pass where Fabric/NeoForge still have the camera transform applied. POST_LEVEL fires after
// that state is unwound, but the bypass draw still needs it because SnowstormParticle.render emits
// vertices relative to the camera just like vanilla particles do.
private val modelViewMatrix = Matrix4f()
private var captured = false
@@ -87,8 +87,8 @@ object SnowstormShaderBypassRenderer {
val lightTexture = minecraft.gameRenderer.lightTexture()
val tesselator = Tesselator.getInstance()
// Re-establish the world's camera view so the camera-relative quads built by render() land
// in the right place; projection is still the world's 3D projection at this point in the frame.
// Restore the world's camera view so the quads built relative to the camera by render()
// land in the right place; projection is still the world's 3D projection at this point in the frame.
val modelViewStack = RenderSystem.getModelViewStack()
modelViewStack.pushMatrix()
modelViewStack.set(modelViewMatrix)
@@ -98,7 +98,7 @@ object SnowstormShaderBypassRenderer {
lightTexture.turnOnLightLayer()
// Group by the particle's own render type (ParticleMaterials.ALPHA/ADD/BLEND/OPAQUE). Calling
// its begin() sets the IDENTICAL GL state vanilla would use, so the look matches no-shaders.
// its begin() sets the IDENTICAL GL state vanilla would use, so the look matches the no shader path.
for ((renderType, particles) in visible.groupBy { it.particleTextureSheet }) {
val buffer = renderType.begin(tesselator, minecraft.textureManager) ?: continue
for (particle in particles) {
@@ -363,11 +363,11 @@ object SnowstormParticleReader {
ExpressionParticleTinting()
}
val environmentLighting = componentsJson.has("minecraft:particle_appearance_lighting")
// NOTE: per-particle dynamic-light levels are intentionally NOT read from the .particle.json.
// Snowstorm (the Bedrock particle editor) rebuilds the components block on export and silently
// drops any unknown component, so a custom light field here would be lost whenever a file is
// re-saved in Snowstorm. Instead light levels live in a separate Cobblemon data file and are
// merged onto the effect after load (see BedrockParticleOptionsRepository.loadLightConfig).
// Light levels are intentionally not read from the .particle.json. Snowstorm (the Bedrock
// particle editor) rebuilds the components block on export and silently drops unknown components,
// so a custom light field here would be lost the next time a file is saved. Light levels instead
// live in a separate Cobblemon data file, merged onto the effect after load (see
// BedrockParticleOptionsRepository.loadLightConfig).
val collision = collisionJson?.let {
var collides = true
val radius = it.get("collision_radius")?.asString?.asExpression() ?: run {
@@ -45,8 +45,8 @@ data class RenderEvent(
* Fired after [LevelRenderer.renderLevel] has fully finished — on a shaderpack, this is after
* Iris/OptiFine has composited the world to the main framebuffer and restored vanilla shaders.
* Draws here are NOT intercepted by the shaderpack, so they render with vanilla shaders while
* still depth-testing against the full (opaque + translucent) world depth. Used to render
* Cobblemon's snowstorm particles with a vanilla-identical look under shaders.
* still depth testing against the full (opaque + translucent) world depth. Used to render
* Cobblemon's snowstorm particles so they look like vanilla under shaders.
*/
POST_LEVEL
}
+5 -5
View File
@@ -69,11 +69,11 @@ dependencies {
localRuntime("org.antlr:antlr4-runtime:4.13.1")
// Dynamic lighting (DEV RUNTIME ONLY). Cobblemon compiles against the LambDynamicLights API
// (modCompileOnly, mojmap — see common/build.gradle.kts); this provides the running mod in dev so
// `runClient` actually exercises the move-particle dynamic light. Pinned to 4.1.6 to match that
// API AND fabric-loader 0.17.2 (4.8.9 needs loader >=0.18.4). Loom does NOT surface a mod's JIJ'd
// dependency mods to the dev run, so its four bundled deps are listed explicitly (from gegy +
// Maven Central). Bump all five together if LDL is updated.
// (modCompileOnly, mojmap; see common/build.gradle.kts); this provides the running mod in dev so
// runClient actually exercises the dynamic lights. Pinned to 4.1.6 to match that API and fabric
// loader 0.17.2 (4.8.9 needs loader 0.18.4 or newer). Loom does not surface a mod's bundled
// dependency mods to the dev run, so its four are listed explicitly (from gegy + Maven Central).
// Bump all five together if LambDynamicLights is updated.
modLocalRuntime("maven.modrinth:lambdynamiclights:4.1.6+1.21.1")
modLocalRuntime("dev.lambdaurora.lambdynamiclights:lambdynamiclights-api:4.1.6+1.21.1")
modLocalRuntime("dev.lambdaurora:spruceui:6.2.1+1.21")
@@ -47,6 +47,7 @@ import net.fabricmc.fabric.api.client.rendering.v1.ColorProviderRegistry
import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry
import net.fabricmc.fabric.api.client.rendering.v1.HudRenderCallback
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderContext
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents
import net.fabricmc.fabric.api.resource.IdentifiableResourceReloadListener
import net.fabricmc.fabric.api.resource.ResourceManagerHelper
@@ -158,51 +159,29 @@ class CobblemonFabricClient: ClientModInitializer, CobblemonClientImplementation
ClientPlayConnectionEvents.DISCONNECT.register { _, client -> client.player?.let { PlatformEvents.CLIENT_PLAYER_LOGOUT.post(ClientPlayerEvent.Logout(it)) } }
ItemTooltipCallback.EVENT.register { stack, context, type, lines -> PlatformEvents.CLIENT_ITEM_TOOLTIP.post(ItemTooltipEvent(stack, context, type, lines)) }
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> PlatformEvents.RENDER.post(
RenderEvent(
stage = RenderEvent.Stage.TRANSLUCENT,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
// Capture the final in-level camera transform before Fabric unwinds render state.
WorldRenderEvents.LAST.register { context -> PlatformEvents.RENDER.post(
RenderEvent(
stage = RenderEvent.Stage.LAST,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
// Fires after the level (and any shaderpack composite) has finished — the only point where a
// draw is NOT intercepted by Iris/OptiFine. Used by SnowstormShaderBypassRenderer.
WorldRenderEvents.END.register { context -> PlatformEvents.RENDER.post(
RenderEvent(
stage = RenderEvent.Stage.POST_LEVEL,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
WorldRenderEvents.AFTER_TRANSLUCENT.register { context -> postRenderStage(RenderEvent.Stage.TRANSLUCENT, context) }
// LAST captures the camera transform while it is still applied; POST_LEVEL fires after the level
// (and any shaderpack composite) finishes, the only point a draw is NOT intercepted by Iris/OptiFine.
WorldRenderEvents.LAST.register { context -> postRenderStage(RenderEvent.Stage.LAST, context) }
WorldRenderEvents.END.register { context -> postRenderStage(RenderEvent.Stage.POST_LEVEL, context) }
CobblemonModelPredicateRegistry.registerPredicates()
}
private fun postRenderStage(stage: RenderEvent.Stage, context: WorldRenderContext) {
PlatformEvents.RENDER.post(
RenderEvent(
stage = stage,
levelRenderer = context.worldRenderer(),
poseStack = context.matrixStack() ?: PoseStack(),
modelViewMatrix = context.positionMatrix(),
projectionMatrix = context.projectionMatrix(),
tickCounter = context.tickCounter(),
camera = context.camera()
)
)
}
override fun registerLayer(modelLayer: ModelLayerLocation, supplier: Supplier<LayerDefinition>) {
EntityModelLayerRegistry.registerModelLayer(modelLayer) { supplier.get() }
}