From c9553fb46053050bf2f78d2f8b6e0304eddfbbe3 Mon Sep 17 00:00:00 2001
From: Joseph Walton-Rivers <joseph@walton-rivers.uk>
Date: Sun, 20 Nov 2022 12:58:09 +0000
Subject: [PATCH] adapt the old loader interface to make use of packed database

---
 demo/demo/main.cpp                     |   2 +-
 demo/demo/models/viewer.cpp            |   6 +-
 demo/demo/rollball.cpp                 |   4 +-
 demo/demo/topdown.cpp                  |   2 +-
 fggl/assets/CMakeLists.txt             |   1 +
 fggl/assets/types.cpp                  |  50 +++++++++
 fggl/audio/openal/audio.cpp            |  11 +-
 fggl/audio/openal/module.cpp           |   2 +-
 fggl/data/assimp/module.cpp            | 144 +++++++++++++++++--------
 fggl/entity/loader/loader.cpp          |  12 +--
 fggl/entity/module.cpp                 |   4 +-
 fggl/gfx/ogl4/meshes.cpp               |  18 ++--
 fggl/gfx/ogl4/models.cpp               |   4 +-
 fggl/gfx/ogl4/module.cpp               |   4 +-
 include/fggl/assets/loader.hpp         | 118 +++++++++++++++-----
 include/fggl/assets/manager.hpp        |  14 +--
 include/fggl/assets/packed/adapter.hpp |  65 +++++++++--
 include/fggl/assets/packed/direct.hpp  |  17 ---
 include/fggl/assets/types.hpp          |  51 ++++++++-
 include/fggl/audio/openal/audio.hpp    |   4 +-
 include/fggl/entity/loader/loader.hpp  |   8 +-
 include/fggl/gfx/ogl4/fallback.hpp     |   8 +-
 include/fggl/gfx/ogl4/models.hpp       |   4 +-
 include/fggl/mesh/mesh.hpp             |  24 +++--
 include/fggl/util/guid.hpp             |  21 ++++
 25 files changed, 435 insertions(+), 163 deletions(-)
 create mode 100644 fggl/assets/types.cpp

diff --git a/demo/demo/main.cpp b/demo/demo/main.cpp
index 109fd0e..fbb3a1a 100644
--- a/demo/demo/main.cpp
+++ b/demo/demo/main.cpp
@@ -108,7 +108,7 @@ int main(int argc, const char* argv[]) {
 	{
 		auto* assets = app.service<fggl::assets::AssetManager>();
 		auto* loader = app.service<fggl::assets::Loader>();
-		loader->load("ui/click.ogg", fggl::audio::OGG_VORBIS, assets);
+		loader->load("ui/click.ogg", fggl::audio::ASSET_CLIP_SHORT, assets);
 	}
 
 	auto* windowing = app.service<fggl::display::WindowService>();
diff --git a/demo/demo/models/viewer.cpp b/demo/demo/models/viewer.cpp
index e073c29..277cbae 100644
--- a/demo/demo/models/viewer.cpp
+++ b/demo/demo/models/viewer.cpp
@@ -38,7 +38,8 @@ namespace demo {
 		manager.add<fggl::math::Transform>(model);
 		auto& mesh = manager.add<fggl::mesh::StaticMultiMesh3D>(model);
 
-		auto* meshData = assets->get<fggl::mesh::MultiMesh3D>("backpack/backpack.obj");
+		auto assetRef = fggl::assets::make_asset_id_rt("viewer", "backpack/backpack.obj" );
+		auto* meshData = assets->get<fggl::mesh::MultiMesh3D>( assetRef );
 		if ( meshData == nullptr ) {
 			fggl::debug::warning("loading model did not work!");
 		} else {
@@ -113,7 +114,8 @@ namespace demo {
 		auto *loader = owner().service<fggl::assets::Loader>();
 		auto *manager = owner().service<fggl::assets::AssetManager>();
 
-		loader->load("backpack/backpack.obj", fggl::data::models::ASSIMP_MODEL, manager, "viewer");
+		auto backPackRef = fggl::assets::make_asset_id_rt("viewer", "backpack/backpack.obj");
+		loader->loadChain(backPackRef, manager);
 
 		// create camera
 		setup_camera(world());
diff --git a/demo/demo/rollball.cpp b/demo/demo/rollball.cpp
index c594b32..24aa3da 100644
--- a/demo/demo/rollball.cpp
+++ b/demo/demo/rollball.cpp
@@ -87,8 +87,8 @@ namespace demo {
 
 		// asset loader
 		auto* assetLoader = m_owner.service<fggl::assets::Loader>();
-		assetLoader->load("rollball.yml", fggl::entity::PROTOTYPE_ASSET, entFactory);
-		assetLoader->load("rollball.yml", fggl::entity::SCENE, this);
+		assetLoader->load("rollball.yml", fggl::entity::ENTITY_PROTOTYPE, entFactory);
+		assetLoader->load("rollball.yml", fggl::entity::ENTITY_SCENE, this);
 
 		// collectable callbacks
 		/*auto* collectableCallbacks = world().get<fggl::phys::CollisionCallbacks>(prefabs.collectable);
diff --git a/demo/demo/topdown.cpp b/demo/demo/topdown.cpp
index 0ad73c8..8069a3c 100644
--- a/demo/demo/topdown.cpp
+++ b/demo/demo/topdown.cpp
@@ -132,7 +132,7 @@ void TopDown::activate() {
 
 	fggl::debug::log(fggl::debug::Level::info, "TopDown::activate()");
 	auto* assetLoader = m_owner.service<fggl::assets::Loader>();
-	assetLoader->load("topdown.yml", fggl::entity::PROTOTYPE_ASSET);
+	assetLoader->load("topdown.yml", fggl::entity::ENTITY_PROTOTYPE);
 
 	auto* factory = m_owner.service<fggl::entity::EntityFactory>();
 	//fggl::ecs3::load_prototype_file(world(), *storage, "topdown.yml");
diff --git a/fggl/assets/CMakeLists.txt b/fggl/assets/CMakeLists.txt
index d39a513..1ce3b6d 100644
--- a/fggl/assets/CMakeLists.txt
+++ b/fggl/assets/CMakeLists.txt
@@ -1,4 +1,5 @@
 target_sources(fggl PRIVATE
         module.cpp
+        types.cpp
         packed/module.cpp
 )
\ No newline at end of file
diff --git a/fggl/assets/types.cpp b/fggl/assets/types.cpp
new file mode 100644
index 0000000..ef1a812
--- /dev/null
+++ b/fggl/assets/types.cpp
@@ -0,0 +1,50 @@
+/*
+ * This file is part of FGGL.
+ *
+ * FGGL is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any
+ * later version.
+ *
+ * FGGL is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
+ * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with FGGL.
+ * If not, see <https://www.gnu.org/licenses/>.
+ */
+
+//
+// Created by webpigeon on 20/11/22.
+//
+
+#include "fggl/assets/types.hpp"
+
+namespace fggl::assets {
+	AssetID make_asset_id_rt(const std::string &pack, const std::string &path, const std::string &view) {
+		auto fullPath = pack + ":" + path;
+		if (!view.empty()) {
+			fullPath += "[" + view + "]";
+		}
+
+		#ifndef NDEBUG
+		util::internString(fullPath.c_str());
+		#endif
+
+		auto hash = util::hash_fnv1a_64(fullPath.c_str());
+		return AssetID::make(hash);
+	}
+
+	AssetID asset_from_user(const std::string &input, const std::string &pack) {
+		if (input.find(':') != 0) {
+			// probably fully qualified
+			#ifndef NDEBUG
+			util::internString(input.c_str());
+			#endif
+			auto hash = util::hash_fnv1a_64(input.c_str());
+			return AssetID::make(hash);
+		}
+
+		// probably local
+		return make_asset_id_rt(pack, input);
+	}
+
+}
\ No newline at end of file
diff --git a/fggl/audio/openal/audio.cpp b/fggl/audio/openal/audio.cpp
index d763a6f..3eb308b 100644
--- a/fggl/audio/openal/audio.cpp
+++ b/fggl/audio/openal/audio.cpp
@@ -35,13 +35,13 @@
 
 namespace fggl::audio::openal {
 
-	assets::AssetRefRaw load_vorbis(assets::Loader* /*loader*/, const assets::AssetGUID &guid, const assets::AssetData &data, void* userPtr) {
-		auto *filePath = std::get<assets::AssetPath *>(data);
+	assets::AssetRefRaw load_vorbis(assets::Loader* /*loader*/, const assets::AssetID &guid, const assets::LoaderContext &data, void* userPtr) {
 		auto *manager = static_cast<assets::AssetManager*>(userPtr);
+		auto filePath = data.assetPath;
 
 		// vorbis
 		auto* clip = new AudioClipShort();
-		clip->sampleCount = stb_vorbis_decode_filename( filePath->c_str(), &clip->channels, &clip->sampleRate, &clip->data);
+		clip->sampleCount = stb_vorbis_decode_filename( filePath.c_str(), &clip->channels, &clip->sampleRate, &clip->data);
 		debug::info("clip loaded: channels={}, sampleRate={}, sampleCount={}", clip->channels, clip->sampleRate, clip->sampleCount);
 
 		if ( clip->sampleCount == -1 ) {
@@ -65,7 +65,7 @@ namespace fggl::audio::openal {
 		return false;
 	}
 
-	assets::AssetTypeID check_vorbis(std::filesystem::path path) {
+	assets::AssetTypeID check_vorbis(std::filesystem::path path ) {
 		if ( path.extension() != ".ogg" ) {
 			return assets::INVALID_ASSET_TYPE;
 		}
@@ -96,7 +96,8 @@ namespace fggl::audio::openal {
 		debug::info("beginning audio: {}", filename);
 
 		// load audio clip into temp storage
-		auto* clip = m_assets->get<AudioClipShort>(filename);
+		auto assetRef = assets::make_asset_id_rt("core", filename);
+		auto* clip = m_assets->get<AudioClipShort>(assetRef);
 		if ( clip == nullptr ) {
 			debug::warning("audio asset requested, but not loaded: {}", filename);
 			return;
diff --git a/fggl/audio/openal/module.cpp b/fggl/audio/openal/module.cpp
index 89aeb85..964d5c1 100644
--- a/fggl/audio/openal/module.cpp
+++ b/fggl/audio/openal/module.cpp
@@ -26,7 +26,7 @@ namespace fggl::audio {
 
 			{
 				auto *assetLoader = services.get<assets::Loader>();
-				assetLoader->setFactory(OGG_VORBIS, openal::load_vorbis, assets::LoadType::PATH);
+				assetLoader->setFactory( ASSET_CLIP_SHORT, openal::load_vorbis, assets::LoadType::PATH);
 			}
 
 			{
diff --git a/fggl/data/assimp/module.cpp b/fggl/data/assimp/module.cpp
index 4ae1bb6..afa6b5f 100644
--- a/fggl/data/assimp/module.cpp
+++ b/fggl/data/assimp/module.cpp
@@ -41,7 +41,7 @@ namespace fggl::data::models {
 		return {vec.x, vec.y};
 	}
 
-	static void process_mesh(mesh::Mesh3D& mesh, aiMesh* assimpMesh, const aiScene* scene) {
+	static void process_mesh(mesh::Mesh3D& mesh, aiMesh* assimpMesh, const aiScene* scene, const std::vector<assets::AssetID>& assets) {
 		assert( assimpMesh != nullptr );
 		assert( scene != nullptr );
 
@@ -71,30 +71,28 @@ namespace fggl::data::models {
 
 		// process material
 		if ( assimpMesh->mMaterialIndex >= 0) {
-			mesh.material = "_MAT_" + std::to_string(assimpMesh->mMaterialIndex);
+			mesh.material = assets[assimpMesh->mMaterialIndex];
 		}
 	}
 
-	static void process_node(mesh::MultiMesh3D& mesh, aiNode* node, const aiScene* scene) {
+	static void process_node(mesh::MultiMesh3D& mesh, aiNode* node, const aiScene* scene, const std::vector<assets::AssetID>& assets) {
 		for ( auto idx = 0U; idx < node->mNumMeshes; ++idx ) {
 			auto *assimpMesh = scene->mMeshes[ node->mMeshes[idx] ];
 
 			// process assimp submesh
 			mesh::Mesh3D meshData;
-			process_mesh(meshData, assimpMesh, scene);
+			process_mesh(meshData, assimpMesh, scene, assets);
 
 			mesh.meshes.push_back(meshData);
 		}
 
 		for ( auto idx = 0U; idx < node->mNumChildren; ++idx) {
-			process_node(mesh, node->mChildren[idx], scene);
+			process_node(mesh, node->mChildren[idx], scene, assets);
 		}
 	}
 
-	static void process_material(const assets::AssetGUID& guid, aiMaterial* assimpMat, assets::Loader* loader, assets::AssetManager* manager) {
 
-		auto lastSlash = guid.rfind('/');
-		auto prefix = guid.substr(0, lastSlash);
+	static void process_material(const assets::AssetID& guid, aiMaterial* assimpMat, assets::Loader* loader, assets::AssetManager* manager, const std::filesystem::path& prefix, const std::string& pack) {
 		mesh::Material* material = new mesh::Material();
 
 		// for each material, calculate what it's name would be then request it
@@ -102,45 +100,60 @@ namespace fggl::data::models {
 			aiString texName;
 			assimpMat->GetTexture( aiTextureType_DIFFUSE, i, &texName );
 
-			auto textureGuid = prefix + "/" + texName.C_Str();
-			loader->load(textureGuid, DATA_TEXTURE2D, manager);
+			auto texID = assets::make_asset_id_rt(pack, prefix / texName.C_Str() );
+			if ( !manager->has(texID) ) {
+				debug::info("triggered JIT upload for {}, use the chain loader", texID);
+				loader->load(texID, manager);
+			}
 
-			material->diffuseTextures.push_back(textureGuid);
+			material->diffuseTextures.push_back(texID);
 		}
 
 		for ( unsigned int i = 0U ; i < assimpMat->GetTextureCount(aiTextureType_NORMALS); ++i ) {
 			aiString texName;
 			assimpMat->GetTexture( aiTextureType_NORMALS, i, &texName );
 
-			auto textureGuid = prefix + "/" + texName.C_Str();
-			loader->load(textureGuid, DATA_TEXTURE2D, manager);
+			auto texID = assets::make_asset_id_rt(pack, prefix / texName.C_Str() );
+			if ( !manager->has(texID) ) {
+				debug::info("triggered JIT upload for {}, use the chain loader", texID);
+				loader->load(texID, manager);
+			}
 
-			material->normalTextures.push_back(textureGuid);
+			material->normalTextures.push_back(texID);
 		}
 
 		for ( unsigned int i = 0U ; i < assimpMat->GetTextureCount(aiTextureType_SPECULAR); ++i ) {
 			aiString texName;
 			assimpMat->GetTexture( aiTextureType_SPECULAR, i, &texName );
 
-			auto textureGuid = prefix + "/" + texName.C_Str();
-			loader->load(textureGuid, DATA_TEXTURE2D, manager);
+			auto texID = assets::make_asset_id_rt(pack, prefix / texName.C_Str() );
+			if ( !manager->has(texID) ) {
+				debug::info("triggered JIT upload for {}, use the chain loader", texID);
+				loader->load(texID, manager);
+			}
 
-			material->specularTextures.push_back(textureGuid);
+			material->specularTextures.push_back(texID);
 		}
 
 		manager->set( guid, material );
 	}
 
 
-	static assets::AssetRefRaw load_assimp_model(assets::Loader* loader, const assets::AssetGUID& guid, const assets::AssetData& data, void* userPtr) {
-		auto *filePath = std::get<assets::AssetPath *>(data);
+	static assets::AssetRefRaw load_assimp_model(assets::Loader* loader, const assets::AssetID& guid, const assets::LoaderContext& data, void* userPtr) {
+//		auto *filePath = std::get<assets::AssetPath *>(data);
 		assets::AssetManager* manager = (assets::AssetManager*)userPtr;
+		auto filePath = data.assetPath;
+
+		if ( manager->has(guid) ) {
+			// asset in DB, what do you want me to do?
+			return nullptr;
+		}
 
 		// assimp stuff
 		Assimp::Importer importer;
 
 		// import the scene from disk
-		const aiScene *scene = importer.ReadFile(filePath->c_str(), aiProcess_Triangulate | aiProcess_FlipUVs);
+		const aiScene *scene = importer.ReadFile(filePath.c_str(), aiProcess_Triangulate | aiProcess_FlipUVs);
 		if ( !scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode ) {
 			debug::warning("unable to load required model asset");
 			return nullptr;
@@ -148,25 +161,29 @@ namespace fggl::data::models {
 
 		debug::debug("Processing assimp mesh, {} meshes, {} materials, {} textures", scene->mNumMeshes, scene->mNumMaterials, scene->mNumTextures);
 
+		// calculate the mapping from material => asset mappings
+		auto parentRel = std::filesystem::relative( filePath.parent_path(), filePath.parent_path().parent_path() );
+		std::vector<assets::AssetID> matAssets;
+		for ( uint64_t idx = 0; idx < scene->mNumMaterials; ++idx ) {
+
+			// FIXME to calculate this properly we need to know the pack name, root and relative path
+			aiString matName = scene->mMaterials[idx]->GetName();
+			auto matRel = parentRel / matName.C_Str();
+			auto matIDMinusPack = assets::make_asset_id_rt( data.pack, matRel);
+
+			matAssets.push_back(matIDMinusPack);
+		}
+
 		// now we can try importing the mesh data
 		mesh::MultiMesh3D* packedMesh = new mesh::MultiMesh3D();
-		process_node( *packedMesh, scene->mRootNode, scene);
-
-		std::map<std::string, std::string> materials;
-		{
-			// prefix the materials if required
-			for (auto& mesh : packedMesh->meshes) {
-				if (!mesh.material.empty()) {
-					mesh.material = guid + mesh.material;
-				}
-			}
+		process_node( *packedMesh, scene->mRootNode, scene, matAssets);
 
-			// try and decode the material data
-			for (auto idx = 0; idx < scene->mNumMaterials; ++idx) {
-				auto matName =  guid +"_MAT_" + std::to_string(idx);
-				process_material(matName, scene->mMaterials[idx], loader, manager);
-				packedMesh->materials.push_back(matName);
-			}
+		// now we try importing the materials
+		for (auto idx = 0u; idx < scene->mNumMaterials; ++idx ) {
+			auto *mat = scene->mMaterials[idx];
+			auto matID = matAssets[idx];
+
+			process_material(matID, mat, loader, manager, parentRel, data.pack);
 		}
 
 		// FIXME asset loading system needs rework, this is bonkers.
@@ -174,15 +191,20 @@ namespace fggl::data::models {
 		return nullptr;
 	}
 
-	static assets::AssetRefRaw load_assimp_texture(assets::Loader* loader, const assets::AssetGUID& guid, const assets::AssetData& data, void* userPtr) {
-		auto *filePath = std::get<assets::AssetPath *>(data);
+	static assets::AssetRefRaw load_assimp_texture(assets::Loader* loader, const assets::AssetID& guid, const assets::LoaderContext& data, void* userPtr) {
 		assets::AssetManager* manager = (assets::AssetManager*)userPtr;
 
+		if ( manager->has(guid) ) {
+			// already loaded.
+			return nullptr;
+		}
+
+		auto filePath = data.assetPath;
 		stbi_set_flip_vertically_on_load(true);
 
 		//load the texture data into memory
 		data::Texture2D* image = new data::Texture2D();
-		image->data = stbi_load(filePath->c_str(), &image->size.x, &image->size.y, &image->channels, 3);
+		image->data = stbi_load(filePath.c_str(), &image->size.x, &image->size.y, &image->channels, 3);
 
 		if ( image->data == nullptr ) {
 			debug::warning("error reading texture: {}", stbi_failure_reason());
@@ -230,10 +252,6 @@ namespace fggl::data::models {
 		return assets::INVALID_ASSET_TYPE;
 	}
 
-	static bool load_model_assimp(std::filesystem::path filePath, assets::MemoryBlock& block) {
-		return false; //TODO
-	}
-
 	static assets::AssetTypeID is_model_assimp(std::filesystem::path filePath) {
 		if ( filePath.extension() == ".obj" ){
 			return MODEL_MULTI3D;
@@ -242,16 +260,50 @@ namespace fggl::data::models {
 		return assets::INVALID_ASSET_TYPE;
 	}
 
+	static bool extract_requirements(const std::string& packName, std::filesystem::path packRoot, assets::ResourceRecord& rr) {
+		Assimp::Importer importer;
+		const aiScene *scene = importer.ReadFile( rr.m_path, aiProcess_Triangulate | aiProcess_FlipUVs);
+		if ( !scene || scene->mFlags & AI_SCENE_FLAGS_INCOMPLETE || !scene->mRootNode ) {
+			debug::warning("unable to load required model asset");
+			return false;
+		}
+
+		// we want to find out about dependencies
+		auto baseDir = std::filesystem::relative( rr.m_path.parent_path(), packRoot );
+		for ( auto idx = 0U; idx < scene->mNumMaterials; ++idx) {
+			const auto *mat = scene->mMaterials[idx];
+
+			std::array matTypes { aiTextureType_DIFFUSE, aiTextureType_NORMALS, aiTextureType_SPECULAR };
+			for ( auto& matType : matTypes ) {
+				const auto diffCount = mat->GetTextureCount(matType);
+				for (auto idx2 = 0U; idx2 < diffCount; ++idx2) {
+					aiString texName;
+					mat->GetTexture(matType, idx2, &texName);
+
+					auto assetPath = baseDir / texName.C_Str();
+					auto assetRequired = assets::make_asset_id_rt(packName, assetPath);
+					rr.m_requires.push_back(assetRequired);
+				}
+			}
+
+		}
+
+		return true;
+	}
+
 	bool AssimpModule::factory(modules::ModuleService service, modules::Services &serviceManager) {
 		if ( service == MODEL_PROVIDER ) {
 			auto* assetLoader = serviceManager.get<assets::Loader>();
-			assetLoader->setFactory( ASSIMP_MODEL, load_assimp_model, assets::LoadType::PATH );
-			assetLoader->setFactory( DATA_TEXTURE2D, load_assimp_texture, assets::LoadType::PATH );
+			assetLoader->setFactory( MODEL_MULTI3D, load_assimp_model, assets::LoadType::PATH );
+			assetLoader->setFactory( TEXTURE_RGBA, load_assimp_texture, assets::LoadType::PATH );
 
+			// new loading system
 			auto* checkin = serviceManager.get<assets::CheckinAdapted>();
 			checkin->setLoader( MIME_JPG, load_tex_stb, is_tex_stb );
 			checkin->setLoader( MIME_PNG, load_tex_stb, is_tex_stb );
-			checkin->setLoader( MIME_OBJ, load_model_assimp, is_model_assimp );
+
+			checkin->setLoader( MIME_OBJ, assets::NEEDS_CHECKIN, is_model_assimp );
+			checkin->setProcessor( MIME_OBJ, extract_requirements);
 		}
 	}
 
diff --git a/fggl/entity/loader/loader.cpp b/fggl/entity/loader/loader.cpp
index ba19af3..076aa57 100644
--- a/fggl/entity/loader/loader.cpp
+++ b/fggl/entity/loader/loader.cpp
@@ -22,14 +22,14 @@
 
 namespace fggl::entity {
 
-	assets::AssetRefRaw load_scene(assets::Loader* loader, const assets::AssetGUID& asset, assets::AssetData data, void* ptr) {
-		auto *filePath = std::get<assets::AssetPath *>(data);
+	assets::AssetRefRaw load_scene(assets::Loader* loader, const assets::AssetID& asset, const assets::LoaderContext& data, void* ptr) {
 		scenes::Game* gamePtr = (scenes::Game*)ptr;
+		auto filePath = data.assetPath;
 
 		// load assets
 		auto* entityFactory = gamePtr->owner().service<EntityFactory>();
 
-		auto nodes = YAML::LoadAllFromFile(filePath->c_str());
+		auto nodes = YAML::LoadAllFromFile(filePath.c_str());
 		for (const auto& node : nodes) {
 			auto scene = node["scene"];
 			if ( !scene ) {
@@ -77,11 +77,11 @@ namespace fggl::entity {
 		return nullptr;
 	}
 
-	assets::AssetRefRaw load_prototype(assets::Loader* loader, const assets::AssetGUID &guid, assets::AssetData data, EntityFactory* factory) {
-		auto *filePath = std::get<assets::AssetPath *>(data);
+	assets::AssetRefRaw load_prototype(assets::Loader* loader, const assets::AssetID &guid, const assets::LoaderContext& data, EntityFactory* factory) {
+		auto filePath = data.assetPath;
 
 		// We need to process the prototypes, and load them into the asset system.
-		auto nodes = YAML::LoadAllFromFile(filePath->c_str());
+		auto nodes = YAML::LoadAllFromFile(filePath.c_str());
 		for (const auto &node : nodes) {
 			auto prefabs = node["prefabs"];
 
diff --git a/fggl/entity/module.cpp b/fggl/entity/module.cpp
index 22cf0a0..5ebda82 100644
--- a/fggl/entity/module.cpp
+++ b/fggl/entity/module.cpp
@@ -44,14 +44,14 @@ namespace fggl::entity {
 
 			// we are responsible for prefabs...
 			auto *assetLoader = services.get<assets::Loader>();
-			assetLoader->setFactory(PROTOTYPE_ASSET, [factory](assets::Loader* loader, const assets::AssetGUID &a, assets::AssetData b, void* ptr) {
+			assetLoader->setFactory(ENTITY_PROTOTYPE, [factory](assets::Loader* loader, const assets::AssetID& a, assets::LoaderContext b, void* ptr) {
 				EntityFactory* facPtr = factory;
 				if ( ptr != nullptr ) {
 					facPtr = (EntityFactory*)ptr;
 				}
 				return load_prototype(loader, a, b, facPtr);
 			}, assets::LoadType::PATH);
-			assetLoader->setFactory(SCENE, load_scene, assets::LoadType::PATH);
+			assetLoader->setFactory(ENTITY_SCENE, load_scene, assets::LoadType::PATH);
 
 			return true;
 		}
diff --git a/fggl/gfx/ogl4/meshes.cpp b/fggl/gfx/ogl4/meshes.cpp
index 961c4f6..34f757b 100644
--- a/fggl/gfx/ogl4/meshes.cpp
+++ b/fggl/gfx/ogl4/meshes.cpp
@@ -158,10 +158,11 @@ namespace fggl::gfx::ogl4 {
 		shader->setUniformF(shader->uniform("lights[0].colour"), {0.5f, 0.5f, 0.5f});
 	}
 
-	static ogl::Texture* upload_texture( std::string name, assets::AssetManager* manager ) {
-		debug::info("loading texture: {}", name);
+	static ogl::Texture* upload_texture( assets::AssetID name, assets::AssetManager* manager ) {
+		debug::info("loading texture: {}", name.get());
 
-		auto* texture = manager->get<ogl::Texture>("ogl_"+name);
+		auto uploadedTex = assets::make_asset_id_rt("ogl", std::to_string(name.get()) );
+		auto* texture = manager->get<ogl::Texture>( uploadedTex );
 		if ( texture != nullptr ) {
 			return texture;
 		}
@@ -177,7 +178,7 @@ namespace fggl::gfx::ogl4 {
 		texture = new ogl::Texture(ogl::TextureType::Tex2D);
 		texture->setData( ogl::InternalImageFormat::RedGreenBlue, image);
 
-		return manager->set("ogl_"+name, texture);
+		return manager->set(uploadedTex, texture);
 	}
 
 	static Material* get_fallback_material(assets::AssetManager* manager) {
@@ -193,14 +194,15 @@ namespace fggl::gfx::ogl4 {
 		return manager->set(FALLBACK_MAT, mat);
 	}
 
-	static Material* upload_material( std::string name, assets::AssetManager* manager ) {
+	static Material* upload_material( assets::AssetID name, assets::AssetManager* manager ) {
 		auto* meshMaterial = manager->get<mesh::Material>(name);
 		if ( meshMaterial == nullptr ){
-			debug::error("attempted to load material {}, but did not exist!", name);
+			debug::error("attempted to load material {}, but did not exist!", name.get());
 			return get_fallback_material(manager);
 		}
 
-		auto* material = manager->get<Material>("ogl_"+name);
+		auto uploadedMat = assets::make_asset_id_rt("ogl", std::to_string(name.get()) );
+		auto* material = manager->get<Material>( uploadedMat );
 		if ( material != nullptr ) {
 			return material;
 		}
@@ -216,7 +218,7 @@ namespace fggl::gfx::ogl4 {
 			material->m_specular = upload_texture(meshMaterial->getPrimarySpecular(), manager);
 		}
 
-		return manager->set("ogl_"+name, material);
+		return manager->set( uploadedMat, material);
 	}
 
 	MeshData upload_mesh(const mesh::Mesh3D& rawMesh, assets::AssetManager* manager) {
diff --git a/fggl/gfx/ogl4/models.cpp b/fggl/gfx/ogl4/models.cpp
index 5eee6ce..ff778b8 100644
--- a/fggl/gfx/ogl4/models.cpp
+++ b/fggl/gfx/ogl4/models.cpp
@@ -67,7 +67,7 @@ namespace fggl::gfx::ogl4 {
 		modelComp.drawType = ogl::Primitive::TRIANGLE;
 	}
 
-	StaticModel* StaticModelRenderer::uploadMesh(assets::AssetGUID guid, const data::Mesh &mesh, bool allowCache) {
+	StaticModel* StaticModelRenderer::uploadMesh(assets::AssetID guid, const data::Mesh &mesh, bool allowCache) {
 		assert( m_assets != nullptr );
 
 		// if the asset has already been uploaded, we don't need to do anything
@@ -117,7 +117,7 @@ namespace fggl::gfx::ogl4 {
 		return modelAsset;
 	}*/
 
-	StaticModelGPU* StaticModelRenderer::uploadMesh2(const assets::AssetGUID& meshName, const data::Mesh &mesh) {
+	StaticModelGPU* StaticModelRenderer::uploadMesh2(const assets::AssetID& meshName, const data::Mesh &mesh) {
 		assert( m_assets != nullptr );
 
 		if ( m_assets->has(meshName) ) {
diff --git a/fggl/gfx/ogl4/module.cpp b/fggl/gfx/ogl4/module.cpp
index a3c0d4e..534b016 100644
--- a/fggl/gfx/ogl4/module.cpp
+++ b/fggl/gfx/ogl4/module.cpp
@@ -125,7 +125,9 @@ namespace fggl::gfx {
 		if ( spec.has("model") ) {
 			// figure out what model we want
 			auto assetStr = spec.get<std::string>("model", "");
-			auto* asset = assetService->get<ogl4::StaticModel>(assetStr);
+			auto assetId = assets::asset_from_user( assetStr );
+
+			auto* asset = assetService->get<ogl4::StaticModel>(assetId);
 
 			if ( asset == nullptr ) {
 				// the asset is not loaded/does not exist
diff --git a/include/fggl/assets/loader.hpp b/include/fggl/assets/loader.hpp
index 0bc18d9..b07a9a6 100644
--- a/include/fggl/assets/loader.hpp
+++ b/include/fggl/assets/loader.hpp
@@ -40,9 +40,10 @@ namespace fggl::assets {
 
 	struct ResourceRequest {
 		AssetGUID m_guid;
-		AssetType m_type;
+		AssetTypeID m_type;
 	};
 
+
 	class Loader {
 		public:
 			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::Loader");
@@ -57,50 +58,111 @@ namespace fggl::assets {
 			Loader(Loader &&) = delete;
 			Loader &operator=(Loader &&) = delete;
 
-			inline void setFactory(AssetType type, Checkin fn, LoadType loading = LoadType::DIRECT) {
+			inline void setFactory(AssetTypeID type, Checkin fn, LoadType loading = LoadType::DIRECT) {
 				m_factories[type] = std::make_pair(fn, loading);
 			}
 
-			inline void unsetFactory(AssetType type) {
+			inline void unsetFactory(AssetTypeID type) {
 				m_factories.erase(type);
 			}
 
-			inline void request(const AssetGUID &guid, const AssetType &type) {
+			inline void request(const AssetGUID &guid, const AssetTypeID &type) {
 				m_requests.push(ResourceRequest{guid, type});
 			}
 
-			void load(const AssetGUID &guid, const AssetType &type, void* userPtr = nullptr, const std::string& pack = "core") {
+			void loadChain(const AssetGUID &guid, const std::string& pack = "core") {
+				loadChain( assets::make_asset_id_rt(pack, guid) );
+			}
 
-				auto assetID = assets::make_asset_id(pack, guid);
-				std::filesystem::path path{};
+			void loadChain(AssetID asset, void* userPtr = nullptr) {
+				if (!m_checkin->exists(asset)) {
+					debug::warning("attempted to chain load unknown assetID {}", asset);
+					return;
+				}
 
-				// integrate the asset search routine, falling back to path scanning if needed
-				if ( m_checkin->exists(assetID) ) {
-					path = m_checkin->getPath(assetID);
-				} else {
-					path = m_storage->resolvePath(data::StorageType::Data, guid);
+				// FIXME use the fancy dependency resolution algorithm from the module system
+				std::vector<AssetID> loadOrder;
+				std::stack<AssetID> openSet;
+				openSet.push(asset);
+				while ( !openSet.empty() ) {
+					auto current = openSet.top();
+					openSet.pop();
+
+					// this WILL (probably) break if an asset ends up in the chain twice
+					loadOrder.push_back( current );
+					const auto& record = m_checkin->find(current);
+					for ( const auto& dep : record.m_requires ) {
+						openSet.push( dep );
+					}
 				}
 
-				auto factoryItr = m_factories.find(type);
-				if ( factoryItr == m_factories.end() ) {
-					debug::error("attempted to load asset with unknown type: {}", type.get());
+				processChain(loadOrder, userPtr);
+			}
+
+			void processChain( std::vector<AssetID>& loadOrder, void* userPtr = nullptr ) {
+				if ( loadOrder.empty() ) {
 					return;
 				}
 
+				debug::info("Starting chain load");
+				for ( auto it = loadOrder.rbegin(); it < loadOrder.rend(); ++it  ) {
+					debug::info(" CHAIN -> loading {}", *it );
+					load( *it, userPtr );
+				}
+				debug::info("Ended chain loader");
+			}
+
+			inline void load(const AssetGUID& guid, const AssetTypeID& type, void* userPtr = nullptr, const std::string& pack = "core" ) {
+				auto assetID = assets::make_asset_id_rt(pack, guid);
+
+				// try checkin load first, falling back if it fails
+				bool checkinLoad = load( assetID, userPtr );
+				if ( !checkinLoad ) {
+					debug::info("could not perform checkin load for {} - missing loaders?", guid);
+
+					auto path = m_storage->resolvePath(data::StorageType::Data, guid);
+					loadDirect(path, assetID, type, pack, userPtr);
+				}
+			}
+
+			bool loadDirect( const std::filesystem::path& path, AssetID asset, AssetTypeID assetType, const std::string& pack, void* userPtr = nullptr ){
+				// check if we know how to load this asset (using old loaders)
+				auto factoryItr = m_factories.find( assetType );
+				if ( factoryItr == m_factories.end() ) {
+					debug::error("attempted to load asset with unknown type: {}", assetType.get() );
+					return false;
+				}
+
+				// perform loading
+				LoaderContext ctx {
+					.pack = pack,
+					.packRoot = "",
+					.assetPath = path
+				};
+
+				// perform loading
 				const auto& [callback, callbackType] = factoryItr->second;
-				switch (callbackType) {
-					case LoadType::DIRECT:
-						// TODO we load the data into main memory and give a pointer to it.
-						debug::log(debug::Level::error, "Tried to load direct asset - no one wrote that yet!");
-						break;
-					case LoadType::STAGED:
-						// TODO we load the data into temp memory and give a pointer to it.
-						debug::log(debug::Level::error, "Tried to load staged asset - no one wrote that yet!");
-						break;
-					case LoadType::PATH:
-						callback(this, guid, AssetData(&path), userPtr);
-						break;
+				if ( callbackType == LoadType::PATH ) {
+					callback(this, asset, ctx, userPtr);
+					return true;
+				}
+
+				debug::log(debug::Level::error, "Tried to use old/unsupported loading method");
+				return false;
+			}
+
+			bool load(const AssetID &assetId, void* userPtr = nullptr) {
+				if ( !m_checkin->exists(assetId) ) {
+					#ifndef NDEBUG
+						debug::warning("asked to load unknown asset: {}", util::guidToString( util::GUID::make(assetId.get()) ) );
+					#else
+						debug::warning("asked to load unknown asset: {}", assetId.get());
+					#endif
+					return false;
 				}
+
+				const auto& record = m_checkin->find( assetId );
+				return loadDirect( record.m_path, assetId, record.m_assetType, record.m_pack, userPtr );
 			}
 
 			void progress() {
@@ -132,7 +194,7 @@ namespace fggl::assets {
 			data::Storage *m_storage;
 			CheckinAdapted *m_checkin;
 			std::queue<ResourceRequest> m_requests;
-			std::map<AssetType, Config> m_factories;
+			std::map<AssetTypeID, Config> m_factories;
 	};
 
 } // namespace fggl::assets
diff --git a/include/fggl/assets/manager.hpp b/include/fggl/assets/manager.hpp
index 99a06ca..125c153 100644
--- a/include/fggl/assets/manager.hpp
+++ b/include/fggl/assets/manager.hpp
@@ -60,7 +60,7 @@ namespace fggl::assets {
 			AssetManager &operator=(AssetManager &&) = delete;
 
 			template<typename T>
-			T* get(const AssetGUID &guid) const {
+			T* get(const AssetID &guid) const {
 				try {
 					const auto &assetRecord = m_registry.at(guid);
 					std::shared_ptr<AssetBoxT<T>> casted = std::dynamic_pointer_cast<AssetBoxT<T>>(assetRecord);
@@ -75,7 +75,7 @@ namespace fggl::assets {
 			}
 
 			template<typename T>
-			T* set(const AssetGUID &guid, T* assetRef) {
+			T* set(const AssetID &guid, T* assetRef) {
 				auto ptr = std::make_shared<AssetBoxT<T>>();
 				ptr->asset = assetRef;
 
@@ -83,11 +83,11 @@ namespace fggl::assets {
 				return (*ptr).asset;
 			}
 
-			inline void require(const AssetGUID &guid) {
+			inline void require(const AssetID &guid) {
 				//m_registry.at(guid).refCount++;
 			}
 
-			inline bool has(const AssetGUID &guid) {
+			inline bool has(const AssetID &guid) {
 				return m_registry.contains(guid);
 			}
 
@@ -95,8 +95,8 @@ namespace fggl::assets {
 				//m_registry.at(guid).refCount--;
 			}
 
-			inline std::vector<AssetGUID> known() {
-				std::vector<AssetGUID> assetList;
+			inline std::vector<AssetID> known() {
+				std::vector<AssetID> assetList;
 				for ( auto& itr : m_registry ) {
 					assetList.push_back(itr.first);
 				}
@@ -104,7 +104,7 @@ namespace fggl::assets {
 			}
 
 		private:
-			std::map<AssetGUID, std::shared_ptr<AssetBox>> m_registry;
+			std::map<AssetID, std::shared_ptr<AssetBox>> m_registry;
 	};
 
 } // namespace fggl::assets
diff --git a/include/fggl/assets/packed/adapter.hpp b/include/fggl/assets/packed/adapter.hpp
index fd787ba..17fb107 100644
--- a/include/fggl/assets/packed/adapter.hpp
+++ b/include/fggl/assets/packed/adapter.hpp
@@ -38,6 +38,8 @@ namespace fggl::assets {
 		AssetID assetID;
 		ResourceType m_fileType;
 		AssetTypeID m_assetType;
+		std::string m_pack;
+		std::vector<AssetID> m_requires;
 	};
 
 	struct ManifestHeader {
@@ -63,8 +65,10 @@ namespace fggl::assets {
 	class CheckinAdapted {
 		public:
 			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::checkin::debug");
+
 			using FilePredicate = std::function<AssetTypeID(std::filesystem::path)>;
 			using FileLoader = std::function<bool(std::filesystem::path, MemoryBlock& block)>;
+			using AssetMetadata = std::function<bool(const std::string& pack, std::filesystem::path packRoot, ResourceRecord&)>;
 
 			CheckinAdapted(data::Storage* storage, RawCheckin* checkSvc) : m_storage(storage), m_checkin(checkSvc) {};
 
@@ -99,9 +103,13 @@ namespace fggl::assets {
 
 			// asset discovery
 			void discover( std::filesystem::path& packDir, bool useCache=true, bool updateCache=false ) {
+				// note we're loading this pack
+				auto packName = packDir.filename();
+				m_packs[packName].rootDir = packDir;
+
 				if ( useCache && has_manifest(packDir)) {
 					// check if we've cached the search
-					load_manifest(packDir.filename(), packDir);
+					load_manifest(packName, packDir);
 					return;
 				}
 
@@ -126,7 +134,7 @@ namespace fggl::assets {
 				if ( updateCache ) {
 					save_manifest(packDir.filename(), packDir, packFiles);
 				}
-				m_packs[ packDir.filename() ] = packFiles;
+				m_packs[ packDir.filename() ].assets = packFiles;
 			}
 
 			inline void setLoader(ResourceType type, FileLoader loader, FilePredicate predicate = nullptr) {
@@ -138,14 +146,28 @@ namespace fggl::assets {
 				}
 			}
 
+			inline void setProcessor(ResourceType type, AssetMetadata metaFunc) {
+				m_metadata[type] = metaFunc;
+			}
+
+			inline const ResourceRecord& find(AssetID asset) const {
+				return m_files.at(asset);
+			}
+
 		private:
 			data::Storage* m_storage;
 			RawCheckin* m_checkin;
 			std::map<AssetID, ResourceRecord> m_files;
 
+			struct PackInfo {
+				std::filesystem::path rootDir;
+				std::vector<AssetID> assets;
+			};
+
 			std::map<ResourceType, FilePredicate> m_predicates;
 			std::map<ResourceType, FileLoader> m_loaders;
-			std::map<std::string, std::vector<AssetID>> m_packs;
+			std::map<ResourceType, AssetMetadata> m_metadata;
+			std::map<std::string, PackInfo> m_packs;
 
 			void process_file(std::filesystem::path path, std::filesystem::path packDir, std::vector<AssetID> packFiles) {
 				for( auto& [rType, pred] : m_predicates ) {
@@ -157,13 +179,23 @@ namespace fggl::assets {
 						auto relPath = std::filesystem::relative(path, packDir);
 						ResourceRecord rr{
 							.m_path = path,
-							.assetID = make_asset_id(packName, relPath.generic_string()),
+							.assetID = make_asset_id_rt(packName, relPath.generic_string()),
 							.m_fileType = rType,
 							.m_assetType = aType,
+							.m_pack = packName,
+							.m_requires = {}
 						};
+
+						// processors (for stuff like dependencies)
+						auto metaProc = m_metadata.find(rType);
+						if ( metaProc != m_metadata.end() ) {
+							metaProc->second( packName, packDir, rr );
+						}
+
+						// store the resulting data
 						m_files[rr.assetID] = rr;
 						packFiles.push_back( rr.assetID );
-						debug::trace("discovered {} ({}) from pack '{}'", rr.assetID.get(), relPath.c_str(), packName.c_str() );
+						debug::trace("discovered {} ({}) from pack '{}'", rr.assetID, relPath.c_str(), packName.c_str() );
 						break;
 					}
 				}
@@ -174,7 +206,7 @@ namespace fggl::assets {
 				return std::filesystem::exists(packManifest);
 			}
 
-			void load_manifest_entry(FILE* file, const std::filesystem::path& packRoot) {
+			void load_manifest_entry(FILE* file, const std::string& packName, const std::filesystem::path& packRoot) {
 				// read our entry ( id, ftype, atype, pathLen )
 				ManifestHeader header{};
 				std::fread(&header, sizeof(ManifestHeader), 1, file);
@@ -184,12 +216,23 @@ namespace fggl::assets {
 				std::fread( relPath, sizeof(char), header.stringSize, file );
 				relPath[ header.stringSize + 1 ] = '\0';
 
+				// read the dependency list
+				uint64_t nDeps = 0;
+				std::fread( &nDeps, sizeof(uint64_t), 1, file);
+				uint64_t *deps = new uint64_t[nDeps];
+				std::fread( deps, sizeof(uint64_t), nDeps, file );
+
+				std::vector<AssetID> depList;
+				for ( uint64_t i = 0; i < nDeps; ++i ) {
+					depList.push_back( AssetID::make(deps[i]) );
+				}
+
 				// calculate and verify path
 				std::filesystem::path fullPath = packRoot / relPath;
 				delete[] relPath;
 
 				if ( !std::filesystem::exists(fullPath) ) {
-					debug::warning("pack manifest for {} contained invalid path {}", packRoot.filename().c_str(), fullPath.c_str());
+					debug::warning("pack manifest for {} contained invalid path {}", packName, fullPath.c_str());
 					return;
 				}
 
@@ -198,10 +241,12 @@ namespace fggl::assets {
 					.m_path = fullPath,
 					.assetID = AssetID::make(header.assetID),
 					.m_fileType = ResourceType::make(header.fileType),
-					.m_assetType = AssetTypeID::make(header.assetType)
+					.m_assetType = AssetTypeID::make(header.assetType),
+					.m_pack = packName,
+					.m_requires = depList
 				};
 				m_files[ rr.assetID ] = rr;
-				m_packs[ packRoot.filename() ].push_back( rr.assetID );
+				m_packs[ packRoot.filename() ].assets.push_back( rr.assetID );
 				debug::trace("discovered {} ({}) from pack {}", rr.assetID.get(), fullPath.c_str(), packRoot.filename().c_str() );
 			}
 
@@ -222,7 +267,7 @@ namespace fggl::assets {
 				uint64_t entries{0};
 				std::fread(&entries, sizeof(uint64_t), 1, file);
 				for ( uint64_t i = 0; i < entries; ++i) {
-					load_manifest_entry(file, packRoot);
+					load_manifest_entry(file, packName, packRoot);
 				}
 
 				std::fclose( file );
diff --git a/include/fggl/assets/packed/direct.hpp b/include/fggl/assets/packed/direct.hpp
index faa01ae..f10796d 100644
--- a/include/fggl/assets/packed/direct.hpp
+++ b/include/fggl/assets/packed/direct.hpp
@@ -35,23 +35,6 @@
  */
 namespace fggl::assets {
 
-	using AssetID = util::OpaqueName<uint64_t, struct AssetIDTag>;
-
-	constexpr AssetID make_asset_id(const std::string& pack, const std::string& path, const std::string& view = "") {
-		auto ref = pack + ":" + path;
-		if ( !view.empty() ) {
-			ref += "[" + view + "]";
-		}
-		return AssetID::make( util::hash_fnv1a_64(ref.c_str()) );
-	}
-
-	using AssetTypeID = util::OpaqueName<uint64_t, struct AssetTypeTag>;
-	constexpr auto INVALID_ASSET_TYPE = AssetTypeID::make(0);
-
-	constexpr AssetTypeID make_asset_type(const char* type) {
-		return AssetTypeID::make( util::hash_fnv1a_64(type) );
-	}
-
 	class RawCheckin {
 		public:
 			constexpr const static modules::ModuleService service = modules::make_service("fggl::assets::checkin");
diff --git a/include/fggl/assets/types.hpp b/include/fggl/assets/types.hpp
index 4abcdc7..8f507b4 100644
--- a/include/fggl/assets/types.hpp
+++ b/include/fggl/assets/types.hpp
@@ -24,12 +24,38 @@
 #include <functional>
 
 #include "fggl/util/safety.hpp"
+#include "fggl/util/guid.hpp"
 
 namespace fggl::assets {
 	using AssetType = util::OpaqueName<std::string_view, struct AssetTag>;
 	using AssetGUID = std::string;
 	using AssetPath = std::filesystem::path;
 
+	using AssetID = util::OpaqueName<uint64_t, struct AssetIDTag>;
+
+	template<unsigned L1, unsigned L2>
+	constexpr AssetID make_asset_id(const char (&pack)[L1], const char (&path)[L2]) {
+		auto hash = util::hash_fnv1a_64( util::cat( pack, ":", path ).c );
+		return AssetID::make( hash );
+	}
+
+	template<unsigned L1, unsigned L2, unsigned L3>
+	constexpr AssetID make_asset_id(const char (&pack)[L1], const char (&path)[L2], const char (&view)[L3]) {
+		auto hash = util::hash_fnv1a_64( util::cat( pack, ":", path, "[", view, "]").c );
+		return AssetID::make( hash );
+	}
+
+	AssetID make_asset_id_rt(const std::string &pack, const std::string &path, const std::string &view = "");
+	AssetID asset_from_user(const std::string &input, const std::string &pack = "core");
+
+	using AssetTypeID = util::OpaqueName<uint64_t, struct AssetTypeTag>;
+	constexpr auto INVALID_ASSET_TYPE = AssetTypeID::make(0);
+
+	constexpr AssetTypeID make_asset_type(const char* type) {
+		return AssetTypeID::make( util::hash_fnv1a_64(type) );
+	}
+
+
 	struct MemoryBlock {
 		std::byte *data;
 		std::size_t size;
@@ -43,9 +69,30 @@ namespace fggl::assets {
 
 	using AssetRefRaw = std::shared_ptr<void>;
 
+	struct LoaderContext {
+		std::string pack;
+		std::filesystem::path packRoot;
+		std::filesystem::path assetPath;
+	};
+
 	class Loader;
-	using AssetData = std::variant<MemoryBlock, AssetPath *, FILE *>;
-	using Checkin = std::function<AssetRefRaw(Loader* loader, const AssetGUID &, const AssetData &, void* userPtr)>;
+	using Checkin = std::function<AssetRefRaw(Loader* loader, const AssetID &, const LoaderContext &, void* userPtr)>;
 }
 
+// formatter
+template<> struct fmt::formatter<fggl::assets::AssetID> {
+	constexpr auto parse(format_parse_context& ctx) -> decltype(ctx.begin()) {
+		return ctx.begin();
+	}
+
+	template <typename FormatContext>
+	auto format(const fggl::assets::AssetID & guid, FormatContext& ctx) const -> decltype(ctx.out()) {
+		#ifndef NDEBUG
+		return fmt::format_to(ctx.out(), "ASSET[{}]", guidToString( fggl::util::GUID::make( guid.get() ) ));
+		#else
+		return fmt::format_to(ctx.out(), "ASSET[{}]", guid.get());
+		#endif
+	}
+};
+
 #endif //FGGL_ASSETS_TYPES_HPP
diff --git a/include/fggl/audio/openal/audio.hpp b/include/fggl/audio/openal/audio.hpp
index cf59cab..ee74dcc 100644
--- a/include/fggl/audio/openal/audio.hpp
+++ b/include/fggl/audio/openal/audio.hpp
@@ -36,10 +36,10 @@ namespace fggl::audio::openal {
 
 	constexpr uint32_t NULL_BUFFER_ID = 0;
 
-	assets::AssetRefRaw load_vorbis(assets::Loader* loader, const assets::AssetGUID &, const assets::AssetData &, void* userPtr);
+	assets::AssetRefRaw load_vorbis(assets::Loader* loader, const assets::AssetID &, const assets::LoaderContext &, void* userPtr);
 
 	bool load_vorbis_short(std::filesystem::path path, assets::MemoryBlock& block);
-	assets::AssetTypeID check_vorbis(std::filesystem::path path);
+	assets::AssetTypeID check_vorbis( std::filesystem::path path );
 
 	enum class AudioType {
 		MONO_8 = AL_FORMAT_MONO8,
diff --git a/include/fggl/entity/loader/loader.hpp b/include/fggl/entity/loader/loader.hpp
index 322b76c..86e6d4a 100644
--- a/include/fggl/entity/loader/loader.hpp
+++ b/include/fggl/entity/loader/loader.hpp
@@ -31,8 +31,8 @@
 
 namespace fggl::entity {
 
-	constexpr auto PROTOTYPE_ASSET = assets::AssetType::make("entity_prototype");
-	constexpr auto SCENE = assets::AssetType::make("entity_scene");
+	constexpr auto ENTITY_PROTOTYPE = assets::make_asset_type("entity/prototype");
+	constexpr auto ENTITY_SCENE = assets::make_asset_type("entity/scene");
 
 	using FactoryFunc = std::function<void(const ComponentSpec &config, EntityManager &, const EntityID &, modules::Services &svc)>;
 	using CustomiseFunc = std::function<void(EntityManager &, const EntityID &)>;
@@ -173,8 +173,8 @@ namespace fggl::entity {
 			}
 	};
 
-	assets::AssetRefRaw load_prototype(assets::Loader* loader, const assets::AssetGUID &guid, assets::AssetData data, EntityFactory* factory);
-	assets::AssetRefRaw load_scene(assets::Loader* loader, const assets::AssetGUID& asset, assets::AssetData data, void* ptr);
+	assets::AssetRefRaw load_prototype(assets::Loader* loader, const assets::AssetID &guid, const assets::LoaderContext& data, EntityFactory* factory);
+	assets::AssetRefRaw load_scene(assets::Loader* loader, const assets::AssetID& asset, const assets::LoaderContext& data, void* ptr);
 
 } // namespace fggl::entity
 
diff --git a/include/fggl/gfx/ogl4/fallback.hpp b/include/fggl/gfx/ogl4/fallback.hpp
index 4efd49b..62d2260 100644
--- a/include/fggl/gfx/ogl4/fallback.hpp
+++ b/include/fggl/gfx/ogl4/fallback.hpp
@@ -19,6 +19,8 @@
 #ifndef FGGL_GFX_OGL4_FALLBACK_HPP
 #define FGGL_GFX_OGL4_FALLBACK_HPP
 
+#include "fggl/assets/types.hpp"
+
 /**
  * Fallback shaders.
  *
@@ -61,9 +63,9 @@ namespace fggl::gfx::ogl4 {
 	constexpr const GLuint TEX_WHITE = 0xFFFFFFFF;
 	constexpr const GLuint TEX_CHECKER = 0x00FF00FF; //FIXME pixel order is reversed?!
 
-	constexpr const char* FALLBACK_TEX = "FALLBACK_TEX";
-	constexpr const char* FALLBACK_MAT = "FALLBACK_MAT";
-	constexpr const char* SOLID_TEX = "SOLID_TEX";
+	constexpr const assets::AssetID FALLBACK_TEX = assets::make_asset_id("fallback", "FALLBACK_TEX");
+	constexpr const assets::AssetID FALLBACK_MAT = assets::make_asset_id("fallback", "FALLBACK_MAT");
+	constexpr const assets::AssetID SOLID_TEX = assets::make_asset_id("fallback", "SOLID_TEX");
 
 } // namespace fggl::gfx::ogl4
 
diff --git a/include/fggl/gfx/ogl4/models.hpp b/include/fggl/gfx/ogl4/models.hpp
index 5b8fba6..fd121d3 100644
--- a/include/fggl/gfx/ogl4/models.hpp
+++ b/include/fggl/gfx/ogl4/models.hpp
@@ -83,8 +83,8 @@ namespace fggl::gfx::ogl4 {
 			StaticModelRenderer &operator=(const StaticModelRenderer &other) = delete;
 			StaticModelRenderer &operator=(StaticModelRenderer &&other) = delete;
 
-			StaticModel* uploadMesh(assets::AssetGUID guid, const data::Mesh& mesh, bool allowCache=true);
-			StaticModelGPU* uploadMesh2(const assets::AssetGUID& meshName, const data::Mesh& mesh);
+			StaticModel* uploadMesh(assets::AssetID guid, const data::Mesh& mesh, bool allowCache=true);
+			StaticModelGPU* uploadMesh2(const assets::AssetID& meshName, const data::Mesh& mesh);
 
 			void render(entity::EntityManager &world, bool debugMode = false) {
 				#ifdef FGGL_ALLOW_DEFERRED_UPLOAD
diff --git a/include/fggl/mesh/mesh.hpp b/include/fggl/mesh/mesh.hpp
index 1da8896..e05e5aa 100644
--- a/include/fggl/mesh/mesh.hpp
+++ b/include/fggl/mesh/mesh.hpp
@@ -20,6 +20,7 @@
 #define FGGL_MESH_MESH_HPP
 
 #include "fggl/math/types.hpp"
+#include "fggl/assets/types.hpp"
 
 #include <vector>
 #include <span>
@@ -52,28 +53,29 @@ namespace fggl::mesh {
 		DIFFUSE, NORMAL
 	};
 
+	constexpr auto MISSING_TEXTURE = assets::AssetID::make(0);
 	struct Material {
 		std::string name;
 		math::vec3 ambient;
 		math::vec3 diffuse;
 		math::vec3 specular;
-		std::vector<std::string> diffuseTextures{};
-		std::vector<std::string> normalTextures{};
-		std::vector<std::string> specularTextures{};
+		std::vector<assets::AssetID> diffuseTextures{};
+		std::vector<assets::AssetID> normalTextures{};
+		std::vector<assets::AssetID> specularTextures{};
 
-		inline std::string getPrimaryDiffuse() {
+		inline assets::AssetID getPrimaryDiffuse() {
 			assert( !diffuseTextures.empty() );
-			return diffuseTextures.empty() ? "" : diffuseTextures[0];
+			return diffuseTextures.empty() ? MISSING_TEXTURE : diffuseTextures[0];
 		}
 
-		inline std::string getPrimaryNormals() {
+		inline assets::AssetID getPrimaryNormals() {
 			assert( !normalTextures.empty() );
-			return normalTextures.empty() ? "" : normalTextures[0];
+			return normalTextures.empty() ? MISSING_TEXTURE : normalTextures[0];
 		}
 
-		inline std::string getPrimarySpecular() {
+		inline assets::AssetID getPrimarySpecular() {
 			assert( !specularTextures.empty() );
-			return specularTextures.empty() ? "" : specularTextures[0];
+			return specularTextures.empty() ? MISSING_TEXTURE : specularTextures[0];
 		}
 	};
 
@@ -81,7 +83,7 @@ namespace fggl::mesh {
 	struct Mesh {
 		std::vector<VertexFormat> data;
 		std::vector<uint32_t> indices;
-		std::string material;
+		assets::AssetID material;
 
 		inline uint32_t append(const VertexFormat& vert) {
 			auto nextIdx = data.size();
@@ -93,7 +95,7 @@ namespace fggl::mesh {
 	template<typename MeshFormat>
 	struct MultiMesh {
 		std::vector<MeshFormat> meshes;
-		std::vector<std::string> materials;
+		std::vector<assets::AssetID> materials;
 
 		MeshFormat& generate() {
 			return meshes.template emplace_back();
diff --git a/include/fggl/util/guid.hpp b/include/fggl/util/guid.hpp
index 7b188c7..6e9eb27 100644
--- a/include/fggl/util/guid.hpp
+++ b/include/fggl/util/guid.hpp
@@ -67,6 +67,27 @@ namespace fggl::util {
 		return hash;
 	}
 
+	template<unsigned N>
+	struct FString {
+		char c[N];
+	};
+
+	// https://stackoverflow.com/a/65440575
+	template<unsigned ...Len>
+	constexpr auto cat(const char (&...strings)[Len]) {
+		constexpr unsigned N = (... + Len) - sizeof...(Len);
+		FString<N + 1> result = {};
+		result.c[N] = '\0';
+
+		char* dst = result.c;
+		for (const char* src : {strings...}) {
+			for (; *src != '\0'; src++, dst++) {
+				*dst = *src;
+			}
+		}
+		return result;
+	}
+
 	// debug-only functions
 	#ifndef NDEBUG
 	GUID internString(const char *str);
-- 
GitLab