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