1 /**
2     hot-reloading functionality
3 
4     currently can reload any file based assets, including shaders, textures, etc.
5 */
6 
7 module re.util.hotreload;
8 
9 import std.format;
10 
11 import re.core;
12 import re.content;
13 import re.util.interop;
14 import re.gfx.raytypes;
15 import optional;
16 static import raylib;
17 
18 interface Reloadable(T) {
19     bool changed();
20     T reload();
21 }
22 
23 class ReloadableFile(T) : Reloadable!T {
24     string[] source_files;
25     long[] file_mod_times;
26 
27     this(string[] source_files) {
28         this.source_files = source_files;
29     }
30 
31     protected long get_file_mod_time(string file) {
32         if (file == null)
33             return 0;
34         return raylib.GetFileModTime(file.c_str);
35     }
36 
37     bool changed() {
38         // make sure we have mod times for each file
39         if (file_mod_times.length != source_files.length) {
40             file_mod_times = [];
41             // preallocate space
42             file_mod_times.length = source_files.length;
43             // now fetch the mod times
44             for (int i = 0; i < source_files.length; i++) {
45                 file_mod_times[i] = get_file_mod_time(source_files[i]);
46             }
47             // this is first time we are updating
48             return true;
49         }
50         // we already have previous entries for mod times, so check if any have changed
51         // get all most recent mod times
52         for (int i = 0; i < source_files.length; i++) {
53             auto prev_mod_time = file_mod_times[i];
54             long new_mod_time = get_file_mod_time(source_files[i]);
55             file_mod_times[i] = new_mod_time;
56             if (prev_mod_time != new_mod_time) {
57                 // at least one file has changed
58                 return true;
59             }
60         }
61         return false;
62     }
63 
64     abstract T reload();
65 }
66 
67 class ReloadableShader : ReloadableFile!Shader {
68     private enum VS_INDEX = 0;
69     private enum FS_INDEX = 1;
70 
71     this(string vs_path, string fs_path) {
72         if (vs_path)
73             vs_path = Core.content.get_path(vs_path);
74         if (fs_path)
75             fs_path = Core.content.get_path(fs_path);
76         super([vs_path, fs_path]);
77     }
78 
79     override Shader reload() {
80         // load shader, bypassing cache
81         auto vs_path = source_files[VS_INDEX];
82         auto fs_path = source_files[FS_INDEX];
83         Core.log.info(format("reloading shader: (vs: %s, fs: %s)", vs_path, fs_path));
84         auto maybe_shader = Core.content.load_shader(vs_path, fs_path, true);
85         if (maybe_shader == none) {
86             assert(0, "failed to reload shader");
87         }
88         return maybe_shader.front;
89     }
90 }
91 
92 @("hotreload-basic")
93 unittest {
94     class ReloadableBag : Reloadable!int {
95         int beans;
96         int external_bean_register;
97 
98         this(int beans) {
99             this.beans = beans;
100             this.external_bean_register = this.beans;
101         }
102 
103         bool changed() {
104             return beans != external_bean_register;
105         }
106 
107         int reload() {
108             beans = external_bean_register;
109             return beans;
110         }
111     }
112 
113     // create bag
114     auto bag = new ReloadableBag(10);
115     assert(bag.beans == 10);
116     // check that it's not changed
117     assert(!bag.changed());
118 
119     // change bag
120     bag.external_bean_register = 20;
121     assert(bag.changed());
122     // reload bag
123     assert(bag.reload() == 20);
124 }
125 
126 @("hotreload-file")
127 unittest {
128     bool fake_changed = false;
129     long init_fake_time = 0;
130     long new_fake_time = 10;
131     int old_beans = 10;
132     int new_beans = 20;
133 
134     class ReloadableMockFileBag : ReloadableFile!int {
135         this(string mock_bean_file) {
136             super([mock_bean_file]);
137         }
138 
139         override long get_file_mod_time(string file) {
140             return fake_changed ? new_fake_time : init_fake_time;
141         }
142 
143         override int reload() {
144             return fake_changed ? new_beans : old_beans;
145         }
146     }
147 
148     // create bag
149     auto bag = new ReloadableMockFileBag("mock_bean_file");
150     // it should be changed the first time, because on loading modtime it triggers a changed event
151     assert(bag.changed());
152     assert(bag.reload() == old_beans);
153     // now, this time it should not be changed
154     assert(!bag.changed());
155     assert(bag.reload() == old_beans);
156     // now, change the modtime
157     fake_changed = true;
158     // it should be changed now
159     assert(bag.changed());
160     assert(bag.reload() == new_beans);
161     // it shold not be changed now
162     assert(!bag.changed());
163     assert(bag.reload() == new_beans);
164 }