-
Notifications
You must be signed in to change notification settings - Fork 1
Assets
The Assets System is a core framework in TaleServer for defining, loading, managing, and synchronizing game data. It provides a type-safe, thread-safe, and extensible architecture for handling all game assets including items, blocks, sounds, particles, environments, and more.
- Overview
- Core Concepts
- Asset Types
- Creating Custom Assets
- Asset Loading
- Asset Retrieval
- Asset Removal
- Asset Packs
- Tagging System
- Dependency Management
- Asset Validation
- Hot Reloading
- Events
- Thread Safety
- Best Practices
The Assets System serves as the central data management layer for TaleServer. Every piece of configurable game content - from items and blocks to sounds and particle effects - is defined as an asset. Assets are:
- JSON-defined: All assets are stored as JSON files that can be easily edited
- Type-safe: Strong typing ensures compile-time safety
- Inheritable: Assets can extend other assets to reduce duplication
- Hot-reloadable: Changes to asset files are detected and applied in real-time during development
- Network-synchronized: Assets are automatically sent to clients when they connect
- Tagged: Assets can be categorized using a flexible tagging system
The foundation of all assets is the JsonAsset interface:
public interface JsonAsset<K> {
K getId();
}This simple interface requires all assets to have a unique identifier of type K (typically String).
Assets that need to be stored in an AssetMap implement JsonAssetWithMap:
public interface JsonAssetWithMap<K, M extends AssetMap<K, ?>> extends JsonAsset<K> {
}This interface links an asset type to its specific storage map implementation.
The AssetStore is the primary manager for a specific asset type. It handles:
- Loading assets from files or programmatically
- Storing assets in the associated
AssetMap - Managing asset dependencies
- Validating assets using codecs
- Firing events when assets are loaded/removed
- Supporting inheritance between assets
Key properties of an AssetStore:
| Property | Description |
|---|---|
path |
The directory path where asset files are located (e.g., "items") |
extension |
File extension for assets (default: ".json") |
codec |
The AssetCodec used to serialize/deserialize assets |
keyFunction |
Function to extract the key from an asset |
loadsAfter |
Set of asset types this store depends on |
loadsBefore |
Set of asset types that depend on this store |
replaceOnRemove |
Function to provide a replacement when an asset is removed |
AssetMap is the abstract storage layer for assets. Different implementations optimize for different use cases:
| Implementation | Use Case |
|---|---|
DefaultAssetMap |
General-purpose storage with case-insensitive keys |
BlockTypeAssetMap |
Optimized for blocks with indexed array storage |
IndexedAssetMap |
Generic indexed storage |
IndexedLookupTableAssetMap |
Fast lookup with lookup table optimization |
Key methods of AssetMap:
// Retrieve an asset by key
T getAsset(K key);
// Get asset from a specific pack
T getAsset(String packKey, K key);
// Get the file path for an asset
Path getPath(K key);
// Get all keys for a file path
Set<K> getKeys(Path path);
// Get child assets (assets that inherit from a key)
Set<K> getChildren(K key);
// Get all assets with a specific tag
Set<K> getKeysForTag(int tagIndex);The AssetRegistry is a static registry that holds all AssetStore instances:
// Get an asset store by asset class
AssetStore<K, T, M> store = AssetRegistry.getAssetStore(MyAsset.class);
// Register a new asset store
AssetRegistry.register(myAssetStore);
// Unregister an asset store
AssetRegistry.unregister(myAssetStore);
// Get all registered stores
Map<Class<?>, AssetStore<?, ?, ?>> stores = AssetRegistry.getStoreMap();The registry also manages the global tag system:
// Get or create a tag index
int tagIndex = AssetRegistry.getOrCreateTagIndex("MyTag");
// Get existing tag index (returns TAG_NOT_FOUND if not exists)
int tagIndex = AssetRegistry.getTagIndex("MyTag");
// Register a client-synchronized tag
AssetRegistry.registerClientTag("ClientTag");TaleServer includes many built-in asset types. Here are the most common ones:
| Asset Type | Description | Path |
|---|---|---|
Item |
Game items (weapons, tools, consumables) | items |
BlockType |
Block definitions | blocktypes |
SoundEvent |
Sound effect definitions | sounds |
ParticleSystem |
Particle effect systems | particles |
Environment |
World environment settings | environments |
Weather |
Weather type definitions | weather |
ModelAsset |
3D model definitions | models |
CraftingRecipe |
Item crafting recipes | recipes |
Projectile |
Projectile configurations | projectiles |
EntityEffect |
Visual/audio effects on entities | effects |
Create a class that implements JsonAssetWithMap:
public class MyCustomAsset implements JsonAssetWithMap<String, DefaultAssetMap<String, MyCustomAsset>> {
// Required: ID field
private String id;
// Required: Extra data for the asset system
private AssetExtraInfo.Data data;
// Your custom fields
private String name;
private int value;
private String[] tags;
@Override
public String getId() {
return id;
}
// Getters and setters...
}Use AssetBuilderCodec to define how your asset is serialized:
public static final AssetBuilderCodec<String, MyCustomAsset> CODEC = AssetBuilderCodec.builder(
MyCustomAsset.class,
MyCustomAsset::new, // Constructor
Codec.STRING, // Key codec
(asset, id) -> asset.id = id, // ID setter
asset -> asset.id, // ID getter
(asset, data) -> asset.data = data, // Data setter
asset -> asset.data // Data getter
)
// Add fields with inheritance support
.<String>appendInherited(
new KeyedCodec<>("Name", Codec.STRING),
(asset, name) -> asset.name = name,
asset -> asset.name,
(asset, parent) -> asset.name = parent.name // Inheritance behavior
)
.add()
// Add fields without inheritance
.<Integer>append(
new KeyedCodec<>("Value", Codec.INTEGER),
(asset, value) -> asset.value = value,
asset -> asset.value
)
.addValidator(Validators.greaterThan(0)) // Add validation
.documentation("The value must be positive") // Add documentation
.add()
.build();Register your asset store during plugin initialization using HytaleAssetStore:
public class MyPlugin extends JavaPlugin {
@Override
protected void setup() {
// Register the asset store
AssetRegistry.register(
HytaleAssetStore.builder(MyCustomAsset.class, new DefaultAssetMap<>())
.setPath("mycustomassets") // Server/mycustomassets/
.setCodec(MyCustomAsset.CODEC)
.setKeyFunction(MyCustomAsset::getId)
.loadsAfter(Item.class) // Dependencies
.build()
);
}
}Assets are automatically loaded from JSON files during server startup. The file structure should match your asset store path:
Server/
mycustomassets/
MyAsset1.json
MyAsset2.json
subfolder/
MyAsset3.json
Example JSON file (MyAsset1.json):
{
"Id": "MyAsset1",
"Name": "My First Asset",
"Value": 100,
"Tags": {
"Category": ["Combat", "Melee"],
"Rarity": ["Common"]
}
}To manually trigger loading:
AssetStore<String, MyCustomAsset, ?> store = AssetRegistry.getAssetStore(MyCustomAsset.class);
// Load from a directory
AssetLoadResult<String, MyCustomAsset> result = store.loadAssetsFromDirectory("PackName", assetsPath);
// Load from specific paths
List<Path> paths = List.of(path1, path2, path3);
AssetLoadResult<String, MyCustomAsset> result = store.loadAssetsFromPaths("PackName", paths);You can create and load assets without JSON files:
MyCustomAsset asset = new MyCustomAsset();
asset.setId("ProgrammaticAsset");
asset.setName("Created in Code");
asset.setValue(50);
AssetStore<String, MyCustomAsset, ?> store = AssetRegistry.getAssetStore(MyCustomAsset.class);
store.loadAssets("PackName", List.of(asset));Assets can inherit from parent assets using the Parent field:
{
"Id": "ChildAsset",
"Parent": "ParentAsset",
"Value": 200
}In this example, ChildAsset inherits all properties from ParentAsset but overrides Value.
Special inheritance keyword "super" inherits from the same asset in a different pack:
{
"Id": "ExistingAsset",
"Parent": "super",
"Value": 300
}// Get the asset store
AssetStore<String, MyCustomAsset, ?> store = AssetRegistry.getAssetStore(MyCustomAsset.class);
// Get the asset map
AssetMap<String, MyCustomAsset> assetMap = store.getAssetMap();
// Retrieve a single asset
MyCustomAsset asset = assetMap.getAsset("MyAsset1");
// Retrieve from a specific pack
MyCustomAsset asset = assetMap.getAsset("PackName", "MyAsset1");
// Get all assets
Map<String, MyCustomAsset> allAssets = assetMap.getAssetMap();
// Get assets by tag
int tagIndex = AssetRegistry.getTagIndex("Combat");
Set<String> combatAssets = assetMap.getKeysForTag(tagIndex);
// Get child assets (assets that inherit from this one)
Set<String> children = assetMap.getChildren("ParentAsset");
// Get asset file path
Path path = assetMap.getPath("MyAsset1");AssetStore<String, MyCustomAsset, ?> store = AssetRegistry.getAssetStore(MyCustomAsset.class);
// Remove specific assets
Set<String> removed = store.removeAssets(Set.of("Asset1", "Asset2"));
// Remove assets by file path
Set<String> removed = store.removeAssetWithPath(path);
// Remove all assets from a pack
store.removeAssetPack("PackName");When an asset is removed:
- All child assets (assets that inherit from it) are also removed
- If
replaceOnRemoveis configured, a replacement asset is provided -
RemovedAssetsEventis fired - Connected clients receive removal packets
Assets can be organized into packs using AssetPack:
// Create a pack from a directory
AssetPack pack = new AssetPack("MyMod", modPath);
// Create a pack from a ZIP file
AssetPack pack = new AssetPack("MyMod", zipPath, fileSystem);
// Check if pack is immutable (cannot write assets)
boolean immutable = pack.isImmutable();
// Get the pack root path
Path root = pack.getRoot();Loading assets from a pack:
AssetRegistryLoader.loadAssets(event, assetPack);Tags provide a flexible way to categorize and query assets. Tags are defined in the asset JSON:
{
"Id": "IronSword",
"Tags": {
"Material": ["Metal", "Iron"],
"Category": ["Weapon", "Melee"],
"Rarity": ["Common"]
}
}Tags are automatically expanded into a flat list. The example above generates:
MaterialMetalIronMaterial=MetalMaterial=IronCategoryWeaponMeleeCategory=WeaponCategory=MeleeRarityCommonRarity=Common
Querying by tag:
// Get or create tag index
int tagIndex = AssetRegistry.getOrCreateTagIndex("Weapon");
// Query assets with tag
Set<String> weapons = assetMap.getKeysForTag(tagIndex);Asset stores can declare dependencies on other asset types:
HytaleAssetStore.builder(MyAsset.class, new DefaultAssetMap<>())
.loadsAfter(Item.class, BlockType.class) // Load after these types
.loadsBefore(Recipe.class) // Load before this type
.build();This ensures assets are loaded in the correct order. The system:
- Converts
loadsBeforetoloadsAfteron dependent stores - Detects circular dependencies
- Loads assets in topological order
Assets are validated during loading using the codec's validators:
AssetBuilderCodec.builder(...)
.<Integer>append(
new KeyedCodec<>("Value", Codec.INTEGER),
(asset, value) -> asset.value = value,
asset -> asset.value
)
.addValidator(Validators.greaterThan(0))
.addValidator(Validators.lessThan(1000))
.add()Use AssetKeyValidator to validate references to other assets:
.addValidator(Item.VALIDATOR_CACHE.getValidator())Validation results are collected in AssetValidationResults:
store.validate(key, results, extraInfo);
results.logOrThrowValidatorExceptions(logger);During development, the AssetMonitor watches for file changes:
// Add file monitoring for a directory
store.addFileMonitor("PackName", assetsPath);
// Remove file monitoring
store.removeFileMonitor(assetsPath);When files change:
- Modified assets are reloaded
- Deleted assets are removed
- New assets are loaded
- Client notifications are sent
-
LoadedAssetsEventandRemovedAssetsEventare fired
Control cache rebuilding with AssetUpdateQuery:
AssetUpdateQuery query = new AssetUpdateQuery(
false, // disableAssetCompare
new AssetUpdateQuery.RebuildCache(
true, // blockTextures
true, // models
true, // modelTextures
false, // mapGeometry
true, // itemIcons
false // commonAssetsRebuild
)
);
store.loadAssetsFromPaths("PackName", paths, query);The asset system fires events through the event bus:
Fired when assets are successfully loaded:
@EventHandler
public void onAssetsLoaded(LoadedAssetsEvent<String, Item> event) {
Map<String, Item> loaded = event.getLoadedAssets();
AssetMap<String, Item> assetMap = event.getAssetMap();
// Process loaded assets...
}Fired when assets are removed:
@EventHandler
public void onAssetsRemoved(RemovedAssetsEvent<String, Item> event) {
Set<String> removed = event.getRemovedKeys();
boolean hasReplacements = event.hasReplacements();
// Handle removal...
}Fired during the asset generation phase:
@EventHandler
public void onGenerateAssets(GenerateAssetsEvent<String, Item> event) {
Map<String, Item> loaded = event.getLoadedAssets();
// Generate derived data...
}Fired when a new asset store is registered:
@EventHandler
public void onStoreRegistered(RegisterAssetStoreEvent event) {
AssetStore<?, ?, ?> store = event.getAssetStore();
// React to new store...
}The asset system is designed for concurrent access:
-
AssetRegistry: Uses
ReentrantReadWriteLockfor store registration -
DefaultAssetMap: Uses
StampedLockfor optimistic reads -
BlockTypeAssetMap: Uses
StampedLockfor indexed access - Tag storage: Uses concurrent hash maps
- Asset operations: Synchronized during modifications
Lock ordering:
- Acquire
AssetRegistry.ASSET_LOCKwrite lock - Modify asset maps
- Fire events
- Release lock
AssetRegistry.ASSET_LOCK.writeLock().lock();
try {
// Modify assets safely
} finally {
AssetRegistry.ASSET_LOCK.writeLock().unlock();
}Use PascalCase with underscores for asset IDs:
-
Iron_Sword(correct) -
iron_sword(incorrect - will log warning) -
IronSword(acceptable but underscores preferred for readability)
Server/
items/
weapons/
swords/
Iron_Sword.json
Steel_Sword.json
axes/
Iron_Axe.json
consumables/
Health_Potion.json
Create base assets for common configurations:
// Base_Weapon.json
{
"Id": "Base_Weapon",
"MaxStack": 1,
"Categories": ["Weapons"]
}
// Iron_Sword.json
{
"Id": "Iron_Sword",
"Parent": "Base_Weapon",
"Name": "Iron Sword",
"Damage": 10
}Always validate asset references:
.addValidator(ItemSoundSet.VALIDATOR_CACHE.getValidator())Add documentation to codec fields:
.<Integer>append(...)
.documentation("Maximum number of items in a stack (1-64)")
.add()Check load results:
AssetLoadResult<String, MyAsset> result = store.loadAssetsFromDirectory(...);
if (!result.getFailedToLoadKeys().isEmpty()) {
logger.warning("Failed to load: " + result.getFailedToLoadKeys());
}
if (!result.getFailedToLoadPaths().isEmpty()) {
logger.warning("Failed files: " + result.getFailedToLoadPaths());
}