Assets & Registry
The Hytale asset system provides a powerful way to register, load, and manage custom game content.
Architecture
Section titled “Architecture”AssetRegistry (Global)├── AssetStore<K, T, M>[] - Type-specific stores│ ├── AssetCodec - Serialization│ ├── AssetMap - Storage/lookup│ └── AssetPack[] - Content packs└── TagSystem - Asset taggingAsset Stores
Section titled “Asset Stores”Registering an Asset Store
Section titled “Registering an Asset Store”Asset stores are registered with the global AssetRegistry:
import com.hypixel.hytale.assetstore.AssetRegistry;import com.hypixel.hytale.assetstore.codec.AssetCodec;import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;import com.hypixel.hytale.server.core.asset.HytaleAssetStore;
@Overrideprotected void setup() { // Register directly with the global AssetRegistry AssetRegistry.register( HytaleAssetStore.builder(MyAsset.class, new IndexedLookupTableAssetMap<>(MyAsset[]::new)) .setPath("MyAssets") .setCodec((AssetCodec) MyAsset.CODEC) .setKeyFunction(MyAsset::getId) .loadsAfter(OtherAsset.class) // Load dependencies .build() );}Asset Store Builder
Section titled “Asset Store Builder”The HytaleAssetStore.Builder provides methods to configure how assets are loaded and managed:
HytaleAssetStore.builder(AssetClass.class, new IndexedLookupTableAssetMap<>(AssetClass[]::new)) .setPath("AssetDirectory") // JSON files location relative to asset pack root .setCodec((AssetCodec) Asset.CODEC) // Serialization codec .setKeyFunction(Asset::getId) // Key extraction function .loadsAfter(Dependency.class) // Load after this asset type .loadsBefore(Dependent.class) // Load before this asset type .setReplaceOnRemove(replacer) // Replacement when asset is removed .setPacketGenerator(packetGen) // Optional: for client sync .setNotificationItemFunction(func) // Optional: for reload notifications .build();Asset Map Types
Section titled “Asset Map Types”Hytale provides different asset map implementations:
// For indexed lookups (most common)new IndexedLookupTableAssetMap<>(MyAsset[]::new)
// For simple key-value storagenew DefaultAssetMap<String, MyAsset>()Creating Assets
Section titled “Creating Assets”Asset Class
Section titled “Asset Class”Assets extend JsonAssetWithMap and define a codec for serialization:
import com.hypixel.hytale.assetstore.AssetRegistry;import com.hypixel.hytale.assetstore.AssetStore;import com.hypixel.hytale.assetstore.codec.AssetCodecMapCodec;import com.hypixel.hytale.assetstore.map.IndexedLookupTableAssetMap;import com.hypixel.hytale.assetstore.map.JsonAssetWithMap;import com.hypixel.hytale.codec.Codec;import com.hypixel.hytale.codec.KeyedCodec;import com.hypixel.hytale.codec.builder.BuilderCodec;
public class MyAssetimplements JsonAssetWithMap<String, IndexedLookupTableAssetMap<String, MyAsset>> {
// Define the codec with polymorphic type support public static final AssetCodecMapCodec<String, MyAsset> CODEC = new AssetCodecMapCodec<>( Codec.STRING, (asset, key) -> asset.id = key, // Key setter asset -> asset.id, // Key getter (asset, data) -> asset.data = data, // Extra data setter asset -> asset.data // Extra data getter );
// Abstract codec for shared fields (if supporting inheritance) public static final BuilderCodec<MyAsset> ABSTRACT_CODEC = BuilderCodec.builder(MyAsset.class, MyAsset::new) .append(new KeyedCodec<>("Name", Codec.STRING), (obj, val) -> obj.name = val, obj -> obj.name) .add() .append(new KeyedCodec<>("Value", Codec.INTEGER), (obj, val) -> obj.value = val, obj -> obj.value) .add() .build();
private static AssetStore<String, MyAsset, IndexedLookupTableAssetMap<String, MyAsset>> ASSET_STORE;
private String id; private Object data; // AssetExtraInfo.Data private String name; private int value;
public static AssetStore<String, MyAsset, IndexedLookupTableAssetMap<String, MyAsset>> getAssetStore() { if (ASSET_STORE == null) { ASSET_STORE = AssetRegistry.getAssetStore(MyAsset.class); } return ASSET_STORE; }
public static IndexedLookupTableAssetMap<String, MyAsset> getAssetMap() { return getAssetStore().getAssetMap(); }
@Override public String getId() { return id; } public String getName() { return name; } public int getValue() { return value; }}Asset JSON File
Section titled “Asset JSON File”{ "Id": "my_asset_id", "Name": "My Asset", "Value": 42}Codec System
Section titled “Codec System”BuilderCodec
Section titled “BuilderCodec”Type-safe serialization for structured data:
public static final BuilderCodec<MyData> CODEC = BuilderCodec.builder(MyData.class, MyData::new) .append(new KeyedCodec<>("Name", Codec.STRING), (obj, val) -> obj.name = val, obj -> obj.name) .add() .append(new KeyedCodec<>("Count", Codec.INTEGER), (obj, val) -> obj.count = val, obj -> obj.count) .add() .append(new KeyedCodec<>("Items", Codec.list(Item.CODEC)), (obj, val) -> obj.items = val, obj -> obj.items) .add() .build();Built-in Codecs
Section titled “Built-in Codecs”Codec.STRING // StringCodec.BOOLEAN // booleanCodec.INTEGER // intCodec.LONG // longCodec.FLOAT // floatCodec.DOUBLE // doubleCodec.BYTE // byteCodec.SHORT // shortCodec.INT_ARRAY // int[]Codec.FLOAT_ARRAY // float[]Codec.UUID_STRING // UUID as stringCodec.PATH // PathCodec.INSTANT // InstantCodec.DURATION // DurationCollection Codecs
Section titled “Collection Codecs”// List codecCodec<List<String>> stringList = Codec.list(Codec.STRING);
// Map codecCodec<Map<String, Integer>> stringIntMap = Codec.map(Codec.STRING, Codec.INTEGER);
// Optional codecCodec<Optional<String>> optionalString = Codec.optional(Codec.STRING);Polymorphic Codecs
Section titled “Polymorphic Codecs”For types with multiple implementations (like Interactions), use AssetCodecMapCodec:
import com.hypixel.hytale.assetstore.codec.AssetCodecMapCodec;
public abstract class Interaction { // Polymorphic codec that routes to different implementations based on "Type" field public static final AssetCodecMapCodec<String, Interaction> CODEC = new AssetCodecMapCodec<>( Codec.STRING, (t, k) -> t.id = k, t -> t.id, (t, data) -> t.data = data, t -> t.data );}
// Implementations are registered with the codecInteraction.CODEC.register("Click", ClickInteraction.class, ClickInteraction.CODEC);Interaction.CODEC.register("Hold", HoldInteraction.class, HoldInteraction.CODEC);Registration in plugin using the codec registry:
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
@Overrideprotected void setup() { // Register custom interaction type with the Interaction codec getCodecRegistry(Interaction.CODEC) .register("MyInteraction", MyInteraction.class, MyInteraction.CODEC);}Real-World Example: Custom Interaction
Section titled “Real-World Example: Custom Interaction”Here is how the built-in ProjectileModule registers a custom interaction:
// From ProjectileModule.java@Overrideprotected void setup() { this.getCodecRegistry(Interaction.CODEC) .register("Projectile", ProjectileInteraction.class, ProjectileInteraction.CODEC);}Asset Loading
Section titled “Asset Loading”Load from Directory
Section titled “Load from Directory”AssetStore<String, MyAsset, ?> store = AssetRegistry.getAssetStore(MyAsset.class);
// Load all assets from a directorystore.loadAssetsFromDirectory(path);Load Specific Paths
Section titled “Load Specific Paths”store.loadAssetsFromPaths(packId, List.of( Paths.get("assets/my_asset_1.json"), Paths.get("assets/my_asset_2.json")));Load Events
Section titled “Load Events”Listen for asset loading/unloading:
@Overrideprotected void setup() { getEventRegistry().register( LoadedAssetsEvent.class, MyAsset.class, this::onAssetsLoaded );
getEventRegistry().register( RemovedAssetsEvent.class, MyAsset.class, this::onAssetsRemoved );}
private void onAssetsLoaded(LoadedAssetsEvent<MyAsset> event) { for (MyAsset asset : event.getAssets()) { getLogger().info("Loaded asset: " + asset.getId()); }}Asset Access
Section titled “Asset Access”Get Asset by Key
Section titled “Get Asset by Key”DefaultAssetMap<String, MyAsset> map = MyAsset.getAssetMap();MyAsset asset = map.getAsset("my_asset_id");
if (asset != null) { // Use asset}Iterate All Assets
Section titled “Iterate All Assets”for (String key : map.getKeys()) { MyAsset asset = map.getAsset(key); // Process asset}Check Asset Existence
Section titled “Check Asset Existence”boolean exists = map.containsKey("my_asset_id");Asset Inheritance
Section titled “Asset Inheritance”Assets can inherit from parent assets:
public static final BuilderCodec<MyAsset> CODEC = BuilderCodec.builder(MyAsset.class, MyAsset::new) .append(new KeyedCodec<>("Parent", Codec.STRING), (obj, val) -> obj.parentId = val, obj -> obj.parentId) .addInherited() // Mark as inheritable .append(new KeyedCodec<>("Name", Codec.STRING), (obj, val) -> obj.name = val, obj -> obj.name) .add() .build();JSON with inheritance:
{ "Id": "my_child_asset", "Parent": "my_parent_asset", "Name": "Overridden Name"}Asset Validation
Section titled “Asset Validation”Custom Validators
Section titled “Custom Validators”public static final BuilderCodec<MyAsset> CODEC = BuilderCodec.builder(MyAsset.class, MyAsset::new) .append(new KeyedCodec<>("Value", Codec.INTEGER), (obj, val) -> obj.value = val, obj -> obj.value) .validator((obj, results) -> { if (obj.value < 0) { results.addError("Value must be non-negative"); } }) .add() .build();Asset Reference Validation
Section titled “Asset Reference Validation”// Validates that referenced assets existnew AssetKeyValidator<>(MyOtherAsset::getAssetMap)Content Packs
Section titled “Content Packs”AssetPack Structure
Section titled “AssetPack Structure”public class AssetPack { String name; // Pack identifier Path root; // Filesystem root boolean isImmutable; // Prevents modification PluginManifest manifest;}Loading from Packs
Section titled “Loading from Packs”Assets are automatically loaded from registered content packs during server startup.
Registry Types
Section titled “Registry Types”Plugin Registries
Section titled “Plugin Registries”The PluginBase class provides several registry accessors for registering different types of content:
// Asset registry (via PluginBase.getAssetRegistry())// Note: For global asset stores, use AssetRegistry.register() directlygetAssetRegistry().register(assetStore);
// Codec registry for polymorphic types (via PluginBase)getCodecRegistry(ParentCodec.CODEC).register("Type", MyClass.class, MyClass.CODEC);
// Block state registrygetBlockStateRegistry().registerBlockState(StateClass.class, "name", CODEC);
// Client feature registrygetClientFeatureRegistry().register(ClientFeature.MyFeature);getClientFeatureRegistry().registerClientTag("MyTag");
// Entity store registry (for components and systems)getEntityStoreRegistry().registerComponent(MyComponent.class, "Name", MyComponent.CODEC);getEntityStoreRegistry().registerSystem(new MySystem());getEntityStoreRegistry().registerResource(MyResource.class, MyResource::new);
// Chunk store registrygetChunkStoreRegistry().registerComponent(MyChunkComponent.class, "Name", CODEC);
// Command registrygetCommandRegistry().registerCommand(new MyCommand());
// Event registrygetEventRegistry().register(SomeEvent.class, this::onEvent);
// Task registrygetTaskRegistry().scheduleTask(task);Thread Safety
Section titled “Thread Safety”Asset operations use read-write locks:
// Global asset lockAssetRegistry.ASSET_LOCK.readLock(); // For readsAssetRegistry.ASSET_LOCK.writeLock(); // For writes
// Always released in finally blockAsset Loading Flow
Section titled “Asset Loading Flow”1. loadAssetsFromPaths() ↓2. Discover filesystem paths ↓3. Create RawAsset objects ↓4. loadAssets0() - Parallel decoding ↓5. GenerateAssetsEvent dispatch ↓6. assetMap.putAll() - Insert into storage ↓7. loadContainedAssets() - Recursive children ↓8. LoadedAssetsEvent dispatch ↓9. Send update packets to clientsBest Practices
Section titled “Best Practices”- Define clear codecs - Type-safe serialization prevents errors
- Use load ordering - Declare dependencies with loadsAfter/loadsBefore
- Validate assets - Add validators to catch invalid data
- Handle events - React to asset loading/unloading
- Use inheritance - Reduce duplication with parent assets
- Cache asset references - Store frequently accessed assets
- Thread-safe access - Use proper locking for modifications