Create a Custom Asset Type: Part 3

This part will cover the following topics:

  • How to write an importer

You can find the whole source code in its git repo: example-text-file-asset

Table of Content

Custom importer for text files

In this part, we will add the ability to import a text file into the Engine. To implement an importer, we need the following APIs:

Nameheader fileDescription
tm_asset_io_apifoundation/asset_io.hThis API has the importer interface.
tm_temp_allocator_apifoundation/temp_allocator.hWe will use this to allocate temporary memory.
tm_allocator_apifoundation/allocator.hWe will use this for permanent memory allocations. tm_allocator_api supports a number of different allocators, for example the system allocator. We need this one later when we rewrite our reimport.
tm_path_apifoundation/path.hUsed for splitting and joining file system paths.
tm_api_registry_apifoundation/api_registry.hWe use this to get access to APIs from the API registry.
tm_task_system_apifoundation/task_system.hAllows us to spawm tasks

We include these header files and retrieve the APis from the API registry.

Note: tm_api_registry_api can be retrived from the reg parameter in the tm_load_plugin function. tm_global_api_registry = reg;

The Machinery has a generic interface for asset importers. It requires a bunch of functions to be able to work as intended. The struct we need to implement is called tm_asset_io_i. It requires us to set the following members:

MemberDescription
enabled()Should return true if the importer is active.
can_import()Optional. Should return true for the file extensions that can be imported by this interface.
can_reimport()Optional. Should return true for Truth assets that can be reimported.
importer_extensions_string()Optional. Extensions that can be imported by this interface.
importer_description_string()Optional. Descriptions for the extensions in importer_extensions_string().
import_asset()Implements the import. Since imports can be slow, they are typically implemented as background tasks and this function should return the ID of the background task from tm_task_system_api.

All these members expect a function pointer. Therefore, we need to provide the functionality.

To implement the first functions, we need to do the following steps:

//... other includes
#include <foundation/carray_print.inl>
#include <foundation/string.inl>
#include <foundation/localizer.h>
//... other code
static bool asset_io__enabled(struct tm_asset_io_o *inst) { return true; }
static bool asset_io__can_import(struct tm_asset_io_o *inst,
                                 const char *extension) {
  return tm_strcmp_ignore_case(extension, "txt") == 0;
}
static bool asset_io__can_reimport(struct tm_asset_io_o *inst,
                                   struct tm_the_truth_o *tt,
                                   tm_tt_id_t asset) {
  const tm_tt_id_t object = tm_the_truth_api->get_subobject(
      tt, tm_tt_read(tt, asset), TM_TT_PROP__ASSET__OBJECT);
  return object.type ==
         tm_the_truth_api
             ->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET)
             .u64;
}
static void asset_io__importer_extensions_string(struct tm_asset_io_o *inst,
                                                 char **output,
                                                 struct tm_temp_allocator_i *ta,
                                                 const char *separator) {
  tm_carray_temp_printf(output, ta, "txt");
}
static void
asset_io__importer_description_string(struct tm_asset_io_o *inst, char **output,
                                      struct tm_temp_allocator_i *ta,
                                      const char *separator) {
  tm_carray_temp_printf(output, ta, ".txt");
}

Let us go through them:

  • enabled() returns true because we want the importer to work.
  • asset_io__can_import() compares the extension with the one we want to support.

Note: string.inl which we need to include for tm_strcmp_ignore_case() uses the tm_localizer_api for some of its functionality, that's why we need it.

  • asset_io__can_reimport() checks if the object type matches our type.

TM_TT_PROP__ASSET__OBJECT is the property of the TM_TT_TYPE__ASSET type which holds the object associated with the asset.

The last two functions append .txt to the file extensions and descriptions. Note that the argument output is a carray. We can use tm_carray_temp_printf() to append to that array.

Note: carray_print.h requires tm_sprintf_api`. Therefore, we need to include the right header here.

Import Task Setup

To run the import as a background task we need to queue a task using the tm_task_manager_api from our asset_io__import_asset() function. Task functions take a single void *userdata argument. Since we typically want to pass more than one thing to the task, we put everything the task needs in a struct and pass a pointer to that struct as the userdata. The task function casts this void * to the desired type and can then make use of the data.

The task needs to know the location of the file that is to be imported. It also needs access to some semi-global objects, such as the Truth that the file should be imported to, and an allocator to use for memory allocations. The struct could look like this:

struct task__import_txt {
  uint64_t bytes;
  struct tm_asset_io_import args;
  char file[8];
};

The tm_asset_io_import field will used be copied from the parameter passed to asset_io__import_asset() to the struct.

The function itself looks like this:

static uint64_t asset_io__import_asset(struct tm_asset_io_o *inst,
                                       const char *file,
                                       const struct tm_asset_io_import *args) {
  const uint64_t bytes = sizeof(struct task__import_txt) + strlen(file);
  struct task__import_txt *task = tm_alloc(args->allocator, bytes);
  *task = (struct task__import_txt){
      .bytes = bytes,
      .args = *args,
  };
  strcpy(task->file, file);
  return task_system->run_task(task__import_txt, task, "Import Text File",
                               tm_tt_task_owner(args->tt), false);
}

Important: The task is the memory owner and needs to clean it up at the end of the execution!

The line task_system->run_task(task__import_txt, task, "Import Text File"); queues the task task import_txt() , with the data task, and returns its id. The id can be used to query for when the background task has completed.

Info: For more information on the task system check the documentation.

Import Task Implementation

The import task should import the data and clean up afterwards.

The function signature is:

static void task__import_txt(void *data, uint64_t task_id) {}

We need to cast ptr to our previously defined data type task__import_txt. The task id can be used by the task callback function to provide task progress updates. In this example, we do not use it.

For more information on how to update the status of a task so that it is shown in the editor, see the documentation.

To implement the import we retrieve the data passed in the struct and then implement the import as in the previous chapter. The reimport works the same as the import, except we add the buffer to an existing object instead of creating a new one:

static void task__import_txt(void *data, uint64_t task_id) {
  struct task__import_txt *task = (struct task__import_txt *)data;
  const struct tm_asset_io_import *args = &task->args;
  const char *txt_file = task->file;
  tm_the_truth_o *tt = args->tt;
}

Another thing we should consider is error checking:

  • Does the file exist?
  • Can we read the expected number of bytes from the file?

Since we are running as a background task, we will report any errors through the logging API: tm_logger_api. Errors reported that way will appear in the Console tab of the UI:

static void task__import_txt(void *data, uint64_t task_id) {
  struct task__import_txt *task = (struct task__import_txt *)data;
  const struct tm_asset_io_import *args = &task->args;
  const char *txt_file = task->file;
  tm_the_truth_o *tt = args->tt;
  tm_file_stat_t stat = tm_os_api->file_system->stat(txt_file);
  if (stat.exists) {
  } else {
    tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not find %s \n",
                          txt_file);
  }
  tm_free(args->allocator, task, task->bytes);
}

Now we combine all the knowledge from this chapter and the previous chapter. We need to create a new asset via code for the import, and for the reimport, we need to update an existing file. Before we do all of this, let us first read the file and create the buffer.

static void task__import_txt(void *data, uint64_t task_id) {
  struct task__import_txt *task = (struct task__import_txt *)data;
  const struct tm_asset_io_import *args = &task->args;
  const char *txt_file = task->file;
  tm_the_truth_o *tt = args->tt;
  tm_file_stat_t stat = tm_os_api->file_system->stat(txt_file);
  if (stat.exists) {
    tm_buffers_i *buffers = tm_the_truth_api->buffers(tt);
    void *buffer = buffers->allocate(buffers->inst, stat.size, false);
    tm_file_o f = tm_os_api->file_io->open_input(txt_file);
    const int64_t read = tm_os_api->file_io->read(f, buffer, stat.size);
    tm_os_api->file_io->close(f);
  } else {
    tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not find %s \n",
                          txt_file);
  }
  tm_free(args->allocator, task, task->bytes);
}

After this, we should ensure that the file size matches the size of the read data.

static void task__import_txt(void *data, uint64_t task_id) {
  struct task__import_txt *task = (struct task__import_txt *)data;
  const struct tm_asset_io_import *args = &task->args;
  const char *txt_file = task->file;
  tm_the_truth_o *tt = args->tt;
  tm_file_stat_t stat = tm_os_api->file_system->stat(txt_file);
  if (stat.exists) {
    tm_buffers_i *buffers = tm_the_truth_api->buffers(tt);
    void *buffer = buffers->allocate(buffers->inst, stat.size, false);
    tm_file_o f = tm_os_api->file_io->open_input(txt_file);
    const int64_t read = tm_os_api->file_io->read(f, buffer, stat.size);
    tm_os_api->file_io->close(f);

    if (read == (int64_t)stat.size) {
    } else {
      tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not read %s\n",
                            txt_file);
    }
  } else {
    tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not find %s \n",
                          txt_file);
  }
  tm_free(args->allocator, task, task->bytes);
}

With this out of the way, we can use our knowledge from the last part.

  • How to add an asset via code.

The first step was to create the new object and add the data to it.

const uint32_t buffer_id = buffers->add(buffers->inst, buffer, stat.size, 0);
const tm_tt_type_t plugin_asset_type =
    tm_the_truth_api->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET);
const tm_tt_id_t asset_id = tm_the_truth_api->create_object_of_type(
    tt, plugin_asset_type, TM_TT_NO_UNDO_SCOPE);
tm_the_truth_object_o *asset_obj = tm_the_truth_api->write(tt, asset_id);
tm_the_truth_api->set_buffer(tt, asset_obj, TM_TT_PROP__MY_ASSET__DATA,
                             buffer_id);
tm_the_truth_api->set_string(tt, asset_obj, TM_TT_PROP__MY_ASSET__FILE,
                             txt_file);
tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);

After that, we can use the tm_asset_browser_add_asset_api to add the asset to the asset browser.

struct tm_asset_browser_add_asset_api *add_asset =
    tm_get_api(tm_global_api_registry, tm_asset_browser_add_asset_api);

We are getting the API first, because we do not need it anywhere else than in this case. Then we need to extract the file name of the imported file. You can do this with the path API's tm_path_api->base() function. Be aware this function requires a tm_str_t which you an create from a normal C string (const char*) via tm_str(). To access the underlaying C string again just call .data on the tm_str_t.

tm_str_t represents strings with a char * and a size, instead of just a char *.

This lets you reason about parts of a string, which you are not able to do with standard NULL-terminated strings.

documentation

We want to add the asset to the folder that currently open in the asset browser. We can ask the tm_asset_browser_add_asset_api what the current folder is. Then we decide if want to select the file. At the end we call tm_asset_browser_add_asset_api->add().

Note: If we wanted to, we could add asset labels to the asset and pass them as the last two arguments of the add() function instead of 0, 0.

struct tm_asset_browser_add_asset_api *add_asset =
    tm_get_api(tm_global_api_registry, tm_asset_browser_add_asset_api);

That's it for the import. Before we move on, we need to clean up! No allocation without deallocation!

    tm_free(args->allocator, task, task->bytes);

Info: If you forget to do this, the Engine will inform you that there is a memory leak in the Console log

Now let's bring it all together:

static void task__import_txt(void *data, uint64_t task_id) {
  struct task__import_txt *task = (struct task__import_txt *)data;
  const struct tm_asset_io_import *args = &task->args;
  const char *txt_file = task->file;
  tm_the_truth_o *tt = args->tt;
  tm_file_stat_t stat = tm_os_api->file_system->stat(txt_file);
  if (stat.exists) {
    tm_buffers_i *buffers = tm_the_truth_api->buffers(tt);
    void *buffer = buffers->allocate(buffers->inst, stat.size, false);
    tm_file_o f = tm_os_api->file_io->open_input(txt_file);
    const int64_t read = tm_os_api->file_io->read(f, buffer, stat.size);
    tm_os_api->file_io->close(f);

    if (read == (int64_t)stat.size) {
      const uint32_t buffer_id =
          buffers->add(buffers->inst, buffer, stat.size, 0);
      const tm_tt_type_t plugin_asset_type =
          tm_the_truth_api->object_type_from_name_hash(
              tt, TM_TT_TYPE_HASH__MY_ASSET);
      const tm_tt_id_t asset_id = tm_the_truth_api->create_object_of_type(
          tt, plugin_asset_type, TM_TT_NO_UNDO_SCOPE);
      tm_the_truth_object_o *asset_obj = tm_the_truth_api->write(tt, asset_id);
      tm_the_truth_api->set_buffer(tt, asset_obj, TM_TT_PROP__MY_ASSET__DATA,
                                   buffer_id);
      tm_the_truth_api->set_string(tt, asset_obj, TM_TT_PROP__MY_ASSET__FILE,
                                   txt_file);
      tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);
      const char *asset_name = tm_path_api->base(tm_str(txt_file)).data;
      struct tm_asset_browser_add_asset_api *add_asset =
          tm_get_api(tm_global_api_registry, tm_asset_browser_add_asset_api);
      const tm_tt_id_t current_dir =
          add_asset->current_directory(add_asset->inst, args->ui);
      const bool should_select =
          args->asset_browser.u64 &&
          tm_the_truth_api->version(tt, args->asset_browser) ==
              args->asset_browser_version_at_start;
      add_asset->add(add_asset->inst, current_dir, asset_id, asset_name,
                     args->undo_scope, should_select, args->ui, 0, 0);
    } else {
      tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not read %s\n",
                            txt_file);
    }
  } else {
    tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not find %s \n",
                          txt_file);
  }
  tm_free(args->allocator, task, task->bytes);
}

Enabling Reimport

Our implementation does not yet support reimports. Let us fix this quickly!

tm_asset_io_import has a field called reimport_into of type tm_tt_id_t. When doing a regular import, the value of this field will be (tm_tt_id_t){0}. When reimporting, it will be the ID of the Truth object that we should import into.

To change an existing object instead of creating a new one, we can use the function tm_the_truth_api->retarget_write(). It will make the commit() operation write the changes to an existing object instead of two the new one we just created. After comitting, we can destroy the new (temporary) object:

if (args->reimport_into.u64) {
  tm_the_truth_api->retarget_write(tt, asset_obj, args->reimport_into);
  tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);
  tm_the_truth_api->destroy_object(tt, asset_id, args->undo_scope);
} else {
  tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);
  const char *asset_name = tm_path_api->base(tm_str(txt_file)).data;
  struct tm_asset_browser_add_asset_api *add_asset =
      tm_get_api(tm_global_api_registry, tm_asset_browser_add_asset_api);
  const tm_tt_id_t current_dir =
      add_asset->current_directory(add_asset->inst, args->ui);
  const bool should_select =
      args->asset_browser.u64 &&
      tm_the_truth_api->version(tt, args->asset_browser) ==
          args->asset_browser_version_at_start;
  add_asset->add(add_asset->inst, current_dir, asset_id, asset_name,
                 args->undo_scope, should_select, args->ui, 0, 0);
}

With these changes, the source code now looks as like this:

static void task__import_txt(void *data, uint64_t task_id) {}

Refactor the Custom UI Import Functionality

The last step before in this part of the tutorial is to update what happens when the user picks a new file in the Properties View of the asset. We want this workflow to make use of the asynchronous import functionality we just added to make the user experience smoother. Besides, this will also remove some code duplication.

Let's reuse our import task. We just need to make sure it has all the data it needs. We can check the documentation of tm_asset_io_import to ensure we do not forget anything important.

Besides the name of the file we're importing, we also need:

  • an allocator
  • the Truth to import into
  • the object to reimport into

Now we can write our reimport task code:

const char *file = tm_the_truth_api->get_string(tt, tm_tt_read(tt, object),
                                                TM_TT_PROP__MY_ASSET__FILE);
{
  tm_allocator_i *allocator = tm_allocator_api->system;
  const uint64_t bytes = sizeof(struct task__import_txt) + strlen(file);
  struct task__import_txt *task = tm_alloc(allocator, bytes);
  *task = (struct task__import_txt){
      .bytes = bytes,
      .args = {.allocator = allocator, .tt = tt, .reimport_into = object}};
  strcpy(task->file, file);
  task_system->run_task(task__import_txt, task, "Import Text File",
                        tm_tt_task_owner(args->tt), false);
}

We'll use the system allocator (a global allocator with the same lifetime as the program) to allocate our task, including the bytes needed for the file name string. Remember the layout of our struct:

// -- struct definitions
struct task__import_txt
{
    uint64_t bytes;
    struct tm_asset_io_import args;
    char file[8];
};
// .. other code

We fill out the struct with the needed data, copy the file name, and then ask the task system to run the task:

static float properties__custom_ui(struct tm_properties_ui_args_t *args,
                                   tm_rect_t item_rect, tm_tt_id_t object) {
  tm_the_truth_o *tt = args->tt;
  bool picked = false;
  item_rect.y = tm_properties_view_api->ui_open_path(
      args, item_rect, TM_LOCALIZE_LATER("Import Path"),
      TM_LOCALIZE_LATER("Path that the text file was imported from."), object,
      TM_TT_PROP__MY_ASSET__FILE, "txt", "text files", &picked);
  if (picked) {
    const char *file = tm_the_truth_api->get_string(tt, tm_tt_read(tt, object),
                                                    TM_TT_PROP__MY_ASSET__FILE);
    {
      tm_allocator_i *allocator = tm_allocator_api->system;
      const uint64_t bytes = sizeof(struct task__import_txt) + strlen(file);
      struct task__import_txt *task = tm_alloc(allocator, bytes);
      *task = (struct task__import_txt){
          .bytes = bytes,
          .args = {.allocator = allocator, .tt = tt, .reimport_into = object}};
      strcpy(task->file, file);
      task_system->run_task(task__import_txt, task, "Import Text File",
                            tm_tt_task_owner(args->tt), false);
    }
  }
  return item_rect.y;
}

(For more information on the structure of these functions, please check the previous part)

The End

This is the final part of this walkthrough. By now, you should have a better idea of:

  • How to work with The Truth
  • How to create an asset
  • How to import assets into the Engine
  • How to create a custom UI.

If you want to see a more complex example of an importer, look at the assimp importer example: samples\plugins\assimp.

Full Example of Basic Asset

my_asset.h

#pragma once
#include <foundation/api_types.h>
//... more code
#define TM_TT_TYPE__MY_ASSET "tm_my_asset"
#define TM_TT_TYPE_HASH__MY_ASSET TM_STATIC_HASH("tm_my_asset", 0x1e12ba1f91b99960ULL)

enum
{
    TM_TT_PROP__MY_ASSET__FILE,
    TM_TT_PROP__MY_ASSET__DATA,
};

(Do not forget to run hash.exe when you create a TM_STATIC_HASH)

my_asset.c

// -- api's
static struct tm_api_registry_api *tm_global_api_registry;
static struct tm_the_truth_api *tm_the_truth_api;
static struct tm_properties_view_api *tm_properties_view_api;
static struct tm_os_api *tm_os_api;
static struct tm_path_api *tm_path_api;
static struct tm_temp_allocator_api *tm_temp_allocator_api;
static struct tm_logger_api *tm_logger_api;
static struct tm_localizer_api *tm_localizer_api;
static struct tm_asset_io_api *tm_asset_io_api;
static struct tm_task_system_api *task_system;
static struct tm_allocator_api *tm_allocator_api;
static struct tm_sprintf_api *tm_sprintf_api;

// -- inlcudes

#include <foundation/api_registry.h>
#include <foundation/asset_io.h>
#include <foundation/buffer.h>
#include <foundation/carray_print.inl>
#include <foundation/localizer.h>
#include <foundation/log.h>
#include <foundation/macros.h>
#include <foundation/os.h>
#include <foundation/path.h>
#include <foundation/sprintf.h>
#include <foundation/string.inl>
#include <foundation/task_system.h>
#include <foundation/temp_allocator.h>
#include <foundation/the_truth.h>
#include <foundation/the_truth_assets.h>
#include <foundation/undo.h>

#include <plugins/editor_views/asset_browser.h>
#include <plugins/editor_views/properties.h>

#include "txt.h"
struct task__import_txt
{
    uint64_t bytes;
    struct tm_asset_io_import args;
    char file[8];
};
/////
// -- functions:
////
// --- importer
static void task__import_txt(void *data, uint64_t task_id)
{
    struct task__import_txt *task = (struct task__import_txt *)data;
    const struct tm_asset_io_import *args = &task->args;
    const char *txt_file = task->file;
    tm_the_truth_o *tt = args->tt;
    tm_file_stat_t stat = tm_os_api->file_system->stat(txt_file);
    if (stat.exists)
    {
        tm_buffers_i *buffers = tm_the_truth_api->buffers(tt);
        void *buffer = buffers->allocate(buffers->inst, stat.size, false);
        tm_file_o f = tm_os_api->file_io->open_input(txt_file);
        const int64_t read = tm_os_api->file_io->read(f, buffer, stat.size);
        tm_os_api->file_io->close(f);

        if (read == (int64_t)stat.size)
        {
            const uint32_t buffer_id = buffers->add(buffers->inst, buffer, stat.size, 0);
            const tm_tt_type_t plugin_asset_type = tm_the_truth_api->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET);
            const tm_tt_id_t asset_id = tm_the_truth_api->create_object_of_type(tt, plugin_asset_type, TM_TT_NO_UNDO_SCOPE);
            tm_the_truth_object_o *asset_obj = tm_the_truth_api->write(tt, asset_id);
            tm_the_truth_api->set_buffer(tt, asset_obj, TM_TT_PROP__MY_ASSET__DATA, buffer_id);
            tm_the_truth_api->set_string(tt, asset_obj, TM_TT_PROP__MY_ASSET__FILE, txt_file);
            if (args->reimport_into.u64)
            {
                tm_the_truth_api->retarget_write(tt, asset_obj, args->reimport_into);
                tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);
                tm_the_truth_api->destroy_object(tt, asset_id, args->undo_scope);
            }
            else
            {
                tm_the_truth_api->commit(tt, asset_obj, args->undo_scope);
                const char *asset_name = tm_path_api->base(tm_str(txt_file)).data;
                struct tm_asset_browser_add_asset_api *add_asset = tm_get_api(tm_global_api_registry, tm_asset_browser_add_asset_api);
                const tm_tt_id_t current_dir = add_asset->current_directory(add_asset->inst, args->ui);
                const bool should_select = args->asset_browser.u64 && tm_the_truth_api->version(tt, args->asset_browser) == args->asset_browser_version_at_start;
                add_asset->add(add_asset->inst, current_dir, asset_id, asset_name, args->undo_scope, should_select, args->ui, 0, 0);
            }
        }
        else
        {
            tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not read %s\n", txt_file);
        }
    }
    else
    {
        tm_logger_api->printf(TM_LOG_TYPE_INFO, "import txt:could not find %s \n", txt_file);
    }
    tm_free(args->allocator, task, task->bytes);
}

static bool asset_io__enabled(struct tm_asset_io_o *inst)
{
    return true;
}
static bool asset_io__can_import(struct tm_asset_io_o *inst, const char *extension)
{
    return tm_strcmp_ignore_case(extension, "txt") == 0;
}
static bool asset_io__can_reimport(struct tm_asset_io_o *inst, struct tm_the_truth_o *tt, tm_tt_id_t asset)
{
    const tm_tt_id_t object = tm_the_truth_api->get_subobject(tt, tm_tt_read(tt, asset), TM_TT_PROP__ASSET__OBJECT);
    return object.type == tm_the_truth_api->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET).u64;
}
static void asset_io__importer_extensions_string(struct tm_asset_io_o *inst, char **output, struct tm_temp_allocator_i *ta, const char *separator)
{
    tm_carray_temp_printf(output, ta, "txt");
}
static void asset_io__importer_description_string(struct tm_asset_io_o *inst, char **output, struct tm_temp_allocator_i *ta, const char *separator)
{
    tm_carray_temp_printf(output, ta, ".txt");
}
static uint64_t asset_io__import_asset(struct tm_asset_io_o *inst, const char *file, const struct tm_asset_io_import *args)
{
    const uint64_t bytes = sizeof(struct task__import_txt) + strlen(file);
    struct task__import_txt *task = tm_alloc(args->allocator, bytes);
    *task = (struct task__import_txt){
        .bytes = bytes,
        .args = *args,
    };
    strcpy(task->file, file);
    return task_system->run_task(task__import_txt, task, "Import Text File", tm_tt_task_owner(args->tt), false);
}
static struct tm_asset_io_i txt_asset_io = {
    .enabled = asset_io__enabled,
    .can_import = asset_io__can_import,
    .can_reimport = asset_io__can_reimport,
    .importer_extensions_string = asset_io__importer_extensions_string,
    .importer_description_string = asset_io__importer_description_string,
    .import_asset = asset_io__import_asset};

// -- asset on its own

//custom ui
static float properties__custom_ui(struct tm_properties_ui_args_t *args, tm_rect_t item_rect, tm_tt_id_t object)
{
    tm_the_truth_o *tt = args->tt;
    bool picked = false;
    item_rect.y = tm_properties_view_api->ui_open_path(args, item_rect, TM_LOCALIZE_LATER("Import Path"), TM_LOCALIZE_LATER("Path that the text file was imported from."), object, TM_TT_PROP__MY_ASSET__FILE, "txt", "text files", &picked);
    if (picked)
    {
        const char *file = tm_the_truth_api->get_string(tt, tm_tt_read(tt, object), TM_TT_PROP__MY_ASSET__FILE);
        {
            tm_allocator_i *allocator = tm_allocator_api->system;
            const uint64_t bytes = sizeof(struct task__import_txt) + strlen(file);
            struct task__import_txt *task = tm_alloc(allocator, bytes);
            *task = (struct task__import_txt){
                .bytes = bytes,
                .args = {
                    .allocator = allocator,
                    .tt = tt,
                    .reimport_into = object}};
            strcpy(task->file, file);
            task_system->run_task(task__import_txt, task, "Import Text File", tm_tt_task_owner(args->tt), false);
        }
    }
    return item_rect.y;
}
// -- create truth type
static void create_truth_types(struct tm_the_truth_o *tt)
{
    static tm_the_truth_property_definition_t my_asset_properties[] = {
        {"import_path", TM_THE_TRUTH_PROPERTY_TYPE_STRING},
        {"data", TM_THE_TRUTH_PROPERTY_TYPE_BUFFER},
    };
    const tm_tt_type_t type = tm_the_truth_api->create_object_type(tt, TM_TT_TYPE__MY_ASSET, my_asset_properties, TM_ARRAY_COUNT(my_asset_properties));
    tm_tt_set_aspect(tt, type, tm_tt_assets_file_extension_aspect_i, "txt");
    static tm_properties_aspect_i properties_aspect = {
        .custom_ui = properties__custom_ui,
    };
    tm_tt_set_aspect(tt, type, tm_properties_aspect_i, &properties_aspect);
}

// -- asset browser regsiter interface
static tm_tt_id_t asset_browser_create(struct tm_asset_browser_create_asset_o *inst, tm_the_truth_o *tt, tm_tt_undo_scope_t undo_scope)
{
    const tm_tt_type_t type = tm_the_truth_api->object_type_from_name_hash(tt, TM_TT_TYPE_HASH__MY_ASSET);
    return tm_the_truth_api->create_object_of_type(tt, type, undo_scope);
}
static tm_asset_browser_create_asset_i asset_browser_create_my_asset = {
    .menu_name = TM_LOCALIZE_LATER("New Text File"),
    .asset_name = TM_LOCALIZE_LATER("New Text File"),
    .create = asset_browser_create,
};

// -- load plugin
TM_DLL_EXPORT void tm_load_plugin(struct tm_api_registry_api *reg, bool load)
{
    tm_the_truth_api = tm_get_api(reg, tm_the_truth_api);
    tm_properties_view_api = tm_get_api(reg, tm_properties_view_api);
    tm_os_api = tm_get_api(reg, tm_os_api);
    tm_path_api = tm_get_api(reg, tm_path_api);
    tm_temp_allocator_api = tm_get_api(reg, tm_temp_allocator_api);
    tm_allocator_api = tm_get_api(reg, tm_allocator_api);
    tm_logger_api = tm_get_api(reg, tm_logger_api);
    tm_localizer_api = tm_get_api(reg, tm_localizer_api);
    tm_asset_io_api = tm_get_api(reg, tm_asset_io_api);
    task_system = tm_get_api(reg, tm_task_system_api);
    tm_sprintf_api = tm_get_api(reg, tm_sprintf_api);
    tm_global_api_registry = reg;
    if (load)
        tm_asset_io_api->add_asset_io(&txt_asset_io);
    else
        tm_asset_io_api->remove_asset_io(&txt_asset_io);
    tm_add_or_remove_implementation(reg, load, tm_the_truth_create_types_i, create_truth_types);
    tm_add_or_remove_implementation(reg, load, tm_asset_browser_create_asset_i, &asset_browser_create_my_asset);
}