1 #include "cursesdef.h" // IWYU pragma: associated
2 #include "sdltiles.h" // IWYU pragma: associated
3 
4 #include "cuboid_rectangle.h"
5 #include "point.h"
6 
7 #if defined(TILES)
8 
9 #include <algorithm>
10 #include <array>
11 #include <climits>
12 #include <cmath>
13 #include <cstdint>
14 #include <cstring>
15 #include <exception>
16 #include <fstream>
17 #include <iterator>
18 #include <limits>
19 #include <map>
20 #include <memory>
21 #include <set>
22 #include <stdexcept>
23 #include <type_traits>
24 #include <vector>
25 #if defined(_MSC_VER) && defined(USE_VCPKG)
26 #   include <SDL2/SDL_image.h>
27 #   include <SDL2/SDL_syswm.h>
28 #else
29 #ifdef _WIN32
30 #   include <SDL_syswm.h>
31 #endif
32 #endif
33 
34 #include "avatar.h"
35 #include "cached_options.h"
36 #include "cata_assert.h"
37 #include "cata_tiles.h"
38 #include "cata_utility.h"
39 #include "catacharset.h"
40 #include "color.h"
41 #include "color_loader.h"
42 #include "cursesport.h"
43 #include "debug.h"
44 #include "filesystem.h"
45 #include "flag.h"
46 #include "font_loader.h"
47 #include "game.h"
48 #include "game_ui.h"
49 #include "get_version.h"
50 #include "hash_utils.h"
51 #include "input.h"
52 #include "json.h"
53 #include "map.h"
54 #include "optional.h"
55 #include "options.h"
56 #include "output.h"
57 #include "path_info.h"
58 #include "point.h"
59 #include "sdl_geometry.h"
60 #include "sdl_wrappers.h"
61 #include "sdl_font.h"
62 #include "sdlsound.h"
63 #include "string_formatter.h"
64 #include "ui_manager.h"
65 #include "wcwidth.h"
66 
67 #if defined(__linux__)
68 #   include <cstdlib> // getenv()/setenv()
69 #endif
70 
71 #if defined(_WIN32)
72 #   if 1 // HACK: Hack to prevent reordering of #include "platform_win.h" by IWYU
73 #       include "platform_win.h"
74 #   endif
75 #   include <shlwapi.h>
76 #endif
77 
78 #if defined(__ANDROID__)
79 #include <jni.h>
80 
81 #include "action.h"
82 #include "inventory.h"
83 #include "map.h"
84 #include "vehicle.h"
85 #include "vpart_position.h"
86 #include "worldfactory.h"
87 #endif
88 
89 #define dbg(x) DebugLog((x),D_SDL) << __FILE__ << ":" << __LINE__ << ": "
90 
91 //***********************************
92 //Globals                           *
93 //***********************************
94 
95 std::unique_ptr<cata_tiles> tilecontext;
96 static uint32_t lastupdate = 0;
97 static uint32_t interval = 25;
98 static bool needupdate = false;
99 static bool need_invalidate_framebuffers = false;
100 
101 palette_array windowsPalette;
102 
103 static Font_Ptr font;
104 static Font_Ptr map_font;
105 static Font_Ptr overmap_font;
106 
107 static SDL_Window_Ptr window;
108 static SDL_Renderer_Ptr renderer;
109 static SDL_PixelFormat_Ptr format;
110 static SDL_Texture_Ptr display_buffer;
111 static GeometryRenderer_Ptr geometry;
112 #if defined(__ANDROID__)
113 static SDL_Texture_Ptr touch_joystick;
114 #endif
115 static int WindowWidth;        //Width of the actual window, not the curses window
116 static int WindowHeight;       //Height of the actual window, not the curses window
117 // input from various input sources. Each input source sets the type and
118 // the actual input value (key pressed, mouse button clicked, ...)
119 // This value is finally returned by input_manager::get_input_event.
120 static input_event last_input;
121 
122 static constexpr int ERR = -1;
123 static int inputdelay;         //How long getch will wait for a character to be typed
124 static Uint32 delaydpad =
125     std::numeric_limits<Uint32>::max();     // Used for entering diagonal directions with d-pad.
126 static Uint32 dpad_delay =
127     100;   // Delay in milliseconds between registering a d-pad event and processing it.
128 static bool dpad_continuous = false;  // Whether we're currently moving continuously with the dpad.
129 static int lastdpad = ERR;      // Keeps track of the last dpad press.
130 static int queued_dpad = ERR;   // Queued dpad press, for individual button presses.
131 int fontwidth;          //the width of the font, background is always this size
132 int fontheight;         //the height of the font, background is always this size
133 static int TERMINAL_WIDTH;
134 static int TERMINAL_HEIGHT;
135 static bool fullscreen;
136 static int scaling_factor;
137 
138 static SDL_Joystick *joystick; // Only one joystick for now.
139 
140 using cata_cursesport::curseline;
141 using cata_cursesport::cursecell;
142 static std::vector<curseline> oversized_framebuffer;
143 static std::vector<curseline> terminal_framebuffer;
144 static std::weak_ptr<void> winBuffer; //tracking last drawn window to fix the framebuffer
145 static int fontScaleBuffer; //tracking zoom levels to fix framebuffer w/tiles
146 
147 //***********************************
148 //Non-curses, Window functions      *
149 //***********************************
150 
operator ==(const cata_cursesport::WINDOW * const lhs,const catacurses::window & rhs)151 static bool operator==( const cata_cursesport::WINDOW *const lhs, const catacurses::window &rhs )
152 {
153     return lhs == rhs.get();
154 }
155 
ClearScreen()156 static void ClearScreen()
157 {
158     SetRenderDrawColor( renderer, 0, 0, 0, 255 );
159     RenderClear( renderer );
160 }
161 
InitSDL()162 static void InitSDL()
163 {
164     int init_flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
165     int ret;
166 
167 #if defined(SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING)
168     SDL_SetHint( SDL_HINT_WINDOWS_DISABLE_THREAD_NAMING, "1" );
169 #endif
170 
171 #if defined(__linux__)
172     // https://bugzilla.libsdl.org/show_bug.cgi?id=3472#c5
173     if( SDL_COMPILEDVERSION == SDL_VERSIONNUM( 2, 0, 5 ) ) {
174         const char *xmod = getenv( "XMODIFIERS" );
175         if( xmod && strstr( xmod, "@im=ibus" ) != nullptr ) {
176             setenv( "XMODIFIERS", "@im=none", 1 );
177         }
178     }
179 #endif
180 
181     ret = SDL_Init( init_flags );
182     throwErrorIf( ret != 0, "SDL_Init failed" );
183 
184     ret = TTF_Init();
185     throwErrorIf( ret != 0, "TTF_Init failed" );
186 
187     // cata_tiles won't be able to load the tiles, but the normal SDL
188     // code will display fine.
189     ret = IMG_Init( IMG_INIT_PNG );
190     printErrorIf( ( ret & IMG_INIT_PNG ) != IMG_INIT_PNG,
191                   "IMG_Init failed to initialize PNG support, tiles won't work" );
192 
193     ret = SDL_InitSubSystem( SDL_INIT_JOYSTICK );
194     printErrorIf( ret != 0, "Initializing joystick subsystem failed" );
195 
196     //SDL2 has no functionality for INPUT_DELAY, we would have to query it manually, which is expensive
197     //SDL2 instead uses the OS's Input Delay.
198 
199     atexit( SDL_Quit );
200 }
201 
SetupRenderTarget()202 static bool SetupRenderTarget()
203 {
204     SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_NONE );
205     display_buffer.reset( SDL_CreateTexture( renderer.get(), SDL_PIXELFORMAT_ARGB8888,
206                           SDL_TEXTUREACCESS_TARGET, WindowWidth / scaling_factor, WindowHeight / scaling_factor ) );
207     if( printErrorIf( !display_buffer, "Failed to create window buffer" ) ) {
208         return false;
209     }
210     if( printErrorIf( SDL_SetRenderTarget( renderer.get(), display_buffer.get() ) != 0,
211                       "SDL_SetRenderTarget failed" ) ) {
212         return false;
213     }
214     ClearScreen();
215 
216     return true;
217 }
218 
219 //Registers, creates, and shows the Window!!
WinCreate()220 static void WinCreate()
221 {
222     std::string version = string_format( "Cataclysm: Dark Days Ahead - %s", getVersionString() );
223 
224     // Common flags used for fulscreen and for windowed
225     int window_flags = 0;
226     WindowWidth = TERMINAL_WIDTH * fontwidth * scaling_factor;
227     WindowHeight = TERMINAL_HEIGHT * fontheight * scaling_factor;
228     window_flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
229 
230     if( get_option<std::string>( "SCALING_MODE" ) != "none" ) {
231         SDL_SetHint( SDL_HINT_RENDER_SCALE_QUALITY, get_option<std::string>( "SCALING_MODE" ).c_str() );
232     }
233 
234 #if !defined(__ANDROID__)
235     if( get_option<std::string>( "FULLSCREEN" ) == "fullscreen" ) {
236         window_flags |= SDL_WINDOW_FULLSCREEN;
237         fullscreen = true;
238         SDL_SetHint( SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0" );
239     } else if( get_option<std::string>( "FULLSCREEN" ) == "windowedbl" ) {
240         window_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
241         fullscreen = true;
242         SDL_SetHint( SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0" );
243     } else if( get_option<std::string>( "FULLSCREEN" ) == "maximized" ) {
244         window_flags |= SDL_WINDOW_MAXIMIZED;
245     }
246 #endif
247 
248     int display = std::stoi( get_option<std::string>( "DISPLAY" ) );
249     if( display < 0 || display >= SDL_GetNumVideoDisplays() ) {
250         display = 0;
251     }
252 
253 #if defined(__ANDROID__)
254     // Bugfix for red screen on Samsung S3/Mali
255     // https://forums.libsdl.org/viewtopic.php?t=11445
256     SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 5 );
257     SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 6 );
258     SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 5 );
259 
260     // Fix Back button crash on Android 9
261 #if defined(SDL_HINT_ANDROID_TRAP_BACK_BUTTON )
262     const bool trap_back_button = get_option<bool>( "ANDROID_TRAP_BACK_BUTTON" );
263     SDL_SetHint( SDL_HINT_ANDROID_TRAP_BACK_BUTTON, trap_back_button ? "1" : "0" );
264 #endif
265 
266     // Prevent mouse|touch input confusion
267 #if defined(SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH)
268     SDL_SetHint( SDL_HINT_ANDROID_SEPARATE_MOUSE_AND_TOUCH, "1" );
269 #else
270     SDL_SetHint( SDL_HINT_MOUSE_TOUCH_EVENTS, "0" );
271     SDL_SetHint( SDL_HINT_TOUCH_MOUSE_EVENTS, "0" );
272 #endif
273 #endif
274 
275     ::window.reset( SDL_CreateWindow( version.c_str(),
276                                       SDL_WINDOWPOS_CENTERED_DISPLAY( display ),
277                                       SDL_WINDOWPOS_CENTERED_DISPLAY( display ),
278                                       WindowWidth,
279                                       WindowHeight,
280                                       window_flags
281                                     ) );
282     throwErrorIf( !::window, "SDL_CreateWindow failed" );
283 
284 #if !defined(__ANDROID__)
285     // On Android SDL seems janky in windowed mode so we're fullscreen all the time.
286     // Fullscreen mode is now modified so it obeys terminal width/height, rather than
287     // overwriting it with this calculation.
288     if( window_flags & SDL_WINDOW_FULLSCREEN || window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP
289         || window_flags & SDL_WINDOW_MAXIMIZED ) {
290         SDL_GetWindowSize( ::window.get(), &WindowWidth, &WindowHeight );
291         // Ignore previous values, use the whole window, but nothing more.
292         TERMINAL_WIDTH = WindowWidth / fontwidth / scaling_factor;
293         TERMINAL_HEIGHT = WindowHeight / fontheight / scaling_factor;
294     }
295 #endif
296     // Initialize framebuffer caches
297     terminal_framebuffer.resize( TERMINAL_HEIGHT );
298     for( int i = 0; i < TERMINAL_HEIGHT; i++ ) {
299         terminal_framebuffer[i].chars.assign( TERMINAL_WIDTH, cursecell( "" ) );
300     }
301 
302     oversized_framebuffer.resize( TERMINAL_HEIGHT );
303     for( int i = 0; i < TERMINAL_HEIGHT; i++ ) {
304         oversized_framebuffer[i].chars.assign( TERMINAL_WIDTH, cursecell( "" ) );
305     }
306 
307     const Uint32 wformat = SDL_GetWindowPixelFormat( ::window.get() );
308     format.reset( SDL_AllocFormat( wformat ) );
309     throwErrorIf( !format, "SDL_AllocFormat failed" );
310 
311     int renderer_id = -1;
312 #if !defined(__ANDROID__)
313     bool software_renderer = get_option<std::string>( "RENDERER" ).empty();
314     std::string renderer_name;
315     if( software_renderer ) {
316         renderer_name = "software";
317     } else {
318         renderer_name = get_option<std::string>( "RENDERER" );
319     }
320 
321     if( renderer_name == "direct3d" ) {
322         direct3d_mode = true;
323     }
324 
325     const int numRenderDrivers = SDL_GetNumRenderDrivers();
326     for( int i = 0; i < numRenderDrivers; i++ ) {
327         SDL_RendererInfo ri;
328         SDL_GetRenderDriverInfo( i, &ri );
329         if( renderer_name == ri.name ) {
330             renderer_id = i;
331             DebugLog( D_INFO, DC_ALL ) << "Active renderer: " << renderer_id << "/" << ri.name;
332             break;
333         }
334     }
335 #else
336     bool software_renderer = get_option<bool>( "SOFTWARE_RENDERING" );
337 #endif
338 
339 #if defined(SDL_HINT_RENDER_BATCHING)
340     SDL_SetHint( SDL_HINT_RENDER_BATCHING, get_option<bool>( "RENDER_BATCHING" ) ? "1" : "0" );
341 #endif
342     if( !software_renderer ) {
343         dbg( D_INFO ) << "Attempting to initialize accelerated SDL renderer.";
344 
345         renderer.reset( SDL_CreateRenderer( ::window.get(), renderer_id, SDL_RENDERER_ACCELERATED |
346                                             SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_TARGETTEXTURE ) );
347         if( printErrorIf( !renderer,
348                           "Failed to initialize accelerated renderer, falling back to software rendering" ) ) {
349             software_renderer = true;
350         } else if( !SetupRenderTarget() ) {
351             dbg( D_ERROR ) <<
352                            "Failed to initialize display buffer under accelerated rendering, falling back to software rendering.";
353             software_renderer = true;
354             display_buffer.reset();
355             renderer.reset();
356         }
357     }
358 
359     if( software_renderer ) {
360         if( get_option<bool>( "FRAMEBUFFER_ACCEL" ) ) {
361             SDL_SetHint( SDL_HINT_FRAMEBUFFER_ACCELERATION, "1" );
362         }
363         renderer.reset( SDL_CreateRenderer( ::window.get(), -1,
364                                             SDL_RENDERER_SOFTWARE | SDL_RENDERER_TARGETTEXTURE ) );
365         throwErrorIf( !renderer, "Failed to initialize software renderer" );
366         throwErrorIf( !SetupRenderTarget(),
367                       "Failed to initialize display buffer under software rendering, unable to continue." );
368     }
369 
370     SDL_SetWindowMinimumSize( ::window.get(), fontwidth * FULL_SCREEN_WIDTH * scaling_factor,
371                               fontheight * FULL_SCREEN_HEIGHT * scaling_factor );
372 
373 #if defined(__ANDROID__)
374     // TODO: Not too sure why this works to make fullscreen on Android behave. :/
375     if( window_flags & SDL_WINDOW_FULLSCREEN || window_flags & SDL_WINDOW_FULLSCREEN_DESKTOP
376         || window_flags & SDL_WINDOW_MAXIMIZED ) {
377         SDL_GetWindowSize( ::window.get(), &WindowWidth, &WindowHeight );
378     }
379 
380     // Load virtual joystick texture
381     touch_joystick = CreateTextureFromSurface( renderer, load_image( "android/joystick.png" ) );
382 #endif
383 
384     ClearScreen();
385 
386     // Errors here are ignored, worst case: the option does not work as expected,
387     // but that won't crash
388     if( get_option<std::string>( "HIDE_CURSOR" ) != "show" && SDL_ShowCursor( -1 ) ) {
389         SDL_ShowCursor( SDL_DISABLE );
390     } else {
391         SDL_ShowCursor( SDL_ENABLE );
392     }
393 
394     // Initialize joysticks.
395     int numjoy = SDL_NumJoysticks();
396 
397     if( get_option<bool>( "ENABLE_JOYSTICK" ) && numjoy >= 1 ) {
398         if( numjoy > 1 ) {
399             dbg( D_WARNING ) <<
400                              "You have more than one gamepads/joysticks plugged in, only the first will be used.";
401         }
402         joystick = SDL_JoystickOpen( 0 );
403         printErrorIf( joystick == nullptr, "SDL_JoystickOpen failed" );
404         if( joystick ) {
405             printErrorIf( SDL_JoystickEventState( SDL_ENABLE ) < 0,
406                           "SDL_JoystickEventState(SDL_ENABLE) failed" );
407         }
408     } else {
409         joystick = nullptr;
410     }
411 
412     // Set up audio mixer.
413     init_sound();
414 
415     DebugLog( D_INFO, DC_ALL ) << "USE_COLOR_MODULATED_TEXTURES is set to " <<
416                                get_option<bool>( "USE_COLOR_MODULATED_TEXTURES" );
417     //initialize the alternate rectangle texture for replacing SDL_RenderFillRect
418     if( get_option<bool>( "USE_COLOR_MODULATED_TEXTURES" ) && !software_renderer ) {
419         geometry = std::make_unique<ColorModulatedGeometryRenderer>( renderer );
420     } else {
421         geometry = std::make_unique<DefaultGeometryRenderer>();
422     }
423 }
424 
WinDestroy()425 static void WinDestroy()
426 {
427 #if defined(__ANDROID__)
428     touch_joystick.reset();
429 #endif
430 
431     shutdown_sound();
432     tilecontext.reset();
433 
434     if( joystick ) {
435         SDL_JoystickClose( joystick );
436 
437         joystick = nullptr;
438     }
439     geometry.reset();
440     format.reset();
441     display_buffer.reset();
442     renderer.reset();
443     ::window.reset();
444 }
445 
446 /// Converts a color from colorscheme to SDL_Color.
color_as_sdl(const unsigned char color)447 static inline const SDL_Color &color_as_sdl( const unsigned char color )
448 {
449     return windowsPalette[color];
450 }
451 
452 #if defined(__ANDROID__)
453 void draw_terminal_size_preview();
454 void draw_quick_shortcuts();
455 void draw_virtual_joystick();
456 
457 static bool quick_shortcuts_enabled = true;
458 
459 // For previewing the terminal size with a transparent rectangle overlay when user is adjusting it in the settings
460 static int preview_terminal_width = -1;
461 static int preview_terminal_height = -1;
462 static uint32_t preview_terminal_change_time = 0;
463 
464 extern "C" {
465 
466     static bool visible_display_frame_dirty = false;
467     static bool has_visible_display_frame = false;
468     static SDL_Rect visible_display_frame;
469 
Java_org_libsdl_app_SDLActivity_onNativeVisibleDisplayFrameChanged(JNIEnv * env,jclass jcls,jint left,jint top,jint right,jint bottom)470     JNIEXPORT void JNICALL Java_org_libsdl_app_SDLActivity_onNativeVisibleDisplayFrameChanged(
471         JNIEnv *env, jclass jcls, jint left, jint top, jint right, jint bottom )
472     {
473         ( void )env; // unused
474         ( void )jcls; // unused
475         has_visible_display_frame = true;
476         visible_display_frame_dirty = true;
477         visible_display_frame.x = left;
478         visible_display_frame.y = top;
479         visible_display_frame.w = right - left;
480         visible_display_frame.h = bottom - top;
481     }
482 
483 } // "C"
484 
get_android_render_rect(float DisplayBufferWidth,float DisplayBufferHeight)485 SDL_Rect get_android_render_rect( float DisplayBufferWidth, float DisplayBufferHeight )
486 {
487     // If the display buffer aspect ratio is wider than the display,
488     // draw it at the top of the screen so it doesn't get covered up
489     // by the virtual keyboard. Otherwise just center it.
490     SDL_Rect dstrect;
491     float DisplayBufferAspect = DisplayBufferWidth / static_cast<float>( DisplayBufferHeight );
492     float WindowHeightLessShortcuts = static_cast<float>( WindowHeight );
493     if( !get_option<bool>( "ANDROID_SHORTCUT_OVERLAP" ) && quick_shortcuts_enabled ) {
494         WindowHeightLessShortcuts -= get_option<int>( "ANDROID_SHORTCUT_HEIGHT" );
495     }
496     float WindowAspect = WindowWidth / static_cast<float>( WindowHeightLessShortcuts );
497     if( WindowAspect < DisplayBufferAspect ) {
498         dstrect.x = 0;
499         dstrect.y = 0;
500         dstrect.w = WindowWidth;
501         dstrect.h = WindowWidth / DisplayBufferAspect;
502     } else {
503         dstrect.x = 0.5f * ( WindowWidth - ( WindowHeightLessShortcuts * DisplayBufferAspect ) );
504         dstrect.y = 0;
505         dstrect.w = WindowHeightLessShortcuts * DisplayBufferAspect;
506         dstrect.h = WindowHeightLessShortcuts;
507     }
508 
509     // Make sure the destination rectangle fits within the visible area
510     if( get_option<bool>( "ANDROID_KEYBOARD_SCREEN_SCALE" ) && has_visible_display_frame ) {
511         int vdf_right = visible_display_frame.x + visible_display_frame.w;
512         int vdf_bottom = visible_display_frame.y + visible_display_frame.h;
513         if( vdf_right < dstrect.x + dstrect.w ) {
514             dstrect.w = vdf_right - dstrect.x;
515         }
516         if( vdf_bottom < dstrect.y + dstrect.h ) {
517             dstrect.h = vdf_bottom - dstrect.y;
518         }
519     }
520     return dstrect;
521 }
522 
523 #endif
524 
refresh_display()525 void refresh_display()
526 {
527     needupdate = false;
528     lastupdate = SDL_GetTicks();
529 
530     if( test_mode ) {
531         return;
532     }
533 
534     // Select default target (the window), copy rendered buffer
535     // there, present it, select the buffer as target again.
536     SetRenderTarget( renderer, nullptr );
537 #if defined(__ANDROID__)
538     SDL_Rect dstrect = get_android_render_rect( TERMINAL_WIDTH * fontwidth,
539                        TERMINAL_HEIGHT * fontheight );
540     SetRenderDrawColor( renderer, 0, 0, 0, 255 );
541     RenderClear( renderer );
542     RenderCopy( renderer, display_buffer, NULL, &dstrect );
543 #else
544     RenderCopy( renderer, display_buffer, nullptr, nullptr );
545 #endif
546 #if defined(__ANDROID__)
547     draw_terminal_size_preview();
548     if( g ) {
549         draw_quick_shortcuts();
550     }
551     draw_virtual_joystick();
552 #endif
553     SDL_RenderPresent( renderer.get() );
554     SetRenderTarget( renderer, display_buffer );
555 }
556 
557 // only update if the set interval has elapsed
try_sdl_update()558 static void try_sdl_update()
559 {
560     uint32_t now = SDL_GetTicks();
561     if( now - lastupdate >= interval ) {
562         refresh_display();
563     } else {
564         needupdate = true;
565     }
566 }
567 
568 //for resetting the render target after updating texture caches in cata_tiles.cpp
set_displaybuffer_rendertarget()569 void set_displaybuffer_rendertarget()
570 {
571     SetRenderTarget( renderer, display_buffer );
572 }
573 
invalidate_framebuffer(std::vector<curseline> & framebuffer,const point & p,int width,int height)574 static void invalidate_framebuffer( std::vector<curseline> &framebuffer, const point &p, int width,
575                                     int height )
576 {
577     for( int j = 0, fby = p.y; j < height; j++, fby++ ) {
578         std::fill_n( framebuffer[fby].chars.begin() + p.x, width, cursecell( "" ) );
579     }
580 }
581 
invalidate_framebuffer(std::vector<curseline> & framebuffer)582 static void invalidate_framebuffer( std::vector<curseline> &framebuffer )
583 {
584     for( curseline &i : framebuffer ) {
585         std::fill_n( i.chars.begin(), i.chars.size(), cursecell( "" ) );
586     }
587 }
588 
reinitialize_framebuffer()589 void reinitialize_framebuffer()
590 {
591     static int prev_height = -1;
592     static int prev_width = -1;
593     //Re-initialize the framebuffer with new values.
594     const int new_height = std::max( { TERMY, OVERMAP_WINDOW_HEIGHT, TERRAIN_WINDOW_HEIGHT } );
595     const int new_width = std::max( { TERMX, OVERMAP_WINDOW_WIDTH, TERRAIN_WINDOW_WIDTH } );
596     if( new_height != prev_height || new_width != prev_width ) {
597         prev_height = new_height;
598         prev_width = new_width;
599         oversized_framebuffer.resize( new_height );
600         for( int i = 0; i < new_height; i++ ) {
601             oversized_framebuffer[i].chars.assign( new_width, cursecell( "" ) );
602         }
603         terminal_framebuffer.resize( new_height );
604         for( int i = 0; i < new_height; i++ ) {
605             terminal_framebuffer[i].chars.assign( new_width, cursecell( "" ) );
606         }
607     } else if( need_invalidate_framebuffers ) {
608         need_invalidate_framebuffers = false;
609         invalidate_framebuffer( oversized_framebuffer );
610         invalidate_framebuffer( terminal_framebuffer );
611     }
612 }
613 
invalidate_framebuffer_proportion(cata_cursesport::WINDOW * win)614 static void invalidate_framebuffer_proportion( cata_cursesport::WINDOW *win )
615 {
616     const int oversized_width = std::max( TERMX, std::max( OVERMAP_WINDOW_WIDTH,
617                                           TERRAIN_WINDOW_WIDTH ) );
618     const int oversized_height = std::max( TERMY, std::max( OVERMAP_WINDOW_HEIGHT,
619                                            TERRAIN_WINDOW_HEIGHT ) );
620 
621     // check if the framebuffers/windows have been prepared yet
622     if( oversized_height == 0 || oversized_width == 0 ) {
623         return;
624     }
625     if( !g || win == nullptr ) {
626         return;
627     }
628     if( win == g->w_overmap || win == g->w_terrain ) {
629         return;
630     }
631 
632     // track the dimensions for conversion
633     const point termpixel( win->pos.x * font->width, win->pos.y * font->height );
634     const int termpixel_x2 = termpixel.x + win->width * font->width - 1;
635     const int termpixel_y2 = termpixel.y + win->height * font->height - 1;
636 
637     if( map_font != nullptr && map_font->width != 0 && map_font->height != 0 ) {
638         const int mapfont_x = termpixel.x / map_font->width;
639         const int mapfont_y = termpixel.y / map_font->height;
640         const int mapfont_x2 = std::min( termpixel_x2 / map_font->width, oversized_width - 1 );
641         const int mapfont_y2 = std::min( termpixel_y2 / map_font->height, oversized_height - 1 );
642         const int mapfont_width = mapfont_x2 - mapfont_x + 1;
643         const int mapfont_height = mapfont_y2 - mapfont_y + 1;
644         invalidate_framebuffer( oversized_framebuffer, point( mapfont_x, mapfont_y ), mapfont_width,
645                                 mapfont_height );
646     }
647 
648     if( overmap_font != nullptr && overmap_font->width != 0 && overmap_font->height != 0 ) {
649         const int overmapfont_x = termpixel.x / overmap_font->width;
650         const int overmapfont_y = termpixel.y / overmap_font->height;
651         const int overmapfont_x2 = std::min( termpixel_x2 / overmap_font->width, oversized_width - 1 );
652         const int overmapfont_y2 = std::min( termpixel_y2 / overmap_font->height,
653                                              oversized_height - 1 );
654         const int overmapfont_width = overmapfont_x2 - overmapfont_x + 1;
655         const int overmapfont_height = overmapfont_y2 - overmapfont_y + 1;
656         invalidate_framebuffer( oversized_framebuffer, point( overmapfont_x, overmapfont_y ),
657                                 overmapfont_width,
658                                 overmapfont_height );
659     }
660 }
661 
662 // clear the framebuffer when werase is called on certain windows that don't use the main terminal font
handle_additional_window_clear(WINDOW * win)663 void cata_cursesport::handle_additional_window_clear( WINDOW *win )
664 {
665     if( !g ) {
666         return;
667     }
668     if( win == g->w_terrain || win == g->w_overmap ) {
669         invalidate_framebuffer( oversized_framebuffer );
670     }
671 }
672 
clear_window_area(const catacurses::window & win_)673 void clear_window_area( const catacurses::window &win_ )
674 {
675     cata_cursesport::WINDOW *const win = win_.get<cata_cursesport::WINDOW>();
676     geometry->rect( renderer, point( win->pos.x * fontwidth, win->pos.y * fontheight ),
677                     win->width * fontwidth, win->height * fontheight, color_as_sdl( catacurses::black ) );
678 }
679 
draw_window(Font_Ptr & font,const catacurses::window & w,const point & offset)680 static bool draw_window( Font_Ptr &font, const catacurses::window &w, const point &offset )
681 {
682     if( scaling_factor > 1 ) {
683         SDL_RenderSetLogicalSize( renderer.get(), WindowWidth / scaling_factor,
684                                   WindowHeight / scaling_factor );
685     }
686 
687     cata_cursesport::WINDOW *const win = w.get<cata_cursesport::WINDOW>();
688     //Keeping track of the last drawn window
689     const cata_cursesport::WINDOW *winBuffer = static_cast<cata_cursesport::WINDOW *>
690             ( ::winBuffer.lock().get() );
691     if( !fontScaleBuffer ) {
692         fontScaleBuffer = tilecontext->get_tile_width();
693     }
694     const int fontScale = tilecontext->get_tile_width();
695     //This creates a problem when map_font is different from the regular font
696     //Specifically when showing the overmap
697     //And in some instances of screen change, i.e. inventory.
698     bool oldWinCompatible = false;
699 
700     // clear the oversized buffer proportionally
701     invalidate_framebuffer_proportion( win );
702 
703     // use the oversize buffer when dealing with windows that can have a different font than the main text font
704     bool use_oversized_framebuffer = g && ( w == g->w_terrain || w == g->w_overmap );
705 
706     std::vector<curseline> &framebuffer = use_oversized_framebuffer ? oversized_framebuffer :
707                                           terminal_framebuffer;
708 
709     /*
710     Let's try to keep track of different windows.
711     A number of windows are coexisting on the screen, so don't have to interfere.
712 
713     g->w_terrain, g->w_minimap, g->w_HP, g->w_status, g->w_status2, g->w_messages,
714      g->w_location, and g->w_minimap, can be buffered if either of them was
715      the previous window.
716 
717     g->w_overmap and g->w_omlegend are likewise.
718 
719     Everything else works on strict equality because there aren't yet IDs for some of them.
720     */
721     if( g && ( w == g->w_terrain || w == g->w_minimap ) ) {
722         if( winBuffer == g->w_terrain || winBuffer == g->w_minimap ) {
723             oldWinCompatible = true;
724         }
725     } else if( g && ( w == g->w_overmap || w == g->w_omlegend ) ) {
726         if( winBuffer == g->w_overmap || winBuffer == g->w_omlegend ) {
727             oldWinCompatible = true;
728         }
729     } else {
730         if( win == winBuffer ) {
731             oldWinCompatible = true;
732         }
733     }
734 
735     // TODO: Get this from UTF system to make sure it is exactly the kind of space we need
736     static const std::string space_string = " ";
737 
738     bool update = false;
739     for( int j = 0; j < win->height; j++ ) {
740         if( !win->line[j].touched ) {
741             continue;
742         }
743 
744         const int fby = win->pos.y + j;
745         if( fby >= static_cast<int>( framebuffer.size() ) ) {
746             // prevent indexing outside the frame buffer. This might happen for some parts of the window. FIX #28953.
747             break;
748         }
749 
750         update = true;
751         win->line[j].touched = false;
752         for( int i = 0; i < win->width; i++ ) {
753             const int fbx = win->pos.x + i;
754             if( fbx >= static_cast<int>( framebuffer[fby].chars.size() ) ) {
755                 // prevent indexing outside the frame buffer. This might happen for some parts of the window.
756                 break;
757             }
758 
759             const cursecell &cell = win->line[j].chars[i];
760 
761             const int drawx = offset.x + i * font->width;
762             const int drawy = offset.y + j * font->height;
763             if( drawx + font->width > WindowWidth || drawy + font->height > WindowHeight ) {
764                 // Outside of the display area, would not render anyway
765                 continue;
766             }
767 
768             // Avoid redrawing an unchanged tile by checking the framebuffer cache
769             // TODO: handle caching when drawing normal windows over graphical tiles
770             cursecell &oldcell = framebuffer[fby].chars[fbx];
771 
772             if( oldWinCompatible && cell == oldcell && fontScale == fontScaleBuffer ) {
773                 continue;
774             }
775             oldcell = cell;
776 
777             if( cell.ch.empty() ) {
778                 continue; // second cell of a multi-cell character
779             }
780 
781             // Spaces are used a lot, so this does help noticeably
782             if( cell.ch == space_string ) {
783                 geometry->rect( renderer, point( drawx, drawy ), font->width, font->height,
784                                 color_as_sdl( cell.BG ) );
785                 continue;
786             }
787             const int codepoint = UTF8_getch( cell.ch );
788             const catacurses::base_color FG = cell.FG;
789             const catacurses::base_color BG = cell.BG;
790             int cw = ( codepoint == UNKNOWN_UNICODE ) ? 1 : utf8_width( cell.ch );
791             if( cw < 1 ) {
792                 // utf8_width() may return a negative width
793                 continue;
794             }
795             bool use_draw_ascii_lines_routine = get_option<bool>( "USE_DRAW_ASCII_LINES_ROUTINE" );
796             unsigned char uc = static_cast<unsigned char>( cell.ch[0] );
797             switch( codepoint ) {
798                 case LINE_XOXO_UNICODE:
799                     uc = LINE_XOXO_C;
800                     break;
801                 case LINE_OXOX_UNICODE:
802                     uc = LINE_OXOX_C;
803                     break;
804                 case LINE_XXOO_UNICODE:
805                     uc = LINE_XXOO_C;
806                     break;
807                 case LINE_OXXO_UNICODE:
808                     uc = LINE_OXXO_C;
809                     break;
810                 case LINE_OOXX_UNICODE:
811                     uc = LINE_OOXX_C;
812                     break;
813                 case LINE_XOOX_UNICODE:
814                     uc = LINE_XOOX_C;
815                     break;
816                 case LINE_XXXO_UNICODE:
817                     uc = LINE_XXXO_C;
818                     break;
819                 case LINE_XXOX_UNICODE:
820                     uc = LINE_XXOX_C;
821                     break;
822                 case LINE_XOXX_UNICODE:
823                     uc = LINE_XOXX_C;
824                     break;
825                 case LINE_OXXX_UNICODE:
826                     uc = LINE_OXXX_C;
827                     break;
828                 case LINE_XXXX_UNICODE:
829                     uc = LINE_XXXX_C;
830                     break;
831                 case UNKNOWN_UNICODE:
832                     use_draw_ascii_lines_routine = true;
833                     break;
834                 default:
835                     use_draw_ascii_lines_routine = false;
836                     break;
837             }
838             geometry->rect( renderer, point( drawx, drawy ), font->width * cw, font->height,
839                             color_as_sdl( BG ) );
840             if( use_draw_ascii_lines_routine ) {
841                 font->draw_ascii_lines( renderer, geometry, uc, point( drawx, drawy ), FG );
842             } else {
843                 font->OutputChar( renderer, geometry, cell.ch, point( drawx, drawy ), FG );
844             }
845         }
846     }
847     win->draw = false; //We drew the window, mark it as so
848     //Keeping track of last drawn window and tilemode zoom level
849     ::winBuffer = w.weak_ptr();
850     fontScaleBuffer = tilecontext->get_tile_width();
851 
852     return update;
853 }
854 
draw_window(Font_Ptr & font,const catacurses::window & w)855 static bool draw_window( Font_Ptr &font, const catacurses::window &w )
856 {
857     cata_cursesport::WINDOW *const win = w.get<cata_cursesport::WINDOW>();
858     // Use global font sizes here to make this independent of the
859     // font used for this window.
860     return draw_window( font, w, point( win->pos.x * ::fontwidth, win->pos.y * ::fontheight ) );
861 }
862 
curses_drawwindow(const catacurses::window & w)863 void cata_cursesport::curses_drawwindow( const catacurses::window &w )
864 {
865     if( scaling_factor > 1 ) {
866         SDL_RenderSetLogicalSize( renderer.get(), WindowWidth / scaling_factor,
867                                   WindowHeight / scaling_factor );
868     }
869     WINDOW *const win = w.get<WINDOW>();
870     bool update = false;
871     if( g && w == g->w_terrain && use_tiles ) {
872         // color blocks overlay; drawn on top of tiles and on top of overlay strings (if any).
873         color_block_overlay_container color_blocks;
874 
875         // Strings with colors do be drawn with map_font on top of tiles.
876         std::multimap<point, formatted_text> overlay_strings;
877 
878         // game::w_terrain can be drawn by the tilecontext.
879         // skip the normal drawing code for it.
880         tilecontext->draw(
881             point( win->pos.x * fontwidth, win->pos.y * fontheight ),
882             g->ter_view_p,
883             TERRAIN_WINDOW_TERM_WIDTH * font->width,
884             TERRAIN_WINDOW_TERM_HEIGHT * font->height,
885             overlay_strings,
886             color_blocks );
887 
888         // color blocks overlay
889         if( !color_blocks.second.empty() ) {
890             SDL_BlendMode blend_mode;
891             GetRenderDrawBlendMode( renderer, blend_mode ); // save the current blend mode
892             SetRenderDrawBlendMode( renderer, color_blocks.first ); // set the new blend mode
893             for( const auto &e : color_blocks.second ) {
894                 geometry->rect( renderer, e.first, tilecontext->get_tile_width(),
895                                 tilecontext->get_tile_height(), e.second );
896             }
897             SetRenderDrawBlendMode( renderer, blend_mode ); // set the old blend mode
898         }
899 
900         // overlay strings
901         point prev_coord;
902         int x_offset = 0;
903         int alignment_offset = 0;
904         for( const auto &iter : overlay_strings ) {
905             const point coord = iter.first;
906             const formatted_text ft = iter.second;
907             const utf8_wrapper text( ft.text );
908 
909             // Strings at equal coords are displayed sequentially.
910             if( coord != prev_coord ) {
911                 x_offset = 0;
912             }
913 
914             // Calculate length of all strings in sequence to align them.
915             if( x_offset == 0 ) {
916                 int full_text_length = 0;
917                 const auto range = overlay_strings.equal_range( coord );
918                 for( auto ri = range.first; ri != range.second; ++ri ) {
919                     utf8_wrapper rt( ri->second.text );
920                     full_text_length += rt.display_width();
921                 }
922 
923                 alignment_offset = 0;
924                 if( ft.alignment == text_alignment::center ) {
925                     alignment_offset = full_text_length / 2;
926                 } else if( ft.alignment == text_alignment::right ) {
927                     alignment_offset = full_text_length - 1;
928                 }
929             }
930 
931             int width = 0;
932             for( size_t i = 0; i < text.size(); ++i ) {
933                 const int x0 = win->pos.x * fontwidth;
934                 const int y0 = win->pos.y * fontheight;
935                 const int x = x0 + ( x_offset - alignment_offset + width ) * map_font->width + coord.x;
936                 const int y = y0 + coord.y;
937 
938                 // Clip to window bounds.
939                 if( x < x0 || x > x0 + ( TERRAIN_WINDOW_TERM_WIDTH - 1 ) * font->width
940                     || y < y0 || y > y0 + ( TERRAIN_WINDOW_TERM_HEIGHT - 1 ) * font->height ) {
941                     continue;
942                 }
943 
944                 // TODO: draw with outline / BG color for better readability
945                 const uint32_t ch = text.at( i );
946                 map_font->OutputChar( renderer, geometry, utf32_to_utf8( ch ), point( x, y ), ft.color );
947                 width += mk_wcwidth( ch );
948             }
949 
950             prev_coord = coord;
951             x_offset = width;
952         }
953 
954         invalidate_framebuffer( terminal_framebuffer, win->pos,
955                                 TERRAIN_WINDOW_TERM_WIDTH, TERRAIN_WINDOW_TERM_HEIGHT );
956 
957         update = true;
958     } else if( g && w == g->w_terrain && map_font ) {
959         // When the terrain updates, predraw a black space around its edge
960         // to keep various former interface elements from showing through the gaps
961 
962         //calculate width differences between map_font and font
963         int partial_width = std::max( TERRAIN_WINDOW_TERM_WIDTH * fontwidth - TERRAIN_WINDOW_WIDTH *
964                                       map_font->width, 0 );
965         int partial_height = std::max( TERRAIN_WINDOW_TERM_HEIGHT * fontheight - TERRAIN_WINDOW_HEIGHT *
966                                        map_font->height, 0 );
967         //Gap between terrain and lower window edge
968         if( partial_height > 0 ) {
969             geometry->rect( renderer, point( win->pos.x * map_font->width,
970                                              ( win->pos.y + TERRAIN_WINDOW_HEIGHT ) * map_font->height ),
971                             TERRAIN_WINDOW_WIDTH * map_font->width + partial_width, partial_height,
972                             color_as_sdl( catacurses::black ) );
973         }
974         //Gap between terrain and sidebar
975         if( partial_width > 0 ) {
976             geometry->rect( renderer, point( ( win->pos.x + TERRAIN_WINDOW_WIDTH ) * map_font->width,
977                                              win->pos.y * map_font->height ),
978                             partial_width,
979                             TERRAIN_WINDOW_HEIGHT * map_font->height + partial_height,
980                             color_as_sdl( catacurses::black ) );
981         }
982         // Special font for the terrain window
983         update = draw_window( map_font, w );
984     } else if( g && w == g->w_overmap && overmap_font ) {
985         // Special font for the terrain window
986         update = draw_window( overmap_font, w );
987     } else if( g && w == g->w_pixel_minimap && pixel_minimap_option ) {
988         // ensure the space the minimap covers is "dirtied".
989         // this is necessary when it's the only part of the sidebar being drawn
990         // TODO: Figure out how to properly make the minimap code do whatever it is this does
991         draw_window( font, w );
992 
993         // Make sure the entire minimap window is black before drawing.
994         clear_window_area( w );
995         tilecontext->draw_minimap(
996             point( win->pos.x * fontwidth, win->pos.y * fontheight ),
997             tripoint( get_player_character().pos().xy(), g->ter_view_p.z ),
998             win->width * font->width, win->height * font->height );
999         update = true;
1000 
1001     } else {
1002         // Either not using tiles (tilecontext) or not the w_terrain window.
1003         update = draw_window( font, w );
1004     }
1005     if( update ) {
1006         needupdate = true;
1007     }
1008 }
1009 
1010 static int alt_buffer = 0;
1011 static bool alt_down = false;
1012 
begin_alt_code()1013 static void begin_alt_code()
1014 {
1015     alt_buffer = 0;
1016     alt_down = true;
1017 }
1018 
add_alt_code(char c)1019 static bool add_alt_code( char c )
1020 {
1021     if( alt_down && c >= '0' && c <= '9' ) {
1022         alt_buffer = alt_buffer * 10 + ( c - '0' );
1023         return true;
1024     }
1025     return false;
1026 }
1027 
end_alt_code()1028 static int end_alt_code()
1029 {
1030     alt_down = false;
1031     return alt_buffer;
1032 }
1033 
HandleDPad()1034 static int HandleDPad()
1035 {
1036     // Check if we have a gamepad d-pad event.
1037     if( SDL_JoystickGetHat( joystick, 0 ) != SDL_HAT_CENTERED ) {
1038         // When someone tries to press a diagonal, they likely will
1039         // press a single direction first. Wait a few milliseconds to
1040         // give them time to press both of the buttons for the diagonal.
1041         int button = SDL_JoystickGetHat( joystick, 0 );
1042         int lc = ERR;
1043         if( button == SDL_HAT_LEFT ) {
1044             lc = JOY_LEFT;
1045         } else if( button == SDL_HAT_DOWN ) {
1046             lc = JOY_DOWN;
1047         } else if( button == SDL_HAT_RIGHT ) {
1048             lc = JOY_RIGHT;
1049         } else if( button == SDL_HAT_UP ) {
1050             lc = JOY_UP;
1051         } else if( button == SDL_HAT_LEFTUP ) {
1052             lc = JOY_LEFTUP;
1053         } else if( button == SDL_HAT_LEFTDOWN ) {
1054             lc = JOY_LEFTDOWN;
1055         } else if( button == SDL_HAT_RIGHTUP ) {
1056             lc = JOY_RIGHTUP;
1057         } else if( button == SDL_HAT_RIGHTDOWN ) {
1058             lc = JOY_RIGHTDOWN;
1059         }
1060 
1061         if( delaydpad == std::numeric_limits<Uint32>::max() ) {
1062             delaydpad = SDL_GetTicks() + dpad_delay;
1063             queued_dpad = lc;
1064         }
1065 
1066         // Okay it seems we're ready to process.
1067         if( SDL_GetTicks() > delaydpad ) {
1068 
1069             if( lc != ERR ) {
1070                 if( dpad_continuous && ( lc & lastdpad ) == 0 ) {
1071                     // Continuous movement should only work in the same or similar directions.
1072                     dpad_continuous = false;
1073                     lastdpad = lc;
1074                     return 0;
1075                 }
1076 
1077                 last_input = input_event( lc, input_event_t::gamepad );
1078                 lastdpad = lc;
1079                 queued_dpad = ERR;
1080 
1081                 if( !dpad_continuous ) {
1082                     delaydpad = SDL_GetTicks() + 200;
1083                     dpad_continuous = true;
1084                 } else {
1085                     delaydpad = SDL_GetTicks() + 60;
1086                 }
1087                 return 1;
1088             }
1089         }
1090     } else {
1091         dpad_continuous = false;
1092         delaydpad = std::numeric_limits<Uint32>::max();
1093 
1094         // If we didn't hold it down for a while, just
1095         // fire the last registered press.
1096         if( queued_dpad != ERR ) {
1097             last_input = input_event( queued_dpad, input_event_t::gamepad );
1098             queued_dpad = ERR;
1099             return 1;
1100         }
1101     }
1102 
1103     return 0;
1104 }
1105 
sdl_keycode_opposite_arrow(SDL_Keycode key)1106 static SDL_Keycode sdl_keycode_opposite_arrow( SDL_Keycode key )
1107 {
1108     switch( key ) {
1109         case SDLK_UP:
1110             return SDLK_DOWN;
1111         case SDLK_DOWN:
1112             return SDLK_UP;
1113         case SDLK_LEFT:
1114             return SDLK_RIGHT;
1115         case SDLK_RIGHT:
1116             return SDLK_LEFT;
1117     }
1118     return 0;
1119 }
1120 
sdl_keycode_is_arrow(SDL_Keycode key)1121 static bool sdl_keycode_is_arrow( SDL_Keycode key )
1122 {
1123     return static_cast<bool>( sdl_keycode_opposite_arrow( key ) );
1124 }
1125 
arrow_combo_to_numpad(SDL_Keycode mod,SDL_Keycode key)1126 static int arrow_combo_to_numpad( SDL_Keycode mod, SDL_Keycode key )
1127 {
1128     if( ( mod == SDLK_UP    && key == SDLK_RIGHT ) ||
1129         ( mod == SDLK_RIGHT && key == SDLK_UP ) ) {
1130         return KEY_NUM( 9 );
1131     }
1132     if( ( mod == SDLK_UP    && key == SDLK_UP ) ) {
1133         return KEY_NUM( 8 );
1134     }
1135     if( ( mod == SDLK_UP    && key == SDLK_LEFT ) ||
1136         ( mod == SDLK_LEFT  && key == SDLK_UP ) ) {
1137         return KEY_NUM( 7 );
1138     }
1139     if( ( mod == SDLK_RIGHT && key == SDLK_RIGHT ) ) {
1140         return KEY_NUM( 6 );
1141     }
1142     if( mod == sdl_keycode_opposite_arrow( key ) ) {
1143         return KEY_NUM( 5 );
1144     }
1145     if( ( mod == SDLK_LEFT  && key == SDLK_LEFT ) ) {
1146         return KEY_NUM( 4 );
1147     }
1148     if( ( mod == SDLK_DOWN  && key == SDLK_RIGHT ) ||
1149         ( mod == SDLK_RIGHT && key == SDLK_DOWN ) ) {
1150         return KEY_NUM( 3 );
1151     }
1152     if( ( mod == SDLK_DOWN  && key == SDLK_DOWN ) ) {
1153         return KEY_NUM( 2 );
1154     }
1155     if( ( mod == SDLK_DOWN  && key == SDLK_LEFT ) ||
1156         ( mod == SDLK_LEFT  && key == SDLK_DOWN ) ) {
1157         return KEY_NUM( 1 );
1158     }
1159     return 0;
1160 }
1161 
1162 static int arrow_combo_modifier = 0;
1163 
handle_arrow_combo(SDL_Keycode key)1164 static int handle_arrow_combo( SDL_Keycode key )
1165 {
1166     if( !arrow_combo_modifier ) {
1167         arrow_combo_modifier = key;
1168         return 0;
1169     }
1170     return arrow_combo_to_numpad( arrow_combo_modifier, key );
1171 }
1172 
end_arrow_combo()1173 static void end_arrow_combo()
1174 {
1175     arrow_combo_modifier = 0;
1176 }
1177 
1178 /**
1179  * Translate SDL key codes to key identifiers used by ncurses, this
1180  * allows the input_manager to only consider those.
1181  * @return 0 if the input can not be translated (unknown key?),
1182  * -1 when a ALT+number sequence has been started,
1183  * or something that a call to ncurses getch would return.
1184  */
sdl_keysym_to_curses(const SDL_Keysym & keysym)1185 static int sdl_keysym_to_curses( const SDL_Keysym &keysym )
1186 {
1187 
1188     const std::string diag_mode = get_option<std::string>( "DIAG_MOVE_WITH_MODIFIERS_MODE" );
1189 
1190     if( diag_mode == "mode1" ) {
1191         if( keysym.mod & KMOD_CTRL && sdl_keycode_is_arrow( keysym.sym ) ) {
1192             return handle_arrow_combo( keysym.sym );
1193         } else {
1194             end_arrow_combo();
1195         }
1196     }
1197 
1198     if( diag_mode == "mode2" ) {
1199         //Shift + Cursor Arrow (diagonal clockwise)
1200         if( keysym.mod & KMOD_SHIFT ) {
1201             switch( keysym.sym ) {
1202                 case SDLK_LEFT:
1203                     return inp_mngr.get_first_char_for_action( "LEFTUP" );
1204                 case SDLK_RIGHT:
1205                     return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1206                 case SDLK_UP:
1207                     return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1208                 case SDLK_DOWN:
1209                     return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1210             }
1211         }
1212         //Ctrl + Cursor Arrow (diagonal counter-clockwise)
1213         if( keysym.mod & KMOD_CTRL ) {
1214             switch( keysym.sym ) {
1215                 case SDLK_LEFT:
1216                     return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1217                 case SDLK_RIGHT:
1218                     return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1219                 case SDLK_UP:
1220                     return inp_mngr.get_first_char_for_action( "LEFTUP" );
1221                 case SDLK_DOWN:
1222                     return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1223             }
1224         }
1225     }
1226 
1227     if( diag_mode == "mode3" ) {
1228         //Shift + Cursor Left/RightArrow
1229         if( keysym.mod & KMOD_SHIFT ) {
1230             switch( keysym.sym ) {
1231                 case SDLK_LEFT:
1232                     return inp_mngr.get_first_char_for_action( "LEFTUP" );
1233                 case SDLK_RIGHT:
1234                     return inp_mngr.get_first_char_for_action( "RIGHTUP" );
1235             }
1236         }
1237         //Ctrl + Cursor Left/Right Arrow
1238         if( keysym.mod & KMOD_CTRL ) {
1239             switch( keysym.sym ) {
1240                 case SDLK_LEFT:
1241                     return inp_mngr.get_first_char_for_action( "LEFTDOWN" );
1242                 case SDLK_RIGHT:
1243                     return inp_mngr.get_first_char_for_action( "RIGHTDOWN" );
1244             }
1245         }
1246     }
1247 
1248     if( keysym.mod & KMOD_CTRL && keysym.sym >= 'a' && keysym.sym <= 'z' ) {
1249         // ASCII ctrl codes, ^A through ^Z.
1250         return keysym.sym - 'a' + '\1';
1251     }
1252     switch( keysym.sym ) {
1253         // This is special: allow entering a Unicode character with ALT+number
1254         case SDLK_RALT:
1255         case SDLK_LALT:
1256             begin_alt_code();
1257             return -1;
1258         // The following are simple translations:
1259         case SDLK_KP_ENTER:
1260         case SDLK_RETURN:
1261         case SDLK_RETURN2:
1262             return '\n';
1263         case SDLK_BACKSPACE:
1264         case SDLK_KP_BACKSPACE:
1265             return KEY_BACKSPACE;
1266         case SDLK_DELETE:
1267             return KEY_DC;
1268         case SDLK_ESCAPE:
1269             return KEY_ESCAPE;
1270         case SDLK_TAB:
1271             if( keysym.mod & KMOD_SHIFT ) {
1272                 return KEY_BTAB;
1273             }
1274             return '\t';
1275         case SDLK_LEFT:
1276             return KEY_LEFT;
1277         case SDLK_RIGHT:
1278             return KEY_RIGHT;
1279         case SDLK_UP:
1280             return KEY_UP;
1281         case SDLK_DOWN:
1282             return KEY_DOWN;
1283         case SDLK_PAGEUP:
1284             return KEY_PPAGE;
1285         case SDLK_PAGEDOWN:
1286             return KEY_NPAGE;
1287         case SDLK_HOME:
1288             return KEY_HOME;
1289         case SDLK_END:
1290             return KEY_END;
1291         case SDLK_F1:
1292             return KEY_F( 1 );
1293         case SDLK_F2:
1294             return KEY_F( 2 );
1295         case SDLK_F3:
1296             return KEY_F( 3 );
1297         case SDLK_F4:
1298             return KEY_F( 4 );
1299         case SDLK_F5:
1300             return KEY_F( 5 );
1301         case SDLK_F6:
1302             return KEY_F( 6 );
1303         case SDLK_F7:
1304             return KEY_F( 7 );
1305         case SDLK_F8:
1306             return KEY_F( 8 );
1307         case SDLK_F9:
1308             return KEY_F( 9 );
1309         case SDLK_F10:
1310             return KEY_F( 10 );
1311         case SDLK_F11:
1312             return KEY_F( 11 );
1313         case SDLK_F12:
1314             return KEY_F( 12 );
1315         case SDLK_F13:
1316             return KEY_F( 13 );
1317         case SDLK_F14:
1318             return KEY_F( 14 );
1319         case SDLK_F15:
1320             return KEY_F( 15 );
1321         // Every other key is ignored as there is no curses constant for it.
1322         // TODO: add more if you find more.
1323         default:
1324             return 0;
1325     }
1326 }
1327 
sdl_keysym_to_keycode_evt(const SDL_Keysym & keysym)1328 static input_event sdl_keysym_to_keycode_evt( const SDL_Keysym &keysym )
1329 {
1330     switch( keysym.sym ) {
1331         case SDLK_LCTRL:
1332         case SDLK_LSHIFT:
1333         case SDLK_LALT:
1334         case SDLK_RCTRL:
1335         case SDLK_RSHIFT:
1336         case SDLK_RALT:
1337             return input_event();
1338     }
1339     input_event evt;
1340     evt.type = input_event_t::keyboard_code;
1341     if( keysym.mod & KMOD_CTRL ) {
1342         evt.modifiers.emplace( keymod_t::ctrl );
1343     }
1344     if( keysym.mod & KMOD_ALT ) {
1345         evt.modifiers.emplace( keymod_t::alt );
1346     }
1347     if( keysym.mod & KMOD_SHIFT ) {
1348         evt.modifiers.emplace( keymod_t::shift );
1349     }
1350     evt.sequence.emplace_back( keysym.sym );
1351     return evt;
1352 }
1353 
handle_resize(int w,int h)1354 bool handle_resize( int w, int h )
1355 {
1356     if( ( w != WindowWidth ) || ( h != WindowHeight ) ) {
1357         WindowWidth = w;
1358         WindowHeight = h;
1359         TERMINAL_WIDTH = WindowWidth / fontwidth / scaling_factor;
1360         TERMINAL_HEIGHT = WindowHeight / fontheight / scaling_factor;
1361         need_invalidate_framebuffers = true;
1362         catacurses::stdscr = catacurses::newwin( TERMINAL_HEIGHT, TERMINAL_WIDTH, point_zero );
1363         SetupRenderTarget();
1364         game_ui::init_ui();
1365         ui_manager::screen_resized();
1366         return true;
1367     }
1368     return false;
1369 }
1370 
resize_term(const int cell_w,const int cell_h)1371 void resize_term( const int cell_w, const int cell_h )
1372 {
1373     int w = cell_w * fontwidth * scaling_factor;
1374     int h = cell_h * fontheight * scaling_factor;
1375     SDL_SetWindowSize( window.get(), w, h );
1376     SDL_GetWindowSize( window.get(), &w, &h );
1377     handle_resize( w, h );
1378 }
1379 
toggle_fullscreen_window()1380 void toggle_fullscreen_window()
1381 {
1382     static int restore_win_w = get_option<int>( "TERMINAL_X" ) * fontwidth * scaling_factor;
1383     static int restore_win_h = get_option<int>( "TERMINAL_Y" ) * fontheight * scaling_factor;
1384 
1385     if( fullscreen ) {
1386         if( printErrorIf( SDL_SetWindowFullscreen( window.get(), 0 ) != 0,
1387                           "SDL_SetWindowFullscreen failed" ) ) {
1388             return;
1389         }
1390         SDL_RestoreWindow( window.get() );
1391         SDL_SetWindowSize( window.get(), restore_win_w, restore_win_h );
1392         SDL_SetWindowMinimumSize( window.get(), fontwidth * FULL_SCREEN_WIDTH * scaling_factor,
1393                                   fontheight * FULL_SCREEN_HEIGHT * scaling_factor );
1394     } else {
1395         restore_win_w = WindowWidth;
1396         restore_win_h = WindowHeight;
1397         if( printErrorIf( SDL_SetWindowFullscreen( window.get(), SDL_WINDOW_FULLSCREEN_DESKTOP ) != 0,
1398                           "SDL_SetWindowFullscreen failed" ) ) {
1399             return;
1400         }
1401     }
1402     int nw = 0;
1403     int nh = 0;
1404     SDL_GetWindowSize( window.get(), &nw, &nh );
1405     handle_resize( nw, nh );
1406     fullscreen = !fullscreen;
1407 }
1408 
1409 #if defined(__ANDROID__)
1410 static float finger_down_x = -1.0f; // in pixels
1411 static float finger_down_y = -1.0f; // in pixels
1412 static float finger_curr_x = -1.0f; // in pixels
1413 static float finger_curr_y = -1.0f; // in pixels
1414 static float second_finger_down_x = -1.0f; // in pixels
1415 static float second_finger_down_y = -1.0f; // in pixels
1416 static float second_finger_curr_x = -1.0f; // in pixels
1417 static float second_finger_curr_y = -1.0f; // in pixels
1418 // when did the first finger start touching the screen? 0 if not touching, otherwise the time in milliseconds.
1419 static uint32_t finger_down_time = 0;
1420 // the last time we repeated input for a finger hold, 0 if not touching, otherwise the time in milliseconds.
1421 static uint32_t finger_repeat_time = 0;
1422 // the last time a single tap was detected. used for double-tap detection.
1423 static uint32_t last_tap_time = 0;
1424 // when did the hardware back button start being pressed? 0 if not touching, otherwise the time in milliseconds.
1425 static uint32_t ac_back_down_time = 0;
1426 // has a second finger touched the screen while the first was touching?
1427 static bool is_two_finger_touch = false;
1428 // did this touch start on a quick shortcut?
1429 static bool is_quick_shortcut_touch = false;
1430 static bool quick_shortcuts_toggle_handled = false;
1431 // the current finger repeat delay - will be somewhere between the min/max values depending on user input
1432 uint32_t finger_repeat_delay = 500;
1433 // should we make sure the sdl surface is visible? set to true whenever the SDL window is shown.
1434 static bool needs_sdl_surface_visibility_refresh = true;
1435 
1436 // Quick shortcuts container: maps the touch input context category (std::string) to a std::list of input_events.
1437 using quick_shortcuts_t = std::list<input_event>;
1438 std::map<std::string, quick_shortcuts_t> quick_shortcuts_map;
1439 
1440 // A copy of the last known input_context from the input manager. It's important this is a copy, as there are times
1441 // the input manager has an empty input_context (eg. when player is moving over slow objects) and we don't want our
1442 // quick shortcuts to disappear momentarily.
1443 input_context touch_input_context;
1444 
get_quick_shortcut_name(const std::string & category)1445 std::string get_quick_shortcut_name( const std::string &category )
1446 {
1447     if( category == "DEFAULTMODE" &&
1448         g->check_zone( zone_type_id( "NO_AUTO_PICKUP" ), get_player_character().pos() ) &&
1449         get_option<bool>( "ANDROID_SHORTCUT_ZONE" ) ) {
1450         return "DEFAULTMODE____SHORTCUTS";
1451     }
1452     return category;
1453 }
1454 
android_get_display_density()1455 float android_get_display_density()
1456 {
1457     JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
1458     jobject activity = ( jobject )SDL_AndroidGetActivity();
1459     jclass clazz( env->GetObjectClass( activity ) );
1460     jmethodID method_id = env->GetMethodID( clazz, "getDisplayDensity", "()F" );
1461     jfloat ans = env->CallFloatMethod( activity, method_id );
1462     env->DeleteLocalRef( activity );
1463     env->DeleteLocalRef( clazz );
1464     return ans;
1465 }
1466 
1467 // given the active quick shortcuts, returns the dimensions of each quick shortcut button.
get_quick_shortcut_dimensions(quick_shortcuts_t & qsl,float & border,float & width,float & height)1468 void get_quick_shortcut_dimensions( quick_shortcuts_t &qsl, float &border, float &width,
1469                                     float &height )
1470 {
1471     const float shortcut_dimensions_authored_density = 3.0f; // 480p xxhdpi
1472     float screen_density_scale = android_get_display_density() / shortcut_dimensions_authored_density;
1473     border = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_BORDER" ) );
1474     width = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_WIDTH_MAX" ) );
1475     float min_width = std::floor( screen_density_scale * std::min(
1476                                       get_option<int>( "ANDROID_SHORTCUT_WIDTH_MIN" ),
1477                                       get_option<int>( "ANDROID_SHORTCUT_WIDTH_MAX" ) ) );
1478     float usable_window_width = WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1479                                 0.01f;
1480     if( width * qsl.size() > usable_window_width ) {
1481         width *= usable_window_width / ( width * qsl.size() );
1482         if( width < min_width ) {
1483             width = min_width;
1484         }
1485     }
1486     width = std::floor( width );
1487     height = std::floor( screen_density_scale * get_option<int>( "ANDROID_SHORTCUT_HEIGHT" ) );
1488 }
1489 
1490 // Returns the quick shortcut (if any) under the finger's current position, or finger down position if down == true
get_quick_shortcut_under_finger(bool down=false)1491 input_event *get_quick_shortcut_under_finger( bool down = false )
1492 {
1493 
1494     if( !quick_shortcuts_enabled ) {
1495         return NULL;
1496     }
1497 
1498     quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
1499                                  touch_input_context.get_category() )];
1500 
1501     float border, width, height;
1502     get_quick_shortcut_dimensions( qsl, border, width, height );
1503 
1504     float finger_y = down ? finger_down_y : finger_curr_y;
1505     if( finger_y < WindowHeight - height ) {
1506         return NULL;
1507     }
1508 
1509     int i = 0;
1510     bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1511     float finger_x = down ? finger_down_x : finger_curr_x;
1512     for( std::list<input_event>::iterator it = qsl.begin(); it != qsl.end(); ++it ) {
1513         if( ( i + 1 ) * width > WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1514             0.01f ) {
1515             continue;
1516         }
1517         i++;
1518         if( shortcut_right ) {
1519             if( finger_x > WindowWidth - ( i * width ) ) {
1520                 return &( *it );
1521             }
1522         } else {
1523             if( finger_x < i * width ) {
1524                 return &( *it );
1525             }
1526         }
1527     }
1528 
1529     return NULL;
1530 }
1531 
1532 // when pre-populating a quick shortcut list with defaults, ignore these actions (since they're all handleable by native touch operations)
ignore_action_for_quick_shortcuts(const std::string & action)1533 bool ignore_action_for_quick_shortcuts( const std::string &action )
1534 {
1535     return ( action == "UP"
1536              || action == "DOWN"
1537              || action == "LEFT"
1538              || action == "RIGHT"
1539              || action == "LEFTUP"
1540              || action == "LEFTDOWN"
1541              || action == "RIGHTUP"
1542              || action == "RIGHTDOWN"
1543              || action == "QUIT"
1544              || action == "CONFIRM"
1545              || action == "MOVE_SINGLE_ITEM" // maps to ENTER
1546              || action == "MOVE_ARMOR" // maps to ENTER
1547              || action == "ANY_INPUT"
1548              || action ==
1549              "DELETE_TEMPLATE" // strictly we shouldn't have this one, but I don't like seeing the "d" on the main menu by default. :)
1550            );
1551 }
1552 
1553 // Adds a quick shortcut to a quick_shortcut list, setting shortcut_last_used_action_counter accordingly.
add_quick_shortcut(quick_shortcuts_t & qsl,input_event & event,bool back,bool reset_shortcut_last_used_action_counter)1554 void add_quick_shortcut( quick_shortcuts_t &qsl, input_event &event, bool back,
1555                          bool reset_shortcut_last_used_action_counter )
1556 {
1557     if( reset_shortcut_last_used_action_counter && g ) {
1558         event.shortcut_last_used_action_counter =
1559             g->get_user_action_counter();    // only used for DEFAULTMODE
1560     }
1561     if( back ) {
1562         qsl.push_back( event );
1563     } else {
1564         qsl.push_front( event );
1565     }
1566 }
1567 
1568 // Given a quick shortcut list and a specific key, move that key to the front or back of the list.
reorder_quick_shortcut(quick_shortcuts_t & qsl,int key,bool back)1569 void reorder_quick_shortcut( quick_shortcuts_t &qsl, int key, bool back )
1570 {
1571     for( const auto &event : qsl ) {
1572         if( event.get_first_input() == key ) {
1573             input_event event_copy = event;
1574             qsl.remove( event );
1575             add_quick_shortcut( qsl, event_copy, back, false );
1576             break;
1577         }
1578     }
1579 }
1580 
reorder_quick_shortcuts(quick_shortcuts_t & qsl)1581 void reorder_quick_shortcuts( quick_shortcuts_t &qsl )
1582 {
1583     // Do some manual reordering to make transitions between input contexts more consistent
1584     // Desired order of keys: < > BACKTAB TAB PPAGE NPAGE . . . . ?
1585     bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1586     if( shortcut_right ) {
1587         reorder_quick_shortcut( qsl, KEY_PPAGE, false ); // paging control
1588         reorder_quick_shortcut( qsl, KEY_NPAGE, false );
1589         reorder_quick_shortcut( qsl, KEY_BTAB, false ); // secondary tabs after that
1590         reorder_quick_shortcut( qsl, '\t', false );
1591         reorder_quick_shortcut( qsl, '<', false ); // tabs next
1592         reorder_quick_shortcut( qsl, '>', false );
1593         reorder_quick_shortcut( qsl, '?', false ); // help at the start
1594     } else {
1595         reorder_quick_shortcut( qsl, KEY_NPAGE, false );
1596         reorder_quick_shortcut( qsl, KEY_PPAGE, false ); // paging control
1597         reorder_quick_shortcut( qsl, '\t', false );
1598         reorder_quick_shortcut( qsl, KEY_BTAB, false ); // secondary tabs after that
1599         reorder_quick_shortcut( qsl, '>', false );
1600         reorder_quick_shortcut( qsl, '<', false ); // tabs next
1601         reorder_quick_shortcut( qsl, '?', false ); // help at the start
1602     }
1603 }
1604 
choose_best_key_for_action(const std::string & action,const std::string & category)1605 int choose_best_key_for_action( const std::string &action, const std::string &category )
1606 {
1607     const std::vector<input_event> &events = inp_mngr.get_input_for_action( action, category );
1608     int best_key = -1;
1609     for( const auto &events_event : events ) {
1610         if( events_event.type == input_event_t::keyboard_char && events_event.sequence.size() == 1 ) {
1611             bool is_ascii_char = isprint( events_event.sequence.front() ) &&
1612                                  events_event.sequence.front() < 0xFF;
1613             bool is_best_ascii_char = best_key >= 0 && isprint( best_key ) && best_key < 0xFF;
1614             if( best_key < 0 || ( is_ascii_char && !is_best_ascii_char ) ) {
1615                 best_key = events_event.sequence.front();
1616             }
1617         }
1618     }
1619     return best_key;
1620 }
1621 
add_key_to_quick_shortcuts(int key,const std::string & category,bool back)1622 bool add_key_to_quick_shortcuts( int key, const std::string &category, bool back )
1623 {
1624     if( key > 0 ) {
1625         quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1626         input_event event = input_event( key, input_event_t::keyboard_char );
1627         quick_shortcuts_t::iterator it = std::find( qsl.begin(), qsl.end(), event );
1628         if( it != qsl.end() ) { // already exists
1629             ( *it ).shortcut_last_used_action_counter =
1630                 g->get_user_action_counter(); // make sure we refresh shortcut usage
1631         } else {
1632             add_quick_shortcut( qsl, event, back,
1633                                 true ); // doesn't exist, add it to the shortcuts and refresh shortcut usage
1634             return true;
1635         }
1636     }
1637     return false;
1638 }
add_best_key_for_action_to_quick_shortcuts(std::string action_str,const std::string & category,bool back)1639 bool add_best_key_for_action_to_quick_shortcuts( std::string action_str,
1640         const std::string &category, bool back )
1641 {
1642     int best_key = choose_best_key_for_action( action_str, category );
1643     return add_key_to_quick_shortcuts( best_key, category, back );
1644 }
1645 
add_best_key_for_action_to_quick_shortcuts(action_id action,const std::string & category,bool back)1646 bool add_best_key_for_action_to_quick_shortcuts( action_id action, const std::string &category,
1647         bool back )
1648 {
1649     return add_best_key_for_action_to_quick_shortcuts( action_ident( action ), category, back );
1650 }
1651 
remove_action_from_quick_shortcuts(std::string action_str,const std::string & category)1652 void remove_action_from_quick_shortcuts( std::string action_str, const std::string &category )
1653 {
1654     quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1655     const std::vector<input_event> &events = inp_mngr.get_input_for_action( action_str, category );
1656     for( const auto &event : events ) {
1657         qsl.remove( event );
1658     }
1659 }
1660 
remove_action_from_quick_shortcuts(action_id action,const std::string & category)1661 void remove_action_from_quick_shortcuts( action_id action, const std::string &category )
1662 {
1663     remove_action_from_quick_shortcuts( action_ident( action ), category );
1664 }
1665 
1666 // Returns true if an expired action was removed
remove_expired_actions_from_quick_shortcuts(const std::string & category)1667 bool remove_expired_actions_from_quick_shortcuts( const std::string &category )
1668 {
1669     int remove_turns = get_option<int>( "ANDROID_SHORTCUT_REMOVE_TURNS" );
1670     if( remove_turns <= 0 ) {
1671         return false;
1672     }
1673 
1674     // This should only ever be used on "DEFAULTMODE" category for gameplay shortcuts
1675     if( category != "DEFAULTMODE" ) {
1676         return false;
1677     }
1678 
1679     bool ret = false;
1680     quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1681     quick_shortcuts_t::iterator it = qsl.begin();
1682     while( it != qsl.end() ) {
1683         if( g->get_user_action_counter() - ( *it ).shortcut_last_used_action_counter > remove_turns ) {
1684             it = qsl.erase( it );
1685             ret = true;
1686         } else {
1687             ++it;
1688         }
1689     }
1690     return ret;
1691 }
1692 
remove_stale_inventory_quick_shortcuts()1693 void remove_stale_inventory_quick_shortcuts()
1694 {
1695     if( get_option<bool>( "ANDROID_INVENTORY_AUTOADD" ) ) {
1696         quick_shortcuts_t &qsl = quick_shortcuts_map["INVENTORY"];
1697         quick_shortcuts_t::iterator it = qsl.begin();
1698         bool in_inventory;
1699         int key;
1700         bool valid;
1701         while( it != qsl.end() ) {
1702             key = ( *it ).get_first_input();
1703             valid = inv_chars.valid( key );
1704             in_inventory = false;
1705             if( valid ) {
1706                 Character &player_character = get_player_character();
1707                 in_inventory = player_character.inv->invlet_to_position( key ) != INT_MIN;
1708                 if( !in_inventory ) {
1709                     // We couldn't find this item in the inventory, let's check worn items
1710                     for( const auto &item : player_character.worn ) {
1711                         if( item.invlet == key ) {
1712                             in_inventory = true;
1713                             break;
1714                         }
1715                     }
1716                 }
1717                 if( !in_inventory ) {
1718                     // We couldn't find it in worn items either, check weapon held
1719                     if( player_character.weapon.invlet == key ) {
1720                         in_inventory = true;
1721                     }
1722                 }
1723             }
1724             if( valid && !in_inventory ) {
1725                 it = qsl.erase( it );
1726             } else {
1727                 ++it;
1728             }
1729         }
1730     }
1731 }
1732 
1733 // Draw preview of terminal size when adjusting values
draw_terminal_size_preview()1734 void draw_terminal_size_preview()
1735 {
1736     bool preview_terminal_dirty = preview_terminal_width != get_option<int>( "TERMINAL_X" ) * fontwidth
1737                                   ||
1738                                   preview_terminal_height != get_option<int>( "TERMINAL_Y" ) * fontheight;
1739     if( preview_terminal_dirty ||
1740         ( preview_terminal_change_time > 0 && SDL_GetTicks() - preview_terminal_change_time < 1000 ) ) {
1741         if( preview_terminal_dirty ) {
1742             preview_terminal_width = get_option<int>( "TERMINAL_X" ) * fontwidth;
1743             preview_terminal_height = get_option<int>( "TERMINAL_Y" ) * fontheight;
1744             preview_terminal_change_time = SDL_GetTicks();
1745         }
1746         SetRenderDrawColor( renderer, 255, 255, 255, 255 );
1747         SDL_Rect previewrect = get_android_render_rect( preview_terminal_width, preview_terminal_height );
1748         SDL_RenderDrawRect( renderer.get(), &previewrect );
1749         SetRenderDrawColor( renderer, 0, 0, 0, 255 );
1750     }
1751 }
1752 
1753 // Draw quick shortcuts on top of the game view
draw_quick_shortcuts()1754 void draw_quick_shortcuts()
1755 {
1756 
1757     if( !quick_shortcuts_enabled ||
1758         SDL_IsTextInputActive() ||
1759         ( get_option<bool>( "ANDROID_HIDE_HOLDS" ) && !is_quick_shortcut_touch && finger_down_time > 0 &&
1760           SDL_GetTicks() - finger_down_time >= static_cast<uint32_t>(
1761               get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) ) { // player is swipe + holding in a direction
1762         return;
1763     }
1764 
1765     bool shortcut_right = get_option<std::string>( "ANDROID_SHORTCUT_POSITION" ) == "right";
1766     std::string &category = touch_input_context.get_category();
1767     bool is_default_mode = category == "DEFAULTMODE";
1768     quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name( category )];
1769     if( qsl.empty() || !touch_input_context.get_registered_manual_keys().empty() ) {
1770         if( category == "DEFAULTMODE" ) {
1771             const std::string default_gameplay_shortcuts =
1772                 get_option<std::string>( "ANDROID_SHORTCUT_DEFAULTS" );
1773             for( const auto &c : default_gameplay_shortcuts ) {
1774                 add_key_to_quick_shortcuts( c, category, true );
1775             }
1776         } else {
1777             // This is an empty quick-shortcuts list, let's pre-populate it as best we can from the input context
1778 
1779             // For manual key lists, force-clear them each time since there's no point allowing custom bindings anyway
1780             if( !touch_input_context.get_registered_manual_keys().empty() ) {
1781                 qsl.clear();
1782             }
1783 
1784             // First process registered actions
1785             std::vector<std::string> &registered_actions = touch_input_context.get_registered_actions();
1786             for( std::vector<std::string>::iterator it = registered_actions.begin();
1787                  it != registered_actions.end(); ++it ) {
1788                 std::string &action = *it;
1789                 if( ignore_action_for_quick_shortcuts( action ) ) {
1790                     continue;
1791                 }
1792 
1793                 add_best_key_for_action_to_quick_shortcuts( action, category, !shortcut_right );
1794             }
1795 
1796             // Then process manual keys
1797             std::vector<input_context::manual_key> &registered_manual_keys =
1798                 touch_input_context.get_registered_manual_keys();
1799             for( const auto &manual_key : registered_manual_keys ) {
1800                 input_event event( manual_key.key, input_event_t::keyboard_char );
1801                 add_quick_shortcut( qsl, event, !shortcut_right, true );
1802             }
1803         }
1804     }
1805 
1806     // Only reorder quick shortcuts for non-gameplay lists that are likely to have navigational menu stuff
1807     if( !is_default_mode ) {
1808         reorder_quick_shortcuts( qsl );
1809     }
1810 
1811     float border, width, height;
1812     get_quick_shortcut_dimensions( qsl, border, width, height );
1813     input_event *hovered_quick_shortcut = get_quick_shortcut_under_finger();
1814     SDL_Rect rect;
1815     bool hovered, show_hint;
1816     int i = 0;
1817     for( std::list<input_event>::iterator it = qsl.begin(); it != qsl.end(); ++it ) {
1818         if( ( i + 1 ) * width > WindowWidth * get_option<int>( "ANDROID_SHORTCUT_SCREEN_PERCENTAGE" ) *
1819             0.01f ) {
1820             continue;
1821         }
1822         input_event &event = *it;
1823         std::string text = event.text;
1824         int key = event.get_first_input();
1825         float default_text_scale = std::floor( 0.75f * ( height /
1826                                                font->height ) ); // default for single character strings
1827         float text_scale = default_text_scale;
1828         if( text.empty() || text == " " ) {
1829             text = inp_mngr.get_keyname( key, event.type );
1830             text_scale = std::min( text_scale, 0.75f * ( width / ( font->width * utf8_width( text ) ) ) );
1831         }
1832         hovered = is_quick_shortcut_touch && hovered_quick_shortcut == &event;
1833         show_hint = hovered &&
1834                     SDL_GetTicks() - finger_down_time > static_cast<uint32_t>
1835                     ( get_option<int>( "ANDROID_INITIAL_DELAY" ) );
1836         std::string hint_text;
1837         if( show_hint ) {
1838             if( touch_input_context.get_category() == "INVENTORY" && inv_chars.valid( key ) ) {
1839                 Character &player_character = get_player_character();
1840                 // Special case for inventory items - show the inventory item name as help text
1841                 hint_text = player_character.inv->find_item( player_character.inv->invlet_to_position(
1842                                 key ) ).display_name();
1843                 if( hint_text == "none" ) {
1844                     // We couldn't find this item in the inventory, let's check worn items
1845                     for( const auto &item : player_character.worn ) {
1846                         if( item.invlet == key ) {
1847                             hint_text = item.display_name();
1848                             break;
1849                         }
1850                     }
1851                 }
1852                 if( hint_text == "none" ) {
1853                     // We couldn't find it in worn items either, must be weapon held
1854                     if( player_character.weapon.invlet == key ) {
1855                         hint_text = player_character.weapon.display_name();
1856                     }
1857                 }
1858             } else {
1859                 // All other screens - try and show the action name, either from registered actions or manually registered keys
1860                 hint_text = touch_input_context.get_action_name( touch_input_context.input_to_action( event ) );
1861                 if( hint_text == "ERROR" ) {
1862                     hint_text = touch_input_context.get_action_name_for_manual_key( key );
1863                 }
1864             }
1865             if( hint_text == "ERROR" || hint_text == "none" || hint_text.empty() ) {
1866                 show_hint = false;
1867             }
1868         }
1869         if( shortcut_right )
1870             rect = { WindowWidth - static_cast<int>( ( i + 1 ) * width + border ), static_cast<int>( WindowHeight - height ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1871         else
1872             rect = { static_cast<int>( i * width + border ), static_cast<int>( WindowHeight - height ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1873         if( hovered ) {
1874             SetRenderDrawColor( renderer, 0, 0, 0, 255 );
1875         } else {
1876             SetRenderDrawColor( renderer, 0, 0, 0,
1877                                 get_option<int>( "ANDROID_SHORTCUT_OPACITY_BG" ) * 0.01f * 255.0f );
1878         }
1879         SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_BLEND );
1880         RenderFillRect( renderer, &rect );
1881         if( hovered ) {
1882             // draw a second button hovering above the first one
1883             if( shortcut_right )
1884                 rect = { WindowWidth - static_cast<int>( ( i + 1 ) * width + border ), static_cast<int>( WindowHeight - height * 2.2f ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1885             else
1886                 rect = { static_cast<int>( i * width + border ), static_cast<int>( WindowHeight - height * 2.2f ), static_cast<int>( width - border * 2 ), static_cast<int>( height ) };
1887             SetRenderDrawColor( renderer, 0, 0, 196, 255 );
1888             RenderFillRect( renderer, &rect );
1889 
1890             if( show_hint ) {
1891                 // draw a backdrop for the hint text
1892                 rect = { 0, static_cast<int>( ( WindowHeight - height ) * 0.5f ), static_cast<int>( WindowWidth ), static_cast<int>( height ) };
1893                 SetRenderDrawColor( renderer, 0, 0, 0,
1894                                     get_option<int>( "ANDROID_SHORTCUT_OPACITY_BG" ) * 0.01f * 255.0f );
1895                 RenderFillRect( renderer, &rect );
1896             }
1897         }
1898         SetRenderDrawBlendMode( renderer, SDL_BLENDMODE_NONE );
1899         SDL_RenderSetScale( renderer.get(), text_scale, text_scale );
1900         int text_x, text_y;
1901         if( shortcut_right ) {
1902             text_x = ( WindowWidth - ( i + 0.5f ) * width - ( font->width * utf8_width(
1903                            text ) ) * text_scale * 0.5f ) / text_scale;
1904         } else {
1905             text_x = ( ( i + 0.5f ) * width - ( font->width * utf8_width( text ) ) * text_scale * 0.5f ) /
1906                      text_scale;
1907         }
1908         text_y = ( WindowHeight - ( height + font->height * text_scale ) * 0.5f ) / text_scale;
1909         font->OutputChar( renderer, geometry, text, point( text_x + 1, text_y + 1 ), 0,
1910                           get_option<int>( "ANDROID_SHORTCUT_OPACITY_SHADOW" ) * 0.01f );
1911         font->OutputChar( renderer, geometry, text, point( text_x, text_y ),
1912                           get_option<int>( "ANDROID_SHORTCUT_COLOR" ),
1913                           get_option<int>( "ANDROID_SHORTCUT_OPACITY_FG" ) * 0.01f );
1914         if( hovered ) {
1915             // draw a second button hovering above the first one
1916             font->OutputChar( renderer, geometry, text,
1917                               point( text_x, text_y - ( height * 1.2f / text_scale ) ),
1918                               get_option<int>( "ANDROID_SHORTCUT_COLOR" ) );
1919             if( show_hint ) {
1920                 // draw hint text
1921                 text_scale = default_text_scale;
1922                 hint_text = text + " " + hint_text;
1923                 hint_text = remove_color_tags( hint_text );
1924                 const float safe_margin = 0.9f;
1925                 int hint_length = utf8_width( hint_text );
1926                 if( WindowWidth * safe_margin < font->width * text_scale * hint_length ) {
1927                     text_scale *= ( WindowWidth * safe_margin ) / ( font->width * text_scale *
1928                                   hint_length );    // scale to fit comfortably
1929                 }
1930                 SDL_RenderSetScale( renderer.get(), text_scale, text_scale );
1931                 text_x = ( WindowWidth - ( ( font->width  * hint_length ) * text_scale ) ) * 0.5f / text_scale;
1932                 text_y = ( WindowHeight - font->height * text_scale ) * 0.5f / text_scale;
1933                 font->OutputChar( renderer, geometry, hint_text, point( text_x + 1, text_y + 1 ), 0,
1934                                   get_option<int>( "ANDROID_SHORTCUT_OPACITY_SHADOW" ) * 0.01f );
1935                 font->OutputChar( renderer, geometry, hint_text, point( text_x, text_y ),
1936                                   get_option<int>( "ANDROID_SHORTCUT_COLOR" ),
1937                                   get_option<int>( "ANDROID_SHORTCUT_OPACITY_FG" ) * 0.01f );
1938             }
1939         }
1940         SDL_RenderSetScale( renderer.get(), 1.0f, 1.0f );
1941         i++;
1942         if( ( i + 1 ) * width > WindowWidth ) {
1943             break;
1944         }
1945     }
1946 }
1947 
draw_virtual_joystick()1948 void draw_virtual_joystick()
1949 {
1950 
1951     // Bail out if we don't need to draw the joystick
1952     if( !get_option<bool>( "ANDROID_SHOW_VIRTUAL_JOYSTICK" ) ||
1953         finger_down_time <= 0 ||
1954         SDL_GetTicks() - finger_down_time <= static_cast<uint32_t>
1955         ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ||
1956         is_quick_shortcut_touch ||
1957         is_two_finger_touch ) {
1958         return;
1959     }
1960 
1961     SDL_SetTextureAlphaMod( touch_joystick.get(),
1962                             get_option<int>( "ANDROID_VIRTUAL_JOYSTICK_OPACITY" ) * 0.01f * 255.0f );
1963 
1964     float longest_window_edge = std::max( WindowWidth, WindowHeight );
1965 
1966     SDL_Rect dstrect;
1967 
1968     // Draw deadzone range
1969     dstrect.w = dstrect.h = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) ) * longest_window_edge * 2;
1970     dstrect.x = finger_down_x - dstrect.w / 2;
1971     dstrect.y = finger_down_y - dstrect.h / 2;
1972     RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1973 
1974     // Draw repeat delay range
1975     dstrect.w = dstrect.h = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) +
1976                               get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * longest_window_edge * 2;
1977     dstrect.x = finger_down_x - dstrect.w / 2;
1978     dstrect.y = finger_down_y - dstrect.h / 2;
1979     RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1980 
1981     // Draw current touch position (50% size of repeat delay range)
1982     dstrect.w = dstrect.h = dstrect.w / 2;
1983     dstrect.x = finger_down_x + ( finger_curr_x - finger_down_x ) / 2 - dstrect.w / 2;
1984     dstrect.y = finger_down_y + ( finger_curr_y - finger_down_y ) / 2 - dstrect.h / 2;
1985     RenderCopy( renderer, touch_joystick, NULL, &dstrect );
1986 
1987 }
1988 
update_finger_repeat_delay()1989 void update_finger_repeat_delay()
1990 {
1991     float delta_x = finger_curr_x - finger_down_x;
1992     float delta_y = finger_curr_y - finger_down_y;
1993     float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y );
1994     float longest_window_edge = std::max( WindowWidth, WindowHeight );
1995     float t = clamp<float>( ( dist - ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) *
1996                                        longest_window_edge ) ) /
1997                             std::max( 0.01f, ( get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * longest_window_edge ),
1998                             0.0f, 1.0f );
1999     float repeat_delay_min = static_cast<float>( get_option<int>( "ANDROID_REPEAT_DELAY_MIN" ) );
2000     float repeat_delay_max = static_cast<float>( get_option<int>( "ANDROID_REPEAT_DELAY_MAX" ) );
2001     finger_repeat_delay = lerp<float>( std::max( repeat_delay_min, repeat_delay_max ),
2002                                        std::min( repeat_delay_min, repeat_delay_max ),
2003                                        std::pow( t, get_option<float>( "ANDROID_SENSITIVITY_POWER" ) ) );
2004 }
2005 
2006 // TODO: Is there a better way to detect when string entry is allowed?
2007 // ANY_INPUT seems close but is abused by code everywhere.
2008 // Had a look through and think I've got all the cases but can't be 100% sure.
is_string_input(input_context & ctx)2009 bool is_string_input( input_context &ctx )
2010 {
2011     std::string &category = ctx.get_category();
2012     return category == "STRING_INPUT"
2013            || category == "HELP_KEYBINDINGS"
2014            || category == "NEW_CHAR_DESCRIPTION"
2015            || category == "WORLDGEN_CONFIRM_DIALOG";
2016 }
2017 
get_key_event_from_string(const std::string & str)2018 int get_key_event_from_string( const std::string &str )
2019 {
2020     if( !str.empty() ) {
2021         return str[0];
2022     }
2023     return -1;
2024 }
2025 // This function is triggered on finger up events, OR by a repeating timer for touch hold events.
handle_finger_input(uint32_t ticks)2026 void handle_finger_input( uint32_t ticks )
2027 {
2028 
2029     float delta_x = finger_curr_x - finger_down_x;
2030     float delta_y = finger_curr_y - finger_down_y;
2031     float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y ); // in pixel space
2032     bool handle_diagonals = touch_input_context.is_action_registered( "LEFTUP" );
2033     bool is_default_mode = touch_input_context.get_category() == "DEFAULTMODE";
2034     if( dist > ( get_option<float>( "ANDROID_DEADZONE_RANGE" )*std::max( WindowWidth,
2035                  WindowHeight ) ) ) {
2036         if( !handle_diagonals ) {
2037             if( delta_x >= 0 && delta_y >= 0 ) {
2038                 last_input = input_event( delta_x > delta_y ? KEY_RIGHT : KEY_DOWN, input_event_t::keyboard_char );
2039             } else if( delta_x < 0 && delta_y >= 0 ) {
2040                 last_input = input_event( -delta_x > delta_y ? KEY_LEFT : KEY_DOWN, input_event_t::keyboard_char );
2041             } else if( delta_x >= 0 && delta_y < 0 ) {
2042                 last_input = input_event( delta_x > -delta_y ? KEY_RIGHT : KEY_UP, input_event_t::keyboard_char );
2043             } else if( delta_x < 0 && delta_y < 0 ) {
2044                 last_input = input_event( -delta_x > -delta_y ? KEY_LEFT : KEY_UP, input_event_t::keyboard_char );
2045             }
2046         } else {
2047             if( delta_x > 0 ) {
2048                 if( std::abs( delta_y ) < delta_x * 0.5f ) {
2049                     // swipe right
2050                     last_input = input_event( KEY_RIGHT, input_event_t::keyboard_char );
2051                 } else if( std::abs( delta_y ) < delta_x * 2.0f ) {
2052                     if( delta_y < 0 ) {
2053                         // swipe up-right
2054                         last_input = input_event( JOY_RIGHTUP, input_event_t::gamepad );
2055                     } else {
2056                         // swipe down-right
2057                         last_input = input_event( JOY_RIGHTDOWN, input_event_t::gamepad );
2058                     }
2059                 } else {
2060                     if( delta_y < 0 ) {
2061                         // swipe up
2062                         last_input = input_event( KEY_UP, input_event_t::keyboard_char );
2063                     } else {
2064                         // swipe down
2065                         last_input = input_event( KEY_DOWN, input_event_t::keyboard_char );
2066                     }
2067                 }
2068             } else {
2069                 if( std::abs( delta_y ) < -delta_x * 0.5f ) {
2070                     // swipe left
2071                     last_input = input_event( KEY_LEFT, input_event_t::keyboard_char );
2072                 } else if( std::abs( delta_y ) < -delta_x * 2.0f ) {
2073                     if( delta_y < 0 ) {
2074                         // swipe up-left
2075                         last_input = input_event( JOY_LEFTUP, input_event_t::gamepad );
2076 
2077                     } else {
2078                         // swipe down-left
2079                         last_input = input_event( JOY_LEFTDOWN, input_event_t::gamepad );
2080                     }
2081                 } else {
2082                     if( delta_y < 0 ) {
2083                         // swipe up
2084                         last_input = input_event( KEY_UP, input_event_t::keyboard_char );
2085                     } else {
2086                         // swipe down
2087                         last_input = input_event( KEY_DOWN, input_event_t::keyboard_char );
2088                     }
2089                 }
2090             }
2091         }
2092     } else {
2093         if( ticks - finger_down_time >= static_cast<uint32_t>
2094             ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2095             // Single tap (repeat) - held, so always treat this as a tap
2096             // We only allow repeats for waiting, not confirming in menus as that's a bit silly
2097             if( is_default_mode ) {
2098                 last_input = input_event( get_key_event_from_string( get_option<std::string>( "ANDROID_TAP_KEY" ) ),
2099                                           input_event_t::keyboard_char );
2100             }
2101         } else {
2102             if( last_tap_time > 0 &&
2103                 ticks - last_tap_time < static_cast<uint32_t>( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2104                 // Double tap
2105                 last_input = input_event( is_default_mode ? KEY_ESCAPE : KEY_ESCAPE, input_event_t::keyboard_char );
2106                 last_tap_time = 0;
2107             } else {
2108                 // First tap detected, waiting to decide whether it's a single or a double tap input
2109                 last_tap_time = ticks;
2110             }
2111         }
2112     }
2113 }
2114 
android_is_hardware_keyboard_available()2115 bool android_is_hardware_keyboard_available()
2116 {
2117     JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2118     jobject activity = ( jobject )SDL_AndroidGetActivity();
2119     jclass clazz( env->GetObjectClass( activity ) );
2120     jmethodID method_id = env->GetMethodID( clazz, "isHardwareKeyboardAvailable", "()Z" );
2121     jboolean ans = env->CallBooleanMethod( activity, method_id );
2122     env->DeleteLocalRef( activity );
2123     env->DeleteLocalRef( clazz );
2124     return ans;
2125 }
2126 
android_vibrate()2127 void android_vibrate()
2128 {
2129     int vibration_ms = get_option<int>( "ANDROID_VIBRATION" );
2130     if( vibration_ms > 0 && !android_is_hardware_keyboard_available() ) {
2131         JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2132         jobject activity = ( jobject )SDL_AndroidGetActivity();
2133         jclass clazz( env->GetObjectClass( activity ) );
2134         jmethodID method_id = env->GetMethodID( clazz, "vibrate", "(I)V" );
2135         env->CallVoidMethod( activity, method_id, vibration_ms );
2136         env->DeleteLocalRef( activity );
2137         env->DeleteLocalRef( clazz );
2138     }
2139 }
2140 #endif
2141 
2142 //Check for any window messages (keypress, paint, mousemove, etc)
CheckMessages()2143 static void CheckMessages()
2144 {
2145     SDL_Event ev;
2146     bool quit = false;
2147     bool text_refresh = false;
2148     bool is_repeat = false;
2149     if( HandleDPad() ) {
2150         return;
2151     }
2152 
2153 #if defined(__ANDROID__)
2154     if( visible_display_frame_dirty ) {
2155         needupdate = true;
2156         visible_display_frame_dirty = false;
2157     }
2158 
2159     uint32_t ticks = SDL_GetTicks();
2160 
2161     // Force text input mode if hardware keyboard is available.
2162     if( android_is_hardware_keyboard_available() && !SDL_IsTextInputActive() ) {
2163         SDL_StartTextInput();
2164     }
2165 
2166     // Make sure the SDL surface view is visible, otherwise the "Z" loading screen is visible.
2167     if( needs_sdl_surface_visibility_refresh ) {
2168         needs_sdl_surface_visibility_refresh = false;
2169 
2170         // Call Java show_sdl_surface()
2171         JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2172         jobject activity = ( jobject )SDL_AndroidGetActivity();
2173         jclass clazz( env->GetObjectClass( activity ) );
2174         jmethodID method_id = env->GetMethodID( clazz, "show_sdl_surface", "()V" );
2175         env->CallVoidMethod( activity, method_id );
2176         env->DeleteLocalRef( activity );
2177         env->DeleteLocalRef( clazz );
2178     }
2179 
2180     // Copy the current input context
2181     if( !input_context::input_context_stack.empty() ) {
2182         input_context *new_input_context = *--input_context::input_context_stack.end();
2183         if( new_input_context && *new_input_context != touch_input_context ) {
2184 
2185             // If we were in an allow_text_entry input context, and text input is still active, and we're auto-managing keyboard, hide it.
2186             if( touch_input_context.allow_text_entry &&
2187                 !new_input_context->allow_text_entry &&
2188                 !is_string_input( *new_input_context ) &&
2189                 SDL_IsTextInputActive() &&
2190                 get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2191                 SDL_StopTextInput();
2192             }
2193 
2194             touch_input_context = *new_input_context;
2195             needupdate = true;
2196         }
2197     }
2198 
2199     bool is_default_mode = touch_input_context.get_category() == "DEFAULTMODE";
2200     quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2201                                  touch_input_context.get_category() )];
2202 
2203     // Don't do this logic if we already need an update, otherwise we're likely to overload the game with too much input on hold repeat events
2204     if( !needupdate ) {
2205 
2206         // Check action weightings and auto-add any immediate-surrounding actions as quick shortcuts
2207         // This code is based heavily off action.cpp handle_action_menu() which puts common shortcuts at the top
2208         if( is_default_mode && get_option<bool>( "ANDROID_SHORTCUT_AUTOADD" ) ) {
2209             static int last_moves_since_last_save = -1;
2210             if( last_moves_since_last_save != g->get_moves_since_last_save() ) {
2211                 last_moves_since_last_save = g->get_moves_since_last_save();
2212 
2213                 // Actions to add
2214                 std::set<action_id> actions;
2215 
2216                 // Actions to remove - we only want to remove things that we're 100% sure won't be useful to players otherwise
2217                 std::set<action_id> actions_remove;
2218 
2219                 Character &player_character = get_player_character();
2220                 // Check if we're in a potential combat situation, if so, sort a few actions to the top.
2221                 if( !player_character.get_hostile_creatures( 60 ).empty() ) {
2222                     // Only prioritize movement options if we're not driving.
2223                     if( !player_character.controlling_vehicle ) {
2224                         actions.insert( ACTION_CYCLE_MOVE );
2225                     }
2226                     // Only prioritize fire weapon options if we're wielding a ranged weapon.
2227                     if( player_character.weapon.is_gun() || player_character.weapon.has_flag( flag_REACH_ATTACK ) ) {
2228                         actions.insert( ACTION_FIRE );
2229                     }
2230                 }
2231 
2232                 // If we're already running, make it simple to toggle running to off.
2233                 if( player_character.is_running() ) {
2234                     actions.insert( ACTION_TOGGLE_RUN );
2235                 }
2236                 // If we're already crouching, make it simple to toggle crouching to off.
2237                 if( player_character.is_crouching() ) {
2238                     actions.insert( ACTION_TOGGLE_CROUCH );
2239                 }
2240 
2241                 // We're not already running or in combat, so remove cycle walk/run
2242                 if( std::find( actions.begin(), actions.end(), ACTION_CYCLE_MOVE ) == actions.end() ) {
2243                     actions_remove.insert( ACTION_CYCLE_MOVE );
2244                 }
2245 
2246                 map &here = get_map();
2247                 // Check if we can perform one of our actions on nearby terrain. If so,
2248                 // display that action at the top of the list.
2249                 for( int dx = -1; dx <= 1; dx++ ) {
2250                     for( int dy = -1; dy <= 1; dy++ ) {
2251                         int x = player_character.posx() + dx;
2252                         int y = player_character.posy() + dy;
2253                         int z = player_character.posz();
2254                         const tripoint pos( x, y, z );
2255 
2256                         // Check if we're near a vehicle, if so, vehicle controls should be top.
2257                         {
2258                             const optional_vpart_position vp = here.veh_at( pos );
2259                             vehicle *const veh = veh_pointer_or_null( vp );
2260                             if( veh ) {
2261                                 const int veh_part = vp ? vp->part_index() : -1;
2262                                 if( veh->part_with_feature( veh_part, "CONTROLS", true ) >= 0 ) {
2263                                     actions.insert( ACTION_CONTROL_VEHICLE );
2264                                 }
2265                                 const int openablepart = veh->part_with_feature( veh_part, "OPENABLE", true );
2266                                 if( openablepart >= 0 && veh->is_open( openablepart ) && ( dx != 0 ||
2267                                         dy != 0 ) ) { // an open door adjacent to us
2268                                     actions.insert( ACTION_CLOSE );
2269                                 }
2270                                 const int curtainpart = veh->part_with_feature( veh_part, "CURTAIN", true );
2271                                 if( curtainpart >= 0 && veh->is_open( curtainpart ) && ( dx != 0 || dy != 0 ) ) {
2272                                     actions.insert( ACTION_CLOSE );
2273                                 }
2274                                 const int cargopart = veh->part_with_feature( veh_part, "CARGO", true );
2275                                 if( cargopart >= 0 && ( !veh->get_items( cargopart ).empty() ) ) {
2276                                     actions.insert( ACTION_PICKUP );
2277                                 }
2278                             }
2279                         }
2280 
2281                         if( dx != 0 || dy != 0 ) {
2282                             // Check for actions that work on nearby tiles
2283                             //if( can_interact_at( ACTION_OPEN, pos ) ) {
2284                             // don't bother with open since user can just walk into target
2285                             //}
2286                             if( can_interact_at( ACTION_CLOSE, pos ) ) {
2287                                 actions.insert( ACTION_CLOSE );
2288                             }
2289                             if( can_interact_at( ACTION_EXAMINE, pos ) ) {
2290                                 actions.insert( ACTION_EXAMINE );
2291                             }
2292                         } else {
2293                             // Check for actions that work on own tile only
2294                             if( can_interact_at( ACTION_BUTCHER, pos ) ) {
2295                                 actions.insert( ACTION_BUTCHER );
2296                             } else {
2297                                 actions_remove.insert( ACTION_BUTCHER );
2298                             }
2299 
2300                             if( can_interact_at( ACTION_MOVE_UP, pos ) ) {
2301                                 actions.insert( ACTION_MOVE_UP );
2302                             } else {
2303                                 actions_remove.insert( ACTION_MOVE_UP );
2304                             }
2305 
2306                             if( can_interact_at( ACTION_MOVE_DOWN, pos ) ) {
2307                                 actions.insert( ACTION_MOVE_DOWN );
2308                             } else {
2309                                 actions_remove.insert( ACTION_MOVE_DOWN );
2310                             }
2311                         }
2312 
2313                         // Check for actions that work on nearby tiles and own tile
2314                         if( can_interact_at( ACTION_PICKUP, pos ) ) {
2315                             actions.insert( ACTION_PICKUP );
2316                         }
2317                     }
2318                 }
2319 
2320                 // We're not near a vehicle, so remove control vehicle
2321                 if( std::find( actions.begin(), actions.end(), ACTION_CONTROL_VEHICLE ) == actions.end() ) {
2322                     actions_remove.insert( ACTION_CONTROL_VEHICLE );
2323                 }
2324 
2325                 // We're not able to close anything nearby, so remove it
2326                 if( std::find( actions.begin(), actions.end(), ACTION_CLOSE ) == actions.end() ) {
2327                     actions_remove.insert( ACTION_CLOSE );
2328                 }
2329 
2330                 // We're not able to examine anything nearby, so remove it
2331                 if( std::find( actions.begin(), actions.end(), ACTION_EXAMINE ) == actions.end() ) {
2332                     actions_remove.insert( ACTION_EXAMINE );
2333                 }
2334 
2335                 // We're not able to pickup anything nearby, so remove it
2336                 if( std::find( actions.begin(), actions.end(), ACTION_PICKUP ) == actions.end() ) {
2337                     actions_remove.insert( ACTION_PICKUP );
2338                 }
2339 
2340                 // Check if we can't move because of safe mode - if so, add ability to ignore
2341                 if( g && !g->check_safe_mode_allowed( false ) ) {
2342                     actions.insert( ACTION_IGNORE_ENEMY );
2343                     actions.insert( ACTION_TOGGLE_SAFEMODE );
2344                 } else {
2345                     actions_remove.insert( ACTION_IGNORE_ENEMY );
2346                     actions_remove.insert( ACTION_TOGGLE_SAFEMODE );
2347                 }
2348 
2349                 // Check if we're significantly hungry or thirsty - if so, add eat
2350                 if( player_character.get_hunger() > 100 || player_character.get_thirst() > 40 ) {
2351                     actions.insert( ACTION_EAT );
2352                 }
2353 
2354                 // Check if we're dead tired - if so, add sleep
2355                 if( player_character.get_fatigue() > fatigue_levels::DEAD_TIRED ) {
2356                     actions.insert( ACTION_SLEEP );
2357                 }
2358 
2359                 for( const auto &action : actions ) {
2360                     if( add_best_key_for_action_to_quick_shortcuts( action, touch_input_context.get_category(),
2361                             !get_option<bool>( "ANDROID_SHORTCUT_AUTOADD_FRONT" ) ) ) {
2362                         needupdate = true;
2363                     }
2364                 }
2365 
2366                 size_t old_size = qsl.size();
2367                 for( const auto &action_remove : actions_remove ) {
2368                     remove_action_from_quick_shortcuts( action_remove, touch_input_context.get_category() );
2369                 }
2370                 if( qsl.size() != old_size ) {
2371                     needupdate = true;
2372                 }
2373             }
2374         }
2375 
2376         if( remove_expired_actions_from_quick_shortcuts( touch_input_context.get_category() ) ) {
2377             needupdate = true;
2378         }
2379 
2380         // Toggle quick shortcuts on/off
2381         if( ac_back_down_time > 0 &&
2382             ticks - ac_back_down_time > static_cast<uint32_t>
2383             ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2384             if( !quick_shortcuts_toggle_handled ) {
2385                 quick_shortcuts_enabled = !quick_shortcuts_enabled;
2386                 quick_shortcuts_toggle_handled = true;
2387                 refresh_display();
2388 
2389                 // Display an Android toast message
2390                 {
2391                     JNIEnv *env = ( JNIEnv * )SDL_AndroidGetJNIEnv();
2392                     jobject activity = ( jobject )SDL_AndroidGetActivity();
2393                     jclass clazz( env->GetObjectClass( activity ) );
2394                     jstring toast_message = env->NewStringUTF( quick_shortcuts_enabled ? "Shortcuts visible" :
2395                                             "Shortcuts hidden" );
2396                     jmethodID method_id = env->GetMethodID( clazz, "toast", "(Ljava/lang/String;)V" );
2397                     env->CallVoidMethod( activity, method_id, toast_message );
2398                     env->DeleteLocalRef( activity );
2399                     env->DeleteLocalRef( clazz );
2400                 }
2401             }
2402         }
2403 
2404         // Handle repeating inputs from touch + holds
2405         if( !is_quick_shortcut_touch && !is_two_finger_touch && finger_down_time > 0 &&
2406             ticks - finger_down_time > static_cast<uint32_t>
2407             ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2408             if( ticks - finger_repeat_time > finger_repeat_delay ) {
2409                 handle_finger_input( ticks );
2410                 finger_repeat_time = ticks;
2411                 // Prevent repeating inputs on the next call to this function if there is a fingerup event
2412                 while( SDL_PollEvent( &ev ) ) {
2413                     if( ev.type == SDL_FINGERUP ) {
2414                         second_finger_down_x = second_finger_curr_x = finger_down_x = finger_curr_x = -1.0f;
2415                         second_finger_down_y = second_finger_curr_y = finger_down_y = finger_curr_y = -1.0f;
2416                         is_two_finger_touch = false;
2417                         finger_down_time = 0;
2418                         finger_repeat_time = 0;
2419                         // let the next call decide if needupdate should be true
2420                         break;
2421                     }
2422                 }
2423                 return;
2424             }
2425         }
2426 
2427         // If we received a first tap and not another one within a certain period, this was a single tap, so trigger the input event
2428         if( !is_quick_shortcut_touch && !is_two_finger_touch && last_tap_time > 0 &&
2429             ticks - last_tap_time >= static_cast<uint32_t>
2430             ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2431             // Single tap
2432             last_tap_time = ticks;
2433             last_input = input_event( is_default_mode ? get_key_event_from_string(
2434                                           get_option<std::string>( "ANDROID_TAP_KEY" ) ) : '\n', input_event_t::keyboard_char );
2435             last_tap_time = 0;
2436             return;
2437         }
2438 
2439         // ensure hint text pops up even if player doesn't move finger to trigger a FINGERMOTION event
2440         if( is_quick_shortcut_touch && finger_down_time > 0 &&
2441             ticks - finger_down_time > static_cast<uint32_t>
2442             ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2443             needupdate = true;
2444         }
2445     }
2446 #endif
2447 
2448     last_input = input_event();
2449 
2450     bool need_redraw = false;
2451 
2452     while( SDL_PollEvent( &ev ) ) {
2453         switch( ev.type ) {
2454             case SDL_WINDOWEVENT:
2455                 switch( ev.window.event ) {
2456 #if defined(__ANDROID__)
2457                     // SDL will send a focus lost event whenever the app loses focus (eg. lock screen, switch app focus etc.)
2458                     // If we detect it and the game seems in a saveable state, try and do a quicksave. This is a bit dodgy
2459                     // as the player could be ANYWHERE doing ANYTHING (a sub-menu, interacting with an NPC/computer etc.)
2460                     // but it seems to work so far, and the alternative is the player losing their progress as the app is likely
2461                     // to be destroyed pretty quickly when it goes out of focus due to memory usage.
2462                     case SDL_WINDOWEVENT_FOCUS_LOST:
2463                         if( world_generator &&
2464                             world_generator->active_world &&
2465                             g && g->uquit == QUIT_NO &&
2466                             get_option<bool>( "ANDROID_QUICKSAVE" ) &&
2467                             !std::uncaught_exception() ) {
2468                             g->quicksave();
2469                         }
2470                         break;
2471                     // SDL sends a window size changed event whenever the screen rotates orientation
2472                     case SDL_WINDOWEVENT_SIZE_CHANGED:
2473                         WindowWidth = ev.window.data1;
2474                         WindowHeight = ev.window.data2;
2475                         SDL_Delay( 500 );
2476                         SDL_GetWindowSurface( window.get() );
2477                         refresh_display();
2478                         needupdate = true;
2479                         break;
2480 #endif
2481                     case SDL_WINDOWEVENT_SHOWN:
2482                     case SDL_WINDOWEVENT_MINIMIZED:
2483                     case SDL_WINDOWEVENT_FOCUS_GAINED:
2484                         break;
2485                     case SDL_WINDOWEVENT_EXPOSED:
2486                         need_redraw = true;
2487                         needupdate = true;
2488                         break;
2489                     case SDL_WINDOWEVENT_RESTORED:
2490 #if defined(__ANDROID__)
2491                         needs_sdl_surface_visibility_refresh = true;
2492                         if( android_is_hardware_keyboard_available() ) {
2493                             SDL_StopTextInput();
2494                             SDL_StartTextInput();
2495                         }
2496 #endif
2497                         break;
2498                     case SDL_WINDOWEVENT_RESIZED: {
2499                         restore_on_out_of_scope<input_event> prev_last_input( last_input );
2500                         needupdate = handle_resize( ev.window.data1, ev.window.data2 );
2501                         break;
2502                     }
2503                     default:
2504                         break;
2505                 }
2506                 break;
2507             case SDL_RENDER_TARGETS_RESET:
2508                 need_redraw = true;
2509                 needupdate = true;
2510                 break;
2511             case SDL_KEYDOWN: {
2512 #if defined(__ANDROID__)
2513                 // Toggle virtual keyboard with Android back button. For some reason I get double inputs, so ignore everything once it's already down.
2514                 if( ev.key.keysym.sym == SDLK_AC_BACK && ac_back_down_time == 0 ) {
2515                     ac_back_down_time = ticks;
2516                     quick_shortcuts_toggle_handled = false;
2517                 }
2518 #endif
2519                 is_repeat = ev.key.repeat;
2520                 //hide mouse cursor on keyboard input
2521                 if( get_option<std::string>( "HIDE_CURSOR" ) != "show" && SDL_ShowCursor( -1 ) ) {
2522                     SDL_ShowCursor( SDL_DISABLE );
2523                 }
2524                 keyboard_mode mode = keyboard_mode::keychar;
2525 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
2526                 if( !SDL_IsTextInputActive() ) {
2527                     mode = keyboard_mode::keycode;
2528                 }
2529 #endif
2530                 if( mode == keyboard_mode::keychar ) {
2531                     const int lc = sdl_keysym_to_curses( ev.key.keysym );
2532                     if( lc <= 0 ) {
2533                         // a key we don't know in curses and won't handle.
2534                         break;
2535                     } else if( add_alt_code( lc ) ) {
2536                         // key was handled
2537                     } else {
2538                         last_input = input_event( lc, input_event_t::keyboard_char );
2539 #if defined(__ANDROID__)
2540                         if( !android_is_hardware_keyboard_available() ) {
2541                             if( !is_string_input( touch_input_context ) && !touch_input_context.allow_text_entry ) {
2542                                 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2543                                     SDL_StopTextInput();
2544                                 }
2545 
2546                                 // add a quick shortcut
2547                                 if( !last_input.text.empty() ||
2548                                     !inp_mngr.get_keyname( lc, input_event_t::keyboard_char ).empty() ) {
2549                                     qsl.remove( last_input );
2550                                     add_quick_shortcut( qsl, last_input, false, true );
2551                                     refresh_display();
2552                                 }
2553                             } else if( lc == '\n' || lc == KEY_ESCAPE ) {
2554                                 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2555                                     SDL_StopTextInput();
2556                                 }
2557                             }
2558                         }
2559 #endif
2560                     }
2561                 } else {
2562                     last_input = sdl_keysym_to_keycode_evt( ev.key.keysym );
2563                 }
2564             }
2565             break;
2566             case SDL_KEYUP: {
2567 #if defined(__ANDROID__)
2568                 // Toggle virtual keyboard with Android back button
2569                 if( ev.key.keysym.sym == SDLK_AC_BACK ) {
2570                     if( ticks - ac_back_down_time <= static_cast<uint32_t>
2571                         ( get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2572                         if( SDL_IsTextInputActive() ) {
2573                             SDL_StopTextInput();
2574                         } else {
2575                             SDL_StartTextInput();
2576                         }
2577                     }
2578                     ac_back_down_time = 0;
2579                 }
2580 #endif
2581                 keyboard_mode mode = keyboard_mode::keychar;
2582 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
2583                 if( !SDL_IsTextInputActive() ) {
2584                     mode = keyboard_mode::keycode;
2585                 }
2586 #endif
2587                 is_repeat = ev.key.repeat;
2588                 if( mode == keyboard_mode::keychar ) {
2589                     if( ev.key.keysym.sym == SDLK_LALT || ev.key.keysym.sym == SDLK_RALT ) {
2590                         int code = end_alt_code();
2591                         if( code ) {
2592                             last_input = input_event( code, input_event_t::keyboard_char );
2593                             last_input.text = utf32_to_utf8( code );
2594                         }
2595                     }
2596                 } else if( is_repeat ) {
2597                     last_input = sdl_keysym_to_keycode_evt( ev.key.keysym );
2598                 }
2599             }
2600             break;
2601             case SDL_TEXTINPUT:
2602                 if( !add_alt_code( *ev.text.text ) ) {
2603                     if( strlen( ev.text.text ) > 0 ) {
2604                         const unsigned lc = UTF8_getch( ev.text.text );
2605                         last_input = input_event( lc, input_event_t::keyboard_char );
2606 #if defined(__ANDROID__)
2607                         if( !android_is_hardware_keyboard_available() ) {
2608                             if( !is_string_input( touch_input_context ) && !touch_input_context.allow_text_entry ) {
2609                                 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2610                                     SDL_StopTextInput();
2611                                 }
2612 
2613                                 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2614                                                              touch_input_context.get_category() )];
2615                                 qsl.remove( last_input );
2616                                 add_quick_shortcut( qsl, last_input, false, true );
2617                                 refresh_display();
2618                             } else if( lc == '\n' || lc == KEY_ESCAPE ) {
2619                                 if( get_option<bool>( "ANDROID_AUTO_KEYBOARD" ) ) {
2620                                     SDL_StopTextInput();
2621                                 }
2622                             }
2623                         }
2624 #endif
2625                     } else {
2626                         // no key pressed in this event
2627                         last_input = input_event();
2628                         last_input.type = input_event_t::keyboard_char;
2629                     }
2630                     last_input.text = ev.text.text;
2631                     text_refresh = true;
2632                 }
2633                 break;
2634             case SDL_TEXTEDITING: {
2635                 if( strlen( ev.edit.text ) > 0 ) {
2636                     const unsigned lc = UTF8_getch( ev.edit.text );
2637                     last_input = input_event( lc, input_event_t::keyboard_char );
2638                 } else {
2639                     // no key pressed in this event
2640                     last_input = input_event();
2641                     last_input.type = input_event_t::keyboard_char;
2642                 }
2643                 last_input.edit = ev.edit.text;
2644                 last_input.edit_refresh = true;
2645                 text_refresh = true;
2646             }
2647             break;
2648             case SDL_JOYBUTTONDOWN:
2649                 last_input = input_event( ev.jbutton.button, input_event_t::keyboard_char );
2650                 break;
2651             case SDL_JOYAXISMOTION:
2652                 // on gamepads, the axes are the analog sticks
2653                 // TODO: somehow get the "digipad" values from the axes
2654                 break;
2655             case SDL_MOUSEMOTION:
2656                 if( get_option<std::string>( "HIDE_CURSOR" ) == "show" ||
2657                     get_option<std::string>( "HIDE_CURSOR" ) == "hidekb" ) {
2658                     if( !SDL_ShowCursor( -1 ) ) {
2659                         SDL_ShowCursor( SDL_ENABLE );
2660                     }
2661 
2662                     // Only monitor motion when cursor is visible
2663                     last_input = input_event( MOUSE_MOVE, input_event_t::mouse );
2664                 }
2665                 break;
2666 
2667             case SDL_MOUSEBUTTONUP:
2668                 switch( ev.button.button ) {
2669                     case SDL_BUTTON_LEFT:
2670                         last_input = input_event( MOUSE_BUTTON_LEFT, input_event_t::mouse );
2671                         break;
2672                     case SDL_BUTTON_RIGHT:
2673                         last_input = input_event( MOUSE_BUTTON_RIGHT, input_event_t::mouse );
2674                         break;
2675                 }
2676                 break;
2677 
2678             case SDL_MOUSEWHEEL:
2679                 if( ev.wheel.y > 0 ) {
2680                     last_input = input_event( SCROLLWHEEL_UP, input_event_t::mouse );
2681                 } else if( ev.wheel.y < 0 ) {
2682                     last_input = input_event( SCROLLWHEEL_DOWN, input_event_t::mouse );
2683                 }
2684                 break;
2685 
2686 #if defined(__ANDROID__)
2687             case SDL_FINGERMOTION:
2688                 if( ev.tfinger.fingerId == 0 ) {
2689                     if( !is_quick_shortcut_touch ) {
2690                         update_finger_repeat_delay();
2691                     }
2692                     needupdate = true; // ensure virtual joystick and quick shortcuts redraw as we interact
2693                     finger_curr_x = ev.tfinger.x * WindowWidth;
2694                     finger_curr_y = ev.tfinger.y * WindowHeight;
2695 
2696                     if( get_option<bool>( "ANDROID_VIRTUAL_JOYSTICK_FOLLOW" ) && !is_two_finger_touch ) {
2697                         // If we've moved too far from joystick center, offset joystick center automatically
2698                         float delta_x = finger_curr_x - finger_down_x;
2699                         float delta_y = finger_curr_y - finger_down_y;
2700                         float dist = std::sqrt( delta_x * delta_x + delta_y * delta_y );
2701                         float max_dist = ( get_option<float>( "ANDROID_DEADZONE_RANGE" ) +
2702                                            get_option<float>( "ANDROID_REPEAT_DELAY_RANGE" ) ) * std::max( WindowWidth, WindowHeight );
2703                         if( dist > max_dist ) {
2704                             float delta_ratio = ( dist / max_dist ) - 1.0f;
2705                             finger_down_x += delta_x * delta_ratio;
2706                             finger_down_y += delta_y * delta_ratio;
2707                         }
2708                     }
2709 
2710                 } else if( ev.tfinger.fingerId == 1 ) {
2711                     second_finger_curr_x = ev.tfinger.x * WindowWidth;
2712                     second_finger_curr_y = ev.tfinger.y * WindowHeight;
2713                 }
2714                 break;
2715             case SDL_FINGERDOWN:
2716                 if( ev.tfinger.fingerId == 0 ) {
2717                     finger_down_x = finger_curr_x = ev.tfinger.x * WindowWidth;
2718                     finger_down_y = finger_curr_y = ev.tfinger.y * WindowHeight;
2719                     finger_down_time = ticks;
2720                     finger_repeat_time = 0;
2721                     is_quick_shortcut_touch = get_quick_shortcut_under_finger() != NULL;
2722                     if( !is_quick_shortcut_touch ) {
2723                         update_finger_repeat_delay();
2724                     }
2725                     needupdate = true; // ensure virtual joystick and quick shortcuts redraw as we interact
2726                 } else if( ev.tfinger.fingerId == 1 ) {
2727                     if( !is_quick_shortcut_touch ) {
2728                         second_finger_down_x = second_finger_curr_x = ev.tfinger.x * WindowWidth;
2729                         second_finger_down_y = second_finger_curr_y = ev.tfinger.y * WindowHeight;
2730                         is_two_finger_touch = true;
2731                     }
2732                 }
2733                 break;
2734             case SDL_FINGERUP:
2735                 if( ev.tfinger.fingerId == 0 ) {
2736                     finger_curr_x = ev.tfinger.x * WindowWidth;
2737                     finger_curr_y = ev.tfinger.y * WindowHeight;
2738                     if( is_quick_shortcut_touch ) {
2739                         input_event *quick_shortcut = get_quick_shortcut_under_finger();
2740                         if( quick_shortcut ) {
2741                             last_input = *quick_shortcut;
2742                             if( get_option<bool>( "ANDROID_SHORTCUT_MOVE_FRONT" ) ) {
2743                                 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2744                                                              touch_input_context.get_category() )];
2745                                 reorder_quick_shortcut( qsl, quick_shortcut->get_first_input(), false );
2746                             }
2747                             quick_shortcut->shortcut_last_used_action_counter = g->get_user_action_counter();
2748                         } else {
2749                             // Get the quick shortcut that was originally touched
2750                             quick_shortcut = get_quick_shortcut_under_finger( true );
2751                             if( quick_shortcut &&
2752                                 ticks - finger_down_time <= static_cast<uint32_t>( get_option<int>( "ANDROID_INITIAL_DELAY" ) )
2753                                 &&
2754                                 finger_curr_y < finger_down_y &&
2755                                 finger_down_y - finger_curr_y > std::abs( finger_down_x - finger_curr_x ) ) {
2756                                 // a flick up was detected, remove the quick shortcut!
2757                                 quick_shortcuts_t &qsl = quick_shortcuts_map[get_quick_shortcut_name(
2758                                                              touch_input_context.get_category() )];
2759                                 qsl.remove( *quick_shortcut );
2760                             }
2761                         }
2762                     } else {
2763                         if( is_two_finger_touch ) {
2764                             // handle zoom in/out
2765                             if( is_default_mode ) {
2766                                 float x1 = ( finger_curr_x - finger_down_x );
2767                                 float y1 = ( finger_curr_y - finger_down_y );
2768                                 float d1 = std::sqrt( x1 * x1 + y1 * y1 );
2769 
2770                                 float x2 = ( second_finger_curr_x - second_finger_down_x );
2771                                 float y2 = ( second_finger_curr_y - second_finger_down_y );
2772                                 float d2 = std::sqrt( x2 * x2 + y2 * y2 );
2773 
2774                                 float longest_window_edge = std::max( WindowWidth, WindowHeight );
2775 
2776                                 if( std::max( d1, d2 ) < get_option<float>( "ANDROID_DEADZONE_RANGE" ) * longest_window_edge ) {
2777                                     last_input = input_event( get_key_event_from_string(
2778                                                                   get_option<std::string>( "ANDROID_2_TAP_KEY" ) ), input_event_t::keyboard_char );
2779                                 } else {
2780                                     float dot = ( x1 * x2 + y1 * y2 ) / ( d1 * d2 ); // dot product of two finger vectors, -1 to +1
2781                                     if( dot > 0.0f ) { // both fingers mostly heading in same direction, check for double-finger swipe gesture
2782                                         float dratio = d1 / d2;
2783                                         const float dist_ratio = 0.3f;
2784                                         if( dratio > dist_ratio &&
2785                                             dratio < ( 1.0f /
2786                                                        dist_ratio ) ) { // both fingers moved roughly the same distance, so it's a double-finger swipe!
2787                                             float xavg = 0.5f * ( x1 + x2 );
2788                                             float yavg = 0.5f * ( y1 + y2 );
2789                                             if( xavg > 0 && xavg > std::abs( yavg ) ) {
2790                                                 last_input = input_event( get_key_event_from_string(
2791                                                                               get_option<std::string>( "ANDROID_2_SWIPE_LEFT_KEY" ) ), input_event_t::keyboard_char );
2792                                             } else if( xavg < 0 && -xavg > std::abs( yavg ) ) {
2793                                                 last_input = input_event( get_key_event_from_string(
2794                                                                               get_option<std::string>( "ANDROID_2_SWIPE_RIGHT_KEY" ) ), input_event_t::keyboard_char );
2795                                             } else if( yavg > 0 && yavg > std::abs( xavg ) ) {
2796                                                 last_input = input_event( get_key_event_from_string(
2797                                                                               get_option<std::string>( "ANDROID_2_SWIPE_DOWN_KEY" ) ), input_event_t::keyboard_char );
2798                                             } else {
2799                                                 last_input = input_event( get_key_event_from_string(
2800                                                                               get_option<std::string>( "ANDROID_2_SWIPE_UP_KEY" ) ), input_event_t::keyboard_char );
2801                                             }
2802                                         }
2803                                     } else {
2804                                         // both fingers heading in opposite direction, check for zoom gesture
2805                                         float down_x = finger_down_x - second_finger_down_x;
2806                                         float down_y = finger_down_y - second_finger_down_y;
2807                                         float down_dist = std::sqrt( down_x * down_x + down_y * down_y );
2808 
2809                                         float curr_x = finger_curr_x - second_finger_curr_x;
2810                                         float curr_y = finger_curr_y - second_finger_curr_y;
2811                                         float curr_dist = std::sqrt( curr_x * curr_x + curr_y * curr_y );
2812 
2813                                         const float zoom_ratio = 0.9f;
2814                                         if( curr_dist < down_dist * zoom_ratio ) {
2815                                             last_input = input_event( get_key_event_from_string(
2816                                                                           get_option<std::string>( "ANDROID_PINCH_IN_KEY" ) ), input_event_t::keyboard_char );
2817                                         } else if( curr_dist > down_dist / zoom_ratio ) {
2818                                             last_input = input_event( get_key_event_from_string(
2819                                                                           get_option<std::string>( "ANDROID_PINCH_OUT_KEY" ) ), input_event_t::keyboard_char );
2820                                         }
2821                                     }
2822                                 }
2823                             }
2824                         } else if( ticks - finger_down_time <= static_cast<uint32_t>(
2825                                        get_option<int>( "ANDROID_INITIAL_DELAY" ) ) ) {
2826                             handle_finger_input( ticks );
2827                         }
2828                     }
2829                     second_finger_down_x = second_finger_curr_x = finger_down_x = finger_curr_x = -1.0f;
2830                     second_finger_down_y = second_finger_curr_y = finger_down_y = finger_curr_y = -1.0f;
2831                     is_two_finger_touch = false;
2832                     finger_down_time = 0;
2833                     finger_repeat_time = 0;
2834                     needupdate = true; // ensure virtual joystick and quick shortcuts are updated properly
2835                     refresh_display(); // as above, but actually redraw it now as well
2836                 } else if( ev.tfinger.fingerId == 1 ) {
2837                     if( is_two_finger_touch ) {
2838                         // on second finger release, just remember the x/y position so we can calculate delta once first finger is done
2839                         // is_two_finger_touch will be reset when first finger lifts (see above)
2840                         second_finger_curr_x = ev.tfinger.x * WindowWidth;
2841                         second_finger_curr_y = ev.tfinger.y * WindowHeight;
2842                     }
2843                 }
2844 
2845                 break;
2846 #endif
2847 
2848             case SDL_QUIT:
2849                 quit = true;
2850                 break;
2851         }
2852         if( text_refresh && !is_repeat ) {
2853             break;
2854         }
2855     }
2856     if( need_redraw ) {
2857         restore_on_out_of_scope<input_event> prev_last_input( last_input );
2858         // FIXME: SDL_RENDER_TARGETS_RESET only seems to be fired after the first redraw
2859         // when restoring the window after system sleep, rather than immediately
2860         // on focus gain. This seems to mess up the first redraw and
2861         // causes black screen that lasts ~0.5 seconds before the screen
2862         // contents are redrawn in the following code.
2863         window_dimensions dim = get_window_dimensions( catacurses::stdscr );
2864         ui_manager::invalidate( rectangle<point>( point_zero, dim.window_size_pixel ), false );
2865         ui_manager::redraw();
2866     }
2867     if( needupdate ) {
2868         try_sdl_update();
2869     }
2870     if( quit ) {
2871         catacurses::endwin();
2872         exit( 0 );
2873     }
2874 }
2875 
2876 //***********************************
2877 //Pseudo-Curses Functions           *
2878 //***********************************
2879 
2880 // Calculates the new width of the window
projected_window_width()2881 int projected_window_width()
2882 {
2883     return get_option<int>( "TERMINAL_X" ) * fontwidth;
2884 }
2885 
2886 // Calculates the new height of the window
projected_window_height()2887 int projected_window_height()
2888 {
2889     return get_option<int>( "TERMINAL_Y" ) * fontheight;
2890 }
2891 
init_term_size_and_scaling_factor()2892 static void init_term_size_and_scaling_factor()
2893 {
2894     scaling_factor = 1;
2895     point terminal( get_option<int>( "TERMINAL_X" ), get_option<int>( "TERMINAL_Y" ) );
2896 
2897 #if !defined(__ANDROID__)
2898 
2899     if( get_option<std::string>( "SCALING_FACTOR" ) == "2" ) {
2900         scaling_factor = 2;
2901     } else if( get_option<std::string>( "SCALING_FACTOR" ) == "4" ) {
2902         scaling_factor = 4;
2903     }
2904 
2905     if( scaling_factor > 1 ) {
2906 
2907         int max_width, max_height;
2908 
2909         int current_display_id = std::stoi( get_option<std::string>( "DISPLAY" ) );
2910         SDL_DisplayMode current_display;
2911 
2912         if( SDL_GetDesktopDisplayMode( current_display_id, &current_display ) == 0 ) {
2913             if( get_option<std::string>( "FULLSCREEN" ) == "no" ) {
2914 
2915                 // Make a maximized test window to determine maximum windowed size
2916                 SDL_Window_Ptr test_window;
2917                 test_window.reset( SDL_CreateWindow( "test_window",
2918                                                      SDL_WINDOWPOS_CENTERED_DISPLAY( current_display_id ),
2919                                                      SDL_WINDOWPOS_CENTERED_DISPLAY( current_display_id ),
2920                                                      FULL_SCREEN_WIDTH * fontwidth,
2921                                                      FULL_SCREEN_HEIGHT * fontheight,
2922                                                      SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_MAXIMIZED
2923                                                    ) );
2924 
2925                 SDL_GetWindowSize( test_window.get(), &max_width, &max_height );
2926 
2927                 // If the video subsystem isn't reset the test window messes things up later
2928                 test_window.reset();
2929                 SDL_QuitSubSystem( SDL_INIT_VIDEO );
2930                 SDL_InitSubSystem( SDL_INIT_VIDEO );
2931 
2932             } else {
2933                 // For fullscreen or window borderless maximum size is the display size
2934                 max_width = current_display.w;
2935                 max_height = current_display.h;
2936             }
2937         } else {
2938             dbg( D_WARNING ) << "Failed to get current Display Mode, assuming infinite display size.";
2939             max_width = INT_MAX;
2940             max_height = INT_MAX;
2941         }
2942 
2943         if( terminal.x * fontwidth > max_width ||
2944             FULL_SCREEN_WIDTH * fontwidth * scaling_factor > max_width ) {
2945             if( FULL_SCREEN_WIDTH * fontwidth * scaling_factor > max_width ) {
2946                 dbg( D_WARNING ) << "SCALING_FACTOR set too high for display size, resetting to 1";
2947                 scaling_factor = 1;
2948                 terminal.x = max_width / fontwidth;
2949                 terminal.y = max_height / fontheight;
2950                 get_options().get_option( "SCALING_FACTOR" ).setValue( "1" );
2951             } else {
2952                 terminal.x = max_width / fontwidth;
2953             }
2954         }
2955 
2956         if( terminal.y * fontheight > max_height ||
2957             FULL_SCREEN_HEIGHT * fontheight * scaling_factor > max_height ) {
2958             if( FULL_SCREEN_HEIGHT * fontheight * scaling_factor > max_height ) {
2959                 dbg( D_WARNING ) << "SCALING_FACTOR set too high for display size, resetting to 1";
2960                 scaling_factor = 1;
2961                 terminal.x = max_width / fontwidth;
2962                 terminal.y = max_height / fontheight;
2963                 get_options().get_option( "SCALING_FACTOR" ).setValue( "1" );
2964             } else {
2965                 terminal.y = max_height / fontheight;
2966             }
2967         }
2968 
2969         terminal.x -= terminal.x % scaling_factor;
2970         terminal.y -= terminal.y % scaling_factor;
2971 
2972         terminal.x = std::max( FULL_SCREEN_WIDTH * scaling_factor, terminal.x );
2973         terminal.y = std::max( FULL_SCREEN_HEIGHT * scaling_factor, terminal.y );
2974 
2975         get_options().get_option( "TERMINAL_X" ).setValue(
2976             std::max( FULL_SCREEN_WIDTH * scaling_factor, terminal.x ) );
2977         get_options().get_option( "TERMINAL_Y" ).setValue(
2978             std::max( FULL_SCREEN_HEIGHT * scaling_factor, terminal.y ) );
2979 
2980         get_options().save();
2981     }
2982 
2983 #endif //__ANDROID__
2984 
2985     TERMINAL_WIDTH = terminal.x / scaling_factor;
2986     TERMINAL_HEIGHT = terminal.y / scaling_factor;
2987 }
2988 
2989 //Basic Init, create the font, backbuffer, etc
init_interface()2990 void catacurses::init_interface()
2991 {
2992     last_input = input_event();
2993     inputdelay = -1;
2994 
2995     InitSDL();
2996 
2997     get_options().init();
2998     get_options().load();
2999     set_language(); //Prevent translated language strings from causing an error if language not set
3000 
3001     font_loader fl;
3002     fl.load();
3003     fl.fontwidth = get_option<int>( "FONT_WIDTH" );
3004     fl.fontheight = get_option<int>( "FONT_HEIGHT" );
3005     fl.fontsize = get_option<int>( "FONT_SIZE" );
3006     fl.fontblending = get_option<bool>( "FONT_BLENDING" );
3007     fl.map_fontsize = get_option<int>( "MAP_FONT_SIZE" );
3008     fl.map_fontwidth = get_option<int>( "MAP_FONT_WIDTH" );
3009     fl.map_fontheight = get_option<int>( "MAP_FONT_HEIGHT" );
3010     fl.overmap_fontsize = get_option<int>( "OVERMAP_FONT_SIZE" );
3011     fl.overmap_fontwidth = get_option<int>( "OVERMAP_FONT_WIDTH" );
3012     fl.overmap_fontheight = get_option<int>( "OVERMAP_FONT_HEIGHT" );
3013     ::fontwidth = fl.fontwidth;
3014     ::fontheight = fl.fontheight;
3015 
3016     init_term_size_and_scaling_factor();
3017 
3018     WinCreate();
3019 
3020     dbg( D_INFO ) << "Initializing SDL Tiles context";
3021     tilecontext = std::make_unique<cata_tiles>( renderer, geometry );
3022     try {
3023         tilecontext->load_tileset( get_option<std::string>( "TILES" ), true );
3024     } catch( const std::exception &err ) {
3025         dbg( D_ERROR ) << "failed to check for tileset: " << err.what();
3026         // use_tiles is the cached value of the USE_TILES option.
3027         // most (all?) code refers to this to see if cata_tiles should be used.
3028         // Setting it to false disables this from getting used.
3029         use_tiles = false;
3030     }
3031 
3032     color_loader<SDL_Color>().load( windowsPalette );
3033     init_colors();
3034 
3035     // initialize sound set
3036     load_soundset();
3037 
3038     font = std::make_unique<FontFallbackList>( renderer, format, fl.fontwidth, fl.fontheight,
3039             windowsPalette, fl.typeface, fl.fontsize, fl.fontblending );
3040     map_font = std::make_unique<FontFallbackList>( renderer, format, fl.map_fontwidth,
3041                fl.map_fontheight,
3042                windowsPalette, fl.map_typeface, fl.map_fontsize, fl.fontblending );
3043     overmap_font = std::make_unique<FontFallbackList>( renderer, format, fl.overmap_fontwidth,
3044                    fl.overmap_fontheight,
3045                    windowsPalette, fl.overmap_typeface, fl.overmap_fontsize, fl.fontblending );
3046     stdscr = newwin( get_terminal_height(), get_terminal_width(), point_zero );
3047     //newwin calls `new WINDOW`, and that will throw, but not return nullptr.
3048 
3049 #if defined(__ANDROID__)
3050     // Make sure we initialize preview_terminal_width/height to sensible values
3051     preview_terminal_width = TERMINAL_WIDTH * fontwidth;
3052     preview_terminal_height = TERMINAL_HEIGHT * fontheight;
3053 #endif
3054 }
3055 
3056 // This is supposed to be called from init.cpp, and only from there.
load_tileset()3057 void load_tileset()
3058 {
3059     if( !tilecontext || !use_tiles ) {
3060         return;
3061     }
3062     tilecontext->load_tileset( get_option<std::string>( "TILES" ) );
3063     tilecontext->do_tile_loading_report();
3064 }
3065 
3066 //Ends the terminal, destroy everything
endwin()3067 void catacurses::endwin()
3068 {
3069     tilecontext.reset();
3070     font.reset();
3071     map_font.reset();
3072     overmap_font.reset();
3073     WinDestroy();
3074 }
3075 
3076 template<>
from_rgb(const int r,const int g,const int b)3077 SDL_Color color_loader<SDL_Color>::from_rgb( const int r, const int g, const int b )
3078 {
3079     SDL_Color result;
3080     result.b = b;       //Blue
3081     result.g = g;       //Green
3082     result.r = r;       //Red
3083     result.a = 0xFF;    // Opaque
3084     return result;
3085 }
3086 
set_timeout(const int t)3087 void input_manager::set_timeout( const int t )
3088 {
3089     input_timeout = t;
3090     inputdelay = t;
3091 }
3092 
3093 // This is how we're actually going to handle input events, SDL getch
3094 // is simply a wrapper around this.
get_input_event(const keyboard_mode preferred_keyboard_mode)3095 input_event input_manager::get_input_event( const keyboard_mode preferred_keyboard_mode )
3096 {
3097     if( test_mode ) {
3098         // input should be skipped in caller's code
3099         throw std::runtime_error( "input_manager::get_input_event called in test mode" );
3100     }
3101 
3102 #if !defined(__ANDROID__) && !defined(TARGET_OS_IPHONE)
3103     if( actual_keyboard_mode( preferred_keyboard_mode ) == keyboard_mode::keychar ) {
3104         SDL_StartTextInput();
3105     } else {
3106         SDL_StopTextInput();
3107     }
3108 #else
3109     // TODO: support keycode mode if hardware keyboard is connected?
3110     ( void ) preferred_keyboard_mode;
3111 #endif
3112 
3113     previously_pressed_key = 0;
3114     // standards note: getch is sometimes required to call refresh
3115     // see, e.g., http://linux.die.net/man/3/getch
3116     // so although it's non-obvious, that refresh() call (and maybe InvalidateRect?) IS supposed to be there
3117 
3118     wrefresh( catacurses::stdscr );
3119 
3120     if( inputdelay < 0 ) {
3121         do {
3122             CheckMessages();
3123             if( last_input.type != input_event_t::error ) {
3124                 break;
3125             }
3126             SDL_Delay( 1 );
3127         } while( last_input.type == input_event_t::error );
3128     } else if( inputdelay > 0 ) {
3129         uint32_t starttime = SDL_GetTicks();
3130         uint32_t endtime = 0;
3131         bool timedout = false;
3132         do {
3133             CheckMessages();
3134             endtime = SDL_GetTicks();
3135             if( last_input.type != input_event_t::error ) {
3136                 break;
3137             }
3138             SDL_Delay( 1 );
3139             timedout = endtime >= starttime + inputdelay;
3140             if( timedout ) {
3141                 last_input.type = input_event_t::timeout;
3142             }
3143         } while( !timedout );
3144     } else {
3145         CheckMessages();
3146     }
3147 
3148     if( last_input.type == input_event_t::mouse ) {
3149         SDL_GetMouseState( &last_input.mouse_pos.x, &last_input.mouse_pos.y );
3150     } else if( last_input.type == input_event_t::keyboard_char ) {
3151         previously_pressed_key = last_input.get_first_input();
3152 #if defined(__ANDROID__)
3153         android_vibrate();
3154 #endif
3155     }
3156 #if defined(__ANDROID__)
3157     else if( last_input.type == input_event_t::gamepad ) {
3158         android_vibrate();
3159     }
3160 #endif
3161 
3162     return last_input;
3163 }
3164 
gamepad_available()3165 bool gamepad_available()
3166 {
3167     return joystick != nullptr;
3168 }
3169 
rescale_tileset(int size)3170 void rescale_tileset( int size )
3171 {
3172     tilecontext->set_draw_scale( size );
3173     g->mark_main_ui_adaptor_resize();
3174 }
3175 
get_window_dimensions(const catacurses::window & win,const point & pos,const point & size)3176 static window_dimensions get_window_dimensions( const catacurses::window &win,
3177         const point &pos, const point &size )
3178 {
3179     window_dimensions dim;
3180     if( use_tiles && g && win == g->w_terrain ) {
3181         // tiles might have different dimensions than standard font
3182         dim.scaled_font_size.x = tilecontext->get_tile_width();
3183         dim.scaled_font_size.y = tilecontext->get_tile_height();
3184     } else if( map_font && g && win == g->w_terrain ) {
3185         // map font (if any) might differ from standard font
3186         dim.scaled_font_size.x = map_font->width;
3187         dim.scaled_font_size.y = map_font->height;
3188     } else if( overmap_font && g && win == g->w_overmap ) {
3189         dim.scaled_font_size.x = overmap_font->width;
3190         dim.scaled_font_size.y = overmap_font->height;
3191     } else {
3192         dim.scaled_font_size.x = fontwidth;
3193         dim.scaled_font_size.y = fontheight;
3194     }
3195 
3196     // multiplied by the user's specified scaling factor regardless of whether tiles are in use
3197     dim.scaled_font_size *= get_scaling_factor();
3198 
3199     if( win ) {
3200         cata_cursesport::WINDOW *const pwin = win.get<cata_cursesport::WINDOW>();
3201         dim.window_pos_cell = pwin->pos;
3202         dim.window_size_cell.x = pwin->width;
3203         dim.window_size_cell.y = pwin->height;
3204     } else {
3205         dim.window_pos_cell = pos;
3206         dim.window_size_cell = size;
3207     }
3208 
3209     // the window position is *always* in standard font dimensions!
3210     dim.window_pos_pixel = point( dim.window_pos_cell.x * fontwidth,
3211                                   dim.window_pos_cell.y * fontheight );
3212     // But the size of the window is in the font dimensions of the window.
3213     dim.window_size_pixel.x = dim.window_size_cell.x * dim.scaled_font_size.x;
3214     dim.window_size_pixel.y = dim.window_size_cell.y * dim.scaled_font_size.y;
3215 
3216     return dim;
3217 }
3218 
get_window_dimensions(const catacurses::window & win)3219 window_dimensions get_window_dimensions( const catacurses::window &win )
3220 {
3221     return get_window_dimensions( win, point_zero, point_zero );
3222 }
3223 
get_window_dimensions(const point & pos,const point & size)3224 window_dimensions get_window_dimensions( const point &pos, const point &size )
3225 {
3226     return get_window_dimensions( {}, pos, size );
3227 }
3228 
get_coordinates(const catacurses::window & capture_win_)3229 cata::optional<tripoint> input_context::get_coordinates( const catacurses::window &capture_win_ )
3230 {
3231     if( !coordinate_input_received ) {
3232         return cata::nullopt;
3233     }
3234 
3235     const catacurses::window &capture_win = capture_win_ ? capture_win_ : g->w_terrain;
3236     const window_dimensions dim = get_window_dimensions( capture_win );
3237 
3238     const int &fw = dim.scaled_font_size.x;
3239     const int &fh = dim.scaled_font_size.y;
3240     const point &win_min = dim.window_pos_pixel;
3241     const point &win_size = dim.window_size_pixel;
3242     const point win_max = win_min + win_size;
3243 
3244     // Translate mouse coordinates to map coordinates based on tile size
3245     // Check if click is within bounds of the window we care about
3246     const inclusive_rectangle<point> win_bounds( win_min, win_max );
3247     if( !win_bounds.contains( coordinate ) ) {
3248         return cata::nullopt;
3249     }
3250 
3251     point view_offset;
3252     if( capture_win == g->w_terrain ) {
3253         view_offset = g->ter_view_p.xy();
3254     }
3255 
3256     const point screen_pos = coordinate - win_min;
3257     point p;
3258     if( tile_iso && use_tiles ) {
3259         const float win_mid_x = win_min.x + win_size.x / 2.0f;
3260         const float win_mid_y = -win_min.y + win_size.y / 2.0f;
3261         const int screen_col = std::round( ( screen_pos.x - win_mid_x ) / ( fw / 2.0 ) );
3262         const int screen_row = std::round( ( screen_pos.y - win_mid_y ) / ( fw / 4.0 ) );
3263         const point selected( ( screen_col - screen_row ) / 2, ( screen_row + screen_col ) / 2 );
3264         p = view_offset + selected;
3265     } else {
3266         const point selected( screen_pos.x / fw, screen_pos.y / fh );
3267         p = view_offset + selected - dim.window_size_cell / 2;
3268     }
3269 
3270     return tripoint( p, get_map().get_abs_sub().z );
3271 }
3272 
get_terminal_width()3273 int get_terminal_width()
3274 {
3275     return TERMINAL_WIDTH;
3276 }
3277 
get_terminal_height()3278 int get_terminal_height()
3279 {
3280     return TERMINAL_HEIGHT;
3281 }
3282 
get_scaling_factor()3283 int get_scaling_factor()
3284 {
3285     return scaling_factor;
3286 }
3287 
map_font_width()3288 static int map_font_width()
3289 {
3290     if( use_tiles && tilecontext ) {
3291         return tilecontext->get_tile_width();
3292     }
3293     return ( map_font ? map_font.get() : font.get() )->width;
3294 }
3295 
map_font_height()3296 static int map_font_height()
3297 {
3298     if( use_tiles && tilecontext ) {
3299         return tilecontext->get_tile_height();
3300     }
3301     return ( map_font ? map_font.get() : font.get() )->height;
3302 }
3303 
overmap_font_width()3304 static int overmap_font_width()
3305 {
3306     return ( overmap_font ? overmap_font.get() : font.get() )->width;
3307 }
3308 
overmap_font_height()3309 static int overmap_font_height()
3310 {
3311     return ( overmap_font ? overmap_font.get() : font.get() )->height;
3312 }
3313 
to_map_font_dim_width(int & w)3314 void to_map_font_dim_width( int &w )
3315 {
3316     w = ( w * fontwidth ) / map_font_width();
3317 }
3318 
to_map_font_dim_height(int & h)3319 void to_map_font_dim_height( int &h )
3320 {
3321     h = ( h * fontheight ) / map_font_height();
3322 }
3323 
to_map_font_dimension(int & w,int & h)3324 void to_map_font_dimension( int &w, int &h )
3325 {
3326     to_map_font_dim_width( w );
3327     to_map_font_dim_height( h );
3328 }
3329 
from_map_font_dimension(int & w,int & h)3330 void from_map_font_dimension( int &w, int &h )
3331 {
3332     w = ( w * map_font_width() + fontwidth - 1 ) / fontwidth;
3333     h = ( h * map_font_height() + fontheight - 1 ) / fontheight;
3334 }
3335 
to_overmap_font_dimension(int & w,int & h)3336 void to_overmap_font_dimension( int &w, int &h )
3337 {
3338     w = ( w * fontwidth ) / overmap_font_width();
3339     h = ( h * fontheight ) / overmap_font_height();
3340 }
3341 
is_draw_tiles_mode()3342 bool is_draw_tiles_mode()
3343 {
3344     return use_tiles;
3345 }
3346 
3347 /** Saves a screenshot of the current viewport, as a PNG file, to the given location.
3348 * @param file_path: A full path to the file where the screenshot should be saved.
3349 * @returns `true` if the screenshot generation was successful, `false` otherwise.
3350 */
save_screenshot(const std::string & file_path)3351 bool save_screenshot( const std::string &file_path )
3352 {
3353     // Note: the viewport is returned by SDL and we don't have to manage its lifetime.
3354     SDL_Rect viewport;
3355 
3356     // Get the viewport size (width and height of the screen)
3357     SDL_RenderGetViewport( renderer.get(), &viewport );
3358 
3359     // Create SDL_Surface with depth of 32 bits (note: using zeros for the RGB masks sets a default value, based on the depth; Alpha mask will be 0).
3360     SDL_Surface_Ptr surface = CreateRGBSurface( 0, viewport.w, viewport.h, 32, 0, 0, 0, 0 );
3361 
3362     // Get data from SDL_Renderer and save them into surface
3363     if( printErrorIf( SDL_RenderReadPixels( renderer.get(), nullptr, surface->format->format,
3364                                             surface->pixels, surface->pitch ) != 0,
3365                       "save_screenshot: cannot read data from SDL_Renderer." ) ) {
3366         return false;
3367     }
3368 
3369     // Save screenshot as PNG file
3370     if( printErrorIf( IMG_SavePNG( surface.get(), file_path.c_str() ) != 0,
3371                       std::string( "save_screenshot: cannot save screenshot file: " + file_path ).c_str() ) ) {
3372         return false;
3373     }
3374 
3375     return true;
3376 }
3377 
3378 #ifdef _WIN32
getWindowHandle()3379 HWND getWindowHandle()
3380 {
3381     SDL_SysWMinfo info;
3382     SDL_VERSION( &info.version );
3383     SDL_GetWindowWMInfo( ::window.get(), &info );
3384     return info.info.win.window;
3385 }
3386 #endif
3387 
3388 #endif // TILES
3389 
window_contains_point_relative(const catacurses::window & win,const point & p)3390 bool window_contains_point_relative( const catacurses::window &win, const point &p )
3391 {
3392     const point bound = point( catacurses::getmaxx( win ), catacurses::getmaxy( win ) );
3393     const half_open_rectangle<point> win_bounds( point_zero, bound );
3394     return win_bounds.contains( p );
3395 }
3396