Skip to content

Gestalt Asset Core Quick Start

Arthur Casals edited this page Jul 23, 2019 · 18 revisions

Gestalt Asset Core Quick Start

Defining an Asset type

There are two important classes when defining a new asset type.

Firstly, the AssetData class provides an implementation-agnostic (so not tied to a particular technology like OpenGL) representation of the data needed to create the asset:

public class BookData implements AssetData {
    private String title;
    private String body;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

Then there is the Asset class itself, which may be implementation specific (e.g. OpenGLTexture):

public class Book extends Asset<BookData> {

    private int openBookId;

    public Book(ResourceUrn urn, AssetType<?, BookData> type, BookData data) {
        super(urn, type);
        reload(data);
    }

    @Override
    protected void doReload(BookData data) {
        if (openBookId == 0) {
            openBookId = OpenBooKLibrary.createBook();
        }
        OpenBookLibrary.writeTitle(openBookId, data.getTitle());
        OpenBookLibrary.writeBody(openBookId, data.getBody());
    }

    public String getTitle() {
        return OpenBookLibrary.readTitle(openBookId);
    }

    public String getBody() {
        return OpenBookLibrary.readBody(openBookId);
    }
}

Additionally, you may need a AssetFactory class depending on how the asset type will be registered. A factory class looks like:

public class BookFactory implements AssetFactory<Book, BookData> {

    @Override
    public Book build(ResourceUrn urn, AssetType<? super Book, BookData> type, BookData data) {
        return new Book(urn, type, data);
    }
}

although if your constructor meets the signature, you may be able to get away with a method reference (e.g. (AssetFactory<Book, BookData>) BookAsset::new)

Establishing an AssetTypeManager

AssetTypeManager is is the central manager for all asset types. ModuleAwareAssetTypeManager is the recommended asset type manager that hooks into a ModuleEnvironment and makes available assets from within the module's files.

When setting up the ModuleAwareAssetTypeManager, you can register core asset types - these will survive environment switches with their assets either reloaded (if still available in the new environment) or disposed.

ModuleAwareAssetTypeManager assetTypeManager = new ModuleAwareAssetTypeManager();
assetTypeManager.registerCoreAssetType(Book.class, Book::new, "books");
assetTypeManager.switchEnvironment(moduleEnvironment);

You can also have asset types automatically register using the @RegisterAssetType annotation:

import org.terasology.assets.module.annotations.RegisterAssetType;

@RegisterAssetType(folderName = "books", factoryClass = BookFactory.class)
public class Book extends Asset<BookData> { 
   \\ ...
}

When using the ModuleAwareAssetTypeManager, modules are expected to have the following directory structure for assets:

\assets\assetFolderName - normal assets

\deltas\moduleName\assetFolderName - for asset deltas

\overrides\moduleName\assetFolderName - for overrides

where the assetFolderName is the folderName in the annotation or registerCoreAssetType() method call.

Asset Formats

Specific to the ModuleAwareAssetTypeManager is support for Asset Formats - these are used to load assets from files within modules and convert them into asset data.

For simple formats, this can be done as simple as:

@RegisterAssetFileFormat
public class BookFileFormat extends AbstractAssetFileFormat<BookData> {

    public BookFileFormat() {
        // Supported file extensions
        super("book");
    }

    @Override
    public BookData load(ResourceUrn urn, List<AssetDataFile> inputs) throws IOException {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputs.get(0).openStream(), Charsets.UTF_8))) {
            return new BookData(CharStreams.readLines(reader));
        }
    }
}

Supplemental Formats

It can be desirable to support files that provide additional information regardless of what format is being used (e.g. you might have metadata files to go with textures to provide settings on how they are loaded). This can be achieved with supplemental formats:

@RegisterAssetSupplementalFileFormat
public class TextMetadataFileFormat extends AbstractAssetAlterationFileFormat<TextData> {

    public TextMetadataFileFormat() {
        // File extensions
        super("info");
    }

    @Override
    public void apply(AssetDataFile input, TextData assetData) throws IOException {
        try (InputStreamReader reader = new InputStreamReader(input.openStream(), Charsets.UTF_8)) {
            String metadata = Joiner.on("/n").join(CharStreams.readLines(reader));
            assetData.setMetadata(metadata);
        }
    }
}

Overrides

ModuleAwareAssetTypeManager supports modules providing overrides for asset defined by other modules in their dependency tree - asset files that will be used instead of the original files. These simply go in the appropriate \overrides\moduleName\assetFolder path, where moduleName is the name of the module providing the original asset.

Overrides use the same formats as normal assets, including supplemental formats.

Deltas

ModuleAwareAssetTypeManager supports modules providing deltas for asset defined by other modules in their dependency tree - these are applied to the asset data after it is loaded from the original module, and before they are loaded into assets. These go in the appropriate \deltas\moduleName\assetFolder path, where moduleName is the name of the module providing the original asset.

Deltas are more flexible than overrides because multiple modules can provide deltas, and changes to the original asset can be retained. However they can be harder to implement.

To support deltas specific formats are needed:

@RegisterAssetDeltaFileFormat
public class TextDeltaFileFormat extends AbstractAssetAlterationFileFormat<TextData> {

    public TextDeltaFileFormat() {
        // File extensions
        super("delta");
    }

    @Override
    public void apply(AssetDataFile input, TextData assetData) throws IOException {
        try (InputStreamReader reader = new InputStreamReader(input.openStream(), Charsets.UTF_8)) {
            // Apply changes to the provided TextData based on the input
        }
    }
}

Obtaining Assets

Assets can most easily be obtained by using an AssetManager - a wrapper for an AssetTypeManager providing convenience methods for working with assets. Assets are referred to with a ResourceUrn typically with the structure "moduleName:assetName". If the asset is not yet loaded but available from an AssetDataProducer, the asset will be loaded and returned.

AssetManager assetManager = new AssetManager(assetTypeManager);
Optional<Book> myBook = assetManager.getAsset("engine:mybook", Book.class);

It is also possible to get a set of available asset urns:

Set<ResourceUrn> bookUrns = assetManager.getAvailableAssets(Book.class);

Programatically creating/reloading Assets

Assets can be programatically created by creating the appropriate asset data object and then loading it through an AssetManager. If the asset with the given ResourceUrn already exists it will be reloaded with the new data.

BookData data = new BookData();
Book myBook = assetManager.loadAsset(new ResourceUrn("engine:mybook"), data, Book.class);

In addition to loading over an existing asset, reloading can be triggered directly:

BookData data = new BookData();
myBook.reload(data)

AssetDataProducers

AssetDataProducers produce AssetData programmatically based on a requesting urn - this provides a way to support procedural assets that are identified, reused, loaded on demand, and can be saved and reloaded via asset urn.

@RegisterAssetDataProducer
public class BookDataProducer implements AssetDataProducer<BookData> {

    @Override
    public Set<ResourceUrn> getAvailableAssetUrns() {
        // Optionally provide any AssetUrns this producer can produce
        return Collections.emptySet();
    }

    @Override
    public Set<Name> getModulesProviding(Name resourceName) {
        // Provide the modules that can produce an asset with that name, if any (used for urn resolution)
        return Collections.emptySet();
    }

    @Override
    public ResourceUrn redirect(ResourceUrn urn) {
        // Provides the urn of an asset to actually load when a request is made to load the given urn 
        return urn;
    }

    @Override
    public Optional<BookData> getAssetData(ResourceUrn urn) throws IOException {
        // Produces asset data for the given urn 
        if (urn.getResourceName().equals(new Name("procedural")) && !urn.getFragmentName().isEmpty()) {
            BookData data = new BookData();
            data.setTitle(urn.getFragmentName().toString());
            return Optional.of(data);
        }
        return Optional.empty();
    }
}

Disposing Assets

Assets can be disposed using the dispose() method. This activates their disposal hook to clean up any resources they use and removes them from asset management. After disposal an asset cannot be used or reloaded.

Additionally any instance assets that are no longer being referenced are automatically queued for disposal, which can be trigger by AssetType::processDisposal or AssetTypeManager::disposedUnusedAssets.

All assets are disposed when AssetTypes are closed.

If using ModuleAwareAssetTypeManager, when switching environments any loaded assets not provided by the new environment are disposed.

Disposal Hooks

Each asset has a disposal hook, onto which an action to enact when an asset is disposed can be registered. It is advisable that these actions do not reference the asset - the disposal hook system is designed to allow an asset to be disposed after it is garbage collected, and having the disposal hook reference the asset would prevent it being garbage collected. Often you will want to place the resources that need to be disposed inside the disposal action as a result.

public class OpenGLTexture {
    private final TextureResources resources;

    public OpenGLTexture(ResourceUrn urn, AssetType<?, TextureData> assetType, TextureData data) {
        super(urn, assetType);
        this.resources = new TextureResources();
        getDisposalHook().setDisposeAction(resources);
        reload(data);
    }

    private static class TextureResources implements Runnable {

        private volatile int id;

        private final List<Runnable> disposalSubscribers = Lists.newArrayList();

        @Override
        public void run() {
            if (id != 0) {
               OpenGL.deleteTexture(id);
               id = 0;
            }
        }

        public int getId() {
            return id;
        }
    }
}

Reload assets changed on disk

ModuleAwareAssetTypeManager allows assets changing on disk to be reloaded, using the reloadChangedOnDisk() method. This will work for any asset in a directory module.

Redirects

The ModuleAwareAssetTypeManager supports modules providing redirects - breadcrumbs that point an asset urn to another asset urn. This allows for renaming an asset, or moving an asset to another module, without breaking dependent modules that may make use of an asset.

A redirect file simply has the name of an asset with the .redirect file extension and lives in the appropriate assets subdirectory for the type of asset being redirected. The contents of the file is just the urn to redirect to.

Example - assets/books/test.redirect in the engine module containing:

engine:newTarget

Redirects engine:test to engine:newTarget.

Contextual Asset Resolution

It is possible to attempt to obtain an asset with an incomplete urn (missing the moduleName).

// Will return an asset iff there is only one module providing an asset with the resourceName "myBook"
Optional<Book> myBook = assetManager.getAsset("mybook", Book.class);
// Will return a list of all possible ResourceUrns with an resourceName "myBook"
Set<ResourceUrn> options = resolve("mybook", Book.class);

This can be further tailored by providing a module context to resolve within. This will restrict the search for possible assets to the specified module and its dependency tree. Additionally if an asset from that module has the desired resourceName it will be used.

Optional<Book> myBook = assetManager.getAsset("mybook", Book.class, new Name("engine"));
Set<ResourceUrn> options = resolve("mybook", Book.class, new Name("engine"));

It is also possible to specify a context using the ContextManager - this will automatically be used if no context is specified. This can be leveraged to set the context for all resolutions to a desired module before executing code from that module.

try (Context ignored = ContextManager.beginContext(new Name("engine")) {
    Optional<Book> myBook = assetManager.getAsset("mybook", Book.class);
}
You can’t perform that action at this time.