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