1 /** globally available game core, providing access to most key game services and scene control */
2 
3 module re.core;
4 
5 import std.array;
6 import std.typecons;
7 import std.format;
8 
9 import re.input;
10 import re.content;
11 import re.time;
12 import re.gfx.window;
13 import re.ng.scene;
14 import re.ng.diag;
15 import re.ng.manager;
16 import re.gfx.render_ext;
17 import re.math;
18 import re.util.logger;
19 import re.util.tweens.tween_manager;
20 import jar;
21 static import raylib;
22 
23 /**
24 Core class
25 */
26 abstract class Core {
27     /// logger utility
28     public static Logger log;
29 
30     /// game window
31     public static Window window;
32 
33     /// content manager
34     public static ContentManager content;
35 
36     /// the current scenes
37     private static Scene[] _scenes;
38 
39     /// type registration container
40     public static Jar jar;
41 
42     /// global managers
43     public static Manager[] managers;
44 
45     /// whether to draw debug information
46     public static bool debug_render;
47 
48     /// debugger utility
49     debug public static Debugger debugger;
50 
51     /// whether the game is running
52     public static bool running;
53 
54     version (unittest) {
55         /// the frame limit (used for testing)
56         public static int frame_limit = 60;
57     }
58 
59     /// target frames per second
60     public static int target_fps = 60;
61 
62     /// whether graphics should be disabled
63     public static bool headless = false;
64 
65     /// whether to pause when unfocused
66     public static bool pause_on_focus_lost = true;
67 
68     /// whether to exit when escape pressed
69     public static bool exit_on_escape_pressed = true;
70 
71     /// oversampling factor for internal rendering
72     public static int render_oversample = 1;
73 
74     /// whether to automatically scale things to compensate for hidpi
75     /// NOTE: raylib.ConfigFlags.FLAG_WINDOW_HIGHDPI also exists, but we're not using it right now
76     public static bool auto_compensate_hidpi = true;
77 
78     /// whether to automatically oversample for hidpi
79     public static bool auto_oversample_hidpi = false;
80 
81     /// whether to rescale the mouse position to compensate for hidpi
82     public static bool auto_rescale_mouse_hidpi = false;
83 
84     /// whether to automatically resize the render target to the window size
85     public static bool sync_render_window_resolution = false;
86 
87     /// the default render resolution for all scenes
88     public static Vector2 default_resolution;
89 
90     /// the default texture filtering mode for render targets
91     public static raylib.TextureFilter default_filter_mode
92         = raylib.TextureFilter.TEXTURE_FILTER_POINT;
93 
94     version (vr) {
95         import re.gfx.vr;
96 
97         public static VRSupport vr;
98     }
99 
100     /// sets up a game core
101     this(int width, int height, string title) {
102         log = new Logger(Logger.Verbosity.Info);
103         log.sinks ~= new Logger.ConsoleSink();
104 
105         version (unittest) {
106         } else {
107             log.info("initializing rengfx core");
108         }
109 
110         default_resolution = Vector2(width, height);
111         if (!Core.headless) {
112             window = new Window(width, height);
113             window.initialize();
114             window.set_title(title);
115             if (auto_compensate_hidpi) {
116                 handle_hidpi_compensation();
117             }
118         }
119 
120         // disable default exit key
121         raylib.SetExitKey(raylib.KeyboardKey.KEY_NULL);
122 
123         content = new ContentManager();
124 
125         jar = new Jar();
126 
127         add_manager(new TweenManager());
128 
129         debug {
130             debugger = new Debugger();
131         }
132 
133         version (vr) {
134             import re.gfx.vr;
135 
136             vr = new VRSupport();
137         }
138 
139         version (unittest) {
140         } else {
141             log.info("initializing game");
142         }
143 
144         initialize();
145     }
146 
147     @property public static int fps() {
148         return raylib.GetFPS();
149     }
150 
151     /// sets up the game
152     abstract void initialize();
153 
154     /// starts the game
155     public void run() {
156         running = true;
157         // start the game loop
158         while (running) {
159             if (!headless) {
160                 running = !raylib.WindowShouldClose();
161             }
162 
163             update();
164             draw();
165 
166             version (unittest) {
167                 if (Time.frame_count >= frame_limit) {
168                     running = false;
169                 }
170             }
171         }
172     }
173 
174     /// gracefully exits the game
175     public static void exit() {
176         running = false;
177         version (unittest) {
178         } else {
179             log.info("gracefully exiting");
180         }
181     }
182 
183     protected void update() {
184         // update window
185         if (!Core.headless) {
186             if (pause_on_focus_lost && raylib.IsWindowMinimized()) {
187                 return; // pause
188             }
189             if (exit_on_escape_pressed && raylib.IsKeyPressed(raylib.KeyboardKey.KEY_ESCAPE)) {
190                 exit();
191             }
192             if (raylib.IsWindowResized()) {
193                 handle_window_resize();
194             }
195         }
196 
197         version (unittest) {
198             Time.update(1f / target_fps); // 60 fps
199         } else {
200             Time.update(raylib.GetFrameTime());
201         }
202         foreach (manager; managers) {
203             manager.update();
204         }
205         // update input
206         Input.update();
207         // update scenes
208         foreach (scene; _scenes) {
209             scene.update();
210         }
211         debug {
212             debugger.update();
213         }
214     }
215 
216     protected void draw() {
217         if (Core.headless)
218             return;
219         if (raylib.IsWindowMinimized()) {
220             return; // suppress draw
221         }
222         raylib.BeginDrawing();
223         foreach (scene; _scenes) {
224             // render scene
225             scene.render();
226             // post-render
227             scene.post_render();
228             // composite  (blit)screen render to window
229             // when the scene is rendered, it is rendered to a texture. this texture is then composited onto the main display buffer.
230 
231             version (vr) {
232                 bool vr_distort = false;
233                 if (vr.enabled) {
234                     assert(vr.distortion_shader != raylib.Shader.init, "vr.distortion_shader is not initialized");
235                     vr_distort = true;
236                 }
237 
238                 if (vr_distort)
239                     raylib.BeginShaderMode(vr.distortion_shader);
240             }
241 
242             RenderExt.draw_render_target(scene.render_target, Rectangle(0, 0,
243                     window.width, window.height), scene.composite_mode.color);
244 
245             version (vr) {
246                 if (vr_distort)
247                     raylib.EndShaderMode();
248             }
249         }
250         debug {
251             debugger.render();
252         }
253         raylib.EndDrawing();
254     }
255 
256     public static T get_scene(T)() {
257         import std.algorithm.searching : find;
258 
259         // find a scene matching the type
260         auto matches = _scenes.find!(x => (cast(T) x) !is null);
261         assert(matches.length > 0, "no matching scene was found");
262         return cast(T) matches.front;
263     }
264 
265     public static Nullable!T get_manager(T)() {
266         import std.algorithm.searching : find;
267 
268         // find a manager matching the type
269         auto matches = managers.find!(x => (cast(T) x) !is null);
270         if (matches.length > 0) {
271             return Nullable!T(cast(T) matches.front);
272         }
273         return Nullable!T.init;
274     }
275 
276     /// adds a global manager
277     public T add_manager(T)(T manager) {
278         managers ~= manager;
279         manager.setup();
280         return manager;
281     }
282 
283     @property public static Scene[] scenes() {
284         return _scenes;
285     }
286 
287     @property public static Scene primary_scene() {
288         return _scenes.front;
289     }
290 
291     /// sets the current scenes
292     static void load_scenes(Scene[] new_scenes) {
293         foreach (scene; _scenes) {
294             // end old scenes
295             scene.end();
296             scene = null;
297         }
298         // clear scenes list
299         _scenes = [];
300 
301         _scenes ~= new_scenes;
302         // begin new scenes
303         foreach (scene; _scenes) {
304             scene.begin();
305         }
306     }
307 
308     /// releases all resources and cleans up
309     public void destroy() {
310         version (vr) {
311             if (vr.enabled) {
312                 assert(vr.config != raylib.VrStereoConfig.init, "vr config was not initialized");
313 
314                 raylib.UnloadVrStereoConfig(vr.config);
315             }
316 
317         }
318         debug {
319             debugger.destroy();
320         }
321         content.destroy();
322         load_scenes([]); // end scenes
323         foreach (manager; managers) {
324             manager.destroy();
325         }
326         if (!Core.headless) {
327             window.destroy();
328         }
329     }
330 
331     private void handle_hidpi_compensation() {
332         // when hidpi is enabled,, the window is too small
333         // so we scale the real window but keep the render resolution the same
334         // compute the target window size
335         auto scaled_width = window.width_dpi;
336         auto scaled_height = window.height_dpi;
337         // but, if auto-oversampling is enabled, we need to set the oversample factor
338         if (auto_oversample_hidpi) {
339             render_oversample = cast(int) window.scale_dpi;
340             log.info("auto-oversampling enabled, setting oversample factor to %d", render_oversample);
341         }
342         log.info("resizing window from (%s,%s) to (%s,%s) to compensate for dpi scale: %s",
343             window.width, window.height, scaled_width, scaled_height, window.scale_dpi);
344         window.resize(scaled_width, scaled_height);
345         handle_window_resize();
346         sync_render_resolution();
347         if (auto_rescale_mouse_hidpi) {
348             // set mouse transform to compensate for dpi scale
349             raylib.SetMouseScale(1 / window.scale_dpi, 1 / window.scale_dpi);
350         }
351     }
352 
353     private void handle_window_resize() {
354         log.info("window resized to (%s,%s)", window.width, window.height);
355         // window was resized
356         if (sync_render_window_resolution) {
357             sync_render_resolution();
358         }
359         // notify the active scenes
360         foreach (scene; _scenes) {
361             scene.on_window_resized();
362         }
363     }
364 
365     private void sync_render_resolution() {
366         // since window was resized, update our render resolution
367         // first get the new window size
368         auto render_res_x = window.width;
369         auto render_res_y = window.height;
370         // if (auto_compensate_hidpi) {
371         //     // if hidpi compensation is enabled, we need to scale the window size by the hidpi scale
372         //     render_res_x = cast(int)(render_res_x / window.scale_dpi);
373         //     render_res_y = cast(int)(render_res_y / window.scale_dpi);
374         //     // if hidpi compensation is on then the window size will be scale*resolution
375         //     // so we need to divide by the scale to get the render resolution
376         // }
377         // if oversampling is enabled, we need to multiply by the oversampling factor
378         render_res_x *= render_oversample;
379         render_res_y *= render_oversample;
380         // set the render resolution
381         default_resolution = Vector2(render_res_x, render_res_y);
382         // Core.log.info(format("updating render resolution to %s", default_resolution));
383         Core.log.info(format("updating render resolution to %s (dpi scale %s) (oversample %s)",
384                 default_resolution, window.scale_dpi, render_oversample));
385     }
386 }
387 
388 @("core-basic")
389 unittest {
390     import re.util.test : TestGame;
391     import std.string : format;
392     import std.math : isClose;
393 
394     class Game : TestGame {
395         override void initialize() {
396             // nothing much
397         }
398     }
399 
400     auto game = new Game();
401     game.run();
402 
403     // ensure time has passed
404     auto target_time = Core.frame_limit / Core.target_fps;
405     assert(isClose(Time.total_time, target_time),
406         format("time did not pass (expected: %s, actual: %s)", target_time, Time.total_time));
407 
408     game.destroy(); // clean up
409 
410     assert(game.scenes.length == 0, "scenes were not removed after Game cleanup");
411 }