COI Client’s visual effect system is designed to be extensible — you can add new screen-space effects by implementing theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/ikeepcalm/coi-client/llms.txt
Use this file to discover all available pages before exploring further.
VisualEffect interface and registering the effect in EffectManager.initialize(). Once registered, effects are triggerable from the server via the standard coi-client:effect payload using your effect’s ID, and can be stopped individually or cleared alongside all other active effects.
The VisualEffect Interface
All visual effects implementdev.ua.ikeepcalm.coi.client.effects.VisualEffect. The full interface is:
| Method | Purpose |
|---|---|
getId() | Returns the unique effect identifier that matches the effectId field in the coi-client:effect payload |
getDisplayName() | Human-readable label shown in the developer debug screen (F8 in dev builds) |
getDefaultParams() | Pre-filled param string in the debug screen input field |
start(String params) | Entry point for the effect; parse comma-separated key=value pairs here and record startTime |
render(ctx, w, h, tickDelta) | Called every frame while the effect is in the active list; draw overlays, fills, and bands via GuiGraphicsExtractor |
isFinished() | EffectManager polls this every frame and removes the effect when it returns true |
stop() | Called when params = "stop" is received or EffectManager.stopAll() is invoked; force isFinished() to return true on the next frame |
render() receives a GuiGraphicsExtractor — a Minecraft/Fabric API class from net.minecraft.client.gui that extends GuiGraphics — along with pre-computed screenWidth and screenHeight values. Use ctx.fill(), ctx.fillGradient(), and similar helpers to draw screen-space geometry.Step-by-Step Implementation
Create your effect class
Create Declare a
src/client/java/dev/ua/ikeepcalm/coi/client/effects/impl/MyEffect.java and implement VisualEffect:public static final String ID constant — this is what both the registry and the server use to reference your effect.Parse params in start()
Split the incoming
params string on commas, then split each token on = to extract key-value pairs. Always default to safe values in case a key is missing or the string is blank:Implement render()
Draw your overlay using
GuiGraphicsExtractor. The helper methods mirror GuiGraphics — fill, fillGradient, etc. Screen dimensions are passed in so you never need to query the window yourself:Implement isFinished() and stop()
EffectManager removes the effect as soon as isFinished() returns true. Make stop() force that condition immediately:duration of -1 (the conventional default) means the effect persists until the server explicitly stops it.Register in EffectManager.initialize()
Open
EffectManager.java and add one line to initialize(), alongside the existing registrations:Boilerplate Example
The following is a complete, minimal implementation following the same patterns used byVignetteEffect and FlashEffect:
Registering the Effect
The single line to add insideEffectManager.initialize():
register() stores a Supplier<VisualEffect> factory — a new instance is created each time the effect is triggered, so all state fields (intensity, startTime, etc.) are cleanly reset on every trigger.
Param Parsing Tips
- Use
System.currentTimeMillis()for timing, not game ticks. Effects run on the render thread and are driven by wall-clock time, matching how existing effects likeVignetteEffectandFlashEffecthandleduration. - Parse defensively — always check that
kv.length == 2before accessingkv[1], and wrap numeric parses in try-catch or validate input. Clients receive server-provided strings and bad data should never crash the render loop. stop()must makeisFinished()returntrueimmediately — setduration = 0or a past timestamp so the effect is purged from the active list on the very next render tick.- Keep
render()cheap — avoid allocatingArrayList,Random, or other objects inside the hot path. Seed any random data atstart()time and store results in instance fields (seeCracksEffectfor an example of deferred one-time geometry generation). - Triggering an already-active effect replaces it —
EffectManager.trigger()removes any existing effect with the same ID before callingstart(), so your effect is always re-initialized from scratch on re-trigger. - Photosensitive effects — if your effect involves rapid flashing or strobing, add its
IDto thePHOTOSENSITIVE_EFFECTSset literal inEffectManager.javaso it is skipped when the player has enabledepilepsyModeinHudConfig. The set is declaredprivate static finalwithSet.of(...), so you must edit the source literal directly rather than calling anadd()method at runtime.