1 /++
2  + Authors: Avaxar <avaxar@nekkl.org>
3  + Copyright: Copyright © 2023, Avaxar
4  + License: $(LINK2 https://mit-license.org, MIT License)
5  +/
6 
7 module dsdl2.sdl;
8 @safe:
9 
10 import bindbc.sdl;
11 
12 import std.conv : to;
13 import std.format : format;
14 import std.string : toStringz;
15 
16 /++
17  + SDL exception generated from `SDL_GetError()` or dsdl2-specific exceptions
18  +/
19 final class SDLException : Exception {
20     this(string msg, string file = __FILE__, size_t line = __LINE__) {
21         super(msg, file, line);
22     }
23 
24     this(string file = __FILE__, size_t line = __LINE__) @trusted {
25         super(SDL_GetError().to!string, file, line);
26     }
27 }
28 
29 version (BindSDL_Static) {
30 }
31 else {
32     /++
33      + Loads the SDL2 shared dynamic library, which wraps bindbc-sdl's `loadSDL` function
34      +
35      + Unless if bindbc-sdl is on static mode (by adding a `BindSDL_Static` version), this function will exist and must
36      + be called before any calls are made to the library. Otherwise, a segfault will happen upon any function calls.
37      +
38      + Params:
39      +   libName = name or path to look the SDL2 SO/DLL for, otherwise `null` for default searching path
40      + Throws: `dsdl2.SDLException` if failed to find the library
41      +/
42     void loadSO(string libName = null) @trusted {
43         SDLSupport current = libName is null ? loadSDL() : loadSDL(libName.toStringz());
44         if (current == sdlSupport) {
45             return;
46         }
47 
48         Version wanted = Version(sdlSupport);
49         if (current == SDLSupport.badLibrary) {
50             import std.stdio : writeln;
51 
52             writeln("WARNING: dsdl2 expects SDL ", wanted.format(), ", but got ", getVersion().format(), ".");
53         }
54         else if (current == SDLSupport.noLibrary) {
55             throw new SDLException("No SDL2 library found, especially of version " ~ wanted.format(),
56                 __FILE__, __LINE__);
57         }
58     }
59 }
60 
61 private uint toSDLInitFlags(bool timer, bool audio, bool video, bool joystick, bool haptic, bool gameController,
62     bool events, bool everything, bool noParachute, bool sensor)
63 in {
64     static if (sdlSupport < SDLSupport.v2_0_9) {
65         assert(sensor == false);
66     }
67     else {
68         if (sensor) {
69             assert(getVersion() >= Version(2, 0, 9));
70         }
71     }
72 }
73 do {
74     uint flags = 0;
75 
76     flags |= timer ? SDL_INIT_TIMER : 0;
77     flags |= audio ? SDL_INIT_AUDIO : 0;
78     flags |= video ? SDL_INIT_VIDEO : 0;
79     flags |= joystick ? SDL_INIT_JOYSTICK : 0;
80     flags |= haptic ? SDL_INIT_HAPTIC : 0;
81     flags |= gameController ? SDL_INIT_GAMECONTROLLER : 0;
82     flags |= events ? SDL_INIT_EVENTS : 0;
83     flags |= everything ? SDL_INIT_EVERYTHING : 0;
84     flags |= noParachute ? SDL_INIT_NOPARACHUTE : 0;
85 
86     static if (sdlSupport >= SDLSupport.v2_0_9) {
87         flags |= sensor ? SDL_INIT_SENSOR : 0;
88     }
89 
90     return flags;
91 }
92 
93 /++
94  + Wraps `SDL_Init` which initializes selected subsystems
95  +
96  + Params:
97  +   timer = selects the `SDL_INIT_TIMER` subsystem
98  +   audio = selects the `SDL_INIT_AUDIO` subsystem
99  +   video = selects the `SDL_INIT_VIDEO` subsystem
100  +   joystick = selects the `SDL_INIT_JOYSTICK` subsystem
101  +   haptic = selects the `SDL_INIT_HAPTIC` subsystem
102  +   gameController = selects the `SDL_INIT_GAMECONTROLLER` subsystem
103  +   events = selects the `SDL_INIT_EVENTS` subsystem
104  +   everything = selects the `SDL_INIT_EVERYTHING` subsystem
105  +   noParachute = selects the `SDL_INIT_NOPARACHUTE` subsystem
106  +   sensor = selects the `SDL_INIT_SENSOR` subsystem (from SDL 2.0.9)
107  + Throws: `dsdl2.SDLException` if any selected subsystem failed to initialize
108  + Example:
109  + ---
110  + dsdl2.init(everything : true);
111  + ---
112  +/
113 void init(bool timer = false, bool audio = false, bool video = false, bool joystick = false, bool haptic = false,
114     bool gameController = false, bool events = false, bool everything = false, bool noParachute = false,
115     bool sensor = false) @trusted {
116     uint flags = toSDLInitFlags(timer, audio, video, joystick, haptic, gameController, events, everything,
117         noParachute, sensor);
118 
119     if (SDL_Init(flags) != 0) {
120         throw new SDLException;
121     }
122 }
123 
124 version (unittest) {
125     static this() {
126         version (BindSDL_Static) {
127         }
128         else {
129             loadSO();
130         }
131     }
132 }
133 
134 /++
135  + Wraps `SDL_Quit` which entirely deinitializes SDL2
136  +/
137 void quit() @trusted {
138     SDL_Quit();
139 }
140 
141 version (unittest) {
142     static ~this() {
143         quit();
144     }
145 }
146 
147 /++
148  + Wraps `SDL_QuitSubSystem` which deinitializes specified subsystems
149  +
150  + Params:
151  +   timer = selects the `SDL_INIT_TIMER` subsystem
152  +   audio = selects the `SDL_INIT_AUDIO` subsystem
153  +   video = selects the `SDL_INIT_VIDEO` subsystem
154  +   joystick = selects the `SDL_INIT_JOYSTICK` subsystem
155  +   haptic = selects the `SDL_INIT_HAPTIC` subsystem
156  +   gameController = selects the `SDL_INIT_GAMECONTROLLER` subsystem
157  +   events = selects the `SDL_INIT_EVENTS` subsystem
158  +   everything = selects the `SDL_INIT_EVERYTHING` subsystem
159  +   noParachute = selects the `SDL_INIT_NOPARACHUTE` subsystem
160  +   sensor = selects the `SDL_INIT_SENSOR` subsystem (from SDL 2.0.9)
161  +/
162 void quit(bool timer = false, bool audio = false, bool video = false, bool joystick = false, bool haptic = false,
163     bool gameController = false, bool events = false, bool everything = false, bool noParachute = false,
164     bool sensor = false) @trusted {
165     uint flags = toSDLInitFlags(timer, audio, video, joystick, haptic, gameController, events, everything,
166         noParachute, sensor);
167 
168     SDL_QuitSubSystem(flags);
169 }
170 
171 /++
172  + Wraps `SDL_WasInit` which checks whether particular subsystem(s) is already initialized
173  +
174  + Params:
175  +   timer = selects the `SDL_INIT_TIMER` subsystem
176  +   audio = selects the `SDL_INIT_AUDIO` subsystem
177  +   video = selects the `SDL_INIT_VIDEO` subsystem
178  +   joystick = selects the `SDL_INIT_JOYSTICK` subsystem
179  +   haptic = selects the `SDL_INIT_HAPTIC` subsystem
180  +   gameController = selects the `SDL_INIT_GAMECONTROLLER` subsystem
181  +   events = selects the `SDL_INIT_EVENTS` subsystem
182  +   everything = selects the `SDL_INIT_EVERYTHING` subsystem
183  +   noParachute = selects the `SDL_INIT_NOPARACHUTE` subsystem
184  +   sensor = selects the `SDL_INIT_SENSOR` subsystem (from SDL 2.0.9)
185  +
186  + Returns: `true` if the selected subsystem(s) is initialized, otherwise `false`
187  + Example:
188  + ---
189  + dsdl2.init();
190  + assert(dsdl2.wasInit(video : true) == true);
191  + ---
192  +/
193 bool wasInit(bool timer = false, bool audio = false, bool video = false, bool joystick = false, bool haptic = false,
194     bool gameController = false, bool events = false, bool everything = false, bool noParachute = false,
195     bool sensor = false) @trusted {
196     uint flags = toSDLInitFlags(timer, audio, video, joystick, haptic, gameController, events, everything,
197         noParachute, sensor);
198 
199     return SDL_WasInit(flags) != 0;
200 }
201 
202 /++
203  + D struct that wraps `SDL_version` containing version information
204  +
205  + Example:
206  + ---
207  + import std.stdio;
208  + writeln("We're currently using SDL version ", dsdl2.getVersion().format());
209  + ---
210  +/
211 struct Version {
212     SDL_version sdlVersion; /// Internal `SDL_version` struct
213 
214     this() @disable;
215 
216     /++
217      + Constructs a `dsdl2.Version` from a vanilla `SDL_version` from bindbc-sdl
218      +
219      + Params:
220      +   sdlVersion = the `SDL_version` struct
221      +/
222     this(SDL_version sdlVersion) {
223         this.sdlVersion = sdlVersion;
224     }
225 
226     /++
227      + Constructs a `dsdl2.Version` by feeding in `major`, `minor`, and `patch` version numbers
228      +
229      + Params:
230      +   major = major version number
231      +   minor = minor version number
232      +   patch = patch verion number
233      +/
234     this(ubyte major, ubyte minor, ubyte patch = 0) {
235         this.sdlVersion.major = major;
236         this.sdlVersion.minor = minor;
237         this.sdlVersion.patch = patch;
238     }
239 
240     /++
241      + Compares two `dsdl2.Version`s from chronology
242      +/
243     int opCmp(Version other) const {
244         if (this.major != other.major) {
245             return this.major - other.major;
246         }
247         else if (this.minor != other.minor) {
248             return this.minor - other.minor;
249         }
250         else {
251             return this.patch - other.patch;
252         }
253     }
254     ///
255     unittest {
256         assert(dsdl2.Version(2, 0, 0) == dsdl2.Version(2, 0, 0));
257         assert(dsdl2.Version(2, 0, 1) > dsdl2.Version(2, 0, 0));
258         assert(dsdl2.Version(2, 0, 1) < dsdl2.Version(2, 0, 2));
259         assert(dsdl2.Version(2, 0, 2) >= dsdl2.Version(2, 0, 1));
260         assert(dsdl2.Version(2, 0, 2) <= dsdl2.Version(2, 0, 2));
261     }
262 
263     /++
264      + Formats the `dsdl2.Version` into its construction representation: `"dsdl2.Version(<major>, <minor>, <patch>)"`
265      +
266      + Returns: the formatted `string`
267      +/
268     string toString() const {
269         return "dsdl2.Version(%d, %d, %d)".format(this.major, this.minor, this.patch);
270     }
271 
272     /++
273      + Proxy to the major version value of the `dsdl2.Version`
274      +
275      + Returns: major version value value of the `dsdl2.Version`
276      +/
277     ref inout(ubyte) major() return inout @property {
278         return this.sdlVersion.major;
279     }
280 
281     /++
282      + Proxy to the minor version value of the `dsdl2.Version`
283      +
284      + Returns: minor version value value of the `dsdl2.Version`
285      +/
286     ref inout(ubyte) minor() return inout @property {
287         return this.sdlVersion.minor;
288     }
289 
290     /++
291      + Proxy to the patch version value of the `dsdl2.Version`
292      +
293      + Returns: patch version value value of the `dsdl2.Version`
294      +/
295     ref inout(ubyte) patch() return inout @property {
296         return this.sdlVersion.patch;
297     }
298 
299     /++
300      + Gets the static array representation of the `dsdl2.Version`
301      +
302      + Returns: `major`, `minor`, `patch` as an array
303      +/
304     ubyte[3] array() const @property {
305         return [
306             this.sdlVersion.major, this.sdlVersion.minor, this.sdlVersion.patch
307         ];
308     }
309 
310     /++
311      + Formats the `dsdl2.Version` into `string`: `"<major>.<minor>.<patch>"`
312      +
313      + Returns: the formatted `string`
314      +/
315     string format() const {
316         return "%d.%d.%d".format(this.major, this.minor, this.patch);
317     }
318 }
319 ///
320 unittest {
321     auto minimumVersion = dsdl2.Version(2, 0, 0);
322     auto ourVersion = dsdl2.getVersion();
323     assert(ourVersion >= minimumVersion);
324 }
325 ///
326 unittest {
327     assert(dsdl2.Version(2, 0, 2) > dsdl2.Version(2, 0, 0));
328     assert(dsdl2.Version(2, 2, 0) >= dsdl2.Version(2, 0, 2));
329     assert(dsdl2.Version(3, 0, 0) >= dsdl2.Version(2, 2, 0));
330 }
331 
332 /++
333  + Wraps `SDL_GetVersion` which gets the version of the linked SDL2 library
334  +
335  + Returns: `dsdl2.Version` of the linked SDL2 library
336  +/
337 Version getVersion() @trusted {
338     Version ver = void;
339     SDL_GetVersion(&ver.sdlVersion);
340     return ver;
341 }
342 
343 /++
344  + Wraps `SDL_GetRevision` which returns the revision string of the linked SDL2 library
345  +
346  + Returns: `string` of the revision code
347  +/
348 string getRevision() @trusted {
349     return SDL_GetRevision().to!string;
350 }
351 
352 /++
353  + D enum that wraps `SDL_HintPriority`
354  +/
355 enum HintPriority : SDL_HintPriority {
356     /++
357      + Wraps `SDL_HINT_*` enumeration constants
358      +/
359     default_ = SDL_HINT_DEFAULT,
360     normal = SDL_HINT_NORMAL, /// ditto
361     override_ = SDL_HINT_OVERRIDE /// ditto
362 }
363 
364 /++
365  + Wraps `SDL_SetHintWithPriority` which provides giving a hint to SDL2 in runtime
366  +
367  + Params:
368  +   name = name of the hint
369  +   value = value to set the hint as
370  +   priority = priority of the hint configuration (by default, `dsdl2.HintPriority.normal`)
371  +
372  + Returns: `true` if the hint was set, `false` otherwise
373  +/
374 bool setHint(string name, string value, HintPriority priority = HintPriority.normal) @trusted {
375     return SDL_SetHintWithPriority(name.toStringz(), value.toStringz(), priority) == SDL_TRUE;
376 }
377 
378 static if (sdlSupport >= SDLSupport.v2_26) {
379     /++
380      + Wraps `SDL_ResetHints` (from SDL 2.26) which resets any user-set hints given to SDL2 to default
381      +/
382     void resetHints() @trusted
383     in {
384         assert(getVersion() >= Version(2, 26));
385     }
386     do {
387         SDL_ResetHints();
388     }
389 }
390 
391 /++
392  + Wraps `SDL_GetHint` which gets the value of a specified user-set hint
393  +
394  + Params:
395  +   name = name of the hint
396  + Returns: value of the given `name` of the hint
397  +/
398 string getHint(string name) @trusted {
399     if (const(char)* hint = SDL_GetHint(name.toStringz())) {
400         return hint.to!string;
401     }
402     else {
403         return "";
404     }
405 }
406 
407 static if (sdlSupport >= SDLSupport.v2_0_5) {
408     /++
409      + Wraps `SDL_GetHintBoolean` (from SDL 2.0.5) which gets the value of a specified user-set hint as a `bool`
410      +
411      + Params:
412      +   name = name of the hint
413      +   defaultValue = default returned value if the hint wasn't set
414      + Returns: `bool` value of the given `name` of the hint or `defaultValue` if the hint wasn't set
415      +/
416     bool getHintBool(string name, bool defaultValue = false) @trusted
417     in {
418         assert(getVersion() >= Version(2, 0, 5));
419     }
420     do {
421         return SDL_GetHintBoolean(name.toStringz(), defaultValue) == SDL_TRUE;
422     }
423 }
424 
425 /++
426  + Wraps `SDL_GetTicks` or `SDL_GetTicks64` on SDL 2.0.18 which gets the time since the SDL library was initialized
427  + in milliseconds
428  +
429  + Returns: milliseconds since the initialization of the SDL library
430  +/
431 ulong getTicks() @trusted
432 in {
433     static if (sdlSupport >= SDLSupport.v2_0_18) {
434         assert(getVersion() >= Version(2, 0, 18));
435     }
436 }
437 do {
438     static if (sdlSupport >= SDLSupport.v2_0_18) {
439         return SDL_GetTicks64();
440     }
441     else {
442         return SDL_GetTicks();
443     }
444 }