1 /**
2 * @file
3 * @brief Functions that may be missing from some systems
4 **/
5
6 #include "AppHdr.h"
7
8 #include "libutil.h"
9
10 #include <cctype>
11 #include <cstdarg>
12 #include <cstdio>
13 #include <cstring>
14 #include <sstream>
15
16 #include "colour.h"
17 #include "files.h"
18 #include "message.h"
19 #include "state.h"
20 #include "stringutil.h"
21 #include "tiles-build-specific.h"
22 #include "unicode.h"
23 #include "viewgeom.h"
24
25 #ifdef TARGET_OS_WINDOWS
26 #undef ARRAYSZ
27 #include <windows.h>
28 #undef max
29
30 #ifdef WINMM_PLAY_SOUNDS
31 #include <mmsystem.h>
32 #endif
33 #endif
34
35 #ifdef UNIX
36 #include <csignal>
37 #endif
38
39 #ifdef DGL_ENABLE_CORE_DUMP
40 #include <sys/time.h>
41 #include <sys/resource.h>
42 #endif
43
isqrt(unsigned int a)44 unsigned int isqrt(unsigned int a)
45 {
46 unsigned int rem = 0, root = 0;
47 for (int i = 0; i < 16; i++)
48 {
49 root <<= 1;
50 rem = (rem << 2) + (a >> 30);
51 a <<= 2;
52 if (++root <= rem)
53 rem -= root++;
54 else
55 root--;
56 }
57 return root >> 1;
58 }
59
isqrt_ceil(int x)60 int isqrt_ceil(int x)
61 {
62 if (x <= 0)
63 return 0;
64 return isqrt(x - 1) + 1;
65 }
66
description_type_by_name(const char * desc)67 description_level_type description_type_by_name(const char *desc)
68 {
69 if (!desc)
70 return DESC_PLAIN;
71
72 if (!strcmp("The", desc) || !strcmp("the", desc))
73 return DESC_THE;
74 else if (!strcmp("A", desc) || !strcmp("a", desc))
75 return DESC_A;
76 else if (!strcmp("Your", desc) || !strcmp("your", desc))
77 return DESC_YOUR;
78 else if (!strcmp("its", desc))
79 return DESC_ITS;
80 else if (!strcmp("worn", desc))
81 return DESC_INVENTORY_EQUIP;
82 else if (!strcmp("inv", desc))
83 return DESC_INVENTORY;
84 else if (!strcmp("none", desc))
85 return DESC_NONE;
86 else if (!strcmp("base", desc))
87 return DESC_BASENAME;
88 else if (!strcmp("qual", desc))
89 return DESC_QUALNAME;
90
91 return DESC_PLAIN;
92 }
93
94 // Should return true if the filename contains nothing that
95 // the shell can do damage with.
shell_safe(const char * file)96 bool shell_safe(const char *file)
97 {
98 int match = strcspn(file, "\\`$*?|><&\n!;");
99 return match < 0 || !file[match];
100 }
101
key_is_escape(int key)102 bool key_is_escape(int key)
103 {
104 switch (key)
105 {
106 CASE_ESCAPE
107 return true;
108 default:
109 return false;
110 }
111 }
112
113 // Returns true if s contains tag 'tag', and strips out tag from s.
strip_tag(string & s,const string & tag,bool skip_padding)114 bool strip_tag(string &s, const string &tag, bool skip_padding)
115 {
116 if (s == tag)
117 {
118 s.clear();
119 return true;
120 }
121
122 string::size_type pos;
123
124 if (skip_padding)
125 {
126 if ((pos = s.find(tag)) != string::npos)
127 {
128 s.erase(pos, tag.length());
129 trim_string(s);
130 return true;
131 }
132 return false;
133 }
134
135 if ((pos = s.find(" " + tag + " ")) != string::npos)
136 {
137 // Leave one space intact.
138 s.erase(pos, tag.length() + 1);
139 trim_string(s);
140 return true;
141 }
142
143 if ((pos = s.find(tag + " ")) == 0
144 || ((pos = s.find(" " + tag)) != string::npos
145 && pos + tag.length() + 1 == s.length()))
146 {
147 s.erase(pos, tag.length() + 1);
148 trim_string(s);
149 return true;
150 }
151
152 return false;
153 }
154
strip_multiple_tag_prefix(string & s,const string & tagprefix)155 vector<string> strip_multiple_tag_prefix(string &s, const string &tagprefix)
156 {
157 vector<string> results;
158
159 while (true)
160 {
161 string this_result = strip_tag_prefix(s, tagprefix);
162 if (this_result.empty())
163 break;
164
165 results.push_back(this_result);
166 }
167
168 return results;
169 }
170
strip_tag_prefix(string & s,const string & tagprefix)171 string strip_tag_prefix(string &s, const string &tagprefix)
172 {
173 string::size_type pos = s.find(tagprefix);
174
175 while (pos && pos != string::npos && !isspace(s[pos - 1]))
176 pos = s.find(tagprefix, pos + 1);
177
178 if (pos == string::npos)
179 return "";
180
181 string::size_type ns = s.find(" ", pos);
182 if (ns == string::npos)
183 ns = s.length();
184
185 const string argument = s.substr(pos + tagprefix.length(),
186 ns - pos - tagprefix.length());
187
188 s.erase(pos, ns - pos + 1);
189 trim_string(s);
190
191 return argument;
192 }
193
tag_without_prefix(const string & s,const string & tagprefix)194 const string tag_without_prefix(const string &s, const string &tagprefix)
195 {
196 string::size_type pos = s.find(tagprefix);
197
198 while (pos && pos != string::npos && !isspace(s[pos - 1]))
199 pos = s.find(tagprefix, pos + 1);
200
201 if (pos == string::npos)
202 return "";
203
204 string::size_type ns = s.find(" ", pos);
205 if (ns == string::npos)
206 ns = s.length();
207
208 return s.substr(pos + tagprefix.length(), ns - pos - tagprefix.length());
209 }
210
strip_number_tag(string & s,const string & tagprefix)211 int strip_number_tag(string &s, const string &tagprefix)
212 {
213 const string num = strip_tag_prefix(s, tagprefix);
214 int x;
215 if (num.empty() || !parse_int(num.c_str(), x))
216 return TAG_UNFOUND;
217 return x;
218 }
219
parse_tags(const string & tags)220 unordered_set<string> parse_tags(const string &tags)
221 {
222 vector<string> split_tags = split_string(" ", tags);
223 unordered_set<string> tags_set(split_tags.begin(), split_tags.end());
224 return tags_set;
225 }
226
parse_int(const char * s,int & i)227 bool parse_int(const char *s, int &i)
228 {
229 if (!s || !*s)
230 return false;
231 char *err;
232 long x = strtol(s, &err, 10);
233 if (*err || x < INT_MIN || x > INT_MAX)
234 return false;
235 i = x;
236 return true;
237 }
238
239 /**
240 * Compare two strings, sorting integer numeric parts according to their value.
241 *
242 * "foo123bar" > "foo99bar"
243 * "0.10" > "0.9" (version sort)
244 *
245 * @param a String one.
246 * @param b String two.
247 * @param limit If passed, comparison ends after X numeric parts.
248 * @return As in strcmp().
249 **/
numcmp(const char * a,const char * b,int limit)250 int numcmp(const char *a, const char *b, int limit)
251 {
252 int res;
253
254 do
255 {
256 while (*a && *a == *b && !isadigit(*a))
257 {
258 a++;
259 b++;
260 }
261 if (!isadigit(*a) || !isadigit(*b))
262 return (*a < *b) ? -1 : (*a > *b) ? 1 : 0;
263 while (*a == '0')
264 a++;
265 while (*b == '0')
266 b++;
267 res = 0;
268 while (isadigit(*a))
269 {
270 if (!isadigit(*b))
271 return 1;
272 if (*a != *b && !res)
273 res = (*a < *b) ? -1 : 1;
274 a++;
275 b++;
276 }
277 if (isadigit(*b))
278 return -1;
279 if (res)
280 return res;
281 } while (--limit > 0);
282 return 0;
283 }
284
285 // make STL sort happy
numcmpstr(const string & a,const string & b)286 bool numcmpstr(const string& a, const string& b)
287 {
288 return numcmp(a.c_str(), b.c_str()) == -1;
289 }
290
version_is_stable(const char * v)291 bool version_is_stable(const char *v)
292 {
293 // vulnerable to changes in the versioning scheme
294 for (;; v++)
295 {
296 if (*v == '.' || isadigit(*v))
297 continue;
298 if (*v == '-')
299 return isadigit(v[1]);
300 return true;
301 }
302 }
303
_untag(string & s,const string & pre,const string & post,bool onoff)304 static void inline _untag(string &s, const string &pre,
305 const string &post, bool onoff)
306 {
307 size_t p = 0;
308 while ((p = s.find(pre, p)) != string::npos)
309 {
310 size_t q = s.find(post, p);
311 if (q == string::npos)
312 q = s.length();
313 if (onoff)
314 {
315 s.erase(q, post.length());
316 s.erase(p, pre.length());
317 }
318 else
319 s.erase(p, q - p + post.length());
320 }
321 }
322
untag_tiles_console(string s)323 string untag_tiles_console(string s)
324 {
325 _untag(s, "<tiles>", "</tiles>", is_tiles());
326 _untag(s, "<console>", "</console>", !is_tiles());
327 #ifdef USE_TILE_WEB
328 _untag(s, "<webtiles>", "</webtiles>", true);
329 #else
330 _untag(s, "<webtiles>", "</webtiles>", false);
331 #endif
332 #ifdef USE_TILE_LOCAL
333 _untag(s, "<localtiles>", "</localtiles>", true);
334 _untag(s, "<nomouse>", "</nomouse>", false);
335 #else
336 _untag(s, "<localtiles>", "</localtiles>", false);
337 _untag(s, "<nomouse>", "</nomouse>", true);
338 #endif
339 return s;
340 }
341
colour_string(string in,int col)342 string colour_string(string in, int col)
343 {
344 if (in.empty())
345 return in;
346 const string cols = colour_to_str(col);
347 return "<" + cols + ">" + in + "</" + cols + ">";
348 }
349
350
351
352 #ifndef USE_TILE_LOCAL
_cgettopleft(GotoRegion region)353 static coord_def _cgettopleft(GotoRegion region)
354 {
355 switch (region)
356 {
357 case GOTO_MLIST:
358 return crawl_view.mlistp;
359 case GOTO_STAT:
360 return crawl_view.hudp;
361 case GOTO_MSG:
362 return crawl_view.msgp;
363 case GOTO_DNGN:
364 return crawl_view.viewp;
365 case GOTO_CRT:
366 default:
367 return crawl_view.termp;
368 }
369 }
370
cgetpos(GotoRegion region)371 coord_def cgetpos(GotoRegion region)
372 {
373 const coord_def where = coord_def(wherex(), wherey());
374 return where - _cgettopleft(region) + coord_def(1, 1);
375 }
376
valid_cursor_pos(int x,int y,GotoRegion region)377 bool valid_cursor_pos(int x, int y, GotoRegion region)
378 {
379 const coord_def sz = cgetsize(region);
380 return x >= 1 && y >= 1 && x <= sz.x && y <= sz.y;
381 }
382
383 #ifndef TARGET_OS_WINDOWS
_find_correct_region()384 static GotoRegion _find_correct_region()
385 {
386 for (int r = GOTO_MSG; r < GOTO_MLIST; r++)
387 {
388 GotoRegion region(static_cast<GotoRegion>(r));
389 coord_def pos(cgetpos(region));
390 if (valid_cursor_pos(pos.x, pos.y, region))
391 return region;
392 }
393 // everything on screen is valid in GOTO_CRT; though in principle this
394 // should probably fail for entirely invalid coords somehow
395 return GOTO_CRT;
396 }
397 #endif
398
assert_valid_cursor_pos()399 void assert_valid_cursor_pos()
400 {
401 #ifndef TARGET_OS_WINDOWS
402 GotoRegion region(get_cursor_region());
403 coord_def pos(cgetpos(region));
404 ASSERTM(valid_cursor_pos(pos.x, pos.y, region),
405 "invalid cursor position %d,%d in region %d, should be %d,%d in region %d",
406 pos.x, pos.y, region, cgetpos(_find_correct_region()).x,
407 cgetpos(_find_correct_region()).y, _find_correct_region());
408 #endif
409 }
410
411 static GotoRegion _current_region = GOTO_CRT;
412
cgotoxy(int x,int y,GotoRegion region)413 void cgotoxy(int x, int y, GotoRegion region)
414 {
415 #if defined(ASSERTS) && !defined(TARGET_OS_WINDOWS)
416 if (!valid_cursor_pos(x, y, region))
417 {
418 const coord_def sz = cgetsize(region);
419 // dprf in case it's not safe
420 dprf("screen write out of bounds in region %d (old: %d): (%d,%d) into (%d,%d)",
421 (int) region, (int) _current_region, x, y, sz.x, sz.y);
422 if (you.on_current_level)
423 save_game(false); // should be safe (lol)
424 die( "screen write out of bounds in region %d (old: %d): (%d,%d) into (%d,%d)",
425 (int) region, (int) _current_region, x, y, sz.x, sz.y);
426 }
427 #endif
428
429 const coord_def tl = _cgettopleft(region);
430 _current_region = region;
431 gotoxy_sys(tl.x + x - 1, tl.y + y - 1);
432
433 #ifdef USE_TILE_WEB
434 tiles.cgotoxy(x, y, region);
435 #endif
436 }
437
get_cursor_region()438 GotoRegion get_cursor_region()
439 {
440 return _current_region;
441 }
442
set_cursor_region(GotoRegion region)443 void set_cursor_region(GotoRegion region)
444 {
445 _current_region = region;
446 }
447
clrscr()448 void clrscr()
449 {
450 save_cursor_pos save; // is this generally correct? alternative, set region
451 // to GOTO_CRT.
452 clrscr_sys();
453
454 #ifdef USE_TILE_WEB
455 tiles.clrscr();
456 #endif
457 }
458
459 #endif // !USE_TILE_LOCAL
460
cgetsize(GotoRegion region)461 coord_def cgetsize(GotoRegion region)
462 {
463 switch (region)
464 {
465 case GOTO_MLIST:
466 return crawl_view.mlistsz;
467 case GOTO_STAT:
468 return crawl_view.hudsz;
469 case GOTO_MSG:
470 return crawl_view.msgsz;
471 case GOTO_DNGN:
472 return crawl_view.viewsz;
473 case GOTO_CRT:
474 default:
475 return crawl_view.termsz;
476 }
477 }
478
cscroll(int n,GotoRegion region)479 void cscroll(int n, GotoRegion region)
480 {
481 // only implemented for the message window right now
482 if (region == GOTO_MSG)
483 scroll_message_window(n);
484 }
485
486 mouse_mode mouse_control::ms_current_mode = MOUSE_MODE_NORMAL;
487
mouse_control(mouse_mode mode)488 mouse_control::mouse_control(mouse_mode mode)
489 {
490 #ifdef USE_TILE_WEB
491 // relies on being called before ms_current_mode is updated
492 tiles.update_input_mode(mode);
493 #endif
494
495 m_previous_mode = ms_current_mode;
496 ms_current_mode = mode;
497 }
498
~mouse_control()499 mouse_control::~mouse_control()
500 {
501 #ifdef USE_TILE_WEB
502 tiles.update_input_mode(m_previous_mode);
503 #endif
504 ms_current_mode = m_previous_mode;
505 }
506
unwrap_desc(string && desc)507 string unwrap_desc(string&& desc)
508 {
509 // Don't append a newline to an empty description.
510 if (desc.empty())
511 return "";
512
513 trim_string_right(desc);
514
515 // Lines beginning with a colon are tags with a full entry scope
516 // Only nowrap is supported for now
517 while (desc[0] == ':')
518 {
519 int pos = desc.find("\n");
520 string tag = desc.substr(1, pos - 1);
521 desc.erase(0, pos + 1);
522 if (tag == "nowrap")
523 return move(desc);
524 else if (desc.empty())
525 return "";
526 }
527
528 // An empty line separates paragraphs.
529 desc = replace_all(desc, "\n\n", "\\n\\n");
530 // Indented lines are pre-formatted.
531 desc = replace_all(desc, "\n ", "\\n ");
532
533 // Don't add whitespaces between tags
534 desc = replace_all(desc, ">\n<", "><");
535 // Newlines are still whitespace.
536 desc = replace_all(desc, "\n", " ");
537 // Can force a newline with a literal "\n".
538 desc = replace_all(desc, "\\n", "\n");
539
540 return desc + "\n";
541 }
542
543 #ifdef TARGET_OS_WINDOWS
544 // FIXME: This function should detect if aero is running, but the DwmIsCompositionEnabled
545 // function isn't included in msys, so I don't know how to do that. Instead, I just check
546 // if we are running vista or higher. -rla
_is_aero()547 static bool _is_aero()
548 {
549 OSVERSIONINFOEX osvi;
550 osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
551 if (GetVersionEx((OSVERSIONINFO *) &osvi))
552 return osvi.dwMajorVersion >= 6;
553 else
554 return false;
555 }
556
get_taskbar_pos()557 taskbar_pos get_taskbar_pos()
558 {
559 RECT rect;
560 HWND taskbar = FindWindow("Shell_traywnd", nullptr);
561 if (taskbar && GetWindowRect(taskbar, &rect))
562 {
563 if (rect.right - rect.left > rect.bottom - rect.top)
564 {
565 if (rect.top > 0)
566 return TASKBAR_BOTTOM;
567 else
568 return TASKBAR_TOP;
569 }
570 else
571 {
572 if (rect.left > 0)
573 return TASKBAR_RIGHT;
574 else
575 return TASKBAR_LEFT;
576 }
577 }
578 return TASKBAR_NO;
579 }
580
get_taskbar_size()581 int get_taskbar_size()
582 {
583 RECT rect;
584 int size;
585 taskbar_pos tpos = get_taskbar_pos();
586 HWND taskbar = FindWindow("Shell_traywnd", nullptr);
587
588 if (taskbar && GetWindowRect(taskbar, &rect))
589 {
590 if (tpos & TASKBAR_H)
591 size = rect.bottom - rect.top;
592 else if (tpos & TASKBAR_V)
593 size = rect.right - rect.left;
594 else
595 return 0;
596
597 if (_is_aero())
598 size += 3; // Taskbar behave strangely when aero is active.
599
600 return size;
601 }
602 return 0;
603 }
604
console_handler(DWORD sig)605 static BOOL WINAPI console_handler(DWORD sig)
606 {
607 switch (sig)
608 {
609 case CTRL_C_EVENT:
610 case CTRL_BREAK_EVENT:
611 return true; // block the signal
612 default:
613 return false;
614 case CTRL_CLOSE_EVENT:
615 case CTRL_LOGOFF_EVENT:
616 case CTRL_SHUTDOWN_EVENT:
617 if (crawl_state.seen_hups++)
618 return true; // abort immediately
619
620 #ifndef USE_TILE_LOCAL
621 // Should never happen in tiles -- if it does (Cygwin?), this will
622 // kill the game without saving.
623 w32_insert_escape();
624
625 Sleep(15000); // allow 15 seconds for shutdown, then kill -9
626 #endif
627 return true;
628 }
629 }
630
init_signals()631 void init_signals()
632 {
633 // If there's no console, this will return an error, which we ignore.
634 // For GUI programs there's no controlling terminal, but there might be
635 // one on Cygwin.
636 SetConsoleCtrlHandler(console_handler, true);
637 }
638
release_cli_signals()639 void release_cli_signals()
640 {
641 SetConsoleCtrlHandler(nullptr, false);
642 }
643
text_popup(const string & text,const wchar_t * caption)644 void text_popup(const string& text, const wchar_t *caption)
645 {
646 MessageBoxW(0, OUTW(text), caption, MB_OK);
647 }
648 #else
649 #ifndef USE_TILE_LOCAL // is curses in use?
650
651 /* [ds] This SIGHUP handling is primitive and far from safe, but it
652 * should be better than nothing. Feel free to get rigorous on this.
653 */
handle_hangup(int)654 static void handle_hangup(int)
655 {
656 if (crawl_state.seen_hups++)
657 return;
658
659 // When using Curses, closing stdin will cause any Curses call blocking
660 // on key-presses to immediately return, including any call that was
661 // still blocking in the main thread when the HUP signal was caught.
662 // This should guarantee that the main thread will un-stall and
663 // will eventually return to _input() in main.cc, which will then
664 // gracefully save the game.
665
666 // SAVE CORRUPTING BUG!!! We're in a signal handler, calling free()
667 // when closing the FILE object is likely to lead to lock-ups, and even
668 // if it were a plain kernel-side descriptor, calling functions such
669 // as select() or read() is undefined behaviour.
670 fclose(stdin);
671 }
672 # endif
673
init_signals()674 void init_signals()
675 {
676 #ifdef DGAMELAUNCH
677 // Force timezone to UTC.
678 setenv("TZ", "", 1);
679 tzset();
680 #endif
681
682 #ifdef USE_UNIX_SIGNALS
683 #ifdef SIGQUIT
684 signal(SIGQUIT, SIG_IGN);
685 #endif
686
687 #ifdef SIGINT
688 signal(SIGINT, SIG_IGN);
689 #endif
690
691 # ifdef USE_TILE_LOCAL
692 // Losing the controlling terminal doesn't matter, we continue and will
693 // shut down only when the actual window is closed.
694 signal(SIGHUP, SIG_IGN);
695 # else
696 signal(SIGHUP, handle_hangup);
697 # endif
698 #endif
699
700 #ifdef DGL_ENABLE_CORE_DUMP
701 rlimit lim;
702 if (!getrlimit(RLIMIT_CORE, &lim))
703 {
704 lim.rlim_cur = RLIM_INFINITY;
705 setrlimit(RLIMIT_CORE, &lim);
706 }
707 #endif
708 }
709
release_cli_signals()710 void release_cli_signals()
711 {
712 #ifdef USE_UNIX_SIGNALS
713 signal(SIGQUIT, SIG_DFL);
714 signal(SIGINT, SIG_DFL);
715 #endif
716 }
717
718 #endif
719