1 /**
2  * Defines the static Input class, which is responsible for handling all keyboard/mouse/controller interactions.
3  */
4 module dash.utility.input.input;
5 import dash.utility, dash.core, dash.graphics;
6 
7 import yaml;
8 import derelict.opengl3.gl3;
9 import std.algorithm, std.conv, std.uuid;
10 
11 /// The event type for button events.
12 package alias ButtonEvent = void delegate( ButtonStorageType );
13 /// The event type for axis events.
14 package alias AxisEvent   = void delegate( AxisStorageType );
15 
16 /**
17  * Manages all input events.
18  */
19 final abstract class Input
20 {
21 static:
22 private:
23     static struct Binding
24     {
25     public:
26         @ignore
27         string name;
28 
29         @rename( "Keyboard" ) @byName @optional
30         Keyboard.Buttons[] keyboardButtons;
31 
32         @rename( "Mouse" ) @byName @optional
33         Mouse.Buttons[] mouseButtons;
34 
35         @rename( "MouseAxes" ) @byName @optional
36         Mouse.Axes[] mouseAxes;
37     }
38 
39     /// Bindings directly from Config/Input
40     Binding[string] bindings;
41     /// The file the bindings came from.
42     Resource bindingFile = internalResource;
43 
44     /// The registered button events.
45     Tuple!( UUID, ButtonEvent )[][ string ] buttonEvents;
46     /// The registered axis events.
47     Tuple!( UUID, AxisEvent )[][ string ] axisEvents;
48 
49 public:
50     /**
51      * Processes Config/Input.yml and pulls input string bindings.
52      */
53     void initialize()
54     {
55         try
56         {
57             auto bindingRes = deserializeFileByName!(typeof(bindings))( Resources.InputBindings );
58             bindings = bindingRes[ 0 ];
59             bindingFile = bindingRes[ 1 ];
60 
61             foreach( name, ref binding; bindings )
62                 binding.name = name;
63         }
64         catch( Exception e )
65         {
66             errorf( "Error parsing config file:%s\n", e.toString() );
67         }
68 
69         Keyboard.initialize();
70         Mouse.initialize();
71     }
72 
73     /**
74      * Updates the key states, and calls all key events.
75      */
76     void update()
77     {
78         Keyboard.update();
79         Mouse.update();
80     }
81 
82     /// Called by InputSystem to report changed inputs.
83     package void processDiffs( InputSystem, InputEnum, StateRep )( Tuple!( InputEnum, StateRep )[] diffs )
84     {
85         // Iterate over each diff.
86         foreach( diff; diffs )
87         {
88             // Iterate over each binding.
89             foreach( name, binding; bindings )
90             {
91                 // Get the array of bindings to check and events to call.
92                 static if( is( InputEnum == Keyboard.Buttons ) )
93                 {
94                     auto bindingArray = binding.keyboardButtons;
95                     auto eventArray = buttonEvents;
96                 }
97                 else static if( is( InputEnum == Mouse.Buttons ) )
98                 {
99                     auto bindingArray = binding.mouseButtons;
100                     auto eventArray = buttonEvents;
101                 }
102                 else static if( is( InputEnum == Mouse.Axes ) )
103                 {
104                     auto bindingArray = binding.mouseAxes;
105                     auto eventArray = axisEvents;
106                 }
107                 else static assert( false, "InputEnum unsupported." );
108 
109                 // Check the binding for the changed input.
110                 bindingsLoop:
111                 foreach( button; bindingArray )
112                 {
113                     if( button == diff[ 0 ] && name in eventArray )
114                     {
115                         foreach( eventTup; eventArray[ name ] )
116                         {
117                             eventTup[ 1 ]( diff[ 1 ] );
118                             break bindingsLoop;
119                         }
120                     }
121                 }
122             }
123         }
124     }
125 
126     /**
127      * Gets the state of a string-bound input.
128      *
129      * Params:
130      *      input =         The input to check for.
131      *      checkPrevious = Whether or not to make sure the key was up last frame.
132      */
133     T getState( T = bool )( string input, bool checkPrevious = false ) if( is( T == bool ) || is( T == float ) )
134     {
135         if( auto binding = input in bindings )
136         {
137             static if( is( T == bool ) )
138             {
139                 foreach( key; binding.keyboardButtons )
140                     if( Keyboard.isButtonDown( key, checkPrevious ) )
141                         return true;
142                 foreach( mb; binding.mouseButtons )
143                     if( Mouse.isButtonDown( mb, checkPrevious ) )
144                         return true;
145 
146                 return false;
147             }
148             else static if( is( T == float ) )
149             {
150                 foreach( ma; binding.mouseAxes )
151                 {
152                     auto state = Mouse.getAxisState( ma, checkPrevious );
153                     if( state != 0.0f )
154                         return state;
155                 }
156 
157                 return 0.0f;
158             }
159             else static assert( false, "Unsupported return type." );
160         }
161 
162         throw new Exception( "Input " ~ input ~ " not bound." );
163     }
164 
165     /**
166      * Check if a given button is down.
167      *
168      * Params:
169      *      buttonName =        The name of the button to check.
170      *      checkPrevious =     Whether or not to make sure the button was down last frame.
171      */
172     bool isButtonDown( string buttonName, bool checkPrevious = false )
173     {
174         if( auto binding = buttonName in bindings )
175         {
176             foreach( key; binding.keyboardButtons )
177                 if( Keyboard.isButtonDown( key, checkPrevious ) )
178                     return true;
179 
180             foreach( mb; binding.mouseButtons )
181                 if( Mouse.isButtonDown( mb, checkPrevious ) )
182                     return true;
183         }
184 
185         return false;
186     }
187 
188     /**
189      * Check if a given button is down.
190      *
191      * Params:
192      *      buttonName =        The name of the button to check.
193      *      checkPrevious =     Whether or not to make sure the button was down last frame.
194      */
195     bool isButtonUp( string buttonName, bool checkPrevious = false )
196     {
197         if( auto binding = buttonName in bindings )
198         {
199             foreach( key; binding.keyboardButtons )
200                 if( Keyboard.isButtonUp( key, checkPrevious ) )
201                     return true;
202 
203             foreach( mb; binding.mouseButtons )
204                 if( Mouse.isButtonUp( mb, checkPrevious ) )
205                     return true;
206         }
207 
208         return false;
209     }
210 
211     /**
212      * Add an event for when a button state changes.
213      *
214      * Params:
215      *  buttonName =            The binding name of the button for the event.
216      *  event =                 The event to call when the button changes.
217      *
218      * Returns: The id of the new event.
219      */
220     UUID addButtonEvent( string buttonName, ButtonEvent event )
221     {
222         auto id = randomUUID();
223         buttonEvents[ buttonName ] ~= tuple( id, event );
224         return id;
225     }
226 
227     /**
228      * Add an event for when a button goes down.
229      *
230      * Params:
231      *  buttonName =            The binding name of the button for the event.
232      *  event =                 The event to call when the button changes.
233      *
234      * Returns: The id of the new event.
235      */
236     UUID addButtonDownEvent( string buttonName, ButtonEvent event )
237     {
238         return addButtonEvent( buttonName, ( newState ) { if( newState ) event( newState ); } );
239     }
240 
241     /**
242      * Add an event for when a button goes up.
243      *
244      * Params:
245      *  buttonName =            The binding name of the button for the event.
246      *  event =                 The event to call when the button changes.
247      *
248      * Returns: The id of the new event.
249      */
250     UUID addButtonUpEvent( string buttonName, ButtonEvent event )
251     {
252         return addButtonEvent( buttonName, ( newState ) { if( !newState ) event( newState ); } );
253     }
254 
255     /**
256      * Add an event for when an axis changes.
257      *
258      * Params:
259      *  axisName =              The binding name of the axis for the event.
260      *  event =                 The event to call when the button changes.
261      *
262      * Returns: The id of the new event.
263      */
264     UUID addAxisEvent( string axisName, AxisEvent event )
265     {
266         auto id = randomUUID();
267         axisEvents[ axisName ] ~= tuple( id, event );
268         return id;
269     }
270 
271     /**
272      * Remove a button event.
273      *
274      * Params:
275      *  buttonName =            The binding name of the button for the event.
276      *  event =                 The event to call when the button changes.
277      *
278      * Returns: The id of the new event.
279      */
280     bool removeButtonEvent( UUID id )
281     {
282         foreach( name, ref eventGroup; buttonEvents )
283         {
284             auto i = eventGroup.countUntil!( tup => tup[ 0 ] == id );
285 
286             if( i == -1 )
287                 continue;
288 
289             // Get tasks after one being removed.s
290             auto end = eventGroup[ i+1..$ ];
291             // Get tasks before one being removed.
292             eventGroup = eventGroup[ 0..i ];
293             // Add end back.
294             eventGroup ~= end;
295 
296             return true;
297         }
298 
299         return false;
300     }
301 
302     /**
303      * Gets the position of the cursor.
304      *
305      * Returns:     The position of the mouse cursor.
306      */
307     vec2ui mousePos()
308     {
309         version( DashUseSDL2 )
310         {
311             import dash.graphics;
312 
313             if( !Sdl.get() )
314                 return vec2ui( 0, 0 );
315 
316             auto mouse = Sdl.get().sdl.mouse;
317             return vec2ui( cast(uint)mouse.x, cast(uint)( Graphics.height - mouse.y ) );
318         }
319         else version( Windows )
320         {
321             if( !Win32GL.get() )
322                 return vec2ui( 0, 0 );
323 
324             import dash.graphics;
325             import win32.windows;
326             POINT i;
327             GetCursorPos( &i );
328             ScreenToClient( Win32GL.get().hWnd, &i );
329 
330             return vec2ui( i.x, Graphics.height - i.y );
331         }
332         else
333         {
334             return vec2ui( 0, 0 );
335         }
336     }
337 
338     /**
339      * Gets the world position of the cursor in the active scene.
340      *
341      * Returns:     The position of the mouse cursor in world space.
342      */
343     vec3f mousePosView()
344     {
345         if( !DGame.instance.activeScene )
346         {
347             warning( "No active scene." );
348             return vec3f( 0.0f, 0.0f, 0.0f );
349         }
350 
351         auto scene = DGame.instance.activeScene;
352 
353         if( !scene.camera )
354         {
355             warning( "No camera on active scene." );
356             return vec3f( 0.0f, 0.0f, 0.0f );
357         }
358         vec2ui mouse = mousePos();
359         float depth;
360         int x = mouse.x;
361         int y = mouse.y;
362         auto view = vec3f( 0, 0, 0 );
363 
364         if( x >= 0 && x <= Graphics.width && y >= 0 && y <= Graphics.height )
365         {
366             depth = Graphics.getDepthAtScreenPoint( mouse ); 
367 
368             auto linearDepth = scene.camera.projectionConstants.x / ( scene.camera.projectionConstants.y - depth );
369             //Convert x and y to normalized device coords
370             float screenX = ( mouse.x / cast(float)Graphics.width ) * 2 - 1;
371             float screenY = -( ( mouse.y / cast(float)Graphics.height ) * 2 - 1 );
372 
373             auto viewSpace = scene.camera.inversePerspectiveMatrix * vec4f( screenX, screenY, 1.0f, 1.0f);
374             auto viewRay = vec3f( viewSpace.xy * (1.0f / viewSpace.z), 1.0f);
375             view = viewRay * linearDepth;
376         }
377 
378         return view;
379     }
380 
381     /**
382      * Gets the world position of the cursor in the active scene.
383      *
384      * Returns:     The position of the mouse cursor in world space.
385      */
386     vec3f mousePosWorld()
387     {
388         return (DGame.instance.activeScene.camera.inverseViewMatrix * vec4f( mousePosView(), 1.0f )).xyz;
389     }
390 
391     /**
392      * Gets the world position of the cursor in the active scene.
393      *
394      * Returns:     The GameObject located at the current mouse Position
395      */
396     GameObject mouseObject()
397     {
398         if( !DGame.instance.activeScene )
399         {
400             warning( "No active scene." );
401             return null;
402         }
403 
404         auto scene = DGame.instance.activeScene;
405 
406         if( !scene.camera )
407         {
408             warning( "No camera on active scene." );
409             return null;
410         }
411 
412         vec2ui mouse = mousePos();
413 
414         if( mouse.x >= 0 && mouse.x <= Graphics.width && mouse.y >= 0 && mouse.y <= Graphics.height )
415         {
416             uint id = Graphics.getObjectIDAtScreenPoint( mouse );
417 
418             if( id > 0 )
419             {
420                 return scene[ id ];
421             }
422         }
423 
424         return null;
425     }
426 }
427 
428 unittest
429 {
430     import std.stdio;
431     writeln( "Dash Input isKeyUp unittest" );
432 
433     Config.initialize();
434     Input.initialize();
435     Keyboard.setButtonState( Keyboard.Buttons.Space, true );
436 
437     Keyboard.update();
438     Keyboard.setButtonState( Keyboard.Buttons.Space, false );
439 
440     Keyboard.update();
441     assert( Keyboard.isButtonUp( Keyboard.Buttons.Space, true ) );
442     assert( Keyboard.isButtonUp( Keyboard.Buttons.Space, false ) );
443 
444     Keyboard.update();
445     assert( !Keyboard.isButtonUp( Keyboard.Buttons.Space, true ) );
446     assert( Keyboard.isButtonUp( Keyboard.Buttons.Space, false ) );
447 }
448 
449 unittest
450 {
451     import std.stdio;
452     writeln( "Dash Input addKeyEvent unittest" );
453 
454     Config.initialize();
455     Input.initialize();
456 
457     bool keyDown;
458     Keyboard.addButtonEvent( Keyboard.Buttons.Space, ( keyCode, newState )
459     {
460         keyDown = newState;
461     } );
462 
463     Keyboard.setButtonState( Keyboard.Buttons.Space, true );
464     Input.update();
465     assert( keyDown );
466 
467     Keyboard.setButtonState( Keyboard.Buttons.Space, false );
468     Input.update();
469     assert( !keyDown );
470 }
471 
472 unittest
473 {
474     import std.stdio;
475     writeln( "Dash Input isKeyDown unittest" );
476 
477     Config.initialize();
478     Input.initialize();
479     Keyboard.setButtonState( Keyboard.Buttons.Space, true );
480 
481     Input.update();
482     assert( Keyboard.isButtonDown( Keyboard.Buttons.Space, true ) );
483     assert( Keyboard.isButtonDown( Keyboard.Buttons.Space, false ) );
484 
485     Input.update();
486     assert( !Keyboard.isButtonDown( Keyboard.Buttons.Space, true ) );
487     assert( Keyboard.isButtonDown( Keyboard.Buttons.Space, false ) );
488 }