1 //          Copyright Ferdinand Majerech 2014.
2 // Distributed under the Boost Software License, Version 1.0.
3 //    (See accompanying file LICENSE_1_0.txt or copy at
4 //          http://www.boost.org/LICENSE_1_0.txt)
5 
6 /// Handles user input (keyboard, windowing input such as closing the window, etc.).
7 module platform.inputdevice;
8 
9 
10 import std.algorithm;
11 import std.array;
12 import std.conv;
13 import std.exception;
14 import std.experimental.logger;
15 
16 import derelict.sdl2.sdl;
17 
18 public import platform.inputrecording;
19 public import platform.key;
20 
21 import io.yaml;
22 
23 
24 /// Handles user input (keyboard, mouse, windowing input such as resizing the window, etc.).
25 final class InputDevice
26 {
27 public:
28     /** Status of window resizing.
29      *
30      * Implicitly converts info bool, so checks like if(input.resized) can be made.
31      */
32     struct ResizedStatus
33     {
34         // Has the window been resized?
35         bool resized = false;
36 
37         alias resized this;
38 
39     private:
40         // New width and height of the window after resizing.
41         int width_, height_;
42 
43     public:
44         /// Get the new window width. Can only be called if resized.
45         int width() @safe pure nothrow const @nogc
46         {
47             assert(resized, "Trying to get new width when the window was not resized");
48             return width_;
49         }
50 
51         /// Get the new window height. Can only be called if resized.
52         int height() @safe pure nothrow const @nogc
53         {
54             assert(resized, "Trying to get new height when the window was not resized");
55             return height_;
56         }
57     }
58 
59 package:
60     // Game log.
61     Logger log_;
62 
63 private:
64     // Keeps track of keyboard input.
65     Keyboard keyboard_;
66 
67     // Keeps track of mouse input.
68     Mouse mouse_;
69 
70     // Does the user want to quit the program?
71     bool quit_;
72 
73     // Recording device used for recording input for benchmark demos.
74     InputRecordingDevice recorder_;
75 
76     // State needed for replaying recorded input of one type (e.g. mouse or keyboard).
77     struct ReplayState(Input)
78     {
79         // Currently playing input recording for Input. Null if no recording is playing.
80         Recording!Input recording = null;
81 
82         // A HACK to ensure game state during a replay 'lines up' to state during recording.
83         //
84         // Specifies number of frames after replay() is called to wait before really starting
85         // to replay.
86         //
87         // Without this, replaying recorded input is slightly 'off', e.g. the camera is moved
88         // slightly less far, resulting in entities that were selected when recording not
89         // being selected when replaying, etc.
90         //
91         // TODO: Try to figure out a way to fix this problem without this hack.
92         //       (Note: the one-frame recording delay does not seem to be responsible)
93         //       2014-09-11
94         size_t delay;
95 
96         enum blockName = "block" ~ Input.stringof;
97         // Should the real input be blocked while input is being replayed from recording?
98         Flag!blockName block;
99     }
100 
101     // State needed to replay recorded mouse input.
102     ReplayState!Mouse replayM_;
103     // State needed to replay recorded keyboard input.
104     ReplayState!Keyboard replayK_;
105 
106     // Status of resizing the window (converts to true if window was resized this frame).
107     ResizedStatus resized_;
108 
109     /* Acts as a queue of UTF-32 code points encoded in UTF-8.
110      *
111      * If not empty, one element is popped each frame.
112      */
113     char[] unicodeQueue_;
114 
115 public:
116     /** Construct an InputDevice.
117      *
118      * Params:
119      *
120      * getHeight = Delegate that returns window height.
121      * log       = Game log.
122      */
123     this(long delegate() @safe pure nothrow @nogc getHeight, Logger log) @trusted nothrow
124     {
125         log_      = log;
126         keyboard_ = new Keyboard();
127         mouse_    = new Mouse(getHeight);
128         recorder_ = new InputRecordingDevice(this);
129         SDL_StartTextInput();
130     }
131 
132     /// Destroy the InputDevice. Must be called to ensure deletion of manually allocated memory.
133     ~this() @trusted
134     {
135         SDL_StopTextInput();
136         destroy(recorder_);
137     }
138 
139     /// Get a reference to the recording device to record input with.
140     InputRecordingDevice recorder() @safe pure nothrow @nogc
141     {
142         return recorder_;
143     }
144 
145     /** Start replaying mouse input from a recording.
146      *
147      * The recording will continue to play until it is spent.
148      * Will consume the recording.
149      *
150      * If something is already replaying (from a previous replay() call), it will be
151      * overridden.
152      *
153      * Params:
154      *
155      * recording = The recording to play. Will continue to play until spent. Will be 
156      *             consumed by the InputDevice.
157      * block     = Should input from the real mouse be blocked while replaying?
158      */
159     void replay(Recording!Mouse recording, Flag!"blockMouse" block) @safe pure nothrow @nogc
160     {
161         replayM_ = ReplayState!Mouse(recording, 1, block);
162     }
163 
164     /** Start replaying keyboard input from a recording.
165      *
166      * If something is already replaying (from a previous replay() call), it will be
167      * overridden.
168      *
169      * Params:
170      *
171      * recording = The recording to play. Will continue to play until spent. Will be 
172      *             consumed by the InputDevice.
173      * block     = Should input from the real keyboard be blocked while replaying?
174      */
175     void replay(Recording!Keyboard recording, Flag!"blockKeyboard" block)
176         @safe pure nothrow @nogc
177     {
178         replayK_ = ReplayState!Keyboard(recording, 1, block);
179     }
180 
181     /// Collect user input.
182     void update() @trusted nothrow // @nogc
183     {
184         import std.utf;
185         if(!unicodeQueue_.empty) try
186         {
187             unicodeQueue_.popFront();
188         }
189         catch(UTFException e)
190         {
191             log_.warning("Error in unicode input decoding, clearing unicode input")
192                 .assumeWontThrow;
193             unicodeQueue_.length = 0;
194         }
195         catch(Exception e) { assert(false, "Unexpected exception"); }
196         
197         // Record input from the *previous frame* (avoids recording the current frame
198         // of a stopRecord() call, which could record the input that stopped it)
199         recorder_.update();
200 
201         SDL_PumpEvents();
202         mouse_.clear();
203         keyboard_.clear();
204 
205         if(!replayM_.block) { mouse_.getInput(); }
206         if(!replayK_.block) { keyboard_.getInput(); }
207 
208         handleRecord(mouse_, replayM_);
209         handleRecord(keyboard_, replayK_);
210 
211         resized_ = ResizedStatus.init;
212         SDL_Event e;
213         while(SDL_PollEvent(&e) != 0)
214         {
215             if(!replayM_.block) { mouse_.handleEvent(e); }
216             // Quit if the user closes the window or presses Escape.
217             if(e.type == SDL_QUIT) { quit_ = true; }
218             if(e.type == SDL_WINDOWEVENT)
219             {
220                 if(e.window.event == SDL_WINDOWEVENT_RESIZED)
221                 {
222                     resized_ = ResizedStatus(true, e.window.data1, e.window.data2);
223                 }
224             }
225             if(e.type == SDL_TEXTINPUT)
226             {
227                 unicodeQueue_.assumeSafeAppend();
228                 import core.stdc..string: strlen;
229                 unicodeQueue_ ~= e.text.text[0 .. strlen(e.text.text.ptr)];
230             }
231         }
232 
233         // Our GUI reads backspace/enter through unicode().
234         if(keyboard_.pressed(Key.Return))    { unicodeQueue_ ~= 0x0D; }
235         if(keyboard_.pressed(Key.Backspace)) { unicodeQueue_ ~= 0x08; }
236     }
237 
238     /// Get access to keyboard input.
239     const(Keyboard) keyboard() @safe pure nothrow const @nogc { return keyboard_; }
240 
241     /// Get access to mouse input.
242     const(Mouse) mouse() @safe pure nothrow const @nogc { return mouse_; }
243 
244     /// Status of resizing the window (converts to true if window was resized this frame).
245     ResizedStatus resized() @safe pure nothrow const @nogc { return resized_; }
246 
247     /// Does the user want to quit the program (e.g. by pressing the close window button).
248     bool quit() @safe pure nothrow const @nogc { return quit_; }
249 
250     /** Get the "current" unicode character for text input purposes.
251      *
252      * Text input is pretty complicated and the InputDevice may (at least in theory)
253      * receive more than one unicode character in some frames. These are stored in an
254      * internal queue that is popped once per frame. This accesses the popped value.
255      *
256      * If there are no characters in the queue, 0 is returned. Enter/backspace key presses
257      * are also registered as unicode characters (0x0D/0x0D respectively).
258      *
259      */
260     dchar unicode() @safe pure nothrow const
261     {
262         if(unicodeQueue_.empty) { return 0; }
263 
264         import std.utf;
265         // An error, if any, will be detected/logged on the next frame (no logging here - const)
266         try { return unicodeQueue_.front; }
267         catch(UTFException e) { return 0; }
268         catch(Exception e) { assert(false, "Unexpected exception"); }
269     }
270 
271 
272 private:
273     /** If specified input recording is not null, attempts to play one frame from the recording.
274      *
275      * Params:
276      *
277      * input  = Input affected by the recording. E.g. mouse for a mouse input recording.
278      * replay = The recording to play with some extra state. If the recording is null,
279      *          handleRecord() will do anything. If it's empty, replay will be
280      *          reset to its init value.
281      */
282     static void handleRecord(Input)(Input input, ref ReplayState!Input replay)
283         @safe
284     {
285         scope(exit) if(replay.delay > 0) { --replay.delay; }
286         if(replay.recording is null || replay.delay > 0) { return; }
287 
288         if(replay.recording.empty) 
289         {
290             replay = replay.init;
291         }
292         else
293         {
294             input.handleRecord(replay.recording.front);
295             replay.recording.popFront();
296         }
297     }
298 }
299 
300 
301 import std.typecons;
302 
303 /// Keeps track of which keys are pressed on the keyboard.
304 final class Keyboard
305 {
306 package:
307     // Keyboard data members separated into a struct for easy recording.
308     //
309     // "Base" state because all other state (movement) can be derived from this data
310     // (movement - change of BaseState between frames).
311     struct BaseState
312     {
313         // Unlikely to have more than 256 keys pressed at any one time (we ignore any more).
314         SDL_Keycode[256] pressedKeys_;
315         // The number of values used in pressedKeys_.
316         size_t pressedKeyCount_;
317 
318         // Convert a BaseState to a YAML node.
319         YAMLNode toYAML() @safe nothrow const
320         {
321             auto pressedKeys = pressedKeys_[0 .. pressedKeyCount_];
322             return YAMLNode(pressedKeys.map!(to!string).array.assumeWontThrow);
323         }
324 
325         /* Load a BaseState from a YAML node (produced by BaseState.toYAML()).
326          *
327          * Throws:
328          *
329          * ConvException if any value in the YAML has unexpected format.
330          * YAMLException if the YAML has unexpected layout.
331          */
332         static BaseState fromYAML(ref YAMLNode yaml) @safe
333         {
334             enforce(yaml.length <= pressedKeys_.length,
335                     new YAMLException("Too many pressed keys in a record"));
336             BaseState result;
337             foreach(string key; yaml)
338             {
339                 result.pressedKeys_[result.pressedKeyCount_++] = to!SDL_Keycode(key);
340             }
341             return result;
342         }
343     }
344 
345     BaseState baseState_;
346     alias baseState_ this;
347 
348 private:
349     // pressedKeys_ from the last update, to detect that a key has just been pressed/released.
350     SDL_Keycode[256] pressedKeysLastUpdate_;
351     // The number of values used in pressedKeysLastUpdate_.
352     size_t pressedKeyCountLastUpdate_;
353 
354 public:
355     /// Get the state of specified keyboard key.
356     Flag!"isPressed" key(const Key keycode) @safe pure nothrow const @nogc
357     {
358         auto keys = pressedKeys_[0 .. pressedKeyCount_];
359         return keys.canFind(cast(SDL_Keycode)keycode) ? Yes.isPressed : No.isPressed;
360     }
361 
362     /// Determine if specified key was just pressed.
363     Flag!"pressed" pressed(const Key keycode) @safe pure nothrow const @nogc
364     {
365         // If it is pressed now but wasn't pressed the last frame, it has just been pressed.
366         auto keys = pressedKeysLastUpdate_[0 .. pressedKeyCountLastUpdate_];
367         const sdlKey = cast(SDL_Keycode)keycode;
368         return (key(keycode) && !keys.canFind(sdlKey)) ? Yes.pressed : No.pressed;
369     }
370 
371 private:
372     /// Clear any keyboard state that must be cleared every frame.
373     void clear() @safe pure nothrow @nogc
374     {
375         pressedKeysLastUpdate_[]   = pressedKeys_[];
376         pressedKeyCountLastUpdate_ = pressedKeyCount_;
377         pressedKeyCount_ = 0;
378     }
379 
380     /// Get keyboard input that must be refreshed every frame.
381     void getInput() @system nothrow @nogc
382     {
383         int numKeys;
384         const Uint8* allKeys = SDL_GetKeyboardState(&numKeys);
385         foreach(SDL_Scancode scancode, Uint8 state; allKeys[0 .. numKeys])
386         {
387             if(!state) { continue; }
388             pressedKeys_[pressedKeyCount_++] = SDL_GetKeyFromScancode(scancode);
389         }
390     }
391 
392     /** Load base state from a record.
393      *
394      * Pressed keys on the keyboard are combined with pressed keys loaded from the record.
395      */
396     void handleRecord(ref const BaseState state) @safe pure nothrow @nogc
397     {
398         foreach(key; state.pressedKeys_[0 .. state.pressedKeyCount_])
399         {
400             // Ignore more than 256 keys pressed at the same time.
401             if(pressedKeyCount_ >= pressedKeys_.length) { return; }
402             pressedKeys_[pressedKeyCount_++] = key;
403         }
404     }
405 }
406 
407 /// Keeps track of mouse position, buttons, dragging, etc.
408 final class Mouse
409 {
410 package:
411     // Mouse data members separated into a struct for easy recording.
412     //
413     // "Base" state because all other state (movement) can be derived from this data
414     // (movement - change of BaseState between frames).
415     struct BaseState
416     {
417         // X coordinate of mouse position.
418         int x_;
419         // Y coordinate of mouse position.
420         int y_;
421 
422         // X coordinate of the mouse wheel (if the wheel supports horizontal scrolling).
423         int wheelX_;
424         // Y coordinate of the mouse wheel (aka scrolling with a normal wheel).
425         int wheelY_;
426 
427         // Did the user finish a click with a button during this update?
428         Flag!"click"[Button.max + 1] click_;
429 
430         // Did the user finish a doubleclick with a button during this update?
431         Flag!"doubleClick"[Button.max + 1] doubleClick_;
432 
433         // State of all (well, at most 5) mouse buttons.
434         Flag!"pressed"[Button.max + 1] buttons_;
435 
436         // Convert a BaseState to a YAML node.
437         YAMLNode toYAML() @safe nothrow const
438         {
439             string[] keys;
440             YAMLNode[] values;
441             keys ~= "x"; values ~= YAMLNode(x_);
442             keys ~= "y"; values ~= YAMLNode(y_);
443             keys ~= "wheelX"; values ~= YAMLNode(wheelX_);
444             keys ~= "wheelY"; values ~= YAMLNode(wheelY_);
445 
446             keys ~= "click";
447             values ~= YAMLNode(click_[].map!(to!string).array.assumeWontThrow);
448             keys ~= "doubleClick";
449             values ~= YAMLNode(doubleClick_[].map!(to!string).array.assumeWontThrow);
450             keys ~= "buttons";
451             values ~= YAMLNode(buttons_[].map!(to!string).array.assumeWontThrow);
452             return YAMLNode(keys, values);
453         }
454 
455         /* Load a BaseState from a YAML node (produced by BaseState.toYAML()).
456          *
457          * Throws:
458          *
459          * ConvException if any value in the YAML has unexpected format.
460          * YAMLException if the YAML has unexpected layout.
461          */
462         static BaseState fromYAML(ref YAMLNode yaml) @safe
463         {
464             // Used to load button arrays (buttons_, click_, doubleClick_)
465             void buttonsFromYAML(F)(F[] flags, ref YAMLNode seq)
466             {
467                 enforce(seq.length <= flags.length,
468                         new YAMLException("Too many mouse buttons in recording"));
469                 size_t idx = 0;
470                 foreach(string button; seq) { flags[idx++] = button.to!F; }
471             }
472             BaseState result;
473             foreach(string key, ref YAMLNode value; yaml) switch(key)
474             {
475                 case "x":           result.x_      = value.as!int;                 break;
476                 case "y":           result.y_      = value.as!int;                 break;
477                 case "wheelX":      result.wheelX_ = value.as!int;                 break;
478                 case "wheelY":      result.wheelY_ = value.as!int;                 break;
479                 case "click":       buttonsFromYAML(result.click_[],       value); break;
480                 case "doubleClick": buttonsFromYAML(result.doubleClick_[], value); break;
481                 case "buttons":     buttonsFromYAML(result.buttons_[],     value); break;
482                 default: throw new YAMLException("Unknown key in mouse record: " ~ key);
483             }
484             return result;
485         }
486     }
487 
488     BaseState baseState_;
489     alias baseState_ this;
490 
491 private:
492     // Y movement of mouse since the last update.
493     int xMovement_;
494     // Y movement of mouse since the last update.
495     int yMovement_;
496 
497     // X movement of the wheel since the last update.
498     int wheelYMovement_;
499     // Y movement of the wheel since the last update.
500     int wheelXMovement_;
501 
502     // State of all (well, at most 5) mouse buttons.
503     Flag!"pressed"[Button.max + 1] buttonsLastUpdate_;
504 
505     // Coordinates where each button was last pressed (for dragging).
506     vec2i[Button.max + 1] pressedCoords_;
507 
508     // Gets the current window height.
509     long delegate() @safe pure nothrow @nogc getHeight_;
510 
511     import gl3n_extra.linalg;
512 
513 public:
514 nothrow @nogc:
515     /// Enumerates mouse buttons.
516     enum Button: ubyte
517     {
518         Left    = 0,
519         Middle  = 1,
520         Right   = 2,
521         X1      = 3,
522         X2      = 4,
523         // Using 16 to avoid too big BaseState arrays.
524         Unknown = 16
525     }
526 
527     /** Construct a Mouse and initialize button states.
528      *
529      * Params:
530      *
531      * getHeight = Delegate that returns current window height.
532      */
533     this(long delegate() @safe pure nothrow @nogc getHeight) @safe nothrow
534     {
535         getHeight_ = getHeight;
536         xMovement_ = yMovement_ = 0;
537         getMouseState();
538     }
539 
540 @safe pure const
541 {
542     /// Get X coordinate of mouse position.
543     int x() { return x_; }
544     /// Get Y coordinate of mouse position.
545     int y() { return y_; }
546 
547     /// Get X movement of mouse since the last update.
548     int xMovement() { return xMovement_; }
549     /// Get Y movement of mouse since the last update.
550     int yMovement() { return yMovement_; }
551 
552     /// Get X coordinate of the mouse wheel (if it supports horizontal scrolling).
553     int wheelX() { return wheelX_; }
554     /// Get Y coordinate of the mouse wheel.
555     int wheelY() { return wheelY_; }
556 
557     /// Get the X movement of the wheel since the last update.
558     int wheelXMovement() { return wheelXMovement_; }
559     /// Get the Y movement of the wheel since the last update.
560     int wheelYMovement() { return wheelYMovement_; }
561 
562     /// Did the user finish a double click during this update?
563     Flag!"doubleClick" doubleClicked(Button button) { return doubleClick_[button]; }
564     /// Did the user finish a click during this update?
565     Flag!"click" clicked(Button button) { return click_[button]; }
566     /// Get the state of specified mouse button.
567     Flag!"pressed" button(Button button) { return buttons_[button]; }
568 
569     /// Get the coordinates at which button was last pressed. Useful for dragging.
570     vec2i pressedCoords(Button button) { return pressedCoords_[button]; }
571 }
572 
573 private:
574     /// Handle an SDL event (which may be a mouse event).
575     void handleEvent(ref const SDL_Event e) @system nothrow
576     {
577         static Button button(Uint8 sdlButton) @safe pure nothrow @nogc
578         {
579             switch(sdlButton)
580             {
581                 case SDL_BUTTON_LEFT:   return Button.Left;
582                 case SDL_BUTTON_MIDDLE: return Button.Middle;
583                 case SDL_BUTTON_RIGHT:  return Button.Right;
584                 case SDL_BUTTON_X1:     return Button.X1;
585                 case SDL_BUTTON_X2:     return Button.X2;
586                 // SDL should not report any other value for mouse buttons... but it does.
587                 default: return Button.Unknown; // assert(false, "Unknown mouse button");
588             }
589         }
590         switch(e.type)
591         {
592             case SDL_MOUSEMOTION: break;
593             case SDL_MOUSEWHEEL:
594                 wheelX_ += e.wheel.x;
595                 wheelY_ += e.wheel.y;
596                 // += is needed because there might be multiple wheel events per frame.
597                 wheelXMovement_ += e.wheel.x;
598                 wheelYMovement_ += e.wheel.y;
599                 break;
600             case SDL_MOUSEBUTTONUP:
601                 const b = button(e.button.button);
602                 // Don't set to No.click so we don't override clicks from any playing recording.
603                 if(e.button.clicks > 0)      { click_[b]       = Yes.click; }
604                 if(e.button.clicks % 2 == 0) { doubleClick_[b] = Yes.doubleClick; }
605                 break;
606             case SDL_MOUSEBUTTONDOWN:
607                 // Save the coords where the button was pressed (for dragging).
608                 pressedCoords_[button(e.button.button)] = vec2i(x_, y_);
609                 break;
610             default: break;
611         }
612     }
613 
614     /// Clear any mouse state that must be cleared every frame.
615     void clear() @safe pure nothrow @nogc
616     {
617         xMovement_      = yMovement_      = 0;
618         wheelXMovement_ = wheelYMovement_ = 0;
619         click_[]       = No.click;
620         doubleClick_[] = No.doubleClick;
621         buttonsLastUpdate_[] = buttons_[];
622         buttons_[] = No.pressed;
623     }
624 
625     /// Get mouse input that must be refreshed every frame.
626     void getInput() @safe nothrow 
627     {
628         const oldX = x_; const oldY = y_;
629         getMouseState();
630         xMovement_ = x_ - oldX; yMovement_ = y_ - oldY;
631     }
632 
633     /** Load base state from a record.
634      *
635      * Mouse cursor and wheel coordinates are considered absolute; i.e. recorded coords
636      * override current cursor/wheel position.
637      *
638      * Button clicks are not absolute; any clicks from the record are added to clicks
639      * registered from current input. Same for pressed buttons.
640      */
641     void handleRecord(ref const BaseState state) @safe pure nothrow @nogc
642     {
643         xMovement_ += state.x_ - x_;
644         yMovement_ += state.y_ - y_;
645         x_ = state.x_;
646         y_ = state.y_;
647 
648         wheelXMovement_ += state.wheelX_ - wheelX_;
649         wheelYMovement_ += state.wheelY_ - wheelY_;
650         wheelX_ = state.wheelX_;
651         wheelY_ = state.wheelY_;
652 
653         foreach(button; 0 .. Button.max + 1)
654         {
655             // A button has been 'pressed' from the recording
656             const justPressed = !buttonsLastUpdate_[button] && state.buttons_[button];
657             // If no click in record, we keep the click/no click from user input.
658             if(state.click_[button])       { click_[button]         = Yes.click;       }
659             if(state.doubleClick_[button]) { doubleClick_[button]   = Yes.doubleClick; }
660             if(state.buttons_[button])     { buttons_[button]       = Yes.pressed;     }
661             if(justPressed)                { pressedCoords_[button] = vec2i(x_, y_);   }
662         }
663     }
664 
665     /// Get mouse position and button state.
666     void getMouseState() @trusted nothrow
667     {
668         const buttons = SDL_GetMouseState(&x_, &y_);
669         buttons_[Button.Left]   = buttons & SDL_BUTTON_LMASK  ? Yes.pressed : No.pressed;
670         buttons_[Button.Middle] = buttons & SDL_BUTTON_MMASK  ? Yes.pressed : No.pressed;
671         buttons_[Button.Right]  = buttons & SDL_BUTTON_RMASK  ? Yes.pressed : No.pressed;
672         buttons_[Button.X1]     = buttons & SDL_BUTTON_X1MASK ? Yes.pressed : No.pressed;
673         buttons_[Button.X2]     = buttons & SDL_BUTTON_X2MASK ? Yes.pressed : No.pressed;
674         y_ = cast(int)(getHeight_() - y_);
675     }
676 }
677 
678 // TODO: 'MappedInput' to wrap Mouse/Keyboard. Will read keybrd/mouse mappings from YAML.
679 // Its API will use enum values (InputAction?) instead of keys, e.g. InputAction.Attack,
680 // -||-.Deploy, -||-.SelectAllThisType, etc; InputActions will map to loaded mappings, and
681 // the API will expose mouse *position* (and pos where mouse was last _pressed_), so user
682 // can e.g. detect that InputAction.Select is active (e.g.  left click), and get mouse pos
683 // to know what to select. With mapping, Select can be mapped e.g. to Enter so user can
684 // select units by mouse over + Enter instead of left-click. 2014-08-26
685 //
686 // Would also allow support for e.g. gamepads (not that it makes sense... maybe Steam
687 // Controller?)