1 /** content/asset loader and manager */
2 
3 module re.content;
4 
5 import std.string;
6 import std.file;
7 import std.conv;
8 import std.path;
9 import std.stdio;
10 import std.exception : enforce;
11 import optional;
12 
13 import re.util.cache;
14 import re.util.interop;
15 static import raylib;
16 
17 /// manages external content loading
18 class ContentManager {
19     /// search paths for content
20     public string[] paths;
21 
22     alias Texture2D = raylib.Texture2D;
23     alias Model = raylib.Model;
24     alias Shader = raylib.Shader;
25     alias Music = raylib.Music;
26 
27     private KeyedCache!Texture2D texture_cache;
28     private KeyedCache!Model model_cache;
29     private KeyedCache!Shader shader_cache;
30     private KeyedCache!Music music_cache;
31 
32     /// initializes the content manager
33     this() {
34         // setup caches
35         texture_cache = KeyedCache!Texture2D(tex => raylib.UnloadTexture(tex));
36         model_cache = KeyedCache!Model(mdl => raylib.UnloadModel(mdl));
37         shader_cache = KeyedCache!Shader(shd => raylib.UnloadShader(shd));
38         music_cache = KeyedCache!Music(mus => raylib.UnloadMusicStream(mus));
39     }
40 
41     /// get the physical path to a logical content path
42     public string get_path(string path) {
43         // check if this is already a valid path
44         if (std.file.exists(path)) {
45             return path;
46         }
47         auto base = string.init;
48         alias join_paths = std.path.buildNormalizedPath;
49         // check search paths first
50         foreach (search_path; paths) {
51             // if the combination path exists, then make this base
52             if (std.file.exists(join_paths(search_path, path))) {
53                 base = search_path;
54                 break;
55             }
56         }
57         return join_paths(base, path);
58     }
59 
60     private char* get_path_cstr(string path) {
61         return get_path(path).c_str;
62     }
63 
64     private KeyedCache!T cache_for(T)() {
65         static if (is(T == Texture2D)) {
66             return texture_cache;
67         } else static if (is(T == Model)) {
68             return model_cache;
69         } else static if (is(T == Shader)) {
70             return shader_cache;
71         } else static if (is(T == Music)) {
72             return music_cache;
73         } else {
74             static assert(0, format("no cache found for type %s", typeof(T)));
75         }
76     }
77 
78     private Optional!T load_cached_asset(T)(string path, T delegate(string) load_func) {
79         auto cache = cache_for!T();
80         auto cached = cache.get(path);
81         if (cached.isNull) {
82             auto real_path = get_path(path);
83             if (!exists(real_path)) {
84                 return no!T;
85             }
86             auto asset = load_func(real_path);
87             cache.put(path, asset);
88             return some(asset);
89         } else {
90             auto asset = cached.get;
91             return some(asset);
92         }
93     }
94 
95     /// loads a texture from disk
96     public Optional!Texture2D load_texture2d(string path) {
97         return load_cached_asset!Texture2D(path, (x) => raylib.LoadTexture(x.c_str));
98     }
99 
100     /// loads a model from disk
101     public Optional!Model load_model(string path) {
102         return load_cached_asset!Model(path, (x) => raylib.LoadModel(x.c_str));
103     }
104 
105     // public raylib.ModelAnimation[] load_model_animations(string path) {
106     //     uint num_loaded_anims = 0;
107     //     raylib.ModelAnimation* loaded_anims = raylib.LoadModelAnimations(get_path_cstr(path), &num_loaded_anims);
108     //     auto anims = loaded_anims[0 .. num_loaded_anims]; // access array as slice
109     //     return anims;
110     // }
111 
112     /// loads a shader from disk (vertex shader, fragment shader).
113     /// pass null to either arg to use the default
114     /// since loading shaders is a bit of a pain, the loader uses custom logic
115     public Optional!Shader load_shader(string vs_path, string fs_path, bool bypass_cache = false) {
116         raylib.Shader shd;
117         import std.digest.sha : sha1Of, toHexString;
118 
119         auto path_hash = to!string(sha1Of(vs_path ~ fs_path).toHexString);
120         auto cache = cache_for!Shader();
121         auto cached = cache.get(path_hash);
122         if (cached.isNull || bypass_cache) {
123             auto vs_real_path = get_path(vs_path);
124             auto fs_real_path = get_path(fs_path);
125             if (!exists(vs_real_path) && !exists(fs_real_path)) {
126                 // neither path exists
127                 return no!Shader;
128             }
129             auto vs = vs_path.length > 0 ? get_path_cstr(vs_path) : null;
130             auto fs = fs_path.length > 0 ? get_path_cstr(fs_path) : null;
131             shd = raylib.LoadShader(vs, fs);
132             if (!bypass_cache)
133                 cache.put(path_hash, shd);
134         } else {
135             shd = cached.get;
136         }
137         return some(shd);
138     }
139 
140     /// loads music from disk
141     public Optional!Music load_music(string file_path) {
142         return load_cached_asset!Music(file_path, (x) => raylib.LoadMusicStream(x.c_str));
143     }
144 
145     public void drop_caches() {
146         cache_for!Texture2D().drop();
147         cache_for!Model().drop();
148         cache_for!Shader().drop();
149         cache_for!Music().drop();
150     }
151 
152     /// releases all resources
153     public void destroy() {
154         drop_caches();
155     }
156 }