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