Description
Starky's Randomizer (Library) — API Guide

like the in-game Live Painter & Editor mod randomizers. Any mod (or tooling) can call it directly.
📊 Anonymous counter: Randomizations are counted anonymously toward the public total above (no personal data).
📈 Metrics: This mod also uses HStats to report anonymous mod-usage metrics. No personal data is collected.
NOTE: Both can be disabled.
out as a standard java.awt.image.BufferedImage. That is deliberate: the result is a normal image you can save, send, or feed into any pipeline you like.
- API class: org.starkymods.plugin.api.TextureRandomizer
1. Installing the dependency
"Dependencies": {
"StarkyMods:Starky's Randomizer": ">=1.0.0"
}
The value is a semver range (*, >=1.0.0, >=1.0.0 <2.0.0, …).
The server guarantees Starky's Randomizer is loaded before your mod; if it is missing or version-incompatible, your mod refuses to load at boot (you never get a
runtime NoClassDefFoundError).
dependencies { implementation(files("$hytaleHome/install/$patchline/package/game/latest/Server/HytaleServer.jar"))
compileOnly(files("$hytaleHome/UserData/Mods/Starky's Randomizer-1.0.0.jar"))
}
—
the classes are provided at runtime by the Randomizer plugin's own class loader.
If you bundle them with implementation into your jar,
you create duplicate classes under your own loader and break the shared-class identity
(LinkageError / ClassCastException).
With a declared required dependency you can then
import org.starkymods.plugin.api.TextureRandomizer;
import org.starkymods.plugin.api.TextureRandomizer.PixelChange;
2. The core model — how the API thinks
Three ideas drive everything:
1. In and out is a BufferedImage. You give it an image to read; it never modifies your input.
2. It returns a "change list", not a new image.
Every recolour method returns a List<PixelChange> — a record PixelChange(int x, int y, int argb) — describing exactly which pixels to repaint and to what ARGB colour.
You decide what to do with that list. This is the most important design point (check num. 4):
because the changes are data, you can apply them onto a different image than the one you analysed.
neutral-colour thresholds, and how far a recoloured pixel may drift from the original.
0 = very subtle, 50 = balanced, 100 = full replacement.
Two more rules that always hold:
3. API reference
randomizeTexture(BufferedImage image, int power, boolean allowPattern) → List<PixelChange>
allowPattern = true lets a random overlay pattern (stripes, chevrons, waves…) also be
Never returns null (returns an empty list for a null/empty image).
This is "RANDOMIZE / AUTO-PAINT" and "RANDOMIZE COLORS".
randomizeColorGroup(BufferedImage image, int power, int rank) → List<PixelChange>
Randomizes only one tone group —
rank 0 = the largest tone (by pixel count),
rank 1 = the second-largest, etc.
This is "RANDOMIZE PRIMARY / SECONDARY".
The returned list contains changes for that group's pixels only — no other pixel is ever included.
Returns null if the image has no such dominant group (e.g. asking for rank 1 on a single-tone texture).
So all the browns can be one group and all the greys can be another, regardless of lighting.
(Tunable inside the library via TONE_HUE_BUCKETS and TONE_NEUTRAL_SATURATION.)
addPattern(BufferedImage image, int power, int targetRank) → List<PixelChange>
targetRank =
-1 = the whole texture;
0 = only the primary tone group;
Returns null if targetRank ≥ 0 but no such group exists.
This is "ADD PATTERN".
dominantColorGroup(BufferedImage image, int rank) → OptionalInt
Use it if you want to drive your own region-targeted effect using the same grouping the randomizers use.
toneGroup(int argb) → int
Two pixels with the same key belong to the same tone.
Useful for "is this pixel part of the group I'm targeting?" checks.
apply(BufferedImage image, List<PixelChange> changes) → int
This is the one method that mutates an image — the one you pass to it.
4. ⭐ Getting a texture in, and the result out to use anywhere
The full round trip is: load a BufferedImage → get
Because the changes are plain data,
the image you apply onto does not have to be the one you analysed
—
that is what unlocks "reset only one group" and "build up".
4.1 Getting a BufferedImage IN
BufferedImage img = ImageIO.read(new File("path/to/texture.png"));
// or from a classpath/pack resource:
BufferedImage img = ImageIO.read(getClass().getResourceAsStream("/Common/.../Texture.png"));
BufferedImage img = ImageIO.read(new ByteArrayInputStream(pngBytes));
BufferedImage img = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
// fill img.setRGB(...) however you like
4.2 Applying the changes
BufferedImage img = ImageIO.read(file);
TextureRandomizer.apply(img, TextureRandomizer.randomizeTexture(img, 50, true));
// img now holds the randomized texture
Analyse one image, paint the result onto another.
This is exactly how "reset only the primary group, keep my other edits" works:
BufferedImage base = loadOriginalImage(); // the pristine/base texture
BufferedImage current = loadMyEditedImage(); // your in-progress edits
// Detect + randomize the PRIMARY tone group from the BASE,
// then apply only those changes onto your CURRENT edited image:
List<PixelChange> changes = TextureRandomizer.randomizeColorGroup(base, 50, 0);
if (changes != null) {
TextureRandomizer.apply(current, changes); // only the primary group's pixels change; the rest of your edits stay
}
for (PixelChange c : changes) {
image.setRGB(c.x(), c.y(), c.argb()); // record into your own undo system here
}
—
it maps each PixelChange onto its own document so the edit is undoable.)
4.3 Getting the randomized texture OUT — apply/generate it anywhere
From here it goes wherever you need:
ImageIO.write(img, "png", new File("output/Texture.png"));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ImageIO.write(img, "png", baos);
byte[] pngBytes = baos.toByteArray();
The image/bytes are now ordinary content, so you plug them into whatever asset path you
write the PNG into a watched Common pack folder under a fresh name
(e.g. Items/YourMod/MyTex_123.png) and register/publish it as a CommonAsset, then reference that name from your item/block/model. New unique names are always safe.
write the PNG over an existing common asset name and publish it
(CommonAssetModule.sendAsset(...)).
Connected clients update live; on restart, re-publish it after assets load so it wins.
(This is "Replace Original..." in Live Painter.)
bake the new texture, point the entity's ModelComponent at it, and putComponent it.
which is intentionally outside the Randomizer
—
server mod, build tool, or unit test).
Live Painter & Editor is the reference consumer if you want to
see a full asset-publishing pipeline built on top of these
5. Ready-made recipes
import static org.starkymods.plugin.api.TextureRandomizer.*;
apply(img, randomizeTexture(img, 60, true));
apply(img, randomizeTexture(img, 40, false));
List<PixelChange> p = randomizeColorGroup(img, 50, 0);
if (p != null) apply(img, p); else log("no primary tone group");
List<PixelChange> s = randomizeColorGroup(img, 50, 1);
if (s != null) apply(img, s);
apply(img, addPattern(img, 70, -1));
List<PixelChange> pat = addPattern(img, 70, 0);
if (pat != null) apply(img, pat);
OptionalInt primary = dominantColorGroup(img, 0);
if (primary.isPresent()) {
int key = primary.getAsInt();
for (int y = 0; y < img.getHeight(); y++)
for (int x = 0; x < img.getWidth(); x++) {
int argb = img.getRGB(x, y);
if (((argb >>> 24) & 0xff) != 0 && toneGroup(argb) == key) {
// do something custom to every pixel of the primary tone
}
}
}
6. Notes & gotchas
All recolour methods read the input and return data; only apply() writes
(into the image you hand it).
requested tone group doesn't exist — always check before applying.
randomizeTexture never returns null.
- Alpha 0 is preserved — transparent pixels are skipped, so silhouettes/edges stay clean.
- Randomness is per call.
No seed parameter; each invocation is a fresh variation.
To reproduce a look, keep the resulting image, not the call.
The class is stateless and the methods are pure (each makes its own Random), so different
threads can call it freely — just don't let two threads mutate the same BufferedImage at once.
Each call is O(width × height). Textures are tiny (16² … 256²) so it's instant, but if you batch
many large images, do it off the main server thread.





