UtilityBiome:Build-Time Tools

From MutigerAffe Wiki
Jump to navigation Jump to search

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.
  • Cover and PrefabEntry build new cover and prefab entries.
  • Palettes.DRY_GRASSLAND is a ready-made block swap for dry grassland.
  • RegionGrid lays a cell grid for large Custom biomes. Its allocator() hands out shares of the cells, so biomes on the same grid never overlap. natural() or Warp distort the round areas into natural shapes.
  • VanillaTiers and VanillaPriorities hold 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 an ELLIPSOID: that crashes chunk generation and kicks players. The validator checks this.
  • CaveTier has constants for every tier with its cave environment, for example ZONE1_TIER1 with Env_Zone1_Caves_Plains.
  • CaveKit is 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.

Utility Biome
Installation · Commands · Biome Census · Version History
For modders: Modding API · Chunk Features · Structures · Build-Time Tools
Project page on mutigeraffe.com · Download on CurseForge