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.libs;
14 import re.ng.scene;
15 import re.ng.diag;
16 import re.ng.manager;
17 import re.gfx.render_ext;
18 import re.math;
19 import re.util.logger;
20 import re.util.tweens.tween_manager;
21 import jar;
22 static import raylib;
23 
24 /**
25 Core class
26 */
27 abstract class Core {
28     /// logger utility
29     public static Logger log;
30 
31     /// game window
32     public static Window window;
33 
34     /// content manager
35     public static ContentManager content;
36 
37     /// the current scenes
38     private static Scene[] _scenes;
39 
40     /// type registration container
41     public static Jar jar;
42 
43     /// global managers
44     public static Manager[] managers;
45 
46     /// whether to draw debug information
47     public static bool debug_render;
48 
49     /// debugger utility
50     debug public static Debugger debugger;
51 
52     /// whether the game is running
53     public static bool running;
54 
55     version (unittest) {
56         /// the frame limit (used for testing)
57         public static int frame_limit = 60;
58     }
59 
60     /// target frames per second
61     public static int target_fps = 60;
62 
63     /// whether graphics should be disabled
64     public static bool headless = false;
65 
66     /// whether to pause when unfocused
67     public static bool pause_on_focus_lost = true;
68 
69     /// whether to exit when escape pressed
70     public static bool exit_on_escape_pressed = true;
71 
72     /// whether to automatically scale things to compensate for hidpi
73     /// NOTE: raylib.ConfigFlags.FLAG_WINDOW_HIGHDPI also exists, but we're not using it right now
74     public static bool auto_compensate_hidpi = true;
75 
76     /// the default render resolution for all scenes
77     public static Vector2 default_resolution;
78 
79     /// the default texture filtering mode for render targets
80     public static raylib.TextureFilter default_filter_mode
81         = raylib.TextureFilter.TEXTURE_FILTER_POINT;
82 
83     /// sets up a game core
84     this(int width, int height, string title) {
85         log = new Logger(Logger.Verbosity.Information);
86         log.sinks ~= new Logger.ConsoleSink();
87 
88         version (unittest) {
89         } else {
90             log.info("initializing rengfx core");
91         }
92 
93         default_resolution = Vector2(width, height);
94         if (!Core.headless) {
95             window = new Window(width, height);
96             window.initialize();
97             window.set_title(title);
98             if (auto_compensate_hidpi) {
99                 // resize window according to dpi scale
100                 auto scaled_width = cast(int)(window.width * window.scale_dpi);
101                 auto scaled_height = cast(int)(window.height * window.scale_dpi);
102                 log.info(format("resizing window from (%s,%s) to (%s,%s) to compensate for dpi scale: %s",
103                         window.width, window.height, scaled_width, scaled_height, window.scale_dpi));
104                 window.resize(scaled_width, scaled_height);
105             }
106         }
107 
108         // disable default exit key
109         raylib.SetExitKey(raylib.KeyboardKey.KEY_NULL);
110 
111         content = new ContentManager();
112 
113         jar = new Jar();
114 
115         add_manager(new TweenManager());
116 
117         debug {
118             debugger = new Debugger();
119         }
120 
121         version (unittest) {
122         } else {
123             log.info("initializing game");
124         }
125         
126         // load libraries
127         auto loadlib_result = LibraryDependencies.load_all();
128         assert(loadlib_result, "library dependencies failed to load");
129 
130         initialize();
131     }
132 
133     @property public static int fps() {
134         return raylib.GetFPS();
135     }
136 
137     /// sets up the game
138     abstract void initialize();
139 
140     /// starts the game
141     public void run() {
142         running = true;
143         // start the game loop
144         while (running) {
145             if (!headless) {
146                 running = !raylib.WindowShouldClose();
147             }
148 
149             update();
150             draw();
151 
152             version (unittest) {
153                 if (Time.frame_count >= frame_limit) {
154                     running = false;
155                 }
156             }
157         }
158     }
159 
160     /// gracefully exits the game
161     public static void exit() {
162         running = false;
163         version (unittest) {
164         } else {
165             log.info("gracefully exiting");
166         }
167     }
168 
169     protected void update() {
170         if (pause_on_focus_lost && raylib.IsWindowMinimized()) {
171             return; // pause
172         }
173         if (exit_on_escape_pressed && raylib.IsKeyPressed(raylib.KeyboardKey.KEY_ESCAPE)) {
174             exit();
175         }
176         version (unittest) {
177             Time.update(1f / target_fps); // 60 fps
178         } else {
179             Time.update(raylib.GetFrameTime());
180         }
181         foreach (manager; managers) {
182             manager.update();
183         }
184         Input.update();
185         foreach (scene; _scenes) {
186             scene.update();
187         }
188         debug {
189             debugger.update();
190         }
191     }
192 
193     protected void draw() {
194         if (Core.headless)
195             return;
196         if (raylib.IsWindowMinimized()) {
197             return; // suppress draw
198         }
199         raylib.BeginDrawing();
200         foreach (scene; _scenes) {
201             // render scene
202             scene.render();
203             // post-render
204             scene.post_render();
205             // composite screen render to window
206             // TODO: support better compositing
207             RenderExt.draw_render_target(scene.render_target, Rectangle(0, 0,
208                     window.width, window.height), scene.composite_mode.color);
209         }
210         debug {
211             debugger.render();
212         }
213         raylib.EndDrawing();
214     }
215 
216     public static T get_scene(T)() {
217         import std.algorithm.searching : find;
218 
219         // find a scene matching the type
220         auto matches = _scenes.find!(x => (cast(T) x) !is null);
221         assert(matches.length > 0, "no matching scene was found");
222         return cast(T) matches.front;
223     }
224 
225     public static Nullable!T get_manager(T)() {
226         import std.algorithm.searching : find;
227 
228         // find a manager matching the type
229         auto matches = managers.find!(x => (cast(T) x) !is null);
230         if (matches.length > 0) {
231             return Nullable!T(cast(T) matches.front);
232         }
233         return Nullable!T.init;
234     }
235 
236     /// adds a global manager
237     public T add_manager(T)(T manager) {
238         managers ~= manager;
239         manager.setup();
240         return manager;
241     }
242 
243     @property public static Scene[] scenes() {
244         return _scenes;
245     }
246 
247     @property public static Scene primary_scene() {
248         return _scenes.front;
249     }
250 
251     /// sets the current scenes
252     static void load_scenes(Scene[] new_scenes) {
253         foreach (scene; _scenes) {
254             // end old scenes
255             scene.end();
256             scene = null;
257         }
258         // clear scenes list
259         _scenes = [];
260 
261         _scenes ~= new_scenes;
262         // begin new scenes
263         foreach (scene; _scenes) {
264             scene.begin();
265         }
266     }
267 
268     /// releases all resources and cleans up
269     public void destroy() {
270         debug {
271             debugger.destroy();
272         }
273         content.destroy();
274         load_scenes([]); // end scenes
275         foreach (manager; managers) {
276             manager.destroy();
277         }
278         if (!Core.headless) {
279             window.destroy();
280         }
281     }
282 }
283 
284 @("core-basic")
285 unittest {
286     import re.util.test : TestGame;
287     import std.string : format;
288     import std.math : isClose;
289 
290     class Game : TestGame {
291         override void initialize() {
292             // nothing much
293         }
294     }
295 
296     auto game = new Game();
297     game.run();
298 
299     // ensure time has passed
300     auto target_time = Core.frame_limit / Core.target_fps;
301     assert(isClose(Time.total_time, target_time),
302         format("time did not pass (expected: %s, actual: %s)", target_time, Time.total_time));
303 
304     game.destroy(); // clean up
305 
306     assert(game.scenes.length == 0, "scenes were not removed after Game cleanup");
307 }