Create and manage custom assets for Hytale including models, textures, sounds, particles, and asset packs...
Complete guide for creating and managing custom assets including models, textures, sounds, and particles.
Use this skill when:
Hytale uses a hierarchical asset system:
AssetPack
├── Server/ # Server-side assets
│ └── Content/ # Game content definitions
│ ├── BlockTypes/
│ ├── Items/
│ ├── Entities/
│ └── ...
└── Client/ # Client-side assets
├── Models/
├── Textures/
├── Sounds/
├── Particles/
└── ...
my-plugin/
└── assets/
├── pack.json # Pack metadata
├── Server/
│ └── Content/
│ ├── BlockTypes/
│ │ └── custom_block.blocktype
│ ├── Items/
│ │ └── custom_item.item
│ ├── Entities/
│ │ └── custom_entity.entity
│ ├── Recipes/
│ │ └── custom_recipe.recipe
│ └── Sounds/
│ └── custom_soundset.soundset
└── Client/
├── Models/
│ └── custom_model.model
├── Textures/
│ ├── blocks/
│ │ └── custom_block.png
│ ├── items/
│ │ └── custom_item.png
│ └── entities/
│ └── custom_entity.png
├── Sounds/
│ └── custom/
│ └── sound.ogg
├── Particles/
│ └── custom_particle.particle
└── Animations/
└── custom_animation.animation
File: pack.json
{
"Name": "MyPlugin Assets",
"Id": "MyPlugin",
"Version": "1.0.0",
"Description": "Custom assets for MyPlugin",
"Author": "Your Name",
"Priority": 100,
"Dependencies": ["Hytale:Core"]
}
| Property | Type | Description |
|---|---|---|
Name |
String | Display name |
Id |
String | Unique identifier |
Version |
String | Semantic version |
Description |
String | Pack description |
Author |
String | Creator name |
Priority |
Integer | Load order (higher = later) |
Dependencies |
Array | Required packs |
| Format | Use Case |
|---|---|
| PNG | Standard textures (recommended) |
| TGA | Textures with alpha |
| DDS | Compressed textures |
Location: Client/Textures/blocks/
custom_block.png # All faces
custom_block_top.png # Top face only
custom_block_side.png # Side faces
custom_block_bottom.png # Bottom face
Texture Size: 16x16, 32x32, 64x64 (power of 2)
Location: Client/Textures/items/
custom_item.png # Inventory icon (32x32)
custom_item_held.png # Held model texture
Location: Client/Textures/entities/
custom_entity.png # Main texture atlas
custom_entity_glow.png # Emissive layer
custom_entity_normal.png # Normal map
Hytale uses a custom JSON-based model format.
File: custom_model.model
{
"Parent": "Hytale/Models/base_entity",
"Textures": {
"main": "MyPlugin/Textures/entities/custom_entity"
},
"Bones": [
{
"Name": "root",
"Pivot": [0, 0, 0],
"Children": [
{
"Name": "body",
"Pivot": [0, 12, 0],
"Cubes": [
{
"Origin": [-4, 0, -2],
"Size": [8, 12, 4],
"UV": [0, 0]
}
],
"Children": [
{
"Name": "head",
"Pivot": [0, 24, 0],
"Cubes": [
{
"Origin": [-4, 0, -4],
"Size": [8, 8, 8],
"UV": [0, 16]
}
]
}
]
}
]
}
],
"Animations": {
"idle": "MyPlugin/Animations/custom_idle",
"walk": "MyPlugin/Animations/custom_walk"
}
}
| Property | Type | Description |
|---|---|---|
Parent |
String | Parent model to inherit |
Textures |
Object | Texture bindings |
Bones |
Array | Bone hierarchy |
Animations |
Object | Animation bindings |
Scale |
Float | Overall scale |
TextureSize |
Array | Texture dimensions [W, H] |
| Property | Type | Description |
|---|---|---|
Name |
String | Bone identifier |
Pivot |
Array | Rotation pivot point |
Rotation |
Array | Default rotation [X, Y, Z] |
Cubes |
Array | Geometry cubes |
Children |
Array | Child bones |
Mirror |
Boolean | Mirror UV horizontally |
| Property | Type | Description |
|---|---|---|
Origin |
Array | Position offset |
Size |
Array | Dimensions [W, H, D] |
UV |
Array | Texture UV offset [U, V] |
Inflate |
Float | Expand cube size |
Mirror |
Boolean | Mirror this cube |
File: custom_animation.animation
{
"Length": 1.0,
"Loop": true,
"Bones": {
"body": {
"Rotation": {
"0.0": [0, 0, 0],
"0.5": [5, 0, 0],
"1.0": [0, 0, 0]
}
},
"leftArm": {
"Rotation": {
"0.0": [0, 0, 0],
"0.25": [-30, 0, 0],
"0.5": [0, 0, 0],
"0.75": [30, 0, 0],
"1.0": [0, 0, 0]
}
},
"rightArm": {
"Rotation": {
"0.0": [0, 0, 0],
"0.25": [30, 0, 0],
"0.5": [0, 0, 0],
"0.75": [-30, 0, 0],
"1.0": [0, 0, 0]
}
}
}
}
| Property | Type | Description |
|---|---|---|
Length |
Float | Duration in seconds |
Loop |
Boolean | Loop animation |
Bones |
Object | Per-bone keyframes |
| Channel | Type | Description |
|---|---|---|
Rotation |
Array | Rotation [X, Y, Z] degrees |
Position |
Array | Position offset [X, Y, Z] |
Scale |
Array | Scale [X, Y, Z] |
| Format | Use Case |
|---|---|
| OGG | Music, ambient (recommended) |
| WAV | Short sound effects |
File: custom_sound.soundevent
{
"Sounds": [
{
"Path": "MyPlugin/Sounds/custom/sound1",
"Weight": 1.0
},
{
"Path": "MyPlugin/Sounds/custom/sound2",
"Weight": 0.5
}
],
"Volume": {
"Min": 0.8,
"Max": 1.0
},
"Pitch": {
"Min": 0.9,
"Max": 1.1
},
"Category": "Effects",
"Subtitle": {
"en-US": "Custom sound plays"
}
}
File: custom_soundset.soundset
{
"Sounds": {
"break": "MyPlugin/SoundEvents/custom_break",
"place": "MyPlugin/SoundEvents/custom_place",
"step": "MyPlugin/SoundEvents/custom_step",
"hit": "MyPlugin/SoundEvents/custom_hit"
}
}
File: custom_blocksound.blocksound
{
"Break": {
"Sound": "MyPlugin/SoundEvents/metal_break",
"Volume": 1.0,
"Pitch": 1.0
},
"Place": {
"Sound": "MyPlugin/SoundEvents/metal_place",
"Volume": 1.0
},
"Step": {
"Sound": "MyPlugin/SoundEvents/metal_step",
"Volume": 0.5
},
"Fall": {
"Sound": "MyPlugin/SoundEvents/metal_fall"
}
}
| Category | Description |
|---|---|
Master |
All sounds |
Music |
Background music |
Effects |
Sound effects |
Ambient |
Environmental sounds |
Voice |
Voice/dialogue |
UI |
Interface sounds |
Weather |
Weather sounds |
File: custom_particle.particle
{
"Texture": "MyPlugin/Textures/particles/sparkle",
"MaxParticles": 100,
"EmissionRate": 10,
"Lifetime": {
"Min": 0.5,
"Max": 1.5
},
"Size": {
"Start": 0.2,
"End": 0.0
},
"Color": {
"Start": { "R": 1.0, "G": 0.8, "B": 0.2, "A": 1.0 },
"End": { "R": 1.0, "G": 0.2, "B": 0.0, "A": 0.0 }
},
"Velocity": {
"Min": [-0.5, 1.0, -0.5],
"Max": [0.5, 2.0, 0.5]
},
"Gravity": -0.5,
"Collision": false,
"BlendMode": "Additive"
}
| Property | Type | Description |
|---|---|---|
Texture |
String | Particle texture |
MaxParticles |
Integer | Maximum active particles |
EmissionRate |
Float | Particles per second |
Lifetime |
Range | Particle lifespan |
Size |
Range/Gradient | Size over lifetime |
Color |
Color/Gradient | Color over lifetime |
Velocity |
Range | Initial velocity |
Gravity |
Float | Gravity effect |
Collision |
Boolean | Collide with blocks |
BlendMode |
Enum | Alpha, Additive, Multiply |
File: custom_blockparticle.blockparticle
{
"Break": {
"Particle": "MyPlugin/Particles/shatter",
"Count": 30
},
"Step": {
"Particle": "MyPlugin/Particles/dust",
"Count": 3
},
"Ambient": {
"Particle": "MyPlugin/Particles/glow",
"Rate": 1,
"Radius": 0.5
}
}
File: custom_trail.trail
{
"Texture": "MyPlugin/Textures/trails/slash",
"Width": 0.5,
"Length": 10,
"Lifetime": 0.3,
"Color": {
"Start": { "R": 1.0, "G": 1.0, "B": 1.0, "A": 1.0 },
"End": { "R": 1.0, "G": 1.0, "B": 1.0, "A": 0.0 }
},
"BlendMode": "Additive",
"AttachPoint": "weapon_tip"
}
Assets can inherit from parents:
{
"Parent": "Hytale:Stone",
"DisplayName": { "en-US": "Enchanted Stone" },
"LightLevel": 5
}
Categorize assets with tags:
{
"Tags": {
"Category": ["Building", "Decorative"],
"Material": ["Stone"],
"Origin": ["Natural", "Cave"]
}
}
Used in spawning, loot tables, etc.:
{
"Blocks": {
"MatchAll": ["Material:Stone"],
"MatchAny": ["Category:Natural", "Origin:Cave"],
"Exclude": ["Category:Artificial"]
}
}
File: lang/en-US.json
{
"item.myPlugin.customItem.name": "Custom Item",
"item.myPlugin.customItem.description": "A special custom item",
"block.myPlugin.customBlock.name": "Custom Block",
"entity.myPlugin.customEntity.name": "Custom Creature"
}
| Pattern | Example |
|---|---|
item.{plugin}.{id}.name |
item.myPlugin.sword.name |
block.{plugin}.{id}.name |
block.myPlugin.ore.name |
entity.{plugin}.{id}.name |
entity.myPlugin.mob.name |
ui.{plugin}.{key} |
ui.myPlugin.menuTitle |
@Override
protected void setup() {
// Asset pack is auto-registered if manifest.json has:
// "IncludesAssetPack": true
// Listen for asset loading
getEventRegistry().register(
LoadedAssetsEvent.class,
BlockType.class,
this::onBlockTypesLoaded
);
getEventRegistry().register(
LoadedAssetsEvent.class,
Item.class,
this::onItemsLoaded
);
}
private void onBlockTypesLoaded(LoadedAssetsEvent<BlockType> event) {
// Access loaded block types
for (BlockType block : event.getLoadedAssets()) {
getLogger().atInfo().log("Loaded block: %s", block.getId());
}
}
Assets support hot-reload during development:
/reload assets
Listen for reload events:
getEventRegistry().register(
AssetStoreMonitorEvent.class,
this::onAssetReload
);
private void onAssetReload(AssetStoreMonitorEvent event) {
if (event.getAssetStore().getAssetClass() == BlockType.class) {
getLogger().atInfo().log("Block types reloaded");
// Re-initialize dependent systems
}
}
Register entirely new asset types:
public class MyCustomAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, MyCustomAsset>> {
public static final AssetBuilderCodec<String, MyCustomAsset> CODEC =
AssetBuilderCodec.builder(MyCustomAsset.class, "MyCustomAsset")
.appendRequired(Codec.STRING.fieldOf("Name"), MyCustomAsset::getName, (a, v) -> a.name = v)
.appendOptional(Codec.INT.fieldOf("Value"), MyCustomAsset::getValue, (a, v) -> a.value = v, 0)
.build();
private String id;
private String name;
private int value;
@Override
public String getId() { return id; }
public String getName() { return name; }
public int getValue() { return value; }
}
// Register in plugin
@Override
protected void setup() {
HytaleAssetStore<String, MyCustomAsset, DefaultAssetMap<String, MyCustomAsset>> store =
new HytaleAssetStore<>(
MyCustomAsset.class,
"MyCustomAssets", // Directory name
".mycustomasset", // File extension
MyCustomAsset.CODEC,
DefaultAssetMap::new
);
getAssetRegistry().register(store);
}
See references/asset-formats.md for detailed format specs.
See references/texture-specs.md for texture requirements.