UtilityBiome:Build-Time Tools
The tools packages generate Hytale world-generation files (biomes, caves, ores, resources, spawns) from Java code and check every reference before a file reaches a server. They run in your Gradle build and tests, never in the game, and contain no Hytale imports. Gson comes from the server jar.
Generated files only refer to vanilla and your own files. A mod that uses nothing else from Utility Biome does not need it at runtime, see Modding API.
Gradle plugin
The plugin utilitybiome.content wires everything up. It is included through the submodule (see Modding API).
// build.gradle.kts
plugins { id("utilitybiome.content") }
utilityBiome {
prefix = "MyMod_" // every generated file and ID carries it
content = "com.example.mymod.content.MyModContent" // your ContentPack
}
| Setting | Default | Meaning |
|---|---|---|
prefix |
required for validateAssets |
Mod prefix with underscore, for example BetterBiomes_.
|
content |
required for generation | Fully qualified ContentPack class in the biomes source set.
|
resources |
src/main/resources |
Where files are generated to and what is validated. |
devResources |
src/dev/resources |
Files only for the dev server, never in the jar. |
vanillaIndex |
modules/utilitybiome/vanilla-index/<server_version>.txt |
Vanilla index for the validator. |
dependencyPacks |
Utility Biome's own pack in the submodule | Asset packs your files may refer to. |
The plugin adds the source set biomes (src/biomes/java). It sees the server API and Utility Biome and is never packed into the jar. The tasks:
| Task | Does |
|---|---|
generateBiomes |
Writes the files of your ContentPack into resources. Check them in.
|
checkGeneratedAssets |
Fails if the checked-in files differ from what the content generates (hand edits or a Hytale update). Part of check. Skipped when Assets.zip is not available, for example in CI.
|
validateAssets |
Runs the asset validator over your packs. Part of check.
|
devAssetPack |
Zips resources and devResources for the dev server. runServer depends on it.
|
freshWorld |
Deletes the dev world run/universe/worlds/default but keeps its seed. With -PnewSeed the seed goes too.
|
Generation reads vanilla files from your local Hytale installation. Set hytaleHomeOverride in ~/.gradle/gradle.properties to the Hytale home folder.
ContentPack and Mod
A ContentPack (package tools.content) returns everything your mod generates. It needs a public constructor without parameters.
public final class MyModContent implements ContentPack {
private static final Mod MOD = Mod.of("MyMod");
@Override
public List<GeneratedAsset> assets(VanillaAssets vanilla) {
List<GeneratedAsset> out = new ArrayList<>();
out.addAll(OreSpec.named("MyMod_Tin")
.rarity(Rarity.UNCOMMON).vein(VeinShape.MEDIUM).heights(20, 90)
.zone("Zone1", "MyMod_Ore_Tin_Stone")
.zone("Zone3", "MyMod_Ore_Tin_Shale")
.build());
out.add(BiomeSpec.tile("Zone1_Tier1", MOD.id("Birch_Glade"),
vanilla.biome("Zone1_Tier1", "Tile.Forest_Birch"))
.covers(c -> c.remove(Select.placing("Plant_Flower_*"))));
return out;
}
}
Mod builds names, seeds and paths from one place, so no name without the prefix slips through. The mod name may not contain _, - or ..
Mod mod = Mod.of("FrostMod");
mod.id("Ice_Forest") // FrostMod_Ice_Forest
mod.seed("Icicles") // FrostMod-Icicles
mod.path("PrefabPatterns", "Ice") // Zones.FrostMod.PrefabPatterns.Ice
Mod.file(mod.path("PrefabPatterns", "Ice")) // Server/World/Default/Zones/FrostMod/PrefabPatterns/Ice.json
Never rename a seed afterwards: a changed seed moves things in existing worlds.
VanillaAssets reads vanilla files from Assets.zip: biome(zone, fileName) for a biome, file(path) for any JSON file and files(prefix) to list JSON files under a path.
Biomes
Package tools.biome. Hytale has no inheritance between biomes, so BiomeSpec copies a vanilla biome and changes it step by step. What you do not touch (relief, water, environment, monuments) stays as in the original, so the biome behaves like its model at the edges.
BiomeSpec.tile("Zone1_Tier1", "MyMod_Forest_Dead", vanilla.biome("Zone1_Tier1", "Tile.Forest_Birch"))
.replaceBlocks(Map.of("Soil_Grass", "Soil_Grass_Dry"))
.covers(c -> c.remove(Select.placing("Plant_Flower_*")))
.mapColor("#8f8456");
| Method | Purpose |
|---|---|
tile(zone, name, source) |
A main biome of the zone (Tile.<name>.json), spread by weight.
|
custom(zone, name, source) |
A biome placed on top of others (Custom.<name>.json). Needs customGenerator.
|
customGenerator(noiseMask, priority, biomes...) |
Where a Custom biome appears: inside which biomes, at which spots, and above which other Custom biomes. |
replaceBlocks(map) |
Swaps blocks in covers and layers, all at once (no chains). |
covers(edit), layers(group, edit), prefabs(edit) |
Edit lists of entries with EntryList.
|
monumentsOf(source) |
Takes the monuments (dungeons, outposts) of another biome. |
environmentOf(source) |
Takes the environment (enemies, weather) of another biome. |
tint(edit), mapColor(hex), weight(n), comment(text) |
Tint, map colour, weight (Tile only), comment. |
set(path, value) |
Anything else, by JSON path. |
Helpers:
Select.placing(globs),Select.placingOnly(globs),Select.pattern(globs)pick entries by content instead of position. A selection that matches nothing stops generation, since it is almost always a typo or a changed vanilla file.EntryList:edit,remove,keep,clear,add,insertAfter.CoverandPrefabEntrybuild new cover and prefab entries.Palettes.DRY_GRASSLANDis a ready-made block swap for dry grassland.RegionGridlays a cell grid for large Custom biomes. Itsallocator()hands out shares of the cells, so biomes on the same grid never overlap.natural()orWarpdistort the round areas into natural shapes.VanillaTiersandVanillaPrioritieshold vanilla facts per zone and tier and the priorities of vanilla's Custom biomes.
Every generated file carries a $Comment that names its source. Do not edit generated files by hand.
Utility Biome's own pack contains one shared noise, Zones.UtilityBiome.Noise.Tint.Forest_Patchy, a patchy tint noise for forests. Mods that refer to it need Utility Biome at runtime.
Caves
Package tools.cave. CaveSpec describes a cave for several tiers and produces the nodes, the cave type and one modifier per tier.
CaveSpec hollow = CaveSpec.of(mod, "Frozen_Hollow")
.inTiers(CaveTier.ZONE3_TIER1, CaveTier.ZONE3_TIER2)
.entryTo("Chamber")
.node("Chamber", "Ice chamber", tier -> """
{"Type": "ELLIPSOID", "RadiusX": [8, 12], "RadiusY": [5, 7], "RadiusZ": [8, 12]}""")
.type("Frozen cave", tier -> """
"EntryPoints": {"Seed": "%s", "Density": [0, 0.3], "Scale": 0.005, "Jitter": 0.5},
"FixedEntryHeight": [70, 90], "Pitch": 90""".formatted(mod.seed("Frozen-Hollow")));
List<GeneratedAsset> assets = CaveSpec.build(mod, List.of(hollow));
entryTo(child)creates a safe, empty entry node. The entry node must never be anELLIPSOID: that crashes chunk generation and kicks players. The validator checks this.CaveTierhas constants for every tier with its cave environment, for exampleZONE1_TIER1withEnv_Zone1_Caves_Plains.CaveKitis the lower-level version with the default block and prefab masks.
Ores
Package tools.ore. OreSpec creates an ore the way vanilla does: per zone a vein node, a placement and a modifier that adds it to all tiers of the zone.
List<GeneratedAsset> tin = OreSpec.named("MyMod_Tin")
.rarity(Rarity.UNCOMMON)
.vein(VeinShape.MEDIUM)
.heights(20, 90)
.zone("Zone1", "MyMod_Ore_Tin_Stone")
.zone("Zone3", "MyMod_Ore_Tin_Shale")
.build();
| Rarity | Like in vanilla | About |
|---|---|---|
COMMON |
copper in zone 1 | 40 veins per chunk |
UNCOMMON |
iron in zone 1 | 10 per chunk |
RARE |
cobalt | 2–3 per chunk |
EPIC |
gold | one per 4 chunks |
LEGENDARY |
silver | one per 25 chunks |
The ore block per zone should match the rock of that zone and must exist as a block, either in vanilla or in your own assets. Vein shapes: SMALL (a lump like silver), MEDIUM (core with optional casing like iron, see casing(block)), LARGE (mostly thin veins, rarely a large one, very rarely a motherlode, like copper). Defaults: UNCOMMON, MEDIUM, heights 20–100. hostRock(blocks...) changes which blocks the ore may replace. Because the steps come from vanilla, an ore "as rare as gold" is equally rare in every mod that uses Utility Biome.
Resources
Package tools.resource. ResourceSpec adds covers (plants, stones) or prefabs (buildings, rocks, skeletons) to existing biomes through a modifier, without touching vanilla files.
ResourceSpec.prefabs("MyMod_Watchtowers")
.into("Zones.Zone1_Tier*.Tile.Plains_*")
.prefab("Monuments.Encounter.City_Ruins.Towers.*", 1)
.grid(0.01, 0.05) // cells of ~100 blocks, 5 % of them
.parents("Soil_Grass", "Soil_Grass_Full")
.build();
into takes globs on the dotted path of the biome. Further options: fitHeightmap(), submerge(), sink(blocks), mask(json). For covers use ResourceSpec.cover(name, coverJson).
Spawns
Package tools.spawn. SpawnSpec writes a world NPC spawn file. Spawns have no modifiers, a new file is enough, and the prefix keeps it from replacing a vanilla file.
SpawnSpec.named("MyMod_Spawns_Forest_Boars")
.environments("Env_Zone1_Forests", "Env_Zone1_Autumn")
.npc("Boar", 10, "Group_Small")
.onBlocks("Soil")
.dayTime(6, 18)
.build();
light(min, max) limits the light level (0–100).
Asset validator
AssetValidator (package tools) checks world-generation and modifier files before they reach a server. The task validateAssets runs it, or call it from a JUnit test:
AssetValidator.Report report = AssetValidator.forPrefix("MyMod_")
.vanilla(VanillaIndex.load(Path.of("modules/utilitybiome/vanilla-index/0.7.0.txt")))
.pack(Path.of("src/main/resources"))
.dependency(Path.of("modules/utilitybiome/src/main/resources"))
.validate();
assertTrue(report.isOk(), report.format());
It reports:
- JSON that cannot be read,
"File"references that point nowhere (the world would not load),- prefab globs that match no prefab,
- unknown block, item and environment IDs,
- biome and modifier files without the mod prefix, and files that hide or disable a vanilla file,
- cave types whose entry node is an
ELLIPSOID, - biomes whose tint or environment has a different number of entries than weights, or two or more entries without a noise,
- biome masks that name no Tile biome of the zone,
- modifiers with wrong enum values (a wrong value stops the whole server), no rules or no operations.
VanillaIndex lists everything that exists in vanilla, as one text file per Hytale version (vanilla-index/<version>.txt). References can then be checked without Assets.zip, for example in CI, and a Hytale update shows up as a diff.