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