Skip to content

Gestalt Asset Core Quick Start

Immortius edited this page Dec 5, 2019 · 21 revisions

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. This is the object that is used to produce an asset, often loaded from a file.

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 matches 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. ModuleAwareAssetTypeManagerImpl is the recommended asset type manager that hooks into gestalt-module's ModuleEnvironment and makes available assets from within the module's files.

ModuleAwareAssetTypeManager assetTypeManager = new ModuleAwareAssetTypeManagerImpl();
assetTypeManager.createAssetType(Book.class. Book::new, "book"); // Expects book files to be found in the "book" directory

You can also have asset types defined in modules 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> { 
   \\ ...
}

Calling

assetTypeManager.switchEnvironment(environment)

Will then register all annotated asset types, and make available assets from that environment. It will also clean up all asset types and assets loaded from the previous environment.

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 createAssetType 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.

@RegisterAssetFileFormat
public class BookFileFormat extends AbstractAssetFileFormat<BookData> {

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

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

Formats can be registered by annotation, or by adding them into an asset type's AssetFileDataProducer:

assetTypeManager.getAssetFileDataProducer(bookAssetType).addAssetFormat(new TextBookFormat());

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 (BufferedReader reader = input.openReader()) {
            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 (BufferedReader reader = input.openReader()) {
            // 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);

It is also possible to work with an AssetType directly in much the same manner.

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

If an asset has special resources that need to be disposed, this can be handled by registering a disposal action in their constructor. It is advisable that these actions do not reference the asset - the disposal 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 this. Often you will want to place the resources that need to be disposed inside the disposal action as a result.

public class Book extends Asset<BookData> {

    private BookDisposalAction bookResources;

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

    private static class DisposalAction implements Runnable {

        private static final Logger logger = LoggerFactory.getLogger(DisposalAction.class);
        private ResourceUrn urn;

        public DisposalAction(ResourceUrn urn) {
            this.urn = urn;
        }

        @Override
        public void run() {
            logger.info("Disposed: {}", urn);
        }
    }
}

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

Additionally assets that are garbage collected 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 extends Asset<TextureData> {
    private final TextureResources resources = new TextureResources();

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

    @Override
    protected void doReload(TextureData data) {
        if (resources.id == 0) {
            resources.id = OpenGL.createTexture();
        }
        OpenGL.loadTexture(resources.id, data.bytes);
    }

    private static class TextureResources implements DisposableResource {

        private volatile int id;

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

Any asset that is garbage collected has its disposable resource queued for disposal, which can be triggered as desired - either at an appropriate time like a loading screen, or just every so often:

  assetTypeManager.disposedUnusedAssets();

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 if and only if 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 = assetManager.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 = assetManager.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);
}

Reload assets changed on disk

In Java or Android with API version 26+, it is possible to set up gestalt-asset-core to reload assets when they change on disk by using AutoReloadAssetTypeManager:

  // This is an extended ModuleAwareAssetTypeManager
  AutoReloadAssetTypeManager reloadingTypeManager = new AutoReloadAssetTypeManager();
  // Register asset types, formats, and so forth as usual
  reloadingTypeManager.switchEnvironment(moduleEnvironment);
  // Every frame or as desired, check for changed assets
  reloadingTypeManager.reloadChangedAssets();