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