1 /**
2  * @file
3  * @brief Simple reading of an init file and system variables
4  * @detailed read_init_file is the main function, but read_option_line does
5  * most of the work though. Read through read_init_file to get an overview of
6  * how Crawl loads options. This file also contains a large number of utility
7  * functions for setting particular options and converting between human
8  * readable strings and internal values. (E.g. str_to_enemy_hp_colour,
9  * _weapon_to_str). There is also some code dealing with sorting menus.
10 **/
11 
12 #include "AppHdr.h"
13 
14 #include "initfile.h"
15 
16 #include "json.h"
17 #include "json-wrapper.h"
18 
19 #include <algorithm>
20 #include <cinttypes>
21 #include <cctype>
22 #include <cstdio>
23 #include <cstdlib>
24 #include <iomanip>
25 #include <set>
26 #include <string>
27 
28 #include "ability.h"
29 #include "branch-data-json.h"
30 #include "chardump.h"
31 #include "clua.h"
32 #include "colour.h"
33 #include "defines.h"
34 #include "delay.h"
35 #include "describe.h"
36 #include "directn.h"
37 #include "dlua.h"
38 #include "end.h"
39 #include "errors.h"
40 #include "files.h"
41 #include "game-options.h"
42 #include "ghost.h"
43 #include "invent.h"
44 #include "item-prop.h"
45 #include "items.h"
46 #include "jobs.h"
47 #include "kills.h"
48 #include "libutil.h"
49 #include "macro.h"
50 #include "mapdef.h"
51 #include "message.h"
52 #include "mon-util.h"
53 #include "monster.h"
54 #include "newgame.h"
55 #include "options.h"
56 #include "playable.h"
57 #include "player.h"
58 #include "prompt.h"
59 #include "slot-select-mode.h"
60 #include "species.h"
61 #include "spl-util.h"
62 #include "stash.h"
63 #include "state.h"
64 #include "stringutil.h"
65 #include "syscalls.h"
66 #include "tags.h"
67 #include "tag-version.h"
68 #include "throw.h"
69 #include "travel.h"
70 #include "unwind.h"
71 #include "version.h"
72 #include "viewchar.h"
73 #include "view.h"
74 #include "wizard-option-type.h"
75 #ifdef USE_TILE
76 #include "tilepick.h"
77 #include "rltiles/tiledef-player.h"
78 #endif
79 #include "tiles-build-specific.h"
80 
81 
82 
83 // For finding the executable's path
84 #ifdef TARGET_OS_WINDOWS
85 #define WIN32_LEAN_AND_MEAN
86 #include <windows.h>
87 #include <shlwapi.h>
88 #include <shlobj.h>
89 #elif defined(TARGET_OS_MACOSX)
90 extern char **NXArgv;
91 #ifndef DATA_DIR_PATH
92 #include <unistd.h>
93 #endif
94 #elif defined(UNIX) || defined(TARGET_COMPILER_MINGW)
95 #include <unistd.h>
96 #endif
97 
98 const string game_options::interrupt_prefix = "interrupt_";
99 system_environment SysEnv;
100 
101 // TODO:
102 // because reset_options is called in the constructor, it's a magnet for
103 // static initialization order issues.wrap this in a function per
104 // https://isocpp.org/wiki/faq/ctors#construct-on-first-use-v2
105 game_options Options;
106 
107 static string _get_save_path(string subdir);
108 static string _supported_language_listing();
109 
_first_less(const pair<int,int> & l,const pair<int,int> & r)110 static bool _first_less(const pair<int, int> &l, const pair<int, int> &r)
111 {
112     return l.first < r.first;
113 }
114 
_first_greater(const pair<int,int> & l,const pair<int,int> & r)115 static bool _first_greater(const pair<int, int> &l, const pair<int, int> &r)
116 {
117     return l.first > r.first;
118 }
119 
build_options_list()120 const vector<GameOption*> game_options::build_options_list()
121 {
122 #ifndef DEBUG
123     const bool USING_TOUCH =
124 #if defined(TOUCH_UI)
125         true;
126 #else
127         false;
128 #endif
129 #endif
130     const bool USING_DGL =
131 #if defined(DGAMELAUNCH)
132         true;
133 #else
134         false;
135 #endif
136     const bool USING_UNIX =
137 #if defined(UNIX)
138         true;
139 #else
140         false;
141 #endif
142 
143 #ifdef USE_TILE
144     const bool USING_WEB_TILES =
145 #if defined(USE_TILE_WEB)
146         true;
147 #else
148         false;
149 #endif
150 #endif
151 
152     #define SIMPLE_NAME(_opt) _opt, {#_opt}
153     vector<GameOption*> options = {
154         new BoolGameOption(SIMPLE_NAME(autopickup_starting_ammo), true),
155         new BoolGameOption(SIMPLE_NAME(easy_door), true),
156         new BoolGameOption(SIMPLE_NAME(default_show_all_skills), false),
157         new BoolGameOption(SIMPLE_NAME(read_persist_options), false),
158         new BoolGameOption(SIMPLE_NAME(auto_switch), false),
159         new BoolGameOption(SIMPLE_NAME(suppress_startup_errors), false),
160         new BoolGameOption(SIMPLE_NAME(simple_targeting), false),
161         new BoolGameOption(easy_quit_item_prompts,
162                            { "easy_quit_item_prompts", "easy_quit_item_lists" },
163                            true),
164         new BoolGameOption(easy_unequip,
165                            { "easy_unequip", "easy_armour", "easy_armor" },
166                            true),
167         new BoolGameOption(SIMPLE_NAME(equip_unequip), false),
168         new BoolGameOption(SIMPLE_NAME(jewellery_prompt), false),
169         new BoolGameOption(SIMPLE_NAME(easy_door), true),
170         new BoolGameOption(SIMPLE_NAME(warn_hatches), false),
171         new BoolGameOption(SIMPLE_NAME(enable_recast_spell), true),
172         new BoolGameOption(SIMPLE_NAME(auto_hide_spells), false),
173         new BoolGameOption(SIMPLE_NAME(blink_brightens_background), false),
174         new BoolGameOption(SIMPLE_NAME(bold_brightens_foreground), false),
175         new BoolGameOption(SIMPLE_NAME(best_effort_brighten_background), false),
176         new BoolGameOption(SIMPLE_NAME(best_effort_brighten_foreground), true),
177         new BoolGameOption(SIMPLE_NAME(allow_extended_colours), true),
178         new BoolGameOption(SIMPLE_NAME(regex_search), false),
179         new BoolGameOption(SIMPLE_NAME(autopickup_search), false),
180         new BoolGameOption(SIMPLE_NAME(show_newturn_mark), true),
181         new BoolGameOption(SIMPLE_NAME(show_game_time), true),
182         new BoolGameOption(SIMPLE_NAME(equip_bar), false),
183         new BoolGameOption(SIMPLE_NAME(animate_equip_bar), false),
184         new BoolGameOption(SIMPLE_NAME(mouse_input), false),
185         new BoolGameOption(SIMPLE_NAME(mlist_allow_alternate_layout), false),
186         new BoolGameOption(SIMPLE_NAME(monster_item_view_coordinates), false),
187         new ListGameOption<text_pattern>(SIMPLE_NAME(monster_item_view_features)),
188         new BoolGameOption(SIMPLE_NAME(messages_at_top), false),
189         new BoolGameOption(SIMPLE_NAME(msg_condense_repeats), true),
190         new BoolGameOption(SIMPLE_NAME(msg_condense_short), true),
191         new BoolGameOption(SIMPLE_NAME(view_lock_x), true),
192         new BoolGameOption(SIMPLE_NAME(view_lock_y), true),
193         new BoolGameOption(SIMPLE_NAME(centre_on_scroll), false),
194         new BoolGameOption(SIMPLE_NAME(symmetric_scroll), true),
195         new BoolGameOption(SIMPLE_NAME(always_show_exclusions), true),
196         new BoolGameOption(SIMPLE_NAME(note_all_skill_levels), false),
197         new BoolGameOption(SIMPLE_NAME(note_skill_max), true),
198         new BoolGameOption(SIMPLE_NAME(note_xom_effects), true),
199         new BoolGameOption(SIMPLE_NAME(note_chat_messages), false),
200         new BoolGameOption(SIMPLE_NAME(note_dgl_messages), true),
201         new BoolGameOption(SIMPLE_NAME(clear_messages), false),
202 #ifdef DEBUG
203         new BoolGameOption(SIMPLE_NAME(show_more), false),
204 #else
205         new BoolGameOption(SIMPLE_NAME(show_more), !USING_TOUCH),
206 #endif
207         new BoolGameOption(SIMPLE_NAME(small_more), false),
208         new BoolGameOption(SIMPLE_NAME(pickup_thrown), true),
209         new BoolGameOption(SIMPLE_NAME(show_travel_trail), USING_DGL),
210         new BoolGameOption(SIMPLE_NAME(use_fake_cursor), USING_UNIX ),
211         new BoolGameOption(SIMPLE_NAME(use_fake_player_cursor), true),
212         new BoolGameOption(SIMPLE_NAME(show_player_species), false),
213         new BoolGameOption(SIMPLE_NAME(use_modifier_prefix_keys), true),
214         new BoolGameOption(SIMPLE_NAME(ability_menu), true),
215         new BoolGameOption(SIMPLE_NAME(easy_floor_use), true),
216         new BoolGameOption(SIMPLE_NAME(bad_item_prompt), true),
217         new BoolGameOption(SIMPLE_NAME(dos_use_background_intensity), true),
218         new BoolGameOption(SIMPLE_NAME(explore_greedy), true),
219         new BoolGameOption(SIMPLE_NAME(explore_auto_rest), true),
220         new BoolGameOption(SIMPLE_NAME(travel_key_stop), true),
221         new BoolGameOption(SIMPLE_NAME(travel_one_unsafe_move), false),
222         new BoolGameOption(SIMPLE_NAME(dump_on_save), true),
223         new BoolGameOption(SIMPLE_NAME(rest_wait_both), false),
224         new BoolGameOption(SIMPLE_NAME(rest_wait_ancestor), false),
225         new BoolGameOption(SIMPLE_NAME(cloud_status), !is_tiles()),
226         new BoolGameOption(SIMPLE_NAME(always_show_zot), false),
227         new BoolGameOption(SIMPLE_NAME(darken_beyond_range), true),
228         new BoolGameOption(SIMPLE_NAME(arena_dump_msgs), false),
229         new BoolGameOption(SIMPLE_NAME(arena_dump_msgs_all), false),
230         new BoolGameOption(SIMPLE_NAME(arena_list_eq), false),
231         new BoolGameOption(SIMPLE_NAME(default_manual_training), false),
232         new BoolGameOption(SIMPLE_NAME(one_SDL_sound_channel), false),
233         new BoolGameOption(SIMPLE_NAME(sounds_on), true),
234         new BoolGameOption(SIMPLE_NAME(launcher_autoquiver), true),
235         new ColourGameOption(SIMPLE_NAME(tc_reachable), BLUE),
236         new ColourGameOption(SIMPLE_NAME(tc_excluded), LIGHTMAGENTA),
237         new ColourGameOption(SIMPLE_NAME(tc_exclude_circle), RED),
238         new ColourGameOption(SIMPLE_NAME(tc_forbidden), LIGHTCYAN),
239         new ColourGameOption(SIMPLE_NAME(tc_dangerous), CYAN),
240         new ColourGameOption(SIMPLE_NAME(tc_disconnected), DARKGREY),
241         // [ds] Default to jazzy colours.
242         new ColourGameOption(SIMPLE_NAME(detected_item_colour), GREEN),
243         new ColourGameOption(SIMPLE_NAME(detected_monster_colour), LIGHTRED),
244         new ColourGameOption(SIMPLE_NAME(remembered_monster_colour), DARKGREY),
245         new ColourGameOption(SIMPLE_NAME(status_caption_colour), BROWN),
246         new ColourGameOption(SIMPLE_NAME(background_colour), BLACK),
247         new ColourGameOption(SIMPLE_NAME(foreground_colour), LIGHTGREY),
248         new CursesGameOption(SIMPLE_NAME(friend_brand),
249                              CHATTR_HILITE | (GREEN << 8)),
250         new CursesGameOption(SIMPLE_NAME(neutral_brand),
251                              CHATTR_HILITE | (LIGHTGREY << 8)),
252         new CursesGameOption(SIMPLE_NAME(stab_brand),
253                              CHATTR_HILITE | (BLUE << 8)),
254         new CursesGameOption(SIMPLE_NAME(may_stab_brand),
255                              CHATTR_HILITE | (YELLOW << 8)),
256         new CursesGameOption(SIMPLE_NAME(feature_item_brand), CHATTR_REVERSE),
257         new CursesGameOption(SIMPLE_NAME(trap_item_brand), CHATTR_REVERSE),
258         new CursesGameOption(SIMPLE_NAME(heap_brand), CHATTR_REVERSE),
259         new IntGameOption(SIMPLE_NAME(note_hp_percent), 5, 0, 100),
260         new IntGameOption(SIMPLE_NAME(hp_warning), 30, 0, 100),
261         new IntGameOption(magic_point_warning, {"mp_warning"}, 0, 0, 100),
262         new IntGameOption(SIMPLE_NAME(autofight_warning), 0, 0, 1000),
263         // These need to be odd, hence allow +1.
264         new IntGameOption(SIMPLE_NAME(view_max_width),
265                       max(VIEW_BASE_WIDTH, VIEW_MIN_WIDTH),
266                       VIEW_MIN_WIDTH, GXM + 1),
267         new IntGameOption(SIMPLE_NAME(view_max_height), max(21, VIEW_MIN_HEIGHT),
268                       VIEW_MIN_HEIGHT, GYM + 1),
269         new IntGameOption(SIMPLE_NAME(mlist_min_height), 4, 0),
270         new IntGameOption(SIMPLE_NAME(msg_min_height), max(7, MSG_MIN_HEIGHT),
271                           MSG_MIN_HEIGHT),
272         new IntGameOption(SIMPLE_NAME(msg_max_height), max(10, MSG_MIN_HEIGHT),
273                           MSG_MIN_HEIGHT),
274         new IntGameOption(SIMPLE_NAME(msg_webtiles_height), -1),
275         new IntGameOption(SIMPLE_NAME(rest_wait_percent), 100, 0, 100),
276         new IntGameOption(SIMPLE_NAME(pickup_menu_limit), 1),
277         new IntGameOption(SIMPLE_NAME(view_delay), DEFAULT_VIEW_DELAY, 0),
278         new IntGameOption(SIMPLE_NAME(fail_severity_to_confirm), 3, -1, 5),
279         new IntGameOption(SIMPLE_NAME(fail_severity_to_quiver), 3, -1, 5),
280         new IntGameOption(SIMPLE_NAME(travel_delay), USING_DGL ? -1 : 20,
281                           -1, 2000),
282         new IntGameOption(SIMPLE_NAME(rest_delay), USING_DGL ? -1 : 0,
283                           -1, 2000),
284         new IntGameOption(SIMPLE_NAME(explore_delay), -1, -1, 2000),
285         new IntGameOption(SIMPLE_NAME(explore_item_greed), 10, -1000, 1000),
286         new IntGameOption(SIMPLE_NAME(explore_wall_bias), 0, 0, 1000),
287         new IntGameOption(SIMPLE_NAME(scroll_margin_x), 2, 0),
288         new IntGameOption(SIMPLE_NAME(scroll_margin_y), 2, 0),
289         new IntGameOption(SIMPLE_NAME(item_stack_summary_minimum), 4),
290         new IntGameOption(SIMPLE_NAME(level_map_cursor_step), 7, 1, 50),
291         new IntGameOption(SIMPLE_NAME(dump_item_origin_price), -1, -1),
292         new IntGameOption(SIMPLE_NAME(dump_message_count), 40),
293         new ListGameOption<text_pattern>(SIMPLE_NAME(confirm_action)),
294         new ListGameOption<text_pattern>(SIMPLE_NAME(drop_filter)),
295         new ListGameOption<text_pattern>(SIMPLE_NAME(note_monsters)),
296         new ListGameOption<text_pattern>(SIMPLE_NAME(note_messages)),
297         new ListGameOption<text_pattern>(SIMPLE_NAME(note_items)),
298         new ListGameOption<text_pattern>(SIMPLE_NAME(auto_exclude)),
299         new ListGameOption<text_pattern>(SIMPLE_NAME(explore_stop_pickup_ignore)),
300         new ColourThresholdOption(hp_colour, {"hp_colour", "hp_color"},
301                                   "50:yellow, 25:red", _first_greater),
302         new ColourThresholdOption(mp_colour, {"mp_colour", "mp_color"},
303                                   "50:yellow, 25:red", _first_greater),
304         new ColourThresholdOption(stat_colour, {"stat_colour", "stat_color"},
305                                   "3:red", _first_less),
306         new StringGameOption(SIMPLE_NAME(sound_file_path), ""),
307         new MultipleChoiceGameOption<travel_open_doors_type>(
308             SIMPLE_NAME(travel_open_doors), travel_open_doors_type::open,
309             {{"avoid", travel_open_doors_type::avoid},
310              {"approach", travel_open_doors_type::approach},
311              {"open", travel_open_doors_type::open},
312              {"false", travel_open_doors_type::_false},
313              {"true", travel_open_doors_type::_true}}),
314 
315 #ifdef DGL_SIMPLE_MESSAGING
316         new BoolGameOption(SIMPLE_NAME(messaging), false),
317 #endif
318 #ifndef DGAMELAUNCH
319         new BoolGameOption(SIMPLE_NAME(name_bypasses_menu), true),
320         new BoolGameOption(SIMPLE_NAME(restart_after_save), true),
321         new BoolGameOption(SIMPLE_NAME(newgame_after_quit), false),
322         new StringGameOption(SIMPLE_NAME(map_file_name), ""),
323         new StringGameOption(SIMPLE_NAME(morgue_dir),
324                              _get_save_path("morgue/")),
325 #endif
326 #ifdef USE_TILE
327         new BoolGameOption(SIMPLE_NAME(tile_skip_title), false),
328         new BoolGameOption(SIMPLE_NAME(tile_menu_icons), true),
329         new BoolGameOption(SIMPLE_NAME(tile_filter_scaling), false),
330         new BoolGameOption(SIMPLE_NAME(tile_force_overlay), false),
331         new TileColGameOption(SIMPLE_NAME(tile_overlay_col), "#646464"),
332         new IntGameOption(SIMPLE_NAME(tile_overlay_alpha_percent), 40, 0, 100),
333         new BoolGameOption(SIMPLE_NAME(tile_show_minihealthbar), true),
334         new BoolGameOption(SIMPLE_NAME(tile_show_minimagicbar), true),
335         new BoolGameOption(SIMPLE_NAME(tile_show_demon_tier), false),
336         new StringGameOption(SIMPLE_NAME(tile_show_threat_levels), ""),
337         new StringGameOption(SIMPLE_NAME(tile_show_items), "!?/=([)}:|"),
338         // disabled by default due to performance issues
339         new BoolGameOption(SIMPLE_NAME(tile_water_anim), !USING_WEB_TILES),
340         new BoolGameOption(SIMPLE_NAME(tile_misc_anim), true),
341         new IntGameOption(SIMPLE_NAME(tile_font_crt_size), 0, 0, INT_MAX),
342         new IntGameOption(SIMPLE_NAME(tile_font_msg_size), 0, 0, INT_MAX),
343         new IntGameOption(SIMPLE_NAME(tile_font_stat_size), 0, 0, INT_MAX),
344         new IntGameOption(SIMPLE_NAME(tile_font_tip_size), 0, 0, INT_MAX),
345         new IntGameOption(SIMPLE_NAME(tile_font_lbl_size), 0, 0, INT_MAX),
346         new IntGameOption(SIMPLE_NAME(tile_cell_pixels), 32, 1, INT_MAX),
347         new IntGameOption(SIMPLE_NAME(tile_map_pixels), 0, 0, INT_MAX),
348         new IntGameOption(SIMPLE_NAME(tile_tooltip_ms), 500, 0, INT_MAX),
349         new IntGameOption(SIMPLE_NAME(tile_update_rate), 1000, 50, INT_MAX),
350         new IntGameOption(SIMPLE_NAME(tile_runrest_rate), 100, 0, INT_MAX),
351         // minimap colours
352         new TileColGameOption(SIMPLE_NAME(tile_branchstairs_col), "#ff7788"),
353         new TileColGameOption(SIMPLE_NAME(tile_deep_water_col), "#001122"),
354         new TileColGameOption(SIMPLE_NAME(tile_door_col), "#775544"),
355         new TileColGameOption(SIMPLE_NAME(tile_downstairs_col), "#ff00ff"),
356         new TileColGameOption(SIMPLE_NAME(tile_excl_centre_col), "#552266"),
357         new TileColGameOption(SIMPLE_NAME(tile_excluded_col), "#552266"),
358         new TileColGameOption(SIMPLE_NAME(tile_explore_horizon_col), "#6B301B"),
359         new TileColGameOption(SIMPLE_NAME(tile_feature_col), "#997700"),
360         new TileColGameOption(SIMPLE_NAME(tile_floor_col), "#333333"),
361         new TileColGameOption(SIMPLE_NAME(tile_item_col), "#005544"),
362         new TileColGameOption(SIMPLE_NAME(tile_lava_col), "#552211"),
363         new TileColGameOption(SIMPLE_NAME(tile_mapped_floor_col), "#222266"),
364         new TileColGameOption(SIMPLE_NAME(tile_mapped_wall_col), "#444499"),
365         new TileColGameOption(SIMPLE_NAME(tile_monster_col), "#660000"),
366         new TileColGameOption(SIMPLE_NAME(tile_plant_col), "#446633"),
367         new TileColGameOption(SIMPLE_NAME(tile_player_col), "white"),
368         new TileColGameOption(SIMPLE_NAME(tile_portal_col), "#ffdd00"),
369         new TileColGameOption(SIMPLE_NAME(tile_trap_col), "#aa6644"),
370         new TileColGameOption(SIMPLE_NAME(tile_unseen_col), "black"),
371         new TileColGameOption(SIMPLE_NAME(tile_upstairs_col), "cyan"),
372         new TileColGameOption(SIMPLE_NAME(tile_transporter_col), "#0000ff"),
373         new TileColGameOption(SIMPLE_NAME(tile_transporter_landing_col), "#5200aa"),
374         new TileColGameOption(SIMPLE_NAME(tile_wall_col), "#666666"),
375         new TileColGameOption(SIMPLE_NAME(tile_water_col), "#114455"),
376         new TileColGameOption(SIMPLE_NAME(tile_window_col), "#558855"),
377         new ListGameOption<string>(SIMPLE_NAME(tile_layout_priority),
378 #ifdef TOUCH_UI
379             split_string(",", "minimap, command, inventory, "
380                               "command2, spell, ability, monster")),
381 #else
382             split_string(",", "minimap, inventory, command, "
383                               "spell, ability, monster")),
384 #endif
385 #endif
386 #ifdef USE_TILE_LOCAL
387         new IntGameOption(SIMPLE_NAME(game_scale), 1, 1, 8),
388         new IntGameOption(SIMPLE_NAME(tile_key_repeat_delay), 200, 0, INT_MAX),
389         new IntGameOption(SIMPLE_NAME(tile_window_width), -90, INT_MIN, INT_MAX),
390         new IntGameOption(SIMPLE_NAME(tile_window_height), -90, INT_MIN, INT_MAX),
391         new IntGameOption(SIMPLE_NAME(tile_window_ratio), 1618, INT_MIN, INT_MAX),
392         new StringGameOption(SIMPLE_NAME(tile_font_crt_file), MONOSPACED_FONT),
393         new StringGameOption(SIMPLE_NAME(tile_font_msg_file), MONOSPACED_FONT),
394         new StringGameOption(SIMPLE_NAME(tile_font_stat_file), MONOSPACED_FONT),
395         new StringGameOption(SIMPLE_NAME(tile_font_tip_file), MONOSPACED_FONT),
396         new StringGameOption(SIMPLE_NAME(tile_font_lbl_file), PROPORTIONAL_FONT),
397         new BoolGameOption(SIMPLE_NAME(tile_single_column_menus), true),
398 #endif
399 #ifdef USE_TILE_WEB
400         new BoolGameOption(SIMPLE_NAME(tile_realtime_anim), false),
401         new BoolGameOption(SIMPLE_NAME(tile_level_map_hide_messages), true),
402         new BoolGameOption(SIMPLE_NAME(tile_level_map_hide_sidebar), false),
403         new BoolGameOption(SIMPLE_NAME(tile_web_mouse_control), true),
404         new StringGameOption(SIMPLE_NAME(tile_font_crt_family), "monospace"),
405         new StringGameOption(SIMPLE_NAME(tile_font_msg_family), "monospace"),
406         new StringGameOption(SIMPLE_NAME(tile_font_stat_family), "monospace"),
407         new StringGameOption(SIMPLE_NAME(tile_font_lbl_family), "monospace"),
408         new StringGameOption(SIMPLE_NAME(glyph_mode_font), "monospace"),
409         new IntGameOption(SIMPLE_NAME(glyph_mode_font_size), 24, 8, 144),
410 #endif
411 #ifdef USE_FT
412         new BoolGameOption(SIMPLE_NAME(tile_font_ft_light), false),
413 #endif
414 #ifdef WIZARD
415         new BoolGameOption(SIMPLE_NAME(fsim_csv), false),
416         new ListGameOption<string>(SIMPLE_NAME(fsim_scale)),
417         new ListGameOption<string>(SIMPLE_NAME(fsim_kit)),
418         new StringGameOption(SIMPLE_NAME(fsim_mode), ""),
419         new StringGameOption(SIMPLE_NAME(fsim_mons), ""),
420         new IntGameOption(SIMPLE_NAME(fsim_rounds), 4000, 1000, 500000),
421 #endif
422 #if !defined(DGAMELAUNCH) || defined(DGL_REMEMBER_NAME)
423         new BoolGameOption(SIMPLE_NAME(remember_name), true),
424 #endif
425     };
426 
427 #undef SIMPLE_NAME
428     return options;
429 }
430 
build_options_map(const vector<GameOption * > & options)431 map<string, GameOption*> game_options::build_options_map(
432     const vector<GameOption*> &options)
433 {
434     map<string, GameOption*> option_map;
435     for (GameOption* option : options)
436         for (string name : option->getNames())
437             option_map[name] = option;
438     return option_map;
439 }
440 
item_class_by_sym(char32_t c)441 object_class_type item_class_by_sym(char32_t c)
442 {
443     switch (c)
444     {
445     case ')':
446         return OBJ_WEAPONS;
447     case '(':
448     case U'\x27b9': //➹
449         return OBJ_MISSILES;
450     case '[':
451         return OBJ_ARMOUR;
452     case '/':
453         return OBJ_WANDS;
454     case '?':
455         return OBJ_SCROLLS;
456     case '"': // Make the amulet symbol equiv to ring -- bwross
457     case '=':
458     case U'\xb0': //°
459         return OBJ_JEWELLERY;
460     case '!':
461         return OBJ_POTIONS;
462     case ':':
463     case '+': // ??? -- was the only symbol working for tile order up to 0.10,
464               // so keeping it for compat purposes (user configs).
465     case U'\x221e': //∞
466         return OBJ_BOOKS;
467     case '|':
468         return OBJ_STAVES;
469     case '0':
470         return OBJ_ORBS;
471     case '}':
472         return OBJ_MISCELLANY;
473     case '&':
474     case 'X':
475     case 'x':
476         return OBJ_CORPSES;
477     case '$':
478     case U'\x20ac': //€
479     case U'\xa3': //£
480     case U'\xa5': //¥ // FR: support more currencies
481         return OBJ_GOLD;
482 #if TAG_MAJOR_VERSION == 34
483     case '\\': // Compat break: used to be staves (why not '|'?).
484         return OBJ_RODS;
485 #endif
486     default:
487         return NUM_OBJECT_CLASSES;
488     }
489 }
490 
491 // Returns MSGCOL_NONE if unmatched else returns 0-15.
_str_to_channel_colour(const string & str)492 static msg_colour_type _str_to_channel_colour(const string &str)
493 {
494     int col = str_to_colour(str);
495     msg_colour_type ret = MSGCOL_NONE;
496     if (col == -1)
497     {
498         if (str == "mute")
499             ret = MSGCOL_MUTED;
500         else if (str == "plain" || str == "off")
501             ret = MSGCOL_PLAIN;
502         else if (str == "default" || str == "on")
503             ret = MSGCOL_DEFAULT;
504         else if (str == "alternate")
505             ret = MSGCOL_ALTERNATE;
506     }
507     else
508         ret = msg_colour(col);
509 
510     return ret;
511 }
512 
513 static const string message_channel_names[] =
514 {
515     "plain", "friend_action", "prompt", "god", "duration", "danger", "warning",
516     "recovery", "sound", "talk", "talk_visual", "intrinsic_gain",
517     "mutation", "monster_spell", "monster_enchant", "friend_spell",
518     "friend_enchant", "monster_damage", "monster_target", "banishment",
519     "equipment", "floor", "multiturn", "examine", "examine_filter", "diagnostic",
520     "error", "tutorial", "orb", "timed_portal", "hell_effect", "monster_warning",
521     "dgl_message",
522 };
523 
524 // returns -1 if unmatched else returns 0--(NUM_MESSAGE_CHANNELS-1)
str_to_channel(const string & str)525 int str_to_channel(const string &str)
526 {
527     COMPILE_CHECK(ARRAYSZ(message_channel_names) == NUM_MESSAGE_CHANNELS);
528 
529     // widespread aliases
530     if (str == "visual")
531         return MSGCH_TALK_VISUAL;
532     else if (str == "spell")
533         return MSGCH_MONSTER_SPELL;
534 
535     for (int ret = 0; ret < NUM_MESSAGE_CHANNELS; ret++)
536     {
537         if (str == message_channel_names[ret])
538             return ret;
539     }
540 
541     return -1;
542 }
543 
channel_to_str(int channel)544 string channel_to_str(int channel)
545 {
546     if (channel < 0 || channel >= NUM_MESSAGE_CHANNELS)
547         return "";
548 
549     return message_channel_names[channel];
550 }
551 
552 
553 // The map used to interpret a crawlrc entry as a starting weapon
554 // type. For most entries, we can just look up which weapon has the entry as
555 // its name; this map contains the exceptions.
556 // This should be const, but operator[] on maps isn't const.
557 static map<string, weapon_type> _special_weapon_map = {
558 
559     // "staff" normally refers to a magical staff, but here we want to
560     // interpret it as a quarterstaff.
561     {"staff",       WPN_QUARTERSTAFF},
562 
563     // These weapons' base names have changed; we want to interpret the old
564     // names correctly.
565     {"sling",       WPN_HUNTING_SLING},
566     {"crossbow",    WPN_HAND_CROSSBOW},
567 
568     // Pseudo-weapons.
569     {"unarmed",     WPN_UNARMED},
570     {"claws",       WPN_UNARMED},
571 
572     {"thrown",      WPN_THROWN},
573     {"rocks",       WPN_THROWN},
574     {"boomerangs",   WPN_THROWN},
575     {"javelins",    WPN_THROWN},
576 
577     {"random",      WPN_RANDOM},
578 
579     {"viable",      WPN_VIABLE},
580 };
581 
582 /**
583  * Interpret a crawlrc entry as a starting weapon type.
584  *
585  * @param str   The value of the crawlrc entry.
586  * @return      The weapon the string refers to, or WPN_UNKNOWN if invalid
587  */
str_to_weapon(const string & str)588 weapon_type str_to_weapon(const string &str)
589 {
590     string str_nospace = str;
591     remove_whitespace(str_nospace);
592 
593     // Synonyms and pseudo-weapons.
594     if (_special_weapon_map.count(str_nospace))
595         return _special_weapon_map[str_nospace];
596 
597     // Real weapons referred to by their standard names.
598     return name_nospace_to_weapon(str_nospace);
599 }
600 
_weapon_to_str(weapon_type wpn_type)601 static string _weapon_to_str(weapon_type wpn_type)
602 {
603     if (wpn_type >= 0 && wpn_type < NUM_WEAPONS)
604         return weapon_base_name(wpn_type);
605 
606     switch (wpn_type)
607     {
608     case WPN_UNARMED:
609         return "claws";
610     case WPN_THROWN:
611         return "thrown";
612     case WPN_VIABLE:
613         return "viable";
614     case WPN_RANDOM:
615     default:
616         return "random";
617     }
618 }
619 
620 // Summon types can be any of mon_summon_type (enum.h), or a relevant summoning
621 // spell.
str_to_summon_type(const string & str)622 int str_to_summon_type(const string &str)
623 {
624     if (str == "clone")
625         return MON_SUMM_CLONE;
626     if (str == "animate")
627         return MON_SUMM_ANIMATE;
628     if (str == "chaos")
629         return MON_SUMM_CHAOS;
630     if (str == "miscast")
631         return MON_SUMM_MISCAST;
632     if (str == "zot")
633         return MON_SUMM_ZOT;
634     if (str == "wrath")
635         return MON_SUMM_WRATH;
636     if (str == "aid")
637         return MON_SUMM_AID;
638 
639     return spell_by_name(str);
640 }
641 
_str_to_fire_types(const string & str)642 static fire_type _str_to_fire_types(const string &str)
643 {
644     if (str == "launcher")
645         return FIRE_LAUNCHER;
646     else if (str == "stone")
647         return FIRE_STONE;
648     else if (str == "rock")
649         return FIRE_ROCK;
650     else if (str == "javelin")
651         return FIRE_JAVELIN;
652     else if (str == "boomerang")
653         return FIRE_BOOMERANG;
654     else if (str == "dart")
655         return FIRE_DART;
656     else if (str == "net")
657         return FIRE_NET;
658     else if (str == "throwing")
659         return FIRE_THROWING;
660     else if (str == "ammo")
661         return FIRE_AMMO;
662     else if (str == "inscribed")
663         return FIRE_INSCRIBED;
664     else if (str == "spell")
665         return FIRE_SPELL;
666     else if (str == "evokable" || str == "evocable")
667         return FIRE_EVOKABLE;
668     else if (str == "ability")
669         return FIRE_ABILITY;
670 
671     return FIRE_NONE;
672 }
673 
gametype_to_str(game_type type)674 string gametype_to_str(game_type type)
675 {
676     switch (type)
677     {
678     case GAME_TYPE_NORMAL:
679         return "normal";
680     case GAME_TYPE_CUSTOM_SEED:
681         return "seeded";
682     case GAME_TYPE_TUTORIAL:
683         return "tutorial";
684     case GAME_TYPE_ARENA:
685         return "arena";
686     case GAME_TYPE_SPRINT:
687         return "sprint";
688     case GAME_TYPE_HINTS:
689         return "hints";
690     default:
691         return "none";
692     }
693 }
694 
695 #ifndef DGAMELAUNCH
_str_to_gametype(const string & s)696 static game_type _str_to_gametype(const string& s)
697 {
698     for (int i = 0; i < NUM_GAME_TYPE; ++i)
699     {
700         game_type t = static_cast<game_type>(i);
701         if (s == gametype_to_str(t))
702             return t;
703     }
704     return NUM_GAME_TYPE;
705 }
706 #endif
707 
708 // XX move to species.cc?
_species_to_str(species_type sp)709 static string _species_to_str(species_type sp)
710 {
711     if (sp == SP_RANDOM)
712         return "random";
713     else if (sp == SP_VIABLE)
714         return "viable";
715     else
716         return species::name(sp);
717 }
718 
719 // XX move to species.cc?
_str_to_species(const string & str)720 static species_type _str_to_species(const string &str)
721 {
722     if (str == "random")
723         return SP_RANDOM;
724     else if (str == "viable")
725         return SP_VIABLE;
726 
727     species_type ret = SP_UNKNOWN;
728     if (str.length() == 2) // scan abbreviations
729         ret = species::from_abbrev(str.c_str());
730 
731     // if we don't have a match, scan the full names
732     if (ret == SP_UNKNOWN && str.length() >= 2)
733         ret = species::from_str_loose(str, true);
734 
735     if (!species::is_starting_species(ret))
736         ret = SP_UNKNOWN;
737 
738     if (ret == SP_UNKNOWN)
739         fprintf(stderr, "Unknown species choice: %s\n", str.c_str());
740 
741     return ret;
742 }
743 
_job_to_str(job_type job)744 static string _job_to_str(job_type job)
745 {
746     if (job == JOB_RANDOM)
747         return "random";
748     else if (job == JOB_VIABLE)
749         return "viable";
750     else
751         return get_job_name(job);
752 }
753 
str_to_job(const string & str)754 job_type str_to_job(const string &str)
755 {
756     if (str == "random")
757         return JOB_RANDOM;
758     else if (str == "viable")
759         return JOB_VIABLE;
760 
761     job_type job = JOB_UNKNOWN;
762 
763     if (str.length() == 2) // scan abbreviations
764         job = get_job_by_abbrev(str.c_str());
765 
766     // if we don't have a match, scan the full names
767     if (job == JOB_UNKNOWN)
768         job = get_job_by_name(str.c_str());
769 
770     if (!is_starting_job(job))
771         job = JOB_UNKNOWN;
772 
773     if (job == JOB_UNKNOWN)
774         fprintf(stderr, "Unknown background choice: %s\n", str.c_str());
775 
776     return job;
777 }
778 
779 // read a value which can be either a boolean (in which case return
780 // 0 for true, -1 for false), or a string of the form PREFIX:NUMBER
781 // (e.g., auto:7), in which case return NUMBER as an int.
_read_bool_or_number(const string & field,int def_value,const string & num_prefix)782 static int _read_bool_or_number(const string &field, int def_value,
783                                 const string& num_prefix)
784 {
785     int ret = def_value;
786 
787     if (field == "true" || field == "1" || field == "yes")
788         ret = 0;
789 
790     if (field == "false" || field == "0" || field == "no")
791         ret = -1;
792 
793     if (starts_with(field, num_prefix))
794         ret = atoi(field.c_str() + num_prefix.size());
795 
796     return ret;
797 }
798 
str_to_enemy_hp_colour(const string & colours,bool prepend)799 void game_options::str_to_enemy_hp_colour(const string &colours, bool prepend)
800 {
801     vector<string> colour_list = split_string(" ", colours, true, true);
802     if (prepend)
803         reverse(colour_list.begin(), colour_list.end());
804     for (const string &colstr : colour_list)
805     {
806         const int col = str_to_colour(colstr);
807         if (col < 0)
808         {
809             Options.report_error("Bad enemy_hp_colour: %s\n", colstr.c_str());
810             return;
811         }
812         else if (prepend)
813             enemy_hp_colour.insert(enemy_hp_colour.begin(), col);
814         else
815             enemy_hp_colour.push_back(col);
816     }
817 }
818 
819 #ifdef USE_TILE
820 static FixedVector<const char*, TAGPREF_MAX>
821     tag_prefs("none", "tutorial", "named", "enemy");
822 
_str_to_tag_pref(const char * opt)823 static tag_pref _str_to_tag_pref(const char *opt)
824 {
825     for (int i = 0; i < TAGPREF_MAX; i++)
826     {
827         if (!strcasecmp(opt, tag_prefs[i]))
828             return (tag_pref)i;
829     }
830 
831     return TAGPREF_ENEMY;
832 }
833 #endif
834 
new_dump_fields(const string & text,bool add,bool prepend)835 void game_options::new_dump_fields(const string &text, bool add, bool prepend)
836 {
837     // Easy; chardump.cc has most of the intelligence.
838     vector<string> fields = split_string(",", text, true, true);
839     if (add)
840     {
841         erase_if(fields, [this](const string &x) { return dump_fields.count(x) > 0; });
842         dump_fields.insert(fields.begin(), fields.end());
843         merge_lists(dump_order, fields, prepend);
844     }
845     else
846     {
847         for (const string &field : fields)
848         {
849             dump_fields.erase(field);
850             erase_val(dump_order, field);
851         }
852     }
853 }
854 
_correct_spelling(const string & str)855 static string _correct_spelling(const string& str)
856 {
857     if (str == "armor_on")
858         return "armour_on";
859     if (str == "armor_off")
860         return "armour_off";
861     if (str == "memorize")
862         return "memorise";
863     if (str == "jewelry_on")
864         return "jewellery_on";
865     return str;
866 }
867 
set_default_activity_interrupts()868 void game_options::set_default_activity_interrupts()
869 {
870     const char *default_activity_interrupts[] =
871     {
872         "interrupt_armour_on = hp_loss, monster_attack, monster, mimic",
873         "interrupt_armour_off = interrupt_armour_on",
874         "interrupt_drop_item = interrupt_armour_on",
875         "interrupt_jewellery_on = interrupt_armour_on",
876         "interrupt_memorise = hp_loss, monster_attack, stat",
877         "interrupt_butcher = interrupt_armour_on, teleport, stat",
878         "interrupt_exsanguinate = interrupt_butcher",
879         "interrupt_revivify = interrupt_butcher",
880         "interrupt_multidrop = hp_loss, monster_attack, teleport, stat",
881         "interrupt_macro = interrupt_multidrop",
882         "interrupt_travel = interrupt_butcher, hit_monster, sense_monster",
883         "interrupt_run = interrupt_travel, message",
884         "interrupt_rest = interrupt_run, full_hp, full_mp, ancestor_hp",
885 
886         // Stair ascents/descents cannot be interrupted except by
887         // teleportation. Attempts to interrupt the delay will just
888         // trash all queued delays, including travel.
889         "interrupt_ascending_stairs = teleport",
890         "interrupt_descending_stairs = teleport",
891         // These are totally uninterruptible by default, since it's
892         // impossible for them to be interrupted anyway.
893         "interrupt_drop_item = ",
894         "interrupt_jewellery_off =",
895     };
896 
897     for (const char* line : default_activity_interrupts)
898         read_option_line(line, false);
899 }
900 
set_activity_interrupt(FixedBitVector<NUM_ACTIVITY_INTERRUPTS> & eints,const string & interrupt)901 void game_options::set_activity_interrupt(
902         FixedBitVector<NUM_ACTIVITY_INTERRUPTS> &eints,
903         const string &interrupt)
904 {
905     if (starts_with(interrupt, interrupt_prefix))
906     {
907         string delay_name =
908             _correct_spelling(interrupt.substr(interrupt_prefix.length()));
909         if (!activity_interrupts.count(delay_name))
910             return report_error("Unknown delay: %s\n", delay_name.c_str());
911 
912         FixedBitVector<NUM_ACTIVITY_INTERRUPTS> &refints =
913             activity_interrupts[delay_name];
914 
915         eints |= refints;
916         return;
917     }
918 
919     activity_interrupt ai = get_activity_interrupt(interrupt);
920     if (ai == activity_interrupt::COUNT)
921     {
922         return report_error("Delay interrupt name \"%s\" not recognised.\n",
923                             interrupt.c_str());
924     }
925 
926     eints.set(static_cast<int>(ai));
927 }
928 
set_activity_interrupt(const string & activity_name,const string & interrupt_names,bool append_interrupts,bool remove_interrupts)929 void game_options::set_activity_interrupt(const string &activity_name,
930                                           const string &interrupt_names,
931                                           bool append_interrupts,
932                                           bool remove_interrupts)
933 {
934     vector<string> interrupts = split_string(",", interrupt_names);
935     auto & eints = activity_interrupts[_correct_spelling(activity_name)];
936 
937     if (remove_interrupts)
938     {
939         FixedBitVector<NUM_ACTIVITY_INTERRUPTS> refints;
940         for (const string &interrupt : interrupts)
941             set_activity_interrupt(refints, interrupt);
942 
943         for (int i = 0; i < NUM_ACTIVITY_INTERRUPTS; ++i)
944             if (refints[i])
945                 eints.set(i, false);
946     }
947     else
948     {
949         if (!append_interrupts)
950             eints.reset();
951 
952         for (const string &interrupt : interrupts)
953             set_activity_interrupt(eints, interrupt);
954     }
955 
956     eints.set(static_cast<int>(activity_interrupt::force));
957 }
958 
959 #if defined(DGAMELAUNCH)
_resolve_dir(string path,string suffix)960 static string _resolve_dir(string path, string suffix)
961 {
962     UNUSED(suffix);
963     return catpath(path, "");
964 }
965 #else
966 
_user_home_dir()967 static string _user_home_dir()
968 {
969 #ifdef TARGET_OS_WINDOWS
970     wchar_t home[MAX_PATH];
971     if (SHGetFolderPathW(0, CSIDL_APPDATA, 0, 0, home))
972         return "./";
973     else
974         return utf16_to_8(home);
975 #else
976     const char *home = getenv("HOME");
977     if (!home || !*home)
978         return "./";
979     else
980         return mb_to_utf8(home);
981 #endif
982 }
983 
_user_home_subpath(const string & subpath)984 static string _user_home_subpath(const string &subpath)
985 {
986     return catpath(_user_home_dir(), subpath);
987 }
988 
_resolve_dir(string path,string suffix)989 static string _resolve_dir(string path, string suffix)
990 {
991     if (path[0] != '~')
992         return catpath(string(path), suffix);
993     else
994         return _user_home_subpath(catpath(path.substr(1), suffix));
995 }
996 #endif
997 
_get_save_path(string subdir)998 static string _get_save_path(string subdir)
999 {
1000     return _resolve_dir(SysEnv.crawl_dir, subdir);
1001 }
1002 
reset_options()1003 void game_options::reset_options()
1004 {
1005     // XXX: do we really need to rebuild the list and map every time?
1006     // Will they ever change within a single execution of Crawl?
1007     // GameOption::value's value will change of course, but not the reference.
1008     deleteAll(option_behaviour);
1009     option_behaviour = build_options_list();
1010     options_by_name = build_options_map(option_behaviour);
1011     for (GameOption* option : option_behaviour)
1012         option->reset();
1013 
1014     filename     = "unknown";
1015     basefilename = "unknown";
1016     line_num     = -1;
1017 
1018     set_default_activity_interrupts();
1019 
1020 #ifdef DEBUG_DIAGNOSTICS
1021     quiet_debug_messages.reset();
1022     quiet_debug_messages.set(DIAG_BEAM);
1023 #ifdef DEBUG_MONSPEAK
1024     quiet_debug_messages.set(DIAG_SPEECH);
1025 #endif
1026 # ifdef DEBUG_MONINDEX
1027     quiet_debug_messages.set(DIAG_MONINDEX);
1028 # endif
1029 #endif
1030 
1031     macro_dir = SysEnv.macro_dir;
1032 
1033     save_dir = _get_save_path("saves/");
1034 #ifdef DGAMELAUNCH
1035     morgue_dir = _get_save_path("morgue/");
1036 #else
1037     if (macro_dir.empty())
1038     {
1039 #ifdef UNIX
1040         macro_dir = _user_home_subpath(".crawl");
1041 #else
1042         macro_dir = "settings/";
1043 #endif
1044     }
1045 #endif
1046 
1047 #if defined(TARGET_OS_MACOSX)
1048     UNUSED(_resolve_dir);
1049 
1050     if (SysEnv.macro_dir.empty())
1051         macro_dir  = _get_save_path("");
1052 #endif
1053 
1054 #if defined(SHARED_DIR_PATH)
1055     shared_dir = _resolve_dir(SHARED_DIR_PATH, "");
1056 #else
1057     shared_dir = save_dir;
1058 #endif
1059 
1060     additional_macro_files.clear();
1061 
1062 #ifdef DGL_SIMPLE_MESSAGING
1063     messaging = true;
1064 #endif
1065 
1066     autopickup_on    = 1;
1067 
1068     game = newgame_def();
1069 
1070     char_set      = CSET_DEFAULT;
1071 
1072     incremental_pregen = true;
1073     pregen_dungeon = false;
1074 
1075     // set it to the .crawlrc default
1076     autopickups.reset();
1077     autopickups.set(OBJ_GOLD);
1078     autopickups.set(OBJ_SCROLLS);
1079     autopickups.set(OBJ_POTIONS);
1080     autopickups.set(OBJ_BOOKS);
1081     autopickups.set(OBJ_JEWELLERY);
1082     autopickups.set(OBJ_WANDS);
1083 
1084     easy_confirm           = easy_confirm_type::safe;
1085     allow_self_target      = confirm_prompt_type::prompt;
1086     skill_focus            = SKM_FOCUS_ON;
1087 
1088     user_note_prefix       = "";
1089 
1090     arena_dump_msgs        = false;
1091     arena_dump_msgs_all    = false;
1092     arena_list_eq          = false;
1093 
1094     // Sort only pickup menus by default.
1095     sort_menus.clear();
1096     set_menu_sort("pickup: true");
1097 
1098     assign_item_slot       = SS_FORWARD;
1099     show_god_gift          = MB_MAYBE;
1100 
1101     explore_stop           = (ES_ITEM | ES_STAIR | ES_PORTAL | ES_BRANCH
1102                               | ES_SHOP | ES_ALTAR | ES_RUNED_DOOR
1103                               | ES_TRANSPORTER | ES_GREEDY_PICKUP_SMART
1104                               | ES_GREEDY_VISITED_ITEM_STACK);
1105 
1106     dump_kill_places       = KDO_ONE_PLACE;
1107     dump_item_origins      = IODS_ARTEFACTS;
1108 
1109     flush_input[ FLUSH_ON_FAILURE ]     = true;
1110     flush_input[ FLUSH_BEFORE_COMMAND ] = false;
1111     flush_input[ FLUSH_ON_MESSAGE ]     = false;
1112     flush_input[ FLUSH_LUA ]            = true;
1113 
1114     fire_items_start       = 0;           // start at slot 'a'
1115 
1116     // Clear fire_order and set up the defaults.
1117     set_fire_order("launcher, throwing, inscribed, spell, evokable, ability",
1118                    false, false);
1119     set_fire_order_spell("all", false, false);
1120     set_fire_order_ability("all", false, false);
1121     fire_order_ability.erase(ABIL_TROG_BERSERK);
1122 
1123     // TODO: what else?
1124     force_targeter =
1125         { SPELL_HAILSTORM, SPELL_STARBURST, SPELL_FROZEN_RAMPARTS,
1126           SPELL_MAXWELLS_COUPLING, SPELL_IGNITION, SPELL_NOXIOUS_BOG };
1127     always_use_static_targeters = false;
1128 
1129     // These are only used internally, and only from the commandline:
1130     // XXX: These need a better place.
1131     sc_entries             = 0;
1132     sc_format              = -1;
1133 
1134 #ifdef DGAMELAUNCH
1135     restart_after_game = MB_FALSE;
1136     restart_after_save = false;
1137     newgame_after_quit = false;
1138     name_bypasses_menu = true;
1139 #else
1140 #ifdef USE_TILE_LOCAL
1141     restart_after_game = MB_TRUE;
1142 #else
1143     restart_after_game = MB_MAYBE;
1144 #endif
1145 #endif
1146 
1147 #ifdef WIZARD
1148 #ifdef DGAMELAUNCH
1149     if (wiz_mode != WIZ_NO)
1150     {
1151         wiz_mode         = WIZ_NEVER;
1152         explore_mode     = WIZ_NEVER;
1153     }
1154 #else
1155     wiz_mode             = WIZ_NO;
1156     explore_mode         = WIZ_NO;
1157 #endif
1158 #endif
1159     terp_files.clear();
1160 
1161 #ifdef USE_TILE_LOCAL
1162 
1163     // window layout
1164     tile_full_screen      = SCREENMODE_AUTO;
1165     tile_use_small_layout = MB_MAYBE;
1166 #endif
1167 
1168 #ifdef USE_TILE
1169     // XXX: arena may now be chosen after options are read.
1170     tile_tag_pref         = crawl_state.game_is_arena() ? TAGPREF_NAMED
1171                                                         : TAGPREF_ENEMY;
1172 
1173     tile_use_monster         = MONS_PROGRAM_BUG;
1174     tile_player_tile         = 0;
1175     tile_weapon_offsets.first  = INT_MAX;
1176     tile_weapon_offsets.second = INT_MAX;
1177     tile_shield_offsets.first  = INT_MAX;
1178     tile_shield_offsets.second = INT_MAX;
1179     tile_viewport_scale = 100;
1180     tile_map_scale      = 60;
1181 #endif
1182 
1183 #ifdef USE_TILE_WEB
1184     tile_display_mode = "tiles";
1185 #endif
1186 
1187     // map each colour to itself as default
1188     for (int i = 0; i < (int)ARRAYSZ(colour); ++i)
1189         colour[i] = i;
1190 
1191     // map each channel to plain (well, default for now since I'm testing)
1192     for (int i = 0; i < NUM_MESSAGE_CHANNELS; ++i)
1193         channels[i] = MSGCOL_DEFAULT;
1194 
1195     // Clear vector options.
1196     dump_order.clear();
1197     dump_fields.clear();
1198     new_dump_fields("header,hiscore,stats,misc,inventory,"
1199                     "skills,spells,overview,mutations,messages,"
1200                     "screenshot,monlist,kills,notes,screenshots,vaults,"
1201                     "skill_gains,action_counts");
1202     // Currently enabled by default for testing in trunk.
1203     if (Version::ReleaseType == VER_ALPHA)
1204         new_dump_fields("turns_by_place");
1205 
1206     use_animations = (UA_BEAM | UA_RANGE | UA_HP | UA_MONSTER_IN_SIGHT
1207                       | UA_PICKUP | UA_MONSTER | UA_PLAYER | UA_BRANCH_ENTRY
1208                       | UA_ALWAYS_ON);
1209 
1210     enemy_hp_colour.clear();
1211     // I think these defaults are pretty ugly but apparently OS X has problems
1212     // with lighter colours
1213     enemy_hp_colour.push_back(GREEN);
1214     enemy_hp_colour.push_back(GREEN);
1215     enemy_hp_colour.push_back(BROWN);
1216     enemy_hp_colour.push_back(BROWN);
1217     enemy_hp_colour.push_back(MAGENTA);
1218     enemy_hp_colour.push_back(RED);
1219 
1220     force_autopickup.clear();
1221     autoinscriptions.clear();
1222     note_skill_levels.reset();
1223     note_skill_levels.set(1);
1224     note_skill_levels.set(5);
1225     note_skill_levels.set(10);
1226     note_skill_levels.set(15);
1227     note_skill_levels.set(27);
1228     auto_spell_letters.clear();
1229     auto_item_letters.clear();
1230     auto_ability_letters.clear();
1231     force_more_message.clear();
1232     flash_screen_message.clear();
1233     sound_mappings.clear();
1234     menu_colour_mappings.clear();
1235     message_colour_mappings.clear();
1236     named_options.clear();
1237 
1238     clear_cset_overrides();
1239 
1240     clear_feature_overrides();
1241     mon_glyph_overrides.clear();
1242     item_glyph_overrides.clear();
1243     item_glyph_cache.clear();
1244 
1245     // Map each category to itself. The user can override in init.txt
1246     kill_map[KC_YOU] = KC_YOU;
1247     kill_map[KC_FRIENDLY] = KC_FRIENDLY;
1248     kill_map[KC_OTHER] = KC_OTHER;
1249 
1250     // Forget any files we remembered as included.
1251     included.clear();
1252 
1253     // Forget variables and such.
1254     aliases.clear();
1255     variables.clear();
1256     constants.clear();
1257 }
1258 
clear_cset_overrides()1259 void game_options::clear_cset_overrides()
1260 {
1261     memset(cset_override, 0, sizeof cset_override);
1262 }
1263 
clear_feature_overrides()1264 void game_options::clear_feature_overrides()
1265 {
1266     feature_colour_overrides.clear();
1267     feature_symbol_overrides.clear();
1268 }
1269 
get_glyph_override(int c)1270 char32_t get_glyph_override(int c)
1271 {
1272     if (c < 0)
1273         c = -c;
1274     if (wcwidth(c) != 1)
1275     {
1276         mprf(MSGCH_ERROR, "Invalid glyph override: %X", c);
1277         c = 0;
1278     }
1279     return c;
1280 }
1281 
read_symbol(string s)1282 static int read_symbol(string s)
1283 {
1284     if (s.empty())
1285         return 0;
1286 
1287     if (s.length() > 1 && s[0] == '\\')
1288         s = s.substr(1);
1289 
1290     {
1291         char32_t c;
1292         const char *nc = s.c_str();
1293         nc += utf8towc(&c, nc);
1294         // no control, combining or CJK characters, please
1295         if (!*nc && wcwidth(c) == 1)
1296             return c;
1297     }
1298 
1299     int base = 10;
1300     if (s.length() > 1 && s[0] == 'x')
1301     {
1302         s = s.substr(1);
1303         base = 16;
1304     }
1305 
1306     char *tail;
1307     return strtoul(s.c_str(), &tail, base);
1308 }
1309 
set_fire_order_ability(const string & s,bool append,bool remove)1310 void game_options::set_fire_order_ability(const string &s, bool append, bool remove)
1311 {
1312     if (!append && !remove)
1313         fire_order_ability.clear();
1314     if (s == "all")
1315     {
1316         if (remove)
1317             fire_order_ability.clear();
1318         else
1319             for (const auto &a : get_defined_abilities())
1320                 fire_order_ability.insert(a);
1321         return;
1322     }
1323     if (s == "attack")
1324     {
1325         for (const auto &a : get_defined_abilities())
1326             if (quiver::is_autofight_combat_ability(a))
1327                 if (remove)
1328                     fire_order_ability.erase(a);
1329                 else
1330                     fire_order_ability.insert(a);
1331         return;
1332     }
1333     vector<string> slots = split_string(",", s);
1334     for (const string &slot : slots)
1335     {
1336         ability_type abil = ability_by_name(slot);
1337         if (abil == ABIL_NON_ABILITY)
1338         {
1339             report_error("Unknown ability '%s'\n", slot.c_str());
1340             return;
1341         }
1342         if (remove)
1343             fire_order_ability.erase(abil);
1344         else
1345             fire_order_ability.insert(abil);
1346     }
1347 }
1348 
set_fire_order_spell(const string & s,bool append,bool remove)1349 void game_options::set_fire_order_spell(const string &s, bool append, bool remove)
1350 {
1351     if (!spell_data_initialized()) // not ready in the first read
1352         return;
1353     if (!append && !remove)
1354         fire_order_spell.clear();
1355     if (s == "all")
1356     {
1357         if (remove)
1358             fire_order_ability.clear();
1359         else
1360             for (int i = SPELL_NO_SPELL; i < NUM_SPELLS; i++)
1361                 if (is_valid_spell(static_cast<spell_type>(i)))
1362                     fire_order_spell.insert(static_cast<spell_type>(i));
1363         return;
1364     }
1365     if (s == "attack")
1366     {
1367         for (int i = SPELL_NO_SPELL; i < NUM_SPELLS; i++)
1368         {
1369             auto sp = static_cast<spell_type>(i);
1370             if (quiver::is_autofight_combat_spell(sp))
1371                 if (remove)
1372                     fire_order_spell.erase(sp);
1373                 else
1374                     fire_order_spell.insert(sp);
1375         }
1376         return;
1377     }
1378     vector<string> slots = split_string(",", s);
1379     for (const string &slot : slots)
1380     {
1381         spell_type spell = spell_by_name(slot);
1382         if (is_valid_spell(spell))
1383         {
1384             if (remove)
1385                 fire_order_spell.erase(spell);
1386             else
1387                 fire_order_spell.insert(spell);
1388         }
1389         else
1390             report_error("Unknown spell '%s'\n", slot.c_str());
1391     }
1392 }
1393 
set_fire_order(const string & s,bool append,bool prepend)1394 void game_options::set_fire_order(const string &s, bool append, bool prepend)
1395 {
1396     if (!append && !prepend)
1397         fire_order.clear();
1398     vector<string> slots = split_string(",", s);
1399     if (prepend)
1400         reverse(slots.begin(), slots.end());
1401     for (const string &slot : slots)
1402         add_fire_order_slot(slot, prepend);
1403 }
1404 
add_fire_order_slot(const string & s,bool prepend)1405 void game_options::add_fire_order_slot(const string &s, bool prepend)
1406 {
1407     unsigned flags = 0;
1408     for (const string &alt : split_string("/", s))
1409         flags |= _str_to_fire_types(alt);
1410 
1411     if (flags)
1412     {
1413         if (prepend)
1414             fire_order.insert(fire_order.begin(), flags);
1415         else
1416             fire_order.push_back(flags);
1417     }
1418 }
1419 
add_force_targeter(const string & s,bool)1420 void game_options::add_force_targeter(const string &s, bool)
1421 {
1422     if (lowercase_string(s) == "all")
1423     {
1424         always_use_static_targeters = true;
1425         return;
1426     }
1427     auto spell = spell_by_name(s, true);
1428     if (is_valid_spell(spell))
1429         force_targeter.insert(spell);
1430     else
1431         report_error("Unknown spell '%s'\n", s.c_str());
1432 }
1433 
remove_force_targeter(const string & s,bool)1434 void game_options::remove_force_targeter(const string &s, bool)
1435 {
1436     if (lowercase_string(s) == "all")
1437     {
1438         always_use_static_targeters = false;
1439         return;
1440     }
1441     auto spell = spell_by_name(s, true);
1442     if (is_valid_spell(spell))
1443         force_targeter.erase(spell);
1444     else
1445         report_error("Unknown spell '%s'\n", s.c_str());
1446 }
1447 
_mons_class_by_string(const string & name)1448 static monster_type _mons_class_by_string(const string &name)
1449 {
1450     const string match = lowercase_string(name);
1451     for (monster_type i = MONS_0; i < NUM_MONSTERS; ++i)
1452     {
1453         const monsterentry *me = get_monster_data(i);
1454         if (!me || me->mc == MONS_PROGRAM_BUG)
1455             continue;
1456 
1457         if (lowercase_string(me->name) == match)
1458             return i;
1459     }
1460     return MONS_0;
1461 }
1462 
_mons_classes_by_glyph(const char letter)1463 static set<monster_type> _mons_classes_by_glyph(const char letter)
1464 {
1465     set<monster_type> matches;
1466     for (monster_type i = MONS_0; i < NUM_MONSTERS; ++i)
1467     {
1468         const monsterentry *me = get_monster_data(i);
1469         if (!me || me->mc == MONS_PROGRAM_BUG)
1470             continue;
1471 
1472         if (me->basechar == letter)
1473             matches.insert(i);
1474     }
1475     return matches;
1476 }
1477 
parse_mon_glyph(const string & s) const1478 cglyph_t game_options::parse_mon_glyph(const string &s) const
1479 {
1480     cglyph_t md;
1481     md.col = 0;
1482     vector<string> phrases = split_string(" ", s);
1483     for (const string &p : phrases)
1484     {
1485         const int col = str_to_colour(p, -1, false);
1486         if (col != -1)
1487             md.col = col;
1488         else
1489             md.ch = p == "_"? ' ' : read_symbol(p);
1490     }
1491     return md;
1492 }
1493 
remove_mon_glyph_override(const string & text,bool)1494 void game_options::remove_mon_glyph_override(const string &text, bool /*prepend*/)
1495 {
1496     vector<string> override = split_string(":", text);
1497 
1498     set<monster_type> matches;
1499     if (override[0].length() == 1)
1500         matches = _mons_classes_by_glyph(override[0][0]);
1501     else
1502     {
1503         const monster_type m = _mons_class_by_string(override[0]);
1504         if (m == MONS_0)
1505         {
1506             report_error("Unknown monster: \"%s\"", text.c_str());
1507             return;
1508         }
1509         matches.insert(m);
1510     }
1511     for (monster_type m : matches)
1512         mon_glyph_overrides.erase(m);;
1513 }
1514 
add_mon_glyph_override(const string & text,bool)1515 void game_options::add_mon_glyph_override(const string &text, bool /*prepend*/)
1516 {
1517     vector<string> override = split_string(":", text);
1518     if (override.size() != 2u)
1519         return;
1520 
1521     set<monster_type> matches;
1522     if (override[0].length() == 1)
1523         matches = _mons_classes_by_glyph(override[0][0]);
1524     else
1525     {
1526         const monster_type m = _mons_class_by_string(override[0]);
1527         if (m == MONS_0)
1528         {
1529             report_error("Unknown monster: \"%s\"", text.c_str());
1530             return;
1531         }
1532         matches.insert(m);
1533     }
1534 
1535     cglyph_t mdisp;
1536 
1537     // Look for monsters first so that "blue devil" works right.
1538     const monster_type n = _mons_class_by_string(override[1]);
1539     if (n != MONS_0)
1540     {
1541         const monsterentry *me = get_monster_data(n);
1542         mdisp.ch = me->basechar;
1543         mdisp.col = me->colour;
1544     }
1545     else
1546         mdisp = parse_mon_glyph(override[1]);
1547 
1548     if (mdisp.ch || mdisp.col)
1549         for (monster_type m : matches)
1550             mon_glyph_overrides[m] = mdisp;
1551 }
1552 
remove_item_glyph_override(const string & text,bool)1553 void game_options::remove_item_glyph_override(const string &text, bool /*prepend*/)
1554 {
1555     string key = text;
1556     trim_string(key);
1557 
1558     erase_if(item_glyph_overrides,
1559              [&key](const item_glyph_override_type& arg)
1560              { return key == arg.first; });
1561 }
1562 
add_item_glyph_override(const string & text,bool prepend)1563 void game_options::add_item_glyph_override(const string &text, bool prepend)
1564 {
1565     vector<string> override = split_string(":", text);
1566     if (override.size() != 2u)
1567         return;
1568 
1569     cglyph_t mdisp = parse_mon_glyph(override[1]);
1570     if (mdisp.ch || mdisp.col)
1571     {
1572         if (prepend)
1573         {
1574             item_glyph_overrides.emplace(item_glyph_overrides.begin(),
1575                                                override[0],mdisp);
1576         }
1577         else
1578             item_glyph_overrides.emplace_back(override[0], mdisp);
1579     }
1580 }
1581 
remove_feature_override(const string & text,bool)1582 void game_options::remove_feature_override(const string &text, bool /*prepend*/)
1583 {
1584     string fname;
1585     string::size_type epos = text.rfind("}");
1586     if (epos != string::npos)
1587         fname = text.substr(0, text.rfind("{",epos));
1588     else
1589         fname = text;
1590 
1591     trim_string(fname);
1592 
1593     vector<dungeon_feature_type> feats = features_by_desc(text_pattern(fname));
1594     for (dungeon_feature_type f : feats)
1595     {
1596         feature_colour_overrides.erase(f);
1597         feature_symbol_overrides.erase(f);
1598     }
1599 }
1600 
add_feature_override(const string & text,bool)1601 void game_options::add_feature_override(const string &text, bool /*prepend*/)
1602 {
1603     string::size_type epos = text.rfind("}");
1604     if (epos == string::npos)
1605         return;
1606 
1607     string::size_type spos = text.rfind("{", epos);
1608     if (spos == string::npos)
1609         return;
1610 
1611     string fname = text.substr(0, spos);
1612     string props = text.substr(spos + 1, epos - spos - 1);
1613     vector<string> iprops = split_string(",", props, true, true);
1614 
1615     if (iprops.size() < 1 || iprops.size() > 7)
1616         return;
1617 
1618     if (iprops.size() < 7)
1619         iprops.resize(7);
1620 
1621     trim_string(fname);
1622     vector<dungeon_feature_type> feats = features_by_desc(text_pattern(fname));
1623     if (feats.empty())
1624         return;
1625 
1626     for (const dungeon_feature_type feat : feats)
1627     {
1628         if (feat >= NUM_FEATURES)
1629             continue; // TODO: handle other object types.
1630 
1631 #define SYM(n, field) if (char32_t s = read_symbol(iprops[n])) \
1632                           feature_symbol_overrides[feat][n] = s; \
1633                       else \
1634                           feature_symbol_overrides[feat][n] = '\0';
1635         SYM(0, symbol);
1636         SYM(1, magic_symbol);
1637 #undef SYM
1638         feature_def &fov(feature_colour_overrides[feat]);
1639 #define COL(n, field) if (colour_t c = str_to_colour(iprops[n], BLACK)) \
1640                           fov.field = c;
1641         COL(2, dcolour);
1642         COL(3, unseen_dcolour);
1643         COL(4, seen_dcolour);
1644         COL(5, em_dcolour);
1645         COL(6, seen_em_dcolour);
1646 #undef COL
1647     }
1648 }
1649 
add_cset_override(dungeon_char_type dc,int symbol)1650 void game_options::add_cset_override(dungeon_char_type dc, int symbol)
1651 {
1652     cset_override[dc] = get_glyph_override(symbol);
1653 }
1654 
find_crawlrc()1655 string find_crawlrc()
1656 {
1657     const char* locations_data[][2] =
1658     {
1659         { SysEnv.crawl_dir.c_str(), "init.txt" },
1660 #ifdef UNIX
1661         { SysEnv.home.c_str(), ".crawl/init.txt" },
1662         { SysEnv.home.c_str(), ".crawlrc" },
1663         { SysEnv.home.c_str(), "init.txt" },
1664 #endif
1665 #ifndef DATA_DIR_PATH
1666         { "", "init.txt" },
1667         { "..", "init.txt" },
1668         { "../settings", "init.txt" },
1669 #endif
1670         { nullptr, nullptr }                // placeholder to mark end
1671     };
1672 
1673     // We'll look for these files in any supplied -rcdirs.
1674     static const char *rc_dir_filenames[] =
1675     {
1676         ".crawlrc",
1677         "init.txt",
1678     };
1679 
1680     // -rc option always wins.
1681     if (!SysEnv.crawl_rc.empty())
1682         return SysEnv.crawl_rc;
1683 
1684     // If we have any rcdirs, look in them for files from the
1685     // rc_dir_names list.
1686     for (const string &rc_dir : SysEnv.rcdirs)
1687     {
1688         for (const string &rc_fn : rc_dir_filenames)
1689         {
1690             const string rc(catpath(rc_dir, rc_fn));
1691             if (file_exists(rc))
1692                 return rc;
1693         }
1694     }
1695 
1696     // Check all possibilities for init.txt
1697     for (int i = 0; locations_data[i][1] != nullptr; ++i)
1698     {
1699         // Don't look at unset options
1700         if (locations_data[i][0] != nullptr)
1701         {
1702             const string rc = catpath(locations_data[i][0],
1703                                       locations_data[i][1]);
1704             if (file_exists(rc))
1705                 return rc;
1706         }
1707     }
1708 
1709     // Last attempt: pick up init.txt from datafile_path, which will
1710     // also search the settings/ directory.
1711     return datafile_path("init.txt", false, false);
1712 }
1713 
1714 static const char* lua_builtins[] =
1715 {
1716     "clua/stash.lua",
1717     "clua/delays.lua",
1718     "clua/autofight.lua",
1719     "clua/automagic.lua",
1720     "clua/kills.lua",
1721 };
1722 
1723 static const char* config_defaults[] =
1724 {
1725     "defaults/autopickup_exceptions.txt",
1726     "defaults/runrest_messages.txt",
1727     "defaults/standard_colours.txt",
1728     "defaults/menu_colours.txt",
1729     "defaults/glyph_colours.txt",
1730     "defaults/messages.txt",
1731     "defaults/misc.txt",
1732 };
1733 
read_init_file(bool runscript)1734 void read_init_file(bool runscript)
1735 {
1736     Options.reset_options();
1737     Options.read_option_line("center_on_scroll := centre_on_scroll"); // alias
1738 
1739     // Load Lua builtins.
1740     if (runscript)
1741     {
1742         for (const char *builtin : lua_builtins)
1743         {
1744             clua.execfile(builtin, false, false);
1745             if (!clua.error.empty())
1746                 mprf(MSGCH_ERROR, "Lua error: %s", clua.error.c_str());
1747         }
1748     }
1749 
1750     // Load default options.
1751     for (const char *def_file : config_defaults)
1752         Options.include(datafile_path(def_file), false, runscript);
1753 
1754     // Load early binding extra options from the command line BEFORE init.txt.
1755     Options.filename     = "extra opts first";
1756     Options.basefilename = "extra opts first";
1757     Options.line_num     = 0;
1758     for (const string &extra : SysEnv.extra_opts_first)
1759     {
1760         Options.line_num++;
1761         Options.read_option_line(extra, true);
1762     }
1763 
1764     // Load init.txt.
1765     const string crawl_rc = find_crawlrc();
1766     const string init_file_name(crawl_rc);
1767 
1768     /**
1769      Mac OS X apps almost always put their user-modifiable configuration files
1770      in the Application Support directory. On Mac OS X when DATA_DIR_PATH is
1771      not defined, place a symbolic link to the init.txt file in crawl_dir
1772      (probably "~/Library/Application Support/Dungeon Crawl Stone Soup") where
1773      the user is likely to go looking for it.
1774      */
1775 #if defined(TARGET_OS_MACOSX) && !defined(DATA_DIR_PATH)
1776     char *cwd = getcwd(NULL, 0);
1777     if (cwd)
1778     {
1779         const string absolute_crawl_rc = is_absolute_path(crawl_rc) ? crawl_rc : catpath(cwd, crawl_rc);
1780         char *resolved = realpath(absolute_crawl_rc.c_str(), NULL);
1781         if (resolved)
1782         {
1783             const string crawl_dir_init = catpath(SysEnv.crawl_dir.c_str(), "init.txt");
1784             symlink(resolved, crawl_dir_init.c_str());
1785             free(resolved);
1786         }
1787         free(cwd);
1788     }
1789 #endif
1790 
1791     FileLineInput f(init_file_name.c_str());
1792 
1793     Options.filename = init_file_name;
1794     Options.line_num = 0;
1795 #ifdef UNIX
1796     Options.basefilename = "~/.crawlrc";
1797 #else
1798     Options.basefilename = "init.txt";
1799 #endif
1800 
1801     if (f.error())
1802         return;
1803     Options.read_options(f, runscript);
1804 
1805     if (Options.read_persist_options)
1806     {
1807         // Read options from a .persist file if one exists.
1808         clua.load_persist();
1809         clua.pushglobal("c_persist.options");
1810         if (lua_isstring(clua, -1))
1811             read_options(lua_tostring(clua, -1), runscript);
1812         lua_pop(clua, 1);
1813     }
1814 
1815     // Load late binding extra options from the command line AFTER init.txt.
1816     Options.filename     = "extra opts last";
1817     Options.basefilename = "extra opts last";
1818     Options.line_num     = 0;
1819     for (const string &extra : SysEnv.extra_opts_last)
1820     {
1821         Options.line_num++;
1822         Options.read_option_line(extra, true);
1823     }
1824 
1825     Options.filename     = init_file_name;
1826     Options.basefilename = get_base_filename(init_file_name);
1827     Options.line_num     = -1;
1828 }
1829 
read_startup_prefs()1830 newgame_def read_startup_prefs()
1831 {
1832 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
1833     FileLineInput fl(get_prefs_filename().c_str());
1834     if (fl.error())
1835         return newgame_def();
1836 
1837     game_options temp;
1838     temp.read_options(fl, false);
1839 
1840     if (!temp.game.allowed_species.empty())
1841         temp.game.species = temp.game.allowed_species[0];
1842     if (!temp.game.allowed_jobs.empty())
1843         temp.game.job = temp.game.allowed_jobs[0];
1844     if (!temp.game.allowed_weapons.empty())
1845         temp.game.weapon = temp.game.allowed_weapons[0];
1846     if (!Options.seed_from_rc)
1847         Options.seed = temp.seed_from_rc;
1848     if (!Options.remember_name)
1849         temp.game.name = "";
1850     return temp.game;
1851 #endif // !DISABLE_STICKY_STARTUP_OPTIONS
1852 }
1853 
1854 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
write_newgame_options(const newgame_def & prefs,FILE * f)1855 static void write_newgame_options(const newgame_def& prefs, FILE *f)
1856 {
1857     if (Options.no_save)
1858         return;
1859     if (prefs.type != NUM_GAME_TYPE)
1860         fprintf(f, "type = %s\n", gametype_to_str(prefs.type).c_str());
1861     if (!prefs.map.empty())
1862         fprintf(f, "map = %s\n", prefs.map.c_str());
1863     if (!prefs.arena_teams.empty())
1864         fprintf(f, "arena_teams = %s\n", prefs.arena_teams.c_str());
1865     fprintf(f, "name = %s\n", prefs.name.c_str());
1866     if (prefs.species != SP_UNKNOWN)
1867         fprintf(f, "species = %s\n", _species_to_str(prefs.species).c_str());
1868     if (prefs.job != JOB_UNKNOWN)
1869         fprintf(f, "background = %s\n", _job_to_str(prefs.job).c_str());
1870     if (prefs.weapon != WPN_UNKNOWN)
1871         fprintf(f, "weapon = %s\n", _weapon_to_str(prefs.weapon).c_str());
1872     if (prefs.seed != 0)
1873         fprintf(f, "game_seed = %" PRIu64 "\n", prefs.seed);
1874     fprintf(f, "fully_random = %s\n", prefs.fully_random ? "yes" : "no");
1875 }
1876 #endif // !DISABLE_STICKY_STARTUP_OPTIONS
1877 
write_newgame_options_file(const newgame_def & prefs)1878 void write_newgame_options_file(const newgame_def& prefs)
1879 {
1880 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
1881     // [ds] Saving startup prefs should work like this:
1882     //
1883     // 1. If the game is started without specifying a game type, always
1884     //    save startup preferences in the base savedir.
1885     // 2. If the game is started with a game type (Sprint), save startup
1886     //    preferences in the game-specific savedir.
1887     //
1888     // The idea is that public servers can use one instance of Crawl
1889     // but present Crawl and Sprint as two separate games in the
1890     // server-specific game menu -- the startup prefs file for Crawl and
1891     // Sprint should never collide, because the public server config will
1892     // specify the game type on the command-line.
1893     //
1894     // For normal users, startup prefs should always be saved in the
1895     // same base savedir so that when they start Crawl with "./crawl"
1896     // or the equivalent, their last game choices will be remembered,
1897     // even if they chose a Sprint game.
1898     //
1899     // Yes, this is unnecessarily complex. Better ideas welcome.
1900     //
1901     unwind_var<game_type> gt(crawl_state.type, Options.game.type);
1902 
1903     string fn = get_prefs_filename();
1904     FILE *f = fopen_u(fn.c_str(), "w");
1905     if (!f)
1906         return;
1907     write_newgame_options(prefs, f);
1908     fclose(f);
1909 #endif // !DISABLE_STICKY_STARTUP_OPTIONS
1910 }
1911 
save_player_name()1912 void save_player_name()
1913 {
1914 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
1915     // Read other preferences
1916     newgame_def prefs = read_startup_prefs();
1917     prefs.name = Options.remember_name ? you.your_name : "";
1918 
1919     // And save
1920     write_newgame_options_file(prefs);
1921 #endif // !DISABLE_STICKY_STARTUP_OPTIONS
1922 }
1923 
1924 #ifndef DISABLE_STICKY_STARTUP_OPTIONS
1925 // TODO: can these functions be generalized? This is called on game end, maybe
1926 // the entire pref should be updated then?
save_seed_pref()1927 void save_seed_pref()
1928 {
1929 #ifndef DGAMELAUNCH
1930     if (!crawl_state.game_standard_levelgen())
1931         return;
1932     // Read other preferences
1933     newgame_def prefs = read_startup_prefs();
1934     prefs.seed = crawl_state.seed;
1935 
1936     // And save
1937     write_newgame_options_file(prefs);
1938 #endif
1939 }
1940 #endif // !DISABLE_STICKY_STARTUP_OPTIONS
1941 
read_options(const string & s,bool runscript,bool clear_aliases)1942 void read_options(const string &s, bool runscript, bool clear_aliases)
1943 {
1944     StringLineInput st(s);
1945     Options.read_options(st, runscript, clear_aliases);
1946 }
1947 
game_options()1948 game_options::game_options()
1949     : seed(0), seed_from_rc(0),
1950     no_save(false), language(lang_t::EN), lang_name(nullptr)
1951 {
1952     reset_options();
1953 }
1954 
~game_options()1955 game_options::~game_options()
1956 {
1957     deleteAll(option_behaviour);
1958 }
1959 
read_options(LineInput & il,bool runscript,bool clear_aliases)1960 void game_options::read_options(LineInput &il, bool runscript,
1961                                 bool clear_aliases)
1962 {
1963     unsigned int line = 0;
1964 
1965     bool inscriptblock = false;
1966     bool inscriptcond  = false;
1967     bool isconditional = false;
1968 
1969     bool l_init        = false;
1970 
1971     if (clear_aliases)
1972     {
1973         aliases.clear();
1974         Options.add_alias("center_on_scroll", "centre_on_scroll"); // old name
1975     }
1976 
1977     dlua_chunk luacond(filename);
1978     dlua_chunk luacode(filename);
1979 
1980 #ifndef CLUA_BINDINGS
1981     bool clua_error_printed = false;
1982 #endif
1983 
1984     while (!il.eof())
1985     {
1986         line_num++;
1987         string s   = il.get_line();
1988         string str = s;
1989         line++;
1990 
1991         trim_string(str);
1992 
1993         // This is to make some efficient comments
1994         if ((str.empty() || str[0] == '#') && !inscriptcond && !inscriptblock)
1995             continue;
1996 
1997         if (!inscriptcond && str[0] == ':')
1998         {
1999             // The init file is now forced into isconditional mode.
2000             isconditional = true;
2001             str = str.substr(1);
2002             if (!str.empty() && runscript)
2003             {
2004                 // If we're in the middle of an option block, close it.
2005                 if (!luacond.empty() && l_init)
2006                 {
2007                     luacond.add(line - 1, "]==])");
2008                     l_init = false;
2009                 }
2010                 luacond.add(line, str);
2011             }
2012             continue;
2013         }
2014         if (!inscriptcond && (starts_with(str, "L<") || starts_with(str, "<")))
2015         {
2016             // The init file is now forced into isconditional mode.
2017             isconditional = true;
2018             inscriptcond  = true;
2019 
2020             str = str.substr(starts_with(str, "L<") ? 2 : 1);
2021             // Is this a one-liner?
2022             if (!str.empty() && str.back() == '>')
2023             {
2024                 inscriptcond = false;
2025                 str = str.substr(0, str.length() - 1);
2026             }
2027 
2028             if (!str.empty() && runscript)
2029             {
2030                 // If we're in the middle of an option block, close it.
2031                 if (!luacond.empty() && l_init)
2032                 {
2033                     luacond.add(line - 1, "]==])");
2034                     l_init = false;
2035                 }
2036                 luacond.add(line, str);
2037             }
2038             continue;
2039         }
2040         else if (inscriptcond && !str.empty()
2041                  && (str.find(">") == str.length() - 1 || str == ">L"))
2042         {
2043             inscriptcond = false;
2044             str = str.substr(0, str.length() - 1);
2045             if (!str.empty() && runscript)
2046                 luacond.add(line, str);
2047             continue;
2048         }
2049         else if (inscriptcond)
2050         {
2051             if (runscript)
2052                 luacond.add(line, s);
2053             continue;
2054         }
2055 
2056         // Handle blocks of Lua
2057         if (!inscriptblock
2058             && (starts_with(str, "Lua{") || starts_with(str, "{")))
2059         {
2060             inscriptblock = true;
2061             luacode.clear();
2062             luacode.set_file(filename);
2063 
2064             // Strip leading Lua[
2065             str = str.substr(starts_with(str, "Lua{") ? 4 : 1);
2066 
2067             if (!str.empty() && str.find("}") == str.length() - 1)
2068             {
2069                 str = str.substr(0, str.length() - 1);
2070                 inscriptblock = false;
2071             }
2072 
2073             if (!str.empty())
2074                 luacode.add(line, str);
2075 
2076             if (!inscriptblock && runscript)
2077             {
2078 #ifdef CLUA_BINDINGS
2079                 if (luacode.run(clua))
2080                 {
2081                     mprf(MSGCH_ERROR, "Lua error: %s",
2082                          luacode.orig_error().c_str());
2083                 }
2084                 luacode.clear();
2085 #else
2086                 if (!clua_error_printed)
2087                 {
2088                     mprf(MSGCH_ERROR, "User lua is disabled in this build! `%s`", str.c_str());
2089                     clua_error_printed = true;
2090                 }
2091 #endif
2092             }
2093 
2094             continue;
2095         }
2096         else if (inscriptblock && (str == "}Lua" || str == "}"))
2097         {
2098             inscriptblock = false;
2099 #ifdef CLUA_BINDINGS
2100             if (runscript)
2101             {
2102                 if (luacode.run(clua))
2103                 {
2104                     mprf(MSGCH_ERROR, "Lua error: %s",
2105                          luacode.orig_error().c_str());
2106                 }
2107             }
2108 #else
2109             if (!clua_error_printed)
2110             {
2111                 mprf(MSGCH_ERROR, "User lua is disabled in this build! `%s`", str.c_str());
2112                 clua_error_printed = true;
2113             }
2114 #endif
2115             luacode.clear();
2116             continue;
2117         }
2118         else if (inscriptblock)
2119         {
2120             luacode.add(line, s);
2121             continue;
2122         }
2123 
2124         if (isconditional && runscript)
2125         {
2126             if (!l_init)
2127             {
2128                 luacond.add(line, "crawl.setopt([==[");
2129                 l_init = true;
2130             }
2131 
2132             luacond.add(line, s);
2133             continue;
2134         }
2135 
2136         read_option_line(str, runscript);
2137     }
2138 
2139     if (runscript && !luacond.empty())
2140     {
2141 #ifdef CLUA_BINDINGS
2142         if (l_init)
2143             luacond.add(line, "]==])");
2144         if (luacond.run(clua))
2145             mprf(MSGCH_ERROR, "Lua error: %s", luacond.orig_error().c_str());
2146 #else
2147         if (!clua_error_printed)
2148         {
2149             mprf(MSGCH_ERROR, "User lua is disabled in this build! (file: %s)", filename.c_str());
2150             clua_error_printed = true;
2151         }
2152 #endif
2153     }
2154 
2155 }
2156 
fixup_options()2157 void game_options::fixup_options()
2158 {
2159     // Validate save_dir
2160     if (!check_mkdir("Save directory", &save_dir))
2161         end(1, false, "Cannot create save directory '%s'", save_dir.c_str());
2162 
2163     if (!SysEnv.morgue_dir.empty())
2164         morgue_dir = SysEnv.morgue_dir;
2165 
2166     if (!check_mkdir("Morgue directory", &morgue_dir))
2167         end(1, false, "Cannot create morgue directory '%s'", morgue_dir.c_str());
2168 }
2169 
_str_to_killcategory(const string & s)2170 static int _str_to_killcategory(const string &s)
2171 {
2172     static const char *kc[] =
2173     {
2174         "you",
2175         "friend",
2176         "other",
2177     };
2178 
2179     for (unsigned i = 0; i < ARRAYSZ(kc); ++i)
2180         if (s == kc[i])
2181             return i;
2182 
2183     return -1;
2184 }
2185 
2186 #ifdef USE_TILE
set_player_tile(const string & field)2187 void game_options::set_player_tile(const string &field)
2188 {
2189     if (field == "normal")
2190     {
2191         tile_use_monster = MONS_0;
2192         tile_player_tile = 0;
2193         return;
2194     }
2195     else if (field == "playermons")
2196     {
2197         tile_use_monster = MONS_PLAYER;
2198         tile_player_tile = 0;
2199         return;
2200     }
2201 
2202     vector<string> fields = split_string(":", field);
2203     // Handle tile:<tile-name> values
2204     if (fields.size() == 2 && fields[0] == "tile")
2205     {
2206         // A variant tile. We have to find the base tile to look this up inthe
2207         // tile index.
2208         if (isdigit(*(fields[1].rbegin())))
2209         {
2210             string base_tname = fields[1];
2211             size_t found = base_tname.rfind('_');
2212             int offset = 0;
2213             tileidx_t base_tile = 0;
2214             if (found != std::string::npos
2215                 && parse_int(fields[1].substr(found + 1).c_str(), offset))
2216             {
2217                 base_tname = base_tname.substr(0, found);
2218                 if (!tile_player_index(base_tname.c_str(), &base_tile))
2219                 {
2220                     report_error("Can't find base tile \"%s\" of variant "
2221                                  "tile \"%s\"", base_tname.c_str(),
2222                                  fields[1].c_str());
2223                     return;
2224                 }
2225                 tile_player_tile = tileidx_mon_clamp(base_tile, offset);
2226             }
2227         }
2228         else if (!tile_player_index(fields[1].c_str(), &tile_player_tile))
2229         {
2230             report_error("Unknown tile: \"%s\"", fields[1].c_str());
2231             return;
2232         }
2233         tile_use_monster = MONS_PLAYER;
2234     }
2235     else if (fields.size() == 2 && fields[0] == "mons")
2236     {
2237         // Handle mons:<monster-name> values
2238         const monster_type m = _mons_class_by_string(fields[1]);
2239         if (m == MONS_0)
2240             report_error("Unknown monster: \"%s\"", fields[1].c_str());
2241         else
2242         {
2243             tile_use_monster = m;
2244             tile_player_tile = 0;
2245         }
2246     }
2247     else
2248     {
2249         report_error("Invalid setting for tile_player_tile: \"%s\"",
2250                      field.c_str());
2251     }
2252 }
2253 
set_tile_offsets(const string & field,bool set_shield)2254 void game_options::set_tile_offsets(const string &field, bool set_shield)
2255 {
2256     bool error = false;
2257     pair<int, int> *offsets;
2258     if (set_shield)
2259         offsets = &tile_shield_offsets;
2260     else
2261         offsets = &tile_weapon_offsets;
2262 
2263     if (field == "reset")
2264     {
2265         offsets->first = INT_MAX;
2266         offsets->second = INT_MAX;
2267         return;
2268     }
2269 
2270     vector<string> offs = split_string(",", field);
2271     if (offs.size() != 2
2272         || !parse_int(offs[0].c_str(), offsets->first)
2273         || abs(offsets->first) > 32
2274         || !parse_int(offs[1].c_str(), offsets->second)
2275         || abs(offsets->second) > 32)
2276     {
2277         report_error("Invalid %s tile offsets: \"%s\"",
2278                      set_shield ? "shield" : "weapon", field.c_str());
2279         error = true;
2280     }
2281 
2282     if (error)
2283     {
2284         offsets->first = INT_MAX;
2285         offsets->second = INT_MAX;
2286     }
2287 }
2288 #endif // USE_TILE
2289 
do_kill_map(const string & from,const string & to)2290 void game_options::do_kill_map(const string &from, const string &to)
2291 {
2292     int ifrom = _str_to_killcategory(from),
2293         ito   = _str_to_killcategory(to);
2294     if (ifrom != -1 && ito != -1)
2295         kill_map[ifrom] = ito;
2296 }
2297 
read_use_animations(const string & field) const2298 use_animations_type game_options::read_use_animations(const string &field) const
2299 {
2300     use_animations_type animations;
2301     vector<string> types = split_string(",", field);
2302     for (const auto &type : types)
2303     {
2304         if (type == "beam")
2305             animations |= UA_BEAM;
2306         else if (type == "range")
2307             animations |= UA_RANGE;
2308         else if (type == "hp")
2309             animations |= UA_HP;
2310         else if (type == "monster_in_sight")
2311             animations |= UA_MONSTER_IN_SIGHT;
2312         else if (type == "pickup")
2313             animations |= UA_PICKUP;
2314         else if (type == "monster")
2315             animations |= UA_MONSTER;
2316         else if (type == "player")
2317             animations |= UA_PLAYER;
2318         else if (type == "branch_entry")
2319             animations |= UA_BRANCH_ENTRY;
2320     }
2321 
2322     return animations;
2323 }
2324 
read_explore_stop_conditions(const string & field) const2325 int game_options::read_explore_stop_conditions(const string &field) const
2326 {
2327     int conditions = 0;
2328     vector<string> stops = split_string(",", field);
2329     for (const string &stop : stops)
2330     {
2331         const string c = replace_all_of(stop, " ", "_");
2332         if (c == "item" || c == "items")
2333             conditions |= ES_ITEM;
2334         else if (c == "greedy_pickup")
2335             conditions |= ES_GREEDY_PICKUP;
2336         else if (c == "greedy_pickup_gold")
2337             conditions |= ES_GREEDY_PICKUP_GOLD;
2338         else if (c == "greedy_pickup_smart")
2339             conditions |= ES_GREEDY_PICKUP_SMART;
2340         else if (c == "greedy_pickup_thrown")
2341             conditions |= ES_GREEDY_PICKUP_THROWN;
2342         else if (c == "shop" || c == "shops")
2343             conditions |= ES_SHOP;
2344         else if (c == "stair" || c == "stairs")
2345             conditions |= ES_STAIR;
2346         else if (c == "branch" || c == "branches")
2347             conditions |= ES_BRANCH;
2348         else if (c == "portal" || c == "portals")
2349             conditions |= ES_PORTAL;
2350         else if (c == "altar" || c == "altars")
2351             conditions |= ES_ALTAR;
2352         else if (c == "runed_door")
2353             conditions |= ES_RUNED_DOOR;
2354         else if (c == "transporter")
2355             conditions |= ES_TRANSPORTER;
2356         else if (c == "greedy_item" || c == "greedy_items")
2357             conditions |= ES_GREEDY_ITEM;
2358         else if (c == "greedy_visited_item_stack")
2359             conditions |= ES_GREEDY_VISITED_ITEM_STACK;
2360         else if (c == "glowing" || c == "glowing_item"
2361                  || c == "glowing_items")
2362             conditions |= ES_GLOWING_ITEM;
2363         else if (c == "artefact" || c == "artefacts"
2364                  || c == "artifact" || c == "artifacts")
2365             conditions |= ES_ARTEFACT;
2366         else if (c == "rune" || c == "runes")
2367             conditions |= ES_RUNE;
2368     }
2369     return conditions;
2370 }
2371 
2372 // Note the distinction between:
2373 // 1. aliases "ae := autopickup_exception" "ae += useless_item"
2374 //    stored in game_options.aliases.
2375 // 2. variables "$slots := abc" "spell_slots += Dispel undead:$slots"
2376 //    stored in game_options.variables.
2377 // 3. constant variables "$slots = abc", "constant = slots".
2378 //    stored in game_options.variables, but with an extra entry in
2379 //    game_options.constants.
add_alias(const string & key,const string & val)2380 void game_options::add_alias(const string &key, const string &val)
2381 {
2382     if (key[0] == '$')
2383     {
2384         string name = key.substr(1);
2385         // Don't alter if it's a constant.
2386         if (constants.count(name))
2387             return;
2388         variables[name] = val;
2389     }
2390     else
2391         aliases[key] = val;
2392 }
2393 
unalias(const string & key) const2394 string game_options::unalias(const string &key) const
2395 {
2396     return lookup(aliases, key, key);
2397 }
2398 
2399 #define IS_VAR_CHAR(c) (isaalpha(c) || c == '_' || c == '-')
2400 
expand_vars(const string & field) const2401 string game_options::expand_vars(const string &field) const
2402 {
2403     string field_out = field;
2404 
2405     string::size_type curr_pos = 0;
2406 
2407     // Only try 100 times, so as to not get stuck in infinite recursion.
2408     for (int i = 0; i < 100; i++)
2409     {
2410         string::size_type dollar_pos = field_out.find("$", curr_pos);
2411 
2412         if (dollar_pos == string::npos || field_out.size() == (dollar_pos + 1))
2413             break;
2414 
2415         string::size_type start_pos = dollar_pos + 1;
2416 
2417         if (!IS_VAR_CHAR(field_out[start_pos]))
2418             continue;
2419 
2420         string::size_type end_pos;
2421         for (end_pos = start_pos; end_pos + 1 < field_out.size(); end_pos++)
2422         {
2423             if (!IS_VAR_CHAR(field_out[end_pos + 1]))
2424                 break;
2425         }
2426 
2427         string var_name = field_out.substr(start_pos, end_pos - start_pos + 1);
2428 
2429         auto x = variables.find(var_name);
2430 
2431         if (x == variables.end())
2432         {
2433             curr_pos = end_pos + 1;
2434             continue;
2435         }
2436 
2437         string dollar_plus_name = "$";
2438         dollar_plus_name += var_name;
2439 
2440         field_out = replace_all(field_out, dollar_plus_name, x->second);
2441 
2442         // Start over at beginning
2443         curr_pos = 0;
2444     }
2445 
2446     return field_out;
2447 }
2448 
add_message_colour_mappings(const string & field,bool prepend,bool subtract)2449 void game_options::add_message_colour_mappings(const string &field,
2450                                                bool prepend, bool subtract)
2451 {
2452     vector<string> fragments = split_string(",", field);
2453     if (prepend)
2454         reverse(fragments.begin(), fragments.end());
2455     for (const string &fragment : fragments)
2456         add_message_colour_mapping(fragment, prepend, subtract);
2457 }
2458 
parse_message_filter(const string & filter)2459 message_filter game_options::parse_message_filter(const string &filter)
2460 {
2461     string::size_type pos = filter.find(":");
2462     if (pos && pos != string::npos)
2463     {
2464         string prefix = filter.substr(0, pos);
2465         int channel = str_to_channel(prefix);
2466         if (channel != -1 || prefix == "any")
2467         {
2468             string s = filter.substr(pos + 1);
2469             trim_string(s);
2470             return message_filter(channel, s);
2471         }
2472     }
2473 
2474     return message_filter(filter);
2475 }
2476 
add_message_colour_mapping(const string & field,bool prepend,bool subtract)2477 void game_options::add_message_colour_mapping(const string &field,
2478                                               bool prepend, bool subtract)
2479 {
2480     vector<string> cmap = split_string(":", field, true, true, 1);
2481 
2482     if (cmap.size() != 2)
2483         return;
2484 
2485     const int col = str_to_colour(cmap[0]);
2486     msg_colour_type mcol;
2487     if (cmap[0] == "mute")
2488         mcol = MSGCOL_MUTED;
2489     else if (col == -1)
2490         return;
2491     else
2492         mcol = msg_colour(col);
2493 
2494     message_colour_mapping m = { parse_message_filter(cmap[1]), mcol };
2495     if (subtract)
2496         remove_matching(message_colour_mappings, m);
2497     else if (prepend)
2498         message_colour_mappings.insert(message_colour_mappings.begin(), m);
2499     else
2500         message_colour_mappings.push_back(m);
2501 }
2502 
2503 // Option syntax is:
2504 // sort_menu = [menu_type:]yes|no|auto:n[:sort_conditions]
set_menu_sort(string field)2505 void game_options::set_menu_sort(string field)
2506 {
2507     if (field.empty())
2508         return;
2509 
2510     menu_sort_condition cond(field);
2511 
2512     // Overrides all previous settings.
2513     if (cond.mtype == menu_type::any)
2514         sort_menus.clear();
2515 
2516     // Override existing values, if necessary.
2517     for (menu_sort_condition &m_cond : sort_menus)
2518         if (m_cond.mtype == cond.mtype)
2519         {
2520             m_cond.sort = cond.sort;
2521             m_cond.cmp  = cond.cmp;
2522             return;
2523         }
2524 
2525     sort_menus.push_back(cond);
2526 }
2527 
2528 // Lots of things use split parse, for some ^= and += should do different things,
2529 // for others they should not. Split parse just pases them along.
split_parse(const string & s,const string & separator,void (game_options::* add)(const string &,bool),bool prepend)2530 void game_options::split_parse(const string &s, const string &separator,
2531                                void (game_options::*add)(const string &, bool),
2532                                bool prepend)
2533 {
2534     const vector<string> defs = split_string(separator, s);
2535     if (prepend)
2536     {
2537         for ( auto it = defs.rbegin() ; it != defs.rend(); ++it)
2538             (this->*add)(*it, prepend);
2539     }
2540     else
2541     {
2542         for ( auto it = defs.begin() ; it != defs.end(); ++it)
2543             (this->*add)(*it, prepend);
2544     }
2545 }
2546 
set_option_fragment(const string & s,bool)2547 void game_options::set_option_fragment(const string &s, bool /*prepend*/)
2548 {
2549     if (s.empty())
2550         return;
2551 
2552     string::size_type st = s.find(':');
2553     if (st == string::npos)
2554     {
2555         // Boolean option.
2556         if (s[0] == '!')
2557             read_option_line(s.substr(1) + " = false", true);
2558         else
2559             read_option_line(s + " = true", true);
2560     }
2561     else
2562     {
2563         // key:val option.
2564         read_option_line(s.substr(0, st) + " = " + s.substr(st + 1), true);
2565     }
2566 }
2567 
2568 // Not a method of the game_options class since keybindings aren't
2569 // stored in that class.
_bindkey(string field)2570 static void _bindkey(string field)
2571 {
2572     const size_t start_bracket = field.find_first_of('[');
2573     const size_t end_bracket   = field.find_last_of(']');
2574 
2575     if (start_bracket == string::npos
2576         || end_bracket == string::npos
2577         || start_bracket > end_bracket)
2578     {
2579         mprf(MSGCH_ERROR, "Bad bindkey bracketing in '%s'",
2580              field.c_str());
2581         return;
2582     }
2583 
2584     const string key_str = field.substr(start_bracket + 1,
2585                                         end_bracket - start_bracket - 1);
2586     const char *s = key_str.c_str();
2587 
2588     char32_t wc;
2589     vector<char32_t> wchars;
2590     while (int l = utf8towc(&wc, s))
2591     {
2592         s += l;
2593         wchars.push_back(wc);
2594     }
2595 
2596     int key;
2597 
2598     // TODO: Function keys.
2599     if (wchars.size() == 0)
2600     {
2601         mprf(MSGCH_ERROR, "No key in bindkey directive '%s'",
2602              field.c_str());
2603         return;
2604     }
2605     else if (wchars.size() == 1)
2606         key = wchars[0];
2607     else if (wchars.size() == 2)
2608     {
2609         // Ctrl + non-ascii is meaningless here.
2610         if (wchars[0] != '^' || wchars[1] > 127)
2611         {
2612             mprf(MSGCH_ERROR, "Invalid key '%s' in bindkey directive '%s'",
2613                  key_str.c_str(), field.c_str());
2614             return;
2615         }
2616 
2617         key = CONTROL(wchars[1]);
2618     }
2619     else if (wchars[0] == '\\')
2620     {
2621         // does this need to validate non-widechars?
2622         keyseq ks = parse_keyseq(key_str);
2623         if (ks.size() != 1)
2624         {
2625             mprf(MSGCH_ERROR, "Invalid keyseq '%s' in bindkey directive '%s'",
2626                 key_str.c_str(), field.c_str());
2627         }
2628         key = ks[0];
2629     }
2630     else
2631     {
2632         mprf(MSGCH_ERROR, "Invalid key '%s' in bindkey directive '%s'",
2633              key_str.c_str(), field.c_str());
2634         return;
2635     }
2636 
2637     const size_t start_name = field.find_first_not_of(' ', end_bracket + 1);
2638     if (start_name == string::npos)
2639     {
2640         mprf(MSGCH_ERROR, "No command name for bindkey directive '%s'",
2641              field.c_str());
2642         return;
2643     }
2644 
2645     const string       name = field.substr(start_name);
2646     const command_type cmd  = name_to_command(name);
2647     if (cmd == CMD_NO_CMD)
2648     {
2649         mprf(MSGCH_ERROR, "No command named '%s'", name.c_str());
2650         return;
2651     }
2652 
2653     bind_command_to_key(cmd, key);
2654 }
2655 
_is_autopickup_ban(pair<text_pattern,bool> entry)2656 static bool _is_autopickup_ban(pair<text_pattern, bool> entry)
2657 {
2658     return !entry.second;
2659 }
2660 
read_option_line(const string & str,bool runscript)2661 void game_options::read_option_line(const string &str, bool runscript)
2662 {
2663 #define NEWGAME_OPTION(_opt, _conv, _type)                                     \
2664     if (plain)                                                                 \
2665         _opt.clear();                                                          \
2666     for (const auto &part : split_string(",", field))                          \
2667     {                                                                          \
2668         if (minus_equal)                                                       \
2669         {                                                                      \
2670             auto it2 = find(_opt.begin(), _opt.end(), _conv(part));            \
2671             if (it2 != _opt.end())                                             \
2672                 _opt.erase(it2);                                               \
2673         }                                                                      \
2674         else                                                                   \
2675             _opt.push_back(_conv(part));                                       \
2676     }
2677     string key    = "";
2678     string subkey = "";
2679     string field  = "";
2680 
2681     bool plus_equal  = false;
2682     bool caret_equal = false;
2683     bool minus_equal = false;
2684     rc_line_type line_type = RCFILE_LINE_EQUALS;
2685 
2686     const int first_equals = str.find('=');
2687 
2688     // all lines with no equal-signs we ignore
2689     if (first_equals < 0)
2690         return;
2691 
2692     field = str.substr(first_equals + 1);
2693     field = expand_vars(field);
2694 
2695     string prequal = trimmed_string(str.substr(0, first_equals));
2696 
2697     // Is this a case of key += val?
2698     if (prequal.length() && prequal[prequal.length() - 1] == '+')
2699     {
2700         plus_equal = true;
2701         line_type = RCFILE_LINE_PLUS;
2702         prequal = prequal.substr(0, prequal.length() - 1);
2703         trim_string(prequal);
2704     }
2705     else if (prequal.length() && prequal[prequal.length() - 1] == '-')
2706     {
2707         minus_equal = true;
2708         line_type = RCFILE_LINE_MINUS;
2709         prequal = prequal.substr(0, prequal.length() - 1);
2710         trim_string(prequal);
2711     }
2712     else if (prequal.length() && prequal[prequal.length() - 1] == '^')
2713     {
2714         caret_equal = true;
2715         line_type = RCFILE_LINE_CARET;
2716         prequal = prequal.substr(0, prequal.length() - 1);
2717         trim_string(prequal);
2718     }
2719     else if (prequal.length() && prequal[prequal.length() - 1] == ':')
2720     {
2721         prequal = prequal.substr(0, prequal.length() - 1);
2722         trim_string(prequal);
2723         trim_string(field);
2724 
2725         add_alias(prequal, field);
2726         return;
2727     }
2728 
2729     bool plain = !plus_equal && !minus_equal && !caret_equal;
2730 
2731     prequal = unalias(prequal);
2732 
2733     const string::size_type first_dot = prequal.find('.');
2734     if (first_dot != string::npos)
2735     {
2736         key    = prequal.substr(0, first_dot);
2737         subkey = prequal.substr(first_dot + 1);
2738     }
2739     else
2740     {
2741         // no subkey (dots are okay in value field)
2742         key    = prequal;
2743     }
2744 
2745     // Clean up our data...
2746     lowercase(trim_string(key));
2747     lowercase(trim_string(subkey));
2748 
2749     // some fields want capitals... none care about external spaces
2750     trim_string(field);
2751 
2752     // Keep unlowercased field around
2753     const string orig_field = field;
2754 
2755     if (key != "name" && key != "crawl_dir" && key != "macro_dir"
2756         && key != "combo"
2757         && key != "species" && key != "background" && key != "job"
2758         && key != "race" && key != "class" && key != "ban_pickup"
2759         && key != "autopickup_exceptions"
2760         && key != "explore_stop_pickup_ignore"
2761         && key != "stop_travel"
2762         && key != "force_more_message"
2763         && key != "flash_screen_message"
2764         && key != "confirm_action"
2765         && key != "drop_filter" && key != "lua_file" && key != "terp_file"
2766         && key != "note_items" && key != "autoinscribe"
2767         && key != "note_monsters" && key != "note_messages"
2768         && key != "display_char" && !starts_with(key, "cset") // compatibility
2769         && key != "dungeon" && key != "feature"
2770         && key != "mon_glyph" && key != "item_glyph"
2771         && key != "fire_items_start"
2772         && key != "opt" && key != "option"
2773         && key != "menu_colour" && key != "menu_color"
2774         && key != "message_colour" && key != "message_color"
2775         && key != "levels" && key != "level" && key != "entries"
2776         && key != "include" && key != "bindkey"
2777         && key != "spell_slot"
2778         && key != "item_slot"
2779         && key != "ability_slot"
2780         && key != "sound" && key != "hold_sound" && key != "sound_file_path"
2781         && key.find("font") == string::npos)
2782     {
2783         lowercase(field);
2784     }
2785 
2786     GameOption *const *option = map_find(options_by_name, key);
2787     if (option)
2788     {
2789         const string error = (*option)->loadFromString(field, line_type);
2790         if (!error.empty())
2791             report_error("%s", error.c_str());
2792     }
2793     else if (key == "include")
2794         include(field, true, runscript);
2795     else if (key == "opt" || key == "option")
2796         split_parse(field, ",", &game_options::set_option_fragment);
2797     else if (key == "autopickup")
2798     {
2799         // clear out autopickup
2800         autopickups.reset();
2801 
2802         char32_t c;
2803         for (const char* tp = field.c_str(); int s = utf8towc(&c, tp); tp += s)
2804         {
2805             object_class_type type = item_class_by_sym(c);
2806 
2807             if (type < NUM_OBJECT_CLASSES)
2808                 autopickups.set(type);
2809             else
2810                 report_error("Bad object type '%*s' for autopickup.\n", s, tp);
2811         }
2812     }
2813 #if !defined(DGAMELAUNCH) || defined(DGL_REMEMBER_NAME)
2814     else if (key == "name")
2815     {
2816         // field is already cleaned up from trim_string()
2817         game.name = field;
2818     }
2819 #endif
2820     else if (key == "char_set")
2821     {
2822         if (field == "ascii")
2823             char_set = CSET_ASCII;
2824         else if (field == "default")
2825             char_set = CSET_DEFAULT;
2826         else
2827             report_error("Bad character set, using default: %s\n", field.c_str());
2828     }
2829     else if (key == "language")
2830     {
2831         if (!set_lang(field.c_str()))
2832         {
2833             report_error("No translations for language '%s'.\n"
2834                          "Languages with at least partial translation: %s",
2835                          field.c_str(), _supported_language_listing().c_str());
2836         }
2837     }
2838     else if (key == "fake_lang")
2839         set_fake_langs(field);
2840     else if (key == "default_autopickup")
2841     {
2842         if (read_bool(field, true))
2843             autopickup_on = 1;
2844         else
2845             autopickup_on = 0;
2846     }
2847     else if (key == "easy_confirm")
2848     {
2849         // decide when to allow both 'Y'/'N' and 'y'/'n' on yesno() prompts
2850         if (field == "none")
2851             easy_confirm = easy_confirm_type::none;
2852         else if (field == "safe")
2853             easy_confirm = easy_confirm_type::safe;
2854         else if (field == "all")
2855             easy_confirm = easy_confirm_type::all;
2856     }
2857     else if (key == "allow_self_target")
2858     {
2859         if (field == "yes")
2860             allow_self_target = confirm_prompt_type::none;
2861         else if (field == "no")
2862             allow_self_target = confirm_prompt_type::cancel;
2863         else if (field == "prompt")
2864             allow_self_target = confirm_prompt_type::prompt;
2865     }
2866     else if (key == "lua_file" && runscript)
2867     {
2868 #ifdef CLUA_BINDINGS
2869         clua.execfile(field.c_str(), false, false);
2870         if (!clua.error.empty())
2871             mprf(MSGCH_ERROR, "Lua error: %s", clua.error.c_str());
2872 #else
2873         mprf(MSGCH_ERROR, "lua_file failed: clua not enabled on this build!");
2874 #endif
2875     }
2876     else if (key == "terp_file" && runscript)
2877         terp_files.push_back(field);
2878     else if (key == "colour" || key == "color")
2879     {
2880         const int orig_col   = str_to_colour(subkey);
2881         const int result_col = str_to_colour(field);
2882 
2883         if (orig_col != -1 && result_col != -1)
2884             colour[orig_col] = result_col;
2885         else
2886         {
2887             report_error("Bad colour -- %s=%d or %s=%d\n",
2888                      subkey.c_str(), orig_col, field.c_str(), result_col);
2889         }
2890     }
2891     else if (key == "channel")
2892     {
2893         const int chnl = str_to_channel(subkey);
2894         const msg_colour_type col  = _str_to_channel_colour(field);
2895 
2896         if (chnl != -1 && col != MSGCOL_NONE)
2897             channels[chnl] = col;
2898         else if (chnl == -1)
2899             report_error("Bad channel -- %s", subkey.c_str());
2900         else if (col == MSGCOL_NONE)
2901             report_error("Bad colour -- %s", field.c_str());
2902     }
2903     else if (key == "use_animations")
2904     {
2905         if (plain)
2906             use_animations = UA_ALWAYS_ON;
2907 
2908         const auto new_animations = read_use_animations(field);
2909         if (minus_equal)
2910             use_animations &= ~new_animations;
2911         else
2912             use_animations |= new_animations;
2913     }
2914     else if (starts_with(key, interrupt_prefix))
2915     {
2916         set_activity_interrupt(key.substr(interrupt_prefix.length()),
2917                                field,
2918                                plus_equal || caret_equal,
2919                                minus_equal);
2920     }
2921     else if (key == "display_char"
2922              || starts_with(key, "cset")) // compatibility with old rcfiles
2923     {
2924         for (const string &over : split_string(",", field))
2925         {
2926             vector<string> mapping = split_string(":", over);
2927             if (mapping.size() != 2)
2928                 continue;
2929 
2930             dungeon_char_type dc = dchar_by_name(mapping[0]);
2931             if (dc == NUM_DCHAR_TYPES)
2932                 continue;
2933 
2934             add_cset_override(dc, read_symbol(mapping[1]));
2935         }
2936     }
2937     else if (key == "feature" || key == "dungeon")
2938     {
2939         if (plain)
2940            clear_feature_overrides();
2941 
2942         if (minus_equal)
2943             split_parse(field, ";", &game_options::remove_feature_override);
2944         else
2945             split_parse(field, ";", &game_options::add_feature_override);
2946     }
2947     else if (key == "mon_glyph")
2948     {
2949         if (plain)
2950            mon_glyph_overrides.clear();
2951 
2952         if (minus_equal)
2953             split_parse(field, ",", &game_options::remove_mon_glyph_override);
2954         else
2955             split_parse(field, ",", &game_options::add_mon_glyph_override);
2956     }
2957     else if (key == "item_glyph")
2958     {
2959         if (plain)
2960         {
2961             item_glyph_overrides.clear();
2962             item_glyph_cache.clear();
2963         }
2964 
2965         if (minus_equal)
2966             split_parse(field, ",", &game_options::remove_item_glyph_override);
2967         else
2968             split_parse(field, ",", &game_options::add_item_glyph_override, caret_equal);
2969     }
2970     else if (key == "arena_teams")
2971         game.arena_teams = field;
2972     // [ds] Allow changing map only if the map hasn't been set on the
2973     // command-line.
2974     else if (key == "map" && crawl_state.sprint_map.empty())
2975         game.map = field;
2976     // [ds] For dgamelaunch setups, the player should *not* be able to
2977     // set game type in their rc; the only way to set game type for
2978     // DGL builds should be the command-line options.
2979     else if (key == "type")
2980     {
2981 #if defined(DGAMELAUNCH)
2982         game.type = Options.game.type;
2983 #else
2984         game.type = _str_to_gametype(field);
2985 #endif
2986     }
2987     else if (key == "combo")
2988     {
2989         game.allowed_species.clear();
2990         game.allowed_jobs.clear();
2991         game.allowed_weapons.clear();
2992         NEWGAME_OPTION(game.allowed_combos, string, string);
2993     }
2994     else if (key == "fully_random")
2995         game.fully_random = read_bool(field, game.fully_random);
2996     else if (key == "species" || key == "race")
2997     {
2998         game.allowed_combos.clear();
2999         NEWGAME_OPTION(game.allowed_species, _str_to_species,
3000                        species_type);
3001     }
3002     else if (key == "background" || key == "job" || key == "class")
3003     {
3004         game.allowed_combos.clear();
3005         NEWGAME_OPTION(game.allowed_jobs, str_to_job, job_type);
3006     }
3007     else if (key == "weapon")
3008     {
3009         // Choose this weapon for backgrounds that get choice.
3010         game.allowed_combos.clear();
3011         NEWGAME_OPTION(game.allowed_weapons, str_to_weapon, weapon_type);
3012     }
3013     else if (key == "fire_items_start")
3014     {
3015         if (isaalpha(field[0]))
3016             fire_items_start = letter_to_index(field[0]);
3017         else
3018             report_error("Bad fire item start index: %s\n", field.c_str());
3019     }
3020     else if (key == "assign_item_slot")
3021     {
3022         if (field == "forward")
3023             assign_item_slot = SS_FORWARD;
3024         else if (field == "backward")
3025             assign_item_slot = SS_BACKWARD;
3026     }
3027 #ifndef DGAMELAUNCH
3028     else if (key == "restart_after_game")
3029         restart_after_game = read_maybe_bool(field);
3030 #endif
3031     else if (key == "show_god_gift")
3032     {
3033         if (field == "yes")
3034             show_god_gift = MB_TRUE;
3035         else if (field == "unid" || field == "unident" || field == "unidentified")
3036             show_god_gift = MB_MAYBE;
3037         else if (field == "no")
3038             show_god_gift = MB_FALSE;
3039         else
3040             report_error("Unknown show_god_gift value: %s\n", field.c_str());
3041     }
3042     else if (key == "fire_order")
3043         set_fire_order(field, plus_equal, caret_equal);
3044     else if (key == "fire_order_spell" && runscript)
3045         set_fire_order_spell(field, plus_equal || caret_equal, minus_equal);
3046     else if (key == "fire_order_ability" && runscript)
3047         set_fire_order_ability(field, plus_equal || caret_equal, minus_equal);
3048 #ifndef DGAMELAUNCH
3049     // If DATA_DIR_PATH is set, don't set crawl_dir from .crawlrc.
3050 #ifndef DATA_DIR_PATH
3051     else if (key == "crawl_dir")
3052     {
3053         // We shouldn't bother to allocate this a second time
3054         // if the user puts two crawl_dir lines in the init file.
3055         SysEnv.crawl_dir = field;
3056     }
3057 #endif
3058 #ifndef SAVE_DIR_PATH
3059     else if (key == "save_dir")
3060     {
3061         save_dir = field;
3062 #ifndef SHARED_DIR_PATH
3063         shared_dir = save_dir;
3064 #endif
3065     }
3066     else if (key == "macro_dir")
3067         macro_dir = field;
3068 #endif
3069 #endif
3070     else if (key == "view_lock")
3071     {
3072         const bool lock = read_bool(field, true);
3073         view_lock_x = view_lock_y = lock;
3074     }
3075     else if (key == "scroll_margin")
3076     {
3077         int scrollmarg = atoi(field.c_str());
3078         if (scrollmarg < 0)
3079             scrollmarg = 0;
3080         scroll_margin_x = scroll_margin_y = scrollmarg;
3081     }
3082     else if (key == "user_note_prefix")
3083     {
3084         // field is already cleaned up from trim_string()
3085         user_note_prefix = orig_field;
3086     }
3087     else if (key == "skill_focus")
3088     {
3089         if (field == "toggle")
3090             skill_focus = SKM_FOCUS_TOGGLE;
3091         else if (read_bool(field, true))
3092             skill_focus = SKM_FOCUS_ON;
3093         else
3094             skill_focus = SKM_FOCUS_OFF;
3095     }
3096     else if (key == "flush")
3097     {
3098         if (subkey == "failure")
3099         {
3100             flush_input[FLUSH_ON_FAILURE]
3101                 = read_bool(field, flush_input[FLUSH_ON_FAILURE]);
3102         }
3103         else if (subkey == "command")
3104         {
3105             flush_input[FLUSH_BEFORE_COMMAND]
3106                 = read_bool(field, flush_input[FLUSH_BEFORE_COMMAND]);
3107         }
3108         else if (subkey == "message")
3109         {
3110             flush_input[FLUSH_ON_MESSAGE]
3111                 = read_bool(field, flush_input[FLUSH_ON_MESSAGE]);
3112         }
3113         else if (subkey == "lua")
3114         {
3115             flush_input[FLUSH_LUA]
3116                 = read_bool(field, flush_input[FLUSH_LUA]);
3117         }
3118     }
3119     else if (key == "wiz_mode")
3120     {
3121         // wiz_mode is recognised as a legal key in all compiles -- bwr
3122 #ifdef WIZARD
3123     #ifndef DGAMELAUNCH
3124         if (field == "never")
3125             wiz_mode = WIZ_NEVER;
3126         else if (field == "no")
3127             wiz_mode = WIZ_NO;
3128         else if (field == "yes")
3129             wiz_mode = WIZ_YES;
3130         else
3131             report_error("Unknown wiz_mode option: %s\n", field.c_str());
3132     #endif
3133 #endif
3134     }
3135     else if (key == "explore_mode")
3136     {
3137 #ifdef WIZARD
3138     #ifndef DGAMELAUNCH
3139         if (field == "never")
3140             explore_mode = WIZ_NEVER;
3141         else if (field == "no")
3142             explore_mode = WIZ_NO;
3143         else if (field == "yes")
3144             explore_mode = WIZ_YES;
3145         else
3146             report_error("Unknown explore_mode option: %s\n", field.c_str());
3147     #endif
3148 #endif
3149     }
3150     else if (key == "ban_pickup")
3151     {
3152         // Only remove negative, not positive, exceptions.
3153         if (plain)
3154             erase_if(force_autopickup, _is_autopickup_ban);
3155 
3156         vector<pair<text_pattern, bool> > new_entries;
3157         for (const string &s : split_string(",", field))
3158         {
3159             if (s.empty())
3160                 continue;
3161 
3162             const pair<text_pattern, bool> f_a(s, false);
3163 
3164             if (minus_equal)
3165                 remove_matching(force_autopickup, f_a);
3166             else
3167                 new_entries.push_back(f_a);
3168         }
3169         merge_lists(force_autopickup, new_entries, caret_equal);
3170     }
3171     else if (key == "autopickup_exceptions")
3172     {
3173         if (plain)
3174             force_autopickup.clear();
3175 
3176         vector<pair<text_pattern, bool> > new_entries;
3177         for (const string &s : split_string(",", field))
3178         {
3179             if (s.empty())
3180                 continue;
3181 
3182             pair<text_pattern, bool> f_a;
3183 
3184             if (s[0] == '>')
3185                 f_a = make_pair(s.substr(1), false);
3186             else if (s[0] == '<')
3187                 f_a = make_pair(s.substr(1), true);
3188             else
3189                 f_a = make_pair(s, false);
3190 
3191             if (minus_equal)
3192                 remove_matching(force_autopickup, f_a);
3193             else
3194                 new_entries.push_back(f_a);
3195         }
3196         merge_lists(force_autopickup, new_entries, caret_equal);
3197     }
3198 #ifndef _MSC_VER
3199     // break if-else chain on broken Microsoft compilers with stupid nesting limits
3200     else
3201 #endif
3202 
3203     if (key == "autoinscribe")
3204     {
3205         if (plain)
3206             autoinscriptions.clear();
3207 
3208         const size_t first = field.find_first_of(':');
3209         const size_t last  = field.find_last_of(':');
3210         if (first == string::npos || first != last)
3211         {
3212             return report_error("Autoinscribe string must have exactly "
3213                                 "one colon: %s\n", field.c_str());
3214         }
3215 
3216         if (first == 0)
3217         {
3218             report_error("Autoinscribe pattern is empty: %s\n", field.c_str());
3219             return;
3220         }
3221 
3222         if (last == field.length() - 1)
3223         {
3224             report_error("Autoinscribe result is empty: %s\n", field.c_str());
3225             return;
3226         }
3227 
3228         vector<string> thesplit = split_string(":", field);
3229 
3230         if (thesplit.size() != 2)
3231         {
3232             report_error("Error parsing autoinscribe string: %s\n",
3233                          field.c_str());
3234             return;
3235         }
3236 
3237         pair<text_pattern,string> entry(thesplit[0], thesplit[1]);
3238 
3239         if (minus_equal)
3240             remove_matching(autoinscriptions, entry);
3241         else if (caret_equal)
3242             autoinscriptions.insert(autoinscriptions.begin(), entry);
3243         else
3244             autoinscriptions.push_back(entry);
3245     }
3246     else if (key == "enemy_hp_colour" || key == "enemy_hp_color")
3247     {
3248         if (plain)
3249             enemy_hp_colour.clear();
3250         str_to_enemy_hp_colour(field, caret_equal);
3251     }
3252     else if (key == "monster_list_colour" || key == "monster_list_color")
3253     {
3254         if (plain)
3255             clear_monster_list_colours();
3256 
3257         vector<string> thesplit = split_string(",", field);
3258         for (unsigned i = 0; i < thesplit.size(); ++i)
3259         {
3260             vector<string> insplit = split_string(":", thesplit[i]);
3261 
3262             if (insplit.empty() || insplit.size() > 2
3263                  || insplit.size() == 1 && !minus_equal
3264                  || insplit.size() == 2 && minus_equal)
3265             {
3266                 report_error("Bad monster_list_colour string: %s\n",
3267                              field.c_str());
3268                 break;
3269             }
3270 
3271             const int scolour = minus_equal ? -1 : str_to_colour(insplit[1]);
3272 
3273             // No elemental colours!
3274             if (scolour >= 16 || scolour < 0 && !minus_equal)
3275             {
3276                 report_error("Bad monster_list_colour: %s", insplit[1].c_str());
3277                 break;
3278             }
3279             if (!set_monster_list_colour(insplit[0], scolour))
3280             {
3281                 report_error("Bad monster_list_colour key: %s\n",
3282                              insplit[0].c_str());
3283                 break;
3284             }
3285         }
3286     }
3287 
3288     else if (key == "note_skill_levels")
3289     {
3290         if (plain)
3291             note_skill_levels.reset();
3292         vector<string> thesplit = split_string(",", field);
3293         for (unsigned i = 0; i < thesplit.size(); ++i)
3294         {
3295             int num = atoi(thesplit[i].c_str());
3296             if (num > 0 && num <= 27)
3297                 note_skill_levels.set(num, !minus_equal);
3298             else
3299             {
3300                 report_error("Bad skill level to note -- %s\n",
3301                              thesplit[i].c_str());
3302                 continue;
3303             }
3304         }
3305     }
3306     else if (key == "force_targeter")
3307     {
3308         // first pass through the rc file happens before the spell name cache
3309         // is initialized, just skip it
3310         if (spell_data_initialized())
3311         {
3312             if (plain)
3313             {
3314                 always_use_static_targeters = false;
3315                 force_targeter.clear();
3316             }
3317 
3318             if (minus_equal)
3319                 split_parse(field, ",", &game_options::remove_force_targeter);
3320             else
3321                 split_parse(field, ",", &game_options::add_force_targeter);
3322         }
3323     }
3324     else if (key == "spell_slot"
3325              || key == "item_slot"
3326              || key == "ability_slot")
3327 
3328     {
3329         auto& auto_letters = (key == "item_slot"  ? auto_item_letters
3330                            : (key == "spell_slot" ? auto_spell_letters
3331                                                   : auto_ability_letters));
3332         if (plain)
3333             auto_letters.clear();
3334 
3335         vector<string> thesplit = split_string(":", field);
3336         if (thesplit.size() != 2)
3337         {
3338             return report_error("Error parsing %s string: %s\n",
3339                                 key.c_str(), field.c_str());
3340         }
3341         pair<text_pattern,string> entry(text_pattern(thesplit[0], true),
3342                                         thesplit[1]);
3343 
3344         if (minus_equal)
3345             remove_matching(auto_letters, entry);
3346         else if (caret_equal)
3347             auto_letters.insert(auto_letters.begin(), entry);
3348         else
3349             auto_letters.push_back(entry);
3350     }
3351     else if (key == "sort_menus")
3352     {
3353         for (const string &frag : split_string(";", field))
3354             if (!frag.empty())
3355                 set_menu_sort(frag);
3356     }
3357     else if (key == "force_more_message" || key == "flash_screen_message")
3358     {
3359         vector<message_filter> &filters = (key == "force_more_message" ? force_more_message : flash_screen_message);
3360         if (plain)
3361             filters.clear();
3362 
3363         vector<message_filter> new_entries;
3364         for (const string &fragment : split_string(",", field))
3365         {
3366             if (fragment.empty())
3367                 continue;
3368 
3369             message_filter mf(fragment);
3370 
3371             string::size_type pos = fragment.find(":");
3372             if (pos && pos != string::npos)
3373             {
3374                 string prefix = fragment.substr(0, pos);
3375                 int channel = str_to_channel(prefix);
3376                 if (channel != -1 || prefix == "any")
3377                 {
3378                     string s = fragment.substr(pos + 1);
3379                     mf = message_filter(channel, trim_string(s));
3380                 }
3381             }
3382 
3383             if (minus_equal)
3384                 remove_matching(filters, mf);
3385             else
3386                 new_entries.push_back(mf);
3387         }
3388         merge_lists(filters, new_entries, caret_equal);
3389     }
3390     else if (key == "travel_avoid_terrain")
3391     {
3392         // TODO: allow resetting (need reset_forbidden_terrain())
3393         for (const string &seg : split_string(",", field))
3394             prevent_travel_to(seg);
3395     }
3396     else if (key == "explore_stop")
3397     {
3398         if (plain)
3399             explore_stop = ES_NONE;
3400 
3401         const int new_conditions = read_explore_stop_conditions(field);
3402         if (minus_equal)
3403             explore_stop &= ~new_conditions;
3404         else
3405             explore_stop |= new_conditions;
3406     }
3407     else if (key == "sound" || key == "hold_sound")
3408     {
3409         if (plain)
3410             sound_mappings.clear();
3411 
3412         vector<sound_mapping> new_entries;
3413         for (const string &sub : split_string(",", field))
3414         {
3415             string::size_type cpos = sub.find(":", 0);
3416             if (cpos != string::npos)
3417             {
3418                 sound_mapping entry;
3419                 entry.pattern = sub.substr(0, cpos);
3420                 entry.soundfile = sound_file_path + sub.substr(cpos + 1);
3421                 if (key == "hold_sound")
3422                     entry.interrupt_game = true;
3423                 else
3424                     entry.interrupt_game = false;
3425 
3426                 if (minus_equal)
3427                     remove_matching(sound_mappings, entry);
3428                 else
3429                     new_entries.push_back(entry);
3430             }
3431         }
3432         merge_lists(sound_mappings, new_entries, caret_equal);
3433     }
3434 #ifndef TARGET_COMPILER_VC
3435     // MSVC has a limit on how many if/else if can be chained together.
3436     else
3437 #endif
3438     if (key == "menu_colour" || key == "menu_color")
3439     {
3440         if (plain)
3441             menu_colour_mappings.clear();
3442 
3443         vector<colour_mapping> new_entries;
3444         for (const string &seg : split_string(",", field))
3445         {
3446             // Format is "tag:colour:pattern" or "colour:pattern" (default tag).
3447             // FIXME: arrange so that you can use ':' inside a pattern
3448             vector<string> subseg = split_string(":", seg, false);
3449             string tagname, patname, colname;
3450             if (subseg.size() < 2)
3451                 continue;
3452             if (subseg.size() >= 3)
3453             {
3454                 tagname = subseg[0];
3455                 colname = subseg[1];
3456                 patname = subseg[2];
3457             }
3458             else
3459             {
3460                 colname = subseg[0];
3461                 patname = subseg[1];
3462             }
3463 
3464             colour_mapping mapping;
3465             mapping.tag     = tagname;
3466             mapping.pattern = patname;
3467             const int col = str_to_colour(colname);
3468             mapping.colour = col;
3469 
3470             if (col == -1)
3471                 continue;
3472             else if (minus_equal)
3473                 remove_matching(menu_colour_mappings, mapping);
3474             else
3475                 new_entries.push_back(mapping);
3476         }
3477         merge_lists(menu_colour_mappings, new_entries, caret_equal);
3478     }
3479     else if (key == "message_colour" || key == "message_color")
3480     {
3481         // TODO: support -= here.
3482         if (plain)
3483             message_colour_mappings.clear();
3484 
3485         add_message_colour_mappings(field, caret_equal, minus_equal);
3486     }
3487     else if (key == "dump_order")
3488     {
3489         if (plain)
3490         {
3491             dump_fields.clear();
3492             dump_order.clear();
3493         }
3494 
3495         new_dump_fields(field, !minus_equal, caret_equal);
3496     }
3497     else if (key == "dump_kill_places")
3498     {
3499         dump_kill_places = (field == "none" ? KDO_NO_PLACES :
3500                             field == "all"  ? KDO_ALL_PLACES
3501                                             : KDO_ONE_PLACE);
3502     }
3503     else if (key == "kill_map")
3504     {
3505         // TODO: treat this as a map option (e.g. kill_map.you = friendly)
3506         if (plain && field.empty())
3507         {
3508             kill_map[KC_YOU] = KC_YOU;
3509             kill_map[KC_FRIENDLY] = KC_FRIENDLY;
3510             kill_map[KC_OTHER] = KC_OTHER;
3511         }
3512 
3513         for (const string &s : split_string(",", field))
3514         {
3515             string::size_type cpos = s.find(":", 0);
3516             if (cpos != string::npos)
3517             {
3518                 string from = s.substr(0, cpos);
3519                 string to   = s.substr(cpos + 1);
3520                 do_kill_map(from, to);
3521             }
3522         }
3523     }
3524     else if (key == "dump_item_origins")
3525     {
3526         if (plain)
3527             dump_item_origins = IODS_PRICE;
3528 
3529         for (const string &ch : split_string(",", field))
3530         {
3531             if (ch == "artefacts" || ch == "artifacts"
3532                 || ch == "artefact" || ch == "artifact")
3533             {
3534                 dump_item_origins |= IODS_ARTEFACTS;
3535             }
3536             else if (ch == "ego_arm" || ch == "ego armour"
3537                      || ch == "ego_armour" || ch == "ego armor"
3538                      || ch == "ego_armor")
3539             {
3540                 dump_item_origins |= IODS_EGO_ARMOUR;
3541             }
3542             else if (ch == "ego_weap" || ch == "ego weapon"
3543                      || ch == "ego_weapon" || ch == "ego weapons"
3544                      || ch == "ego_weapons")
3545             {
3546                 dump_item_origins |= IODS_EGO_WEAPON;
3547             }
3548             else if (ch == "jewellery" || ch == "jewelry")
3549                 dump_item_origins |= IODS_JEWELLERY;
3550             else if (ch == "runes")
3551                 dump_item_origins |= IODS_RUNES;
3552             else if (ch == "staves")
3553                 dump_item_origins |= IODS_STAVES;
3554             else if (ch == "books")
3555                 dump_item_origins |= IODS_BOOKS;
3556             else if (ch == "all" || ch == "everything")
3557                 dump_item_origins = IODS_EVERYTHING;
3558         }
3559     }
3560     else if (key == "additional_macro_file")
3561     {
3562         // TODO: this option could probably be improved. For now, keep the
3563         // "= means append" behaviour, and don't allow clearing the list;
3564         // if we rename to "additional_macro_files" then it could work like
3565         // other list options.
3566         const string resolved = resolve_include(orig_field, "macro ");
3567         if (!resolved.empty())
3568             additional_macro_files.push_back(resolved);
3569     }
3570     else if (key == "macros")
3571     {
3572         // orig_field because this function wants capitals
3573         const string possible_error = read_rc_file_macro(orig_field);
3574 
3575         if (!possible_error.empty())
3576             report_error(possible_error.c_str(), orig_field.c_str());
3577     }
3578 #ifdef USE_TILE
3579 #ifdef USE_TILE_LOCAL
3580     else if (key == "tile_full_screen")
3581     {
3582         const maybe_bool fs_val = read_maybe_bool(field);
3583         if (fs_val == MB_TRUE)
3584             tile_full_screen = SCREENMODE_FULL;
3585         else if (fs_val == MB_FALSE)
3586             tile_full_screen = SCREENMODE_WINDOW;
3587         else
3588             tile_full_screen = SCREENMODE_AUTO;
3589     }
3590 #endif // USE_TILE_LOCAL
3591 #ifdef TOUCH_UI
3592     else if (key == "tile_use_small_layout")
3593         tile_use_small_layout = read_maybe_bool(field);
3594 #endif
3595     else if (key == "tile_show_player_species" && field == "true")
3596     {
3597         field = "playermons";
3598         set_player_tile(field);
3599     }
3600     else if (key == "tile_player_tile")
3601         set_player_tile(field);
3602     else if (key == "tile_weapon_offsets")
3603         set_tile_offsets(field, false);
3604     else if (key == "tile_shield_offsets")
3605         set_tile_offsets(field, true);
3606     else if (key == "tile_tag_pref")
3607         tile_tag_pref = _str_to_tag_pref(field.c_str());
3608 #ifdef USE_TILE_WEB
3609     else if (key == "tile_display_mode")
3610     {
3611         if (field == "tiles" || field == "glyphs" || field == "hybrid")
3612             tile_display_mode = field;
3613         else
3614         {
3615             mprf(MSGCH_ERROR, "Unknown value for tile_display_mode: '%s'"
3616                               " (possible values: tiles/glyphs/hybrid",
3617                                                                 field.c_str());
3618         }
3619     }
3620 #endif
3621 #endif // USE_TILE
3622 
3623     else if (key == "bindkey")
3624         _bindkey(field);
3625     else if (key == "constant")
3626     {
3627         if (!variables.count(field))
3628             report_error("No variable named '%s' to make constant", field.c_str());
3629         else if (constants.count(field))
3630             report_error("'%s' is already a constant", field.c_str());
3631         else
3632             constants.insert(field);
3633     }
3634     else if (key == "game_seed")
3635     {
3636         // special handling because of the large type.
3637         uint64_t tmp_seed = 0;
3638         if (sscanf(field.c_str(), "%" SCNu64, &tmp_seed))
3639         {
3640             // seed_from_rc is only ever set here, or by the CLO. The CLO gets
3641             // first crack, so don't overwrite it here.
3642             if (!seed_from_rc)
3643                 seed_from_rc = tmp_seed;
3644         }
3645     }
3646     else if (key == "pregen_dungeon")
3647     {
3648         // TODO: probably convert the underlying values to some kind of enum
3649         // TODO: store these options in a save?
3650         if (field == "true" || field == "full")
3651         {
3652 #ifdef DGAMELAUNCH
3653             report_error(
3654                 "Full pregeneration is not allowed on this build of crawl.");
3655 #else
3656             pregen_dungeon = true;
3657             incremental_pregen = true; // still affects loading games not
3658                                        // started with full pregen
3659 #endif
3660         }
3661         else if (field == "incremental")
3662         {
3663             pregen_dungeon = false;
3664             incremental_pregen = true;
3665         }
3666         else if (field == "false" || field == "classic")
3667             pregen_dungeon = incremental_pregen = false;
3668         else
3669         {
3670             report_error("Unknown value '%s' for pregen_dungeon.",
3671                                                             field.c_str());
3672         }
3673     }
3674 #ifdef USE_TILE
3675     // TODO: generalize these to an option type?
3676     else if (key == "tile_viewport_scale")
3677     {
3678         float tmp_scale;
3679         if (sscanf(field.c_str(), "%f", &tmp_scale))
3680         {
3681             tile_viewport_scale = min(1600, max(20,
3682                                         static_cast<int>(tmp_scale * 100)));
3683         }
3684         else
3685         {
3686             report_error("Expected a decimal value for tile_viewport_scale,"
3687                 " but got '%s'.", field.c_str());
3688         }
3689     }
3690     else if (key == "tile_map_scale")
3691     {
3692         float tmp_scale;
3693         if (sscanf(field.c_str(), "%f", &tmp_scale))
3694         {
3695             tile_map_scale = min(1600, max(20,
3696                                         static_cast<int>(tmp_scale * 100)));
3697         }
3698         else
3699         {
3700             report_error("Expected a decimal value for tile_map_scale,"
3701                 " but got '%s'.", field.c_str());
3702         }
3703     }
3704 #endif
3705 
3706     // Catch-all else, copies option into map
3707     else if (runscript)
3708     {
3709         int setmode = 0;
3710         if (plus_equal)
3711             setmode = 1;
3712         if (minus_equal)
3713             setmode = -1;
3714         if (caret_equal)
3715             setmode = 2;
3716 
3717         if (!clua.callbooleanfn(false, "c_process_lua_option", "ssd",
3718                         key.c_str(), orig_field.c_str(), setmode))
3719         {
3720             if (!clua.error.empty())
3721                 mprf(MSGCH_ERROR, "Lua error: %s", clua.error.c_str());
3722             named_options[key] = orig_field;
3723         }
3724     }
3725 }
3726 
3727 static const map<string, flang_t> fake_lang_names = {
3728     { "dwarven", flang_t::dwarven },
3729     { "dwarf", flang_t::dwarven },
3730 
3731     { "jäger", flang_t::jagerkin },
3732     { "jägerkin", flang_t::jagerkin },
3733     { "jager", flang_t::jagerkin },
3734     { "jagerkin", flang_t::jagerkin },
3735     { "jaeger", flang_t::jagerkin },
3736     { "jaegerkin", flang_t::jagerkin },
3737 
3738     // Due to a historical conflict with actual german, slang names are
3739     // supported. Not the really rude ones, though.
3740     { "de", flang_t::kraut },
3741     { "german", flang_t::kraut },
3742     { "kraut", flang_t::kraut },
3743     { "jerry", flang_t::kraut },
3744     { "fritz", flang_t::kraut },
3745 
3746     { "futhark", flang_t::futhark },
3747     { "runes", flang_t::futhark },
3748     { "runic", flang_t::futhark },
3749 
3750     { "wide", flang_t::wide },
3751     { "doublewidth", flang_t::wide },
3752     { "fullwidth", flang_t::wide },
3753 
3754     { "grunt", flang_t::grunt },
3755     { "sgrunt", flang_t::grunt },
3756     { "!!!", flang_t::grunt },
3757 
3758     { "butt", flang_t::butt },
3759     { "buttbot", flang_t::butt },
3760     { "tef", flang_t::butt },
3761 };
3762 
3763 struct language_def
3764 {
3765     lang_t lang;
3766     const char *code;
3767     set<string> names;
3768 };
3769 
3770 static const language_def lang_data[] =
3771 {
3772     // Use null, not "en", for English so we don't try to look up translations.
3773     { lang_t::EN, nullptr, { "english", "en", "c" } },
3774     { lang_t::CS, "cs", { "czech", "český", "cesky" } },
3775     { lang_t::DA, "da", { "danish", "dansk" } },
3776     { lang_t::DE, "de", { "german", "deutsch" } },
3777     { lang_t::EL, "el", { "greek", "ελληνικά", "ελληνικα" } },
3778     { lang_t::ES, "es", { "spanish", "español", "espanol" } },
3779     { lang_t::FI, "fi", { "finnish", "suomi" } },
3780     { lang_t::FR, "fr", { "french", "français", "francais" } },
3781     { lang_t::HU, "hu", { "hungarian", "magyar" } },
3782     { lang_t::IT, "it", { "italian", "italiano" } },
3783     { lang_t::JA, "ja", { "japanese", "日本人" } },
3784     { lang_t::KO, "ko", { "korean", "한국의" } },
3785     { lang_t::LT, "lt", { "lithuanian", "lietuvos" } },
3786     { lang_t::LV, "lv", { "latvian", "lettish", "latvijas", "latviešu",
3787                           "latvieshu", "latviesu" } },
3788     { lang_t::NL, "nl", { "dutch", "nederlands" } },
3789     { lang_t::PL, "pl", { "polish", "polski" } },
3790     { lang_t::PT, "pt", { "portuguese", "português", "portugues" } },
3791     { lang_t::RU, "ru", { "russian", "русский", "русскии" } },
3792     { lang_t::SV, "sv", { "swedish", "svenska" } },
3793     { lang_t::ZH, "zh", { "chinese", "中国的", "中國的" } },
3794 };
3795 
_supported_language_listing()3796 static string _supported_language_listing()
3797 {
3798     return comma_separated_fn(&lang_data[0], &lang_data[ARRAYSZ(lang_data)],
3799                               [](language_def ld){return ld.code ? ld.code : "en";},
3800                               ",", ",",
3801                               [](language_def){return true;});
3802 }
3803 
set_lang(const char * lc)3804 bool game_options::set_lang(const char *lc)
3805 {
3806     if (!lc)
3807         return false;
3808 
3809     if (lc[0] && lc[1] && (lc[2] == '_' || lc[2] == '-'))
3810         return set_lang(string(lc, 2).c_str());
3811 
3812     const string l = lowercase_string(lc); // Windows returns it capitalized.
3813     for (const auto &ldef : lang_data)
3814     {
3815         if ((ldef.code && l == ldef.code) || ldef.names.count(l))
3816         {
3817             language = ldef.lang;
3818             lang_name = ldef.code;
3819             return true;
3820         }
3821     }
3822 
3823     if (const flang_t * const flang = map_find(fake_lang_names, l))
3824     {
3825         // Handle fake languages for backwards-compatibility with old rcs.
3826         // Override rather than stack, because that's how it used to work.
3827         fake_langs = { { *flang, -1 } };
3828         return true;
3829     }
3830 
3831     return false;
3832 }
3833 
3834 /**
3835  * Apply the player's fake language settings.
3836  *
3837  * @param input     The value of the "fake_lang" field.
3838  */
set_fake_langs(const string & input)3839 void game_options::set_fake_langs(const string &input)
3840 {
3841     fake_langs.clear();
3842     for (const string &flang_text : split_string(",", input))
3843     {
3844         const int MAX_LANGS = 3;
3845         if (fake_langs.size() >= (size_t) MAX_LANGS)
3846         {
3847             report_error("Too many fake langs; maximum is %d", MAX_LANGS);
3848             break;
3849         }
3850 
3851         const vector<string> split_flang = split_string(":", flang_text);
3852         const string flang_name = split_flang[0];
3853         if (split_flang.size() > 2)
3854         {
3855             report_error("Invalid fake-lang format: %s", flang_text.c_str());
3856             continue;
3857         }
3858 
3859         int tval = -1;
3860         const int value = split_flang.size() >= 2
3861                           && parse_int(split_flang[1].c_str(), tval) ? tval : -1;
3862 
3863         const flang_t *flang = map_find(fake_lang_names, flang_name);
3864         if (flang)
3865         {
3866             if (split_flang.size() >= 2)
3867             {
3868                 if (*flang != flang_t::butt)
3869                 {
3870                     report_error("Lang %s doesn't take a value",
3871                                  flang_name.c_str());
3872                     continue;
3873                 }
3874 
3875                 if (value == -1)
3876                 {
3877                     report_error("Invalid value '%s' provided for lang",
3878                                  split_flang[1].c_str());
3879                     continue;
3880                 }
3881             }
3882 
3883             fake_langs.push_back({*flang, value});
3884         }
3885         else
3886             report_error("Unknown language %s!", flang_name.c_str());
3887 
3888     }
3889 }
3890 
3891 // Checks an include file name for safety and resolves it to a readable path.
3892 // If file cannot be resolved, returns the empty string (this does not throw!)
3893 // If file can be resolved, returns the resolved path.
3894 /// @throws unsafe_path if included_file fails the safety check.
resolve_include(string parent_file,string included_file,const vector<string> * rcdirs)3895 string game_options::resolve_include(string parent_file, string included_file,
3896                                      const vector<string> *rcdirs)
3897 {
3898     // Before we start, make sure we convert forward slashes to the platform's
3899     // favoured file separator.
3900     parent_file   = canonicalise_file_separator(parent_file);
3901     included_file = canonicalise_file_separator(included_file);
3902 
3903     // How we resolve include paths:
3904     // 1. If it's an absolute path, use it directly.
3905     // 2. Try the name relative to the parent filename, if supplied.
3906     // 3. Try the name relative to any of the provided rcdirs.
3907     // 4. Try locating the name as a regular data file (also checks for the
3908     //    file name relative to the current directory).
3909     // 5. Fail, and return empty string.
3910 
3911     assert_read_safe_path(included_file);
3912 
3913     // There's only so much you can do with an absolute path.
3914     // Note: absolute paths can only get here if we're not on a
3915     // multiuser system. On multiuser systems assert_read_safe_path()
3916     // will throw an exception if it sees absolute paths.
3917     if (is_absolute_path(included_file))
3918         return file_exists(included_file)? included_file : "";
3919 
3920     if (!parent_file.empty())
3921     {
3922         const string candidate = get_path_relative_to(parent_file,
3923                                                       included_file);
3924         if (file_exists(candidate))
3925             return candidate;
3926     }
3927 
3928     if (rcdirs)
3929     {
3930         for (const string &dir : *rcdirs)
3931         {
3932             const string candidate(catpath(dir, included_file));
3933             if (file_exists(candidate))
3934                 return candidate;
3935         }
3936     }
3937 
3938     return datafile_path(included_file, false, true);
3939 }
3940 
resolve_include(const string & file,const char * type)3941 string game_options::resolve_include(const string &file, const char *type)
3942 {
3943     try
3944     {
3945         const string resolved = resolve_include(filename, file, &SysEnv.rcdirs);
3946 
3947         if (resolved.empty())
3948             report_error("Cannot find %sfile \"%s\".", type, file.c_str());
3949         return resolved;
3950     }
3951     catch (const unsafe_path &err)
3952     {
3953         report_error("Cannot include %sfile: %s", type, err.what());
3954         return "";
3955     }
3956 }
3957 
was_included(const string & file) const3958 bool game_options::was_included(const string &file) const
3959 {
3960     return included.count(file);
3961 }
3962 
include(const string & rawfilename,bool resolve,bool runscript)3963 void game_options::include(const string &rawfilename, bool resolve,
3964                            bool runscript)
3965 {
3966     const string include_file = resolve ? resolve_include(rawfilename)
3967                                         : rawfilename;
3968 
3969     if (was_included(include_file))
3970         return;
3971 
3972     included.insert(include_file);
3973 
3974     // Change filename to the included filename while we're reading it.
3975     unwind_var<string> optfile(filename, include_file);
3976     unwind_var<string> basefile(basefilename, rawfilename);
3977 
3978     // Save current line number
3979     unwind_var<int> currlinenum(line_num, 0);
3980 
3981     // Also unwind any aliases defined in included files.
3982     unwind_var<string_map> unwalias(aliases);
3983 
3984     FileLineInput fl(include_file.c_str());
3985     if (!fl.error())
3986         read_options(fl, runscript, false);
3987 }
3988 
report_error(const char * format,...)3989 void game_options::report_error(const char* format, ...)
3990 {
3991     va_list args;
3992     va_start(args, format);
3993     string error = vmake_stringf(format, args);
3994     va_end(args);
3995 
3996     mprf(MSGCH_ERROR, "Options error: %s (%s:%d)", error.c_str(),
3997          basefilename.c_str(), line_num);
3998 }
3999 
check_string(const char * s)4000 static string check_string(const char *s)
4001 {
4002     return s? s : "";
4003 }
4004 
get_system_environment()4005 void get_system_environment()
4006 {
4007     // The player's name
4008     SysEnv.crawl_name = check_string(getenv("CRAWL_NAME"));
4009 
4010     // The directory which contians init.txt, macro.txt, morgue.txt
4011     // This should end with the appropriate path delimiter.
4012     SysEnv.crawl_dir = check_string(getenv("CRAWL_DIR"));
4013 
4014 #if defined(TARGET_OS_MACOSX) && !defined(DGAMELAUNCH)
4015     if (SysEnv.crawl_dir.empty())
4016     {
4017         SysEnv.crawl_dir
4018             = _user_home_subpath("Library/Application Support/" CRAWL);
4019     }
4020 #endif
4021 
4022 #ifdef SAVE_DIR_PATH
4023     if (SysEnv.crawl_dir.empty())
4024         SysEnv.crawl_dir = SAVE_DIR_PATH;
4025 #endif
4026 
4027 #ifdef DGL_SIMPLE_MESSAGING
4028     // Enable DGL_SIMPLE_MESSAGING only if SIMPLEMAIL and MAIL are set.
4029     const char *simplemail = getenv("SIMPLEMAIL");
4030     if (simplemail && strcmp(simplemail, "0"))
4031     {
4032         const char *mail = getenv("MAIL");
4033         SysEnv.messagefile = mail? mail : "";
4034     }
4035 #endif
4036 
4037     // The full path to the init file -- this overrides CRAWL_DIR.
4038     SysEnv.crawl_rc = check_string(getenv("CRAWL_RC"));
4039 
4040 #ifdef UNIX
4041     // The user's home directory (used to look for ~/.crawlrc file)
4042     SysEnv.home = check_string(getenv("HOME"));
4043 #endif
4044 }
4045 
set_crawl_base_dir(const char * arg)4046 static void set_crawl_base_dir(const char *arg)
4047 {
4048     if (!arg)
4049         return;
4050 
4051     SysEnv.crawl_base = get_parent_directory(arg);
4052 }
4053 
4054 // parse args, filling in Options and game environment as we go.
4055 // returns true if no unknown or malformed arguments were found.
4056 
4057 // Keep this in sync with the option names.
4058 enum commandline_option_type
4059 {
4060     CLO_SCORES,
4061     CLO_NAME,
4062     CLO_RACE,
4063     CLO_CLASS,
4064     CLO_DIR,
4065     CLO_RC,
4066     CLO_RCDIR,
4067     CLO_TSCORES,
4068     CLO_VSCORES,
4069     CLO_SCOREFILE,
4070     CLO_MORGUE,
4071     CLO_MACRO,
4072     CLO_MAPSTAT,
4073     CLO_MAPSTAT_DUMP_DISCONNECT,
4074     CLO_OBJSTAT,
4075     CLO_ITERATIONS,
4076     CLO_FORCE_MAP,
4077     CLO_ARENA,
4078     CLO_DUMP_MAPS,
4079     CLO_TEST,
4080     CLO_SCRIPT,
4081     CLO_BUILDDB,
4082     CLO_HELP,
4083     CLO_VERSION,
4084     CLO_SEED,
4085     CLO_PREGEN,
4086     CLO_SAVE_VERSION,
4087     CLO_SPRINT,
4088     CLO_EXTRA_OPT_FIRST,
4089     CLO_EXTRA_OPT_LAST,
4090     CLO_SPRINT_MAP,
4091     CLO_EDIT_SAVE,
4092     CLO_PRINT_CHARSET,
4093     CLO_TUTORIAL,
4094     CLO_WIZARD,
4095     CLO_EXPLORE,
4096     CLO_NO_SAVE,
4097     CLO_GDB,
4098     CLO_NO_GDB, CLO_NOGDB,
4099     CLO_THROTTLE,
4100     CLO_NO_THROTTLE,
4101     CLO_PLAYABLE_JSON, // JSON metadata for species, jobs, combos.
4102     CLO_BRANCHES_JSON, // JSON metadata for branches.
4103     CLO_SAVE_JSON,
4104     CLO_GAMETYPES_JSON,
4105     CLO_EDIT_BONES,
4106 #ifdef USE_TILE_WEB
4107     CLO_WEBTILES_SOCKET,
4108     CLO_AWAIT_CONNECTION,
4109     CLO_PRINT_WEBTILES_OPTIONS,
4110 #endif
4111 
4112     CLO_NOPS
4113 };
4114 
4115 static const char *cmd_ops[] =
4116 {
4117     "scores", "name", "species", "background", "dir", "rc", "rcdir", "tscores",
4118     "vscores", "scorefile", "morgue", "macro", "mapstat", "dump-disconnect",
4119     "objstat", "iters", "force-map", "arena", "dump-maps", "test", "script",
4120     "builddb", "help", "version", "seed", "pregen", "save-version", "sprint",
4121     "extra-opt-first", "extra-opt-last", "sprint-map", "edit-save",
4122     "print-charset", "tutorial", "wizard", "explore", "no-save", "gdb",
4123     "no-gdb", "nogdb", "throttle", "no-throttle", "playable-json",
4124     "branches-json", "save-json", "gametypes-json", "bones",
4125 #ifdef USE_TILE_WEB
4126     "webtiles-socket", "await-connection", "print-webtiles-options",
4127 #endif
4128 };
4129 
4130 static const int num_cmd_ops = CLO_NOPS;
4131 static bool arg_seen[num_cmd_ops];
4132 
_find_executable_path()4133 static string _find_executable_path()
4134 {
4135     // A lot of OSes give ways to find the location of the running app's
4136     // binary executable. This is useful, because argv[0] can be relative
4137     // when we really need an absolute path in order to locate the game's
4138     // resources.
4139 #if defined (TARGET_OS_WINDOWS)
4140     wchar_t tempPath[MAX_PATH];
4141     if (GetModuleFileNameW(nullptr, tempPath, MAX_PATH))
4142         return utf16_to_8(tempPath);
4143     else
4144         return "";
4145 #elif defined (UNIX)
4146     char tempPath[2048];
4147     const ssize_t rsize =
4148         readlink("/proc/self/exe", tempPath, sizeof(tempPath) - 1);
4149     if (rsize > 0)
4150     {
4151         tempPath[rsize] = 0;
4152         return mb_to_utf8(tempPath);
4153     }
4154     return "";
4155 #elif defined (TARGET_OS_MACOSX)
4156     return mb_to_utf8(NXArgv[0]);
4157 #else
4158     // We don't know how to find the executable's path on this OS.
4159     return "";
4160 #endif
4161 }
4162 
_print_version()4163 static void _print_version()
4164 {
4165     printf("Crawl version %s%s", Version::Long, "\n");
4166     printf("Save file version %d.%d%s", TAG_MAJOR_VERSION, TAG_MINOR_VERSION, "\n");
4167     printf("%s", compilation_info);
4168 }
4169 
_print_save_version(char * name)4170 static void _print_save_version(char *name)
4171 {
4172     try
4173     {
4174         string filename = name;
4175         // Check for the exact filename first, then go by char name.
4176         if (!file_exists(filename))
4177             filename = get_savedir_filename(filename);
4178         package save(filename.c_str(), false);
4179         reader chrf(&save, "chr");
4180 
4181         save_version v = get_save_version(chrf);
4182         if (!v.valid())
4183             fail("Save file is invalid.");
4184         else
4185             printf("Save file version for %s is %d.%d\n", name, v.major, v.minor);
4186     }
4187     catch (ext_fail_exception &fe)
4188     {
4189         fprintf(stderr, "Error: %s\n", fe.what());
4190     }
4191 }
4192 
4193 enum es_command_type
4194 {
4195     ES_LS,
4196     ES_RM,
4197     ES_GET,
4198     ES_PUT,
4199     ES_REPACK,
4200     ES_INFO,
4201     NUM_ES
4202 };
4203 
4204 enum eb_command_type
4205 {
4206     EB_LS,
4207     EB_MERGE,
4208     EB_RM,
4209     EB_REWRITE,
4210     NUM_EB
4211 };
4212 
4213 template <typename T> struct edit_command
4214 {
4215     T cmd;
4216     const char* name;
4217     bool rw;
4218     int min_args, max_args;
4219 };
4220 
4221 static edit_command<es_command_type> es_commands[] =
4222 {
4223     { ES_LS,      "ls",      false, 0, 0, },
4224     { ES_GET,     "get",     false, 1, 2, },
4225     { ES_PUT,     "put",     true,  1, 2, },
4226     { ES_RM,      "rm",      true,  1, 1, },
4227     { ES_REPACK,  "repack",  false, 0, 0, },
4228     { ES_INFO,    "info",    false, 0, 0, },
4229 };
4230 
4231 static edit_command<eb_command_type> eb_commands[] =
4232 {
4233     { EB_LS,       "ls",      false, 0, 2, },
4234     { EB_MERGE,    "merge",   false, 1, 1, },
4235     { EB_RM,       "rm",      true,  1, 1 },
4236     { EB_REWRITE,  "rewrite", true,  0, 1 },
4237 };
4238 
4239 #define FAIL(...) do { fprintf(stderr, __VA_ARGS__); return; } while (0)
_edit_save(int argc,char ** argv)4240 static void _edit_save(int argc, char **argv)
4241 {
4242     if (argc <= 1 || !strcmp(argv[1], "help"))
4243     {
4244         printf("Usage: crawl --edit-save <name> <command>, where <command> may be:\n"
4245                "  ls                          list the chunks\n"
4246                "  info                        list detailed chunk and frag info\n"
4247                "  get <chunk> [<chunkfile>]   extract a chunk into <chunkfile>\n"
4248                "  put <chunk> [<chunkfile>]   import a chunk from <chunkfile>\n"
4249                "     <chunkfile> defaults to \"chunk\"; use \"-\" for stdout/stdin\n"
4250                "  rm <chunk>                  delete a chunk\n"
4251                "  repack                      defrag and reclaim unused space\n"
4252              );
4253         return;
4254     }
4255     const char *name = argv[0];
4256     const char *cmdn = argv[1];
4257 
4258     es_command_type cmd = NUM_ES;
4259     bool rw;
4260 
4261     for (const auto &ec : es_commands)
4262         if (!strcmp(ec.name, cmdn))
4263         {
4264             if (argc < ec.min_args + 2)
4265                 FAIL("Too few arguments for %s.\n", cmdn);
4266             else if (argc > ec.max_args + 2)
4267                 FAIL("Too many arguments for %s.\n", cmdn);
4268             cmd = ec.cmd;
4269             rw = ec.rw;
4270             break;
4271         }
4272     if (cmd == NUM_ES)
4273         FAIL("Unknown command: %s.\n", cmdn);
4274 
4275     try
4276     {
4277         string filename = name;
4278         // Check for the exact filename first, then go by char name.
4279         if (!file_exists(filename))
4280             filename = get_savedir_filename(filename);
4281         package save(filename.c_str(), rw);
4282 
4283         if (cmd == ES_LS)
4284         {
4285             vector<string> list = save.list_chunks();
4286             sort(list.begin(), list.end(), numcmpstr);
4287             for (const string &s : list)
4288                 printf("%s\n", s.c_str());
4289         }
4290         else if (cmd == ES_GET)
4291         {
4292             const char *chunk = argv[2];
4293             if (!*chunk || strlen(chunk) > MAX_CHUNK_NAME_LENGTH)
4294                 FAIL("Invalid chunk name \"%s\".\n", chunk);
4295             if (!save.has_chunk(chunk))
4296                 FAIL("No such chunk in the save file.\n");
4297             chunk_reader inc(&save, chunk);
4298 
4299             const char *file = (argc == 4) ? argv[3] : "chunk";
4300             FILE *f;
4301             if (strcmp(file, "-"))
4302                 f = fopen_u(file, "wb");
4303             else
4304                 f = stdout;
4305             if (!f)
4306                 sysfail("Can't open \"%s\" for writing", file);
4307 
4308             char buf[16384];
4309             while (size_t s = inc.read(buf, sizeof(buf)))
4310                 if (fwrite(buf, 1, s, f) != s)
4311                     sysfail("Error writing \"%s\"", file);
4312 
4313             if (f != stdout)
4314                 if (fclose(f))
4315                     sysfail("Write error on close of \"%s\"", file);
4316         }
4317         else if (cmd == ES_PUT)
4318         {
4319             const char *chunk = argv[2];
4320             if (!*chunk || strlen(chunk) > MAX_CHUNK_NAME_LENGTH)
4321                 FAIL("Invalid chunk name \"%s\".\n", chunk);
4322 
4323             const char *file = (argc == 4) ? argv[3] : "chunk";
4324             FILE *f;
4325             if (strcmp(file, "-"))
4326                 f = fopen_u(file, "rb");
4327             else
4328                 f = stdin;
4329             if (!f)
4330                 sysfail("Can't read \"%s\"", file);
4331             chunk_writer outc(&save, chunk);
4332 
4333             char buf[16384];
4334             while (size_t s = fread(buf, 1, sizeof(buf), f))
4335                 outc.write(buf, s);
4336             if (ferror(f))
4337                 sysfail("Error reading \"%s\"", file);
4338 
4339             if (f != stdin)
4340                 fclose(f);
4341         }
4342         else if (cmd == ES_RM)
4343         {
4344             const char *chunk = argv[2];
4345             if (!*chunk || strlen(chunk) > MAX_CHUNK_NAME_LENGTH)
4346                 FAIL("Invalid chunk name \"%s\".\n", chunk);
4347             if (!save.has_chunk(chunk))
4348                 FAIL("No such chunk in the save file.\n");
4349 
4350             save.delete_chunk(chunk);
4351         }
4352         else if (cmd == ES_REPACK)
4353         {
4354             package save2((filename + ".tmp").c_str(), true, true);
4355             for (const string &chunk : save.list_chunks())
4356             {
4357                 char buf[16384];
4358 
4359                 chunk_reader in(&save, chunk);
4360                 chunk_writer out(&save2, chunk);
4361 
4362                 while (plen_t s = in.read(buf, sizeof(buf)))
4363                     out.write(buf, s);
4364             }
4365             save2.commit();
4366             save.unlink();
4367             rename_u((filename + ".tmp").c_str(), filename.c_str());
4368         }
4369         else if (cmd == ES_INFO)
4370         {
4371             vector<string> list = save.list_chunks();
4372             sort(list.begin(), list.end(), numcmpstr);
4373             plen_t nchunks = list.size();
4374             plen_t frag = save.get_chunk_fragmentation("");
4375             plen_t flen = save.get_size();
4376             plen_t slack = save.get_slack();
4377             printf("Chunks: (size compressed/uncompressed, fragments, name)\n");
4378             for (const string &chunk : list)
4379             {
4380                 int cfrag = save.get_chunk_fragmentation(chunk);
4381                 frag += cfrag;
4382                 int cclen = save.get_chunk_compressed_length(chunk);
4383 
4384                 char buf[16384];
4385                 chunk_reader in(&save, chunk);
4386                 plen_t clen = 0;
4387                 while (plen_t s = in.read(buf, sizeof(buf)))
4388                     clen += s;
4389                 printf("%7d/%7d %3u %s\n", cclen, clen, cfrag, chunk.c_str());
4390             }
4391             // the directory is not a chunk visible from the outside
4392             printf("Fragmentation:    %u/%u (%4.2f)\n", frag, nchunks + 1,
4393                    ((float)frag) / (nchunks + 1));
4394             printf("Unused space:     %u/%u (%u%%)\n", slack, flen,
4395                    100 - (100 * (flen - slack) / flen));
4396             // there's also wasted space due to fragmentation, but since
4397             // it's linear, there's no need to print it
4398         }
4399     }
4400     catch (ext_fail_exception &fe)
4401     {
4402         fprintf(stderr, "Error: %s\n", fe.what());
4403     }
4404 }
4405 
_read_bones_version(const string & filename)4406 static save_version _read_bones_version(const string &filename)
4407 {
4408     reader inf(filename);
4409     if (!inf.valid())
4410     {
4411         string error = "File doesn't exist: " + filename;
4412         throw corrupted_save(error);
4413     }
4414 
4415     inf.set_safe_read(true); // don't die on 0-byte bones
4416     // use lower-level call here, because read_ghost_header fixes up the version
4417     save_version version = get_save_version(inf);
4418     inf.close();
4419     return version;
4420 }
4421 
_write_bones(const string & filename,vector<ghost_demon> ghosts)4422 static void _write_bones(const string &filename, vector<ghost_demon> ghosts)
4423 {
4424     // TODO: duplicates some logic in files.cc
4425     FILE* ghost_file = lk_open_exclusive(filename);
4426     if (!ghost_file)
4427     {
4428         string error = "Couldn't write to bones file " + filename;
4429         throw corrupted_save(error);
4430     }
4431     writer outw(filename, ghost_file);
4432 
4433     write_ghost_version(outw);
4434     tag_write_ghosts(outw, ghosts);
4435 
4436     lk_close(ghost_file);
4437 }
4438 
_bones_ls(const string & filename,const string name_match,bool long_output)4439 static void _bones_ls(const string &filename, const string name_match,
4440                                                             bool long_output)
4441 {
4442     save_version v = _read_bones_version(filename);
4443     cout << "Bones file '" << filename << "', version " << v.major << "."
4444          << v.minor << ":\n";
4445     const vector<ghost_demon> ghosts = load_bones_file(filename, false);
4446     monster m;
4447     if (long_output)
4448     {
4449         init_monsters(); // no monster is valid without this
4450         init_spell_descs();
4451         init_spell_name_cache();
4452         m.reset();
4453         m.type = MONS_PROGRAM_BUG;
4454         m.base_monster = MONS_PHANTOM;
4455     }
4456     int count = 0;
4457     for (auto g : ghosts)
4458     {
4459         // TODO: partial name matching?
4460         if (name_match.size() && name_match != lowercase_string(g.name))
4461             continue;
4462         count++;
4463         if (long_output)
4464         {
4465             // TOOD: line wrapping, some elements of this aren't meaningful at
4466             // the command line
4467             describe_info inf;
4468             m.set_ghost(g);
4469             m.ghost_init(false);
4470             m.type = MONS_PLAYER_GHOST;
4471             monster_info mi(&m);
4472             bool has_stat_desc = false;
4473             get_monster_db_desc(mi, inf, has_stat_desc);
4474             cout << "#######################\n"
4475                  << inf.title << "\n"
4476                  << inf.body.str() << "\n"
4477                  << inf.footer << "\n";
4478         }
4479         else
4480         {
4481             cout << std::setw(12) << std::left << g.name
4482                  << " XL" << std::setw(2) << g.xl << " "
4483                  << combo_type{species_type(g.species), job_type(g.job)}.abbr()
4484                  << "\n";
4485         }
4486     }
4487     if (!count)
4488     {
4489         if (name_match.size())
4490             cout << "No matching ghosts for " << name_match << ".\n";
4491         else
4492             cout << "Empty ghost file.\n";
4493     }
4494     else
4495         cout << count << " ghosts total\n";
4496 }
4497 
_bones_rewrite(const string filename,const string remove,bool dedup)4498 static void _bones_rewrite(const string filename, const string remove, bool dedup)
4499 {
4500     const vector<ghost_demon> ghosts = load_bones_file(filename, false);
4501 
4502     vector<ghost_demon> out;
4503     bool matched = false;
4504     const string remove_lower = lowercase_string(remove);
4505     map<string, int> ghosts_by_name;
4506     int dups = 0;
4507 
4508     for (auto g : ghosts)
4509     {
4510         if (dedup && ghosts_by_name.count(g.name)
4511                                             && ghosts_by_name[g.name] == g.xl)
4512         {
4513             dups++;
4514             continue;
4515         }
4516         if (lowercase_string(g.name) == remove_lower)
4517         {
4518             matched = true;
4519             continue;
4520         }
4521         out.push_back(g);
4522         ghosts_by_name[g.name] = g.xl;
4523     }
4524     if (matched || remove.size() == 0)
4525     {
4526         cout << "Rewriting '" << filename << "'";
4527         if (matched)
4528             cout << " without ghost '" << remove_lower << "'";
4529         if (dups)
4530             cout << ", " << dups << " duplicates removed";
4531         cout << "\n";
4532         unlink_u(filename.c_str());
4533         _write_bones(filename, out);
4534     }
4535     else
4536         cout << "No matching ghosts for '" << remove_lower << "'\n";
4537 }
4538 
_bones_merge(const vector<string> files,const string out_name)4539 static void _bones_merge(const vector<string> files, const string out_name)
4540 {
4541     vector<ghost_demon> out;
4542     for (auto filename : files)
4543     {
4544         auto ghosts = load_bones_file(filename, false);
4545         auto end = ghosts.end();
4546         if (out.size() + ghosts.size() > MAX_GHOSTS)
4547         {
4548             //cout << "ghosts " << out.size() + ghosts.size() - MAX_GHOSTS;
4549             cout << "Too many ghosts! Capping merge at " << MAX_GHOSTS << "\n";
4550             end = ghosts.begin() + (MAX_GHOSTS - out.size());
4551         }
4552         out.insert(out.end(), ghosts.begin(), end);
4553         if (end != ghosts.end())
4554             break;
4555     }
4556     if (file_exists(out_name))
4557         unlink_u(out_name.c_str());
4558     if (out.size() == 0)
4559         cout << "Writing empty bones file";
4560     else
4561         cout << "Writing " << out.size() << " ghosts";
4562     cout << " to " << out_name << "\n";
4563     _write_bones(out_name, out);
4564 }
4565 
_edit_bones(int argc,char ** argv)4566 static void _edit_bones(int argc, char **argv)
4567 {
4568     if (argc <= 1 || !strcmp(argv[1], "help"))
4569     {
4570         printf("Usage: crawl --bones <command> ARGS, where <command> may be:\n"
4571                "  ls <file> [<name>] [--long] List the ghosts in <file>\n"
4572                "                              --long shows full monster descriptions\n"
4573                "  merge <file1> <file2>       Merge two bones files together, rewriting into\n"
4574                "                              <file2>. Capped at %d; read in reverse order.\n"
4575                "  rm <file> <name>            Rewrite a ghost file without <name>\n"
4576                "  rewrite <file> [--dedup]    Rewrite a ghost file, fixing up version etc.\n",
4577                MAX_GHOSTS
4578              );
4579         return;
4580     }
4581     const char *cmdn = argv[0];
4582     const char *name = argv[1];
4583 
4584     eb_command_type cmd = NUM_EB;
4585 
4586     for (const auto &ec : eb_commands)
4587         if (!strcmp(ec.name, cmdn))
4588         {
4589             if (argc < ec.min_args + 2)
4590                 FAIL("Too few arguments for %s.\n", cmdn);
4591             else if (argc > ec.max_args + 2)
4592                 FAIL("Too many arguments for %s.\n", cmdn);
4593             cmd = ec.cmd;
4594             break;
4595         }
4596     if (cmd == NUM_EB)
4597         FAIL("Unknown command: %s.\n", cmdn);
4598 
4599     try
4600     {
4601         if (!file_exists(name))
4602             FAIL("'%s' doesn't exist!\n", name);
4603 
4604         if (cmd == EB_LS)
4605         {
4606             const bool long_out =
4607                            argc == 3 && !strcmp(argv[2], "--long")
4608                         || argc == 4 && !strcmp(argv[3], "--long");
4609             if (argc == 4 && !long_out)
4610                 FAIL("Unknown extra option to ls: '%s'\n", argv[3]);
4611             const string name_match = argc == 3 && !long_out || argc == 4
4612                                     ? string(argv[2])
4613                                     : "";
4614             _bones_ls(name, lowercase_string(name_match), long_out);
4615         }
4616         else if (cmd == EB_REWRITE)
4617         {
4618             const bool dedup = argc == 3 && !strcmp(argv[2], "--dedup");
4619             if (argc == 3 && !dedup)
4620                 FAIL("Unknown extra argument to rewrite: '%s'\n", argv[2]);
4621             _bones_rewrite(name, "", dedup);
4622         }
4623         else if (cmd == EB_RM)
4624         {
4625             const string name_match = argv[2];
4626             _bones_rewrite(name, name_match, false);
4627         }
4628         else if (cmd == EB_MERGE)
4629         {
4630             const string out_name = argv[2];
4631             _bones_merge({out_name, name}, out_name);
4632         }
4633     }
4634     catch (corrupted_save &err)
4635     {
4636         // not a corrupted save per se, just from the future. Try to load the
4637         // versioned bones file if it exists.
4638         if (err.version.valid() && err.version.is_future())
4639         {
4640             FAIL("Bones file '%s' is from the future (%d.%d), this instance of "
4641                  "crawl needs %d.%d.\n", name,
4642                     err.version.major, err.version.minor,
4643                     save_version::current_bones().major,
4644                     save_version::current_bones().minor);
4645         }
4646         else
4647             FAIL("Error: %s\n", err.what());
4648     }
4649     catch (ext_fail_exception &fe)
4650     {
4651         FAIL("Error: %s\n", fe.what());
4652     }
4653 }
4654 
4655 #undef FAIL
4656 
4657 #ifdef USE_TILE_WEB
_write_colour_list(const vector<pair<int,int>> variable,const string & name)4658 static void _write_colour_list(const vector<pair<int, int> > variable,
4659         const string &name)
4660 {
4661     tiles.json_open_array(name);
4662     for (const auto &entry : variable)
4663     {
4664         tiles.json_open_object();
4665         tiles.json_write_int("value", entry.first);
4666         tiles.json_write_string("colour", colour_to_str(entry.second));
4667         tiles.json_close_object();
4668     }
4669     tiles.json_close_array();
4670 }
4671 
_write_vcolour(const string & name,VColour colour)4672 static void _write_vcolour(const string &name, VColour colour)
4673 {
4674     tiles.json_open_object(name);
4675     tiles.json_write_int("r", colour.r);
4676     tiles.json_write_int("g", colour.g);
4677     tiles.json_write_int("b", colour.b);
4678     if (colour.a != 255)
4679         tiles.json_write_int("a", colour.a);
4680     tiles.json_close_object();
4681 }
4682 
_write_minimap_colours()4683 static void _write_minimap_colours()
4684 {
4685     _write_vcolour("tile_unseen_col", Options.tile_unseen_col);
4686     _write_vcolour("tile_floor_col", Options.tile_floor_col);
4687     _write_vcolour("tile_wall_col", Options.tile_wall_col);
4688     _write_vcolour("tile_mapped_floor_col", Options.tile_mapped_floor_col);
4689     _write_vcolour("tile_mapped_wall_col", Options.tile_mapped_wall_col);
4690     _write_vcolour("tile_door_col", Options.tile_door_col);
4691     _write_vcolour("tile_item_col", Options.tile_item_col);
4692     _write_vcolour("tile_monster_col", Options.tile_monster_col);
4693     _write_vcolour("tile_plant_col", Options.tile_plant_col);
4694     _write_vcolour("tile_upstairs_col", Options.tile_upstairs_col);
4695     _write_vcolour("tile_downstairs_col", Options.tile_downstairs_col);
4696     _write_vcolour("tile_branchstairs_col", Options.tile_branchstairs_col);
4697     _write_vcolour("tile_feature_col", Options.tile_feature_col);
4698     _write_vcolour("tile_water_col", Options.tile_water_col);
4699     _write_vcolour("tile_lava_col", Options.tile_lava_col);
4700     _write_vcolour("tile_trap_col", Options.tile_trap_col);
4701     _write_vcolour("tile_excl_centre_col", Options.tile_excl_centre_col);
4702     _write_vcolour("tile_excluded_col", Options.tile_excluded_col);
4703     _write_vcolour("tile_player_col", Options.tile_player_col);
4704     _write_vcolour("tile_deep_water_col", Options.tile_deep_water_col);
4705     _write_vcolour("tile_portal_col", Options.tile_portal_col);
4706     _write_vcolour("tile_transporter_col", Options.tile_transporter_col);
4707     _write_vcolour("tile_transporter_landing_col", Options.tile_transporter_landing_col);
4708     _write_vcolour("tile_explore_horizon_col", Options.tile_explore_horizon_col);
4709 
4710     _write_vcolour("tile_window_col", Options.tile_window_col);
4711 }
4712 
write_webtiles_options(const string & name)4713 void game_options::write_webtiles_options(const string& name)
4714 {
4715     tiles.json_open_object(name);
4716 
4717     _write_colour_list(Options.hp_colour, "hp_colour");
4718     _write_colour_list(Options.mp_colour, "mp_colour");
4719     _write_colour_list(Options.stat_colour, "stat_colour");
4720 
4721     tiles.json_write_bool("tile_show_minihealthbar",
4722                           Options.tile_show_minihealthbar);
4723     tiles.json_write_bool("tile_show_minimagicbar",
4724                           Options.tile_show_minimagicbar);
4725     tiles.json_write_bool("tile_show_demon_tier",
4726                           Options.tile_show_demon_tier);
4727 
4728     tiles.json_write_int("tile_map_pixels", Options.tile_map_pixels);
4729 
4730     tiles.json_write_string("tile_display_mode", Options.tile_display_mode);
4731     tiles.json_write_int("tile_cell_pixels", Options.tile_cell_pixels);
4732     tiles.json_write_int("tile_viewport_scale", Options.tile_viewport_scale);
4733     tiles.json_write_int("tile_map_scale", Options.tile_map_scale);
4734     tiles.json_write_bool("tile_filter_scaling", Options.tile_filter_scaling);
4735     tiles.json_write_bool("tile_water_anim", Options.tile_water_anim);
4736     tiles.json_write_bool("tile_misc_anim", Options.tile_misc_anim);
4737     tiles.json_write_bool("tile_realtime_anim", Options.tile_realtime_anim);
4738     tiles.json_write_bool("tile_level_map_hide_messages",
4739             Options.tile_level_map_hide_messages);
4740     tiles.json_write_bool("tile_level_map_hide_sidebar",
4741             Options.tile_level_map_hide_sidebar);
4742     tiles.json_write_bool("tile_web_mouse_control", Options.tile_web_mouse_control);
4743     tiles.json_write_bool("tile_menu_icons", Options.tile_menu_icons);
4744 
4745     tiles.json_write_string("tile_font_crt_family",
4746             Options.tile_font_crt_family);
4747     tiles.json_write_string("tile_font_stat_family",
4748             Options.tile_font_stat_family);
4749     tiles.json_write_string("tile_font_msg_family",
4750             Options.tile_font_msg_family);
4751     tiles.json_write_string("tile_font_lbl_family",
4752             Options.tile_font_lbl_family);
4753     tiles.json_write_int("tile_font_crt_size", Options.tile_font_crt_size);
4754     tiles.json_write_int("tile_font_stat_size", Options.tile_font_stat_size);
4755     tiles.json_write_int("tile_font_msg_size", Options.tile_font_msg_size);
4756     tiles.json_write_int("tile_font_lbl_size", Options.tile_font_lbl_size);
4757 
4758     tiles.json_write_string("glyph_mode_font", Options.glyph_mode_font);
4759     tiles.json_write_int("glyph_mode_font_size", Options.glyph_mode_font_size);
4760 
4761     tiles.json_write_bool("show_game_time", Options.show_game_time);
4762 
4763     _write_minimap_colours();
4764 
4765     tiles.json_close_object();
4766 }
4767 
_print_webtiles_options()4768 static void _print_webtiles_options()
4769 {
4770     Options.write_webtiles_options("");
4771     printf("%s\n", tiles.get_message().c_str());
4772 }
4773 #endif
4774 
_gametype_to_clo(game_type g)4775 static string _gametype_to_clo(game_type g)
4776 {
4777     switch (g)
4778     {
4779     case GAME_TYPE_CUSTOM_SEED:
4780         return cmd_ops[CLO_SEED];
4781     case GAME_TYPE_TUTORIAL:
4782         return cmd_ops[CLO_TUTORIAL];
4783     case GAME_TYPE_ARENA:
4784         return cmd_ops[CLO_ARENA];
4785     case GAME_TYPE_SPRINT:
4786         return cmd_ops[CLO_SPRINT];
4787     case GAME_TYPE_HINTS: // no CLO?
4788     case GAME_TYPE_NORMAL:
4789     default:
4790         return "";
4791     }
4792 }
4793 
_print_gametypes_json()4794 static void _print_gametypes_json()
4795 {
4796     JsonWrapper json(json_mkobject());
4797 
4798     for (int i = 0; i < NUM_GAME_TYPE; ++i)
4799     {
4800         auto gt = static_cast<game_type>(i);
4801         string c = _gametype_to_clo(gt);
4802         if (c != "")
4803             c = "-" + c;
4804         if (c != "" || gt == GAME_TYPE_NORMAL)
4805         {
4806             json_append_member(json.node, gametype_to_str(gt).c_str(),
4807                                                         json_mkstring(c));
4808         }
4809     }
4810     fprintf(stdout, "%s", json.to_string().c_str());
4811 }
4812 
_check_extra_opt(char * _opt)4813 static bool _check_extra_opt(char* _opt)
4814 {
4815     string opt(_opt);
4816     trim_string(opt);
4817 
4818     if (opt[0] == ':' || opt[0] == '<' || opt[0] == '{'
4819         || starts_with(opt, "L<") || starts_with(opt, "Lua{"))
4820     {
4821         fprintf(stderr, "An extra option can't use Lua (%s)\n",
4822                 _opt);
4823         return false;
4824     }
4825 
4826     if (opt[0] == '#')
4827     {
4828         fprintf(stderr, "An extra option can't be a comment (%s)\n",
4829                 _opt);
4830         return false;
4831     }
4832 
4833     if (opt.find_first_of('=') == string::npos)
4834     {
4835         fprintf(stderr, "An extra opt must contain a '=' (%s)\n",
4836                 _opt);
4837         return false;
4838     }
4839 
4840     vector<string> parts = split_string(opt, "=");
4841     if (opt.find_first_of('=') == 0 || parts[0].length() == 0)
4842     {
4843         fprintf(stderr, "An extra opt must have an option name (%s)\n",
4844                 _opt);
4845         return false;
4846     }
4847 
4848     return true;
4849 }
4850 
parse_args(int argc,char ** argv,bool rc_only)4851 bool parse_args(int argc, char **argv, bool rc_only)
4852 {
4853     COMPILE_CHECK(ARRAYSZ(cmd_ops) == CLO_NOPS);
4854 
4855 #ifndef DEBUG_STATISTICS
4856     const char *dbg_stat_err = "mapstat and objstat are available only in "
4857                                "DEBUG_STATISTICS builds.\n";
4858 #endif
4859 
4860     if (crawl_state.command_line_arguments.empty())
4861     {
4862         crawl_state.command_line_arguments.insert(
4863             crawl_state.command_line_arguments.end(),
4864             argv, argv + argc);
4865     }
4866 
4867     string exe_path = _find_executable_path();
4868 
4869     if (!exe_path.empty())
4870         set_crawl_base_dir(exe_path.c_str());
4871     else
4872         set_crawl_base_dir(argv[0]);
4873 
4874     SysEnv.crawl_exe = get_base_filename(argv[0]);
4875 
4876     SysEnv.rcdirs.clear();
4877     SysEnv.map_gen_iters = 0;
4878 
4879     if (argc < 2)           // no args!
4880         return true;
4881 
4882     char *arg, *next_arg;
4883     int current = 1;
4884     bool nextUsed = false;
4885     int ecount;
4886 
4887     // initialise
4888     for (int i = 0; i < num_cmd_ops; i++)
4889          arg_seen[i] = false;
4890 
4891     if (SysEnv.cmd_args.empty())
4892     {
4893         for (int i = 1; i < argc; ++i)
4894             SysEnv.cmd_args.emplace_back(argv[i]);
4895     }
4896 
4897     while (current < argc)
4898     {
4899         // get argument
4900         arg = argv[current];
4901 
4902         // next argument (if there is one)
4903         if (current+1 < argc)
4904             next_arg = argv[current+1];
4905         else
4906             next_arg = nullptr;
4907 
4908         nextUsed = false;
4909 
4910         // arg MUST begin with '-'
4911         char c = arg[0];
4912         if (c != '-')
4913         {
4914             fprintf(stderr,
4915                     "Option '%s' is invalid; options must be prefixed "
4916                     "with -\n\n", arg);
4917             return false;
4918         }
4919 
4920         // Look for match (we accept both -option and --option).
4921         if (arg[1] == '-')
4922             arg = &arg[2];
4923         else
4924             arg = &arg[1];
4925 
4926         // Mac app bundle executables get a process serial number
4927         if (strncmp(arg, "psn_", 4) == 0)
4928         {
4929             current++;
4930             continue;
4931         }
4932 
4933         int o;
4934         for (o = 0; o < num_cmd_ops; o++)
4935             if (strcasecmp(cmd_ops[o], arg) == 0)
4936                 break;
4937 
4938         // Print the list of commandline options for "--help".
4939         if (o == CLO_HELP)
4940             return false;
4941 
4942         if (o == num_cmd_ops)
4943         {
4944             fprintf(stderr,
4945                     "Unknown option: %s\n\n", argv[current]);
4946             return false;
4947         }
4948 
4949         // Disallow options specified more than once.
4950         if (arg_seen[o])
4951         {
4952             fprintf(stderr, "Duplicate option: %s\n\n", argv[current]);
4953             return false;
4954         }
4955 
4956         // Set arg to 'seen'.
4957         arg_seen[o] = true;
4958 
4959         // Partially parse next argument.
4960         bool next_is_param = false;
4961         if (next_arg != nullptr
4962             && (next_arg[0] != '-' || strlen(next_arg) == 1))
4963         {
4964             next_is_param = true;
4965         }
4966 
4967         // Take action according to the cmd chosen.
4968         switch (o)
4969         {
4970         case CLO_SCORES:
4971         case CLO_TSCORES:
4972         case CLO_VSCORES:
4973             if (!next_is_param)
4974                 ecount = -1;            // default
4975             else // optional number given
4976             {
4977                 ecount = atoi(next_arg);
4978                 if (ecount < 1)
4979                     ecount = 1;
4980 
4981                 if (ecount > SCORE_FILE_ENTRIES)
4982                     ecount = SCORE_FILE_ENTRIES;
4983 
4984                 nextUsed = true;
4985             }
4986 
4987             if (!rc_only)
4988             {
4989                 Options.sc_entries = ecount;
4990 
4991                 if (o == CLO_TSCORES)
4992                     Options.sc_format = SCORE_TERSE;
4993                 else if (o == CLO_VSCORES)
4994                     Options.sc_format = SCORE_VERBOSE;
4995                 else if (o == CLO_SCORES)
4996                     Options.sc_format = SCORE_REGULAR;
4997             }
4998             break;
4999 
5000         case CLO_MAPSTAT:
5001         case CLO_OBJSTAT:
5002 #ifdef DEBUG_STATISTICS
5003             if (o == CLO_MAPSTAT)
5004                 crawl_state.map_stat_gen = true;
5005             else
5006                 crawl_state.obj_stat_gen = true;
5007 #ifdef USE_TILE_LOCAL
5008             crawl_state.tiles_disabled = true;
5009 #endif
5010 
5011             if (!SysEnv.map_gen_iters)
5012                 SysEnv.map_gen_iters = 100;
5013             if (next_is_param)
5014             {
5015                 SysEnv.map_gen_range.reset(new depth_ranges);
5016                 try
5017                 {
5018                     *SysEnv.map_gen_range =
5019                         depth_ranges::parse_depth_ranges(next_arg);
5020                 }
5021                 catch (const bad_level_id &err)
5022                 {
5023                     end(1, false, "Error parsing depths: %s\n", err.what());
5024                 }
5025                 nextUsed = true;
5026             }
5027             break;
5028 #else
5029             end(1, false, "%s", dbg_stat_err);
5030 #endif
5031         case CLO_MAPSTAT_DUMP_DISCONNECT:
5032 #ifdef DEBUG_STATISTICS
5033             crawl_state.map_stat_dump_disconnect = true;
5034 #else
5035             end(1, false, "%s", dbg_stat_err);
5036 #endif
5037         case CLO_ITERATIONS:
5038 #ifdef DEBUG_STATISTICS
5039             if (!next_is_param || !isadigit(*next_arg))
5040                 end(1, false, "Integer argument required for -%s\n", arg);
5041             else
5042             {
5043                 SysEnv.map_gen_iters = atoi(next_arg);
5044                 if (SysEnv.map_gen_iters < 1)
5045                     SysEnv.map_gen_iters = 1;
5046                 else if (SysEnv.map_gen_iters > 10000)
5047                     SysEnv.map_gen_iters = 10000;
5048                 nextUsed = true;
5049             }
5050 #else
5051             end(1, false, "%s", dbg_stat_err);
5052 #endif
5053             break;
5054 
5055         case CLO_FORCE_MAP:
5056 #ifdef DEBUG_STATISTICS
5057             if (!next_is_param)
5058                 end(1, false, "String argument required for -%s\n", arg);
5059             else
5060             {
5061                 crawl_state.force_map = next_arg;
5062                 nextUsed = true;
5063             }
5064 #else
5065             end(1, false, "%s", dbg_stat_err);
5066 #endif
5067             break;
5068 
5069         case CLO_ARENA:
5070             if (!rc_only)
5071             {
5072                 Options.game.type = GAME_TYPE_ARENA;
5073                 Options.restart_after_game = MB_FALSE;
5074             }
5075             if (next_is_param)
5076             {
5077                 if (!rc_only)
5078                     Options.game.arena_teams = next_arg;
5079                 nextUsed = true;
5080             }
5081             break;
5082 
5083         case CLO_DUMP_MAPS:
5084             crawl_state.dump_maps = true;
5085             break;
5086 
5087         case CLO_PLAYABLE_JSON:
5088             fprintf(stdout, "%s", playable_metadata_json().c_str());
5089             end(0);
5090 
5091         case CLO_BRANCHES_JSON:
5092             fprintf(stdout, "%s", branch_data_json().c_str());
5093             end(0);
5094 
5095         case CLO_TEST:
5096             crawl_state.test = true;
5097             if (next_is_param)
5098             {
5099                 if (!(crawl_state.test_list = !strcmp(next_arg, "list")))
5100                     crawl_state.tests_selected = split_string(",", next_arg);
5101                 nextUsed = true;
5102             }
5103             break;
5104 
5105         case CLO_SCRIPT:
5106             crawl_state.test   = true;
5107             crawl_state.script = true;
5108             crawl_state.script_args.clear();
5109             if (current < argc - 1)
5110             {
5111                 crawl_state.tests_selected = split_string(",", next_arg);
5112                 for (int extra = current + 2; extra < argc; ++extra)
5113                     crawl_state.script_args.emplace_back(argv[extra]);
5114                 current = argc;
5115             }
5116             else
5117             {
5118                 end(1, false,
5119                     "-script must specify comma-separated script names");
5120             }
5121             break;
5122 
5123         case CLO_BUILDDB:
5124             if (next_is_param)
5125                 return false;
5126             crawl_state.build_db = true;
5127 #ifdef USE_TILE_LOCAL
5128             crawl_state.tiles_disabled = true;
5129 #endif
5130             break;
5131 
5132         case CLO_GDB:
5133             crawl_state.no_gdb = 0;
5134             break;
5135 
5136         case CLO_NO_GDB:
5137         case CLO_NOGDB:
5138             crawl_state.no_gdb = "GDB disabled via the command line.";
5139             break;
5140 
5141         case CLO_MACRO:
5142             if (!next_is_param)
5143                 return false;
5144             SysEnv.macro_dir = next_arg;
5145             nextUsed = true;
5146             break;
5147 
5148         case CLO_MORGUE:
5149             if (!next_is_param)
5150                 return false;
5151             if (!rc_only)
5152                 SysEnv.morgue_dir = next_arg;
5153             nextUsed = true;
5154             break;
5155 
5156         case CLO_SCOREFILE:
5157             if (!next_is_param)
5158                 return false;
5159             if (!rc_only)
5160                 SysEnv.scorefile = next_arg;
5161             nextUsed = true;
5162             break;
5163 
5164         case CLO_NAME:
5165             if (!next_is_param)
5166                 return false;
5167             if (!rc_only)
5168             {
5169                 Options.game.name = next_arg;
5170                 crawl_state.default_startup_name = Options.game.name;
5171             }
5172             nextUsed = true;
5173             break;
5174 
5175         case CLO_RACE:
5176         case CLO_CLASS:
5177             if (!next_is_param)
5178                 return false;
5179 
5180             if (!rc_only)
5181             {
5182                 if (o == 2)
5183                     Options.game.species = _str_to_species(string(next_arg));
5184 
5185                 if (o == 3)
5186                     Options.game.job = str_to_job(string(next_arg));
5187             }
5188             nextUsed = true;
5189             break;
5190 
5191         case CLO_RCDIR:
5192             // Always parse.
5193             if (!next_is_param)
5194                 return false;
5195 
5196             SysEnv.add_rcdir(next_arg);
5197             nextUsed = true;
5198             break;
5199 
5200         case CLO_DIR:
5201             // Always parse.
5202             if (!next_is_param)
5203                 return false;
5204 
5205             SysEnv.crawl_dir = next_arg;
5206             nextUsed = true;
5207             break;
5208 
5209         case CLO_RC:
5210             // Always parse.
5211             if (!next_is_param)
5212                 return false;
5213 
5214             SysEnv.crawl_rc = next_arg;
5215             nextUsed = true;
5216             break;
5217 
5218         case CLO_HELP:
5219             // Shouldn't happen.
5220             return false;
5221 
5222         case CLO_VERSION:
5223             _print_version();
5224             end(0);
5225 
5226         case CLO_SAVE_VERSION:
5227             // Always parse.
5228             if (!next_is_param)
5229                 return false;
5230 
5231             _print_save_version(next_arg);
5232             end(0);
5233 
5234         case CLO_SAVE_JSON:
5235             // Always parse.
5236             if (!next_is_param)
5237                 return false;
5238 
5239             print_save_json(next_arg);
5240             end(0);
5241 
5242         case CLO_GAMETYPES_JSON:
5243             // Always parse.
5244             _print_gametypes_json();
5245             end(0);
5246 
5247         case CLO_EDIT_SAVE:
5248             // Always parse.
5249             _edit_save(argc - current - 1, argv + current + 1);
5250             end(0);
5251 
5252         case CLO_EDIT_BONES:
5253             _edit_bones(argc - current - 1, argv + current + 1);
5254             end(0);
5255 
5256         case CLO_SEED:
5257             if (!next_is_param)
5258             {
5259                 // show seed choice menu
5260                 Options.game.type = GAME_TYPE_CUSTOM_SEED;
5261                 break;
5262             }
5263 
5264             if (!sscanf(next_arg, "%" SCNu64, &Options.seed_from_rc))
5265                 return false;
5266             Options.seed = Options.seed_from_rc;
5267             nextUsed = true;
5268             break;
5269 
5270         case CLO_PREGEN:
5271             Options.pregen_dungeon = true;
5272             break;
5273 
5274         case CLO_SPRINT:
5275             if (!rc_only)
5276                 Options.game.type = GAME_TYPE_SPRINT;
5277             break;
5278 
5279         case CLO_SPRINT_MAP:
5280             if (!next_is_param)
5281                 return false;
5282 
5283             nextUsed               = true;
5284             crawl_state.sprint_map = next_arg;
5285             Options.game.map       = next_arg;
5286             break;
5287 
5288         case CLO_TUTORIAL:
5289             if (!rc_only)
5290                 Options.game.type = GAME_TYPE_TUTORIAL;
5291             break;
5292 
5293         case CLO_WIZARD:
5294 #ifdef WIZARD
5295             if (!rc_only)
5296                 Options.wiz_mode = WIZ_NO;
5297 #endif
5298             break;
5299 
5300         case CLO_EXPLORE:
5301 #ifdef WIZARD
5302             if (!rc_only)
5303                 Options.explore_mode = WIZ_NO;
5304 #endif
5305             break;
5306 
5307         case CLO_NO_SAVE:
5308             if (!rc_only)
5309                 Options.no_save = true;
5310             break;
5311 
5312 #ifdef USE_TILE_WEB
5313         case CLO_WEBTILES_SOCKET:
5314             nextUsed          = true;
5315             tiles.m_sock_name = next_arg;
5316             break;
5317 
5318         case CLO_AWAIT_CONNECTION:
5319             tiles.m_await_connection = true;
5320             break;
5321 
5322         case CLO_PRINT_WEBTILES_OPTIONS:
5323             if (!rc_only)
5324             {
5325                 _print_webtiles_options();
5326                 end(0);
5327             }
5328             break;
5329 #endif
5330 
5331         case CLO_PRINT_CHARSET:
5332             if (rc_only)
5333                 break;
5334 #ifdef DGAMELAUNCH
5335             // Tell DGL we don't use ancient charsets anymore. The glyph set
5336             // doesn't matter here, just the encoding.
5337             printf("UNICODE\n");
5338 #else
5339             printf("This option is for DGL use only.\n");
5340 #endif
5341             end(0);
5342             break;
5343 
5344         case CLO_THROTTLE:
5345             crawl_state.throttle = true;
5346             break;
5347 
5348         case CLO_NO_THROTTLE:
5349             crawl_state.throttle = false;
5350             break;
5351 
5352         case CLO_EXTRA_OPT_FIRST:
5353             if (!next_is_param)
5354                 return false;
5355 
5356             // Don't print the help message if the opt was wrong
5357             if (!_check_extra_opt(next_arg))
5358                 return true;
5359 
5360             SysEnv.extra_opts_first.emplace_back(next_arg);
5361             nextUsed = true;
5362 
5363             // Can be used multiple times.
5364             arg_seen[o] = false;
5365             break;
5366 
5367         case CLO_EXTRA_OPT_LAST:
5368             if (!next_is_param)
5369                 return false;
5370 
5371             // Don't print the help message if the opt was wrong
5372             if (!_check_extra_opt(next_arg))
5373                 return true;
5374 
5375             SysEnv.extra_opts_last.emplace_back(next_arg);
5376             nextUsed = true;
5377 
5378             // Can be used multiple times.
5379             arg_seen[o] = false;
5380             break;
5381         }
5382 
5383         // Update position.
5384         current++;
5385         if (nextUsed)
5386             current++;
5387     }
5388 
5389     return true;
5390 }
5391 
5392 ///////////////////////////////////////////////////////////////////////
5393 // system_environment
5394 
add_rcdir(const string & dir)5395 void system_environment::add_rcdir(const string &dir)
5396 {
5397     string cdir = canonicalise_file_separator(dir);
5398     if (dir_exists(cdir))
5399         rcdirs.push_back(cdir);
5400     else
5401         end(1, false, "Cannot find -rcdir \"%s\"", cdir.c_str());
5402 }
5403 
5404 ///////////////////////////////////////////////////////////////////////
5405 // menu_sort_condition
5406 
menu_sort_condition(menu_type _mt,int _sort)5407 menu_sort_condition::menu_sort_condition(menu_type _mt, int _sort)
5408     : mtype(_mt), sort(_sort), cmp()
5409 {
5410 }
5411 
menu_sort_condition(const string & s)5412 menu_sort_condition::menu_sort_condition(const string &s)
5413     : mtype(menu_type::any), sort(-1), cmp()
5414 {
5415     string cp = s;
5416     set_menu_type(cp);
5417     set_sort(cp);
5418     set_comparators(cp);
5419 }
5420 
matches(menu_type mt) const5421 bool menu_sort_condition::matches(menu_type mt) const
5422 {
5423     return mtype == menu_type::any || mtype == mt;
5424 }
5425 
set_menu_type(string & s)5426 void menu_sort_condition::set_menu_type(string &s)
5427 {
5428     static struct
5429     {
5430         const string mname;
5431         menu_type mtype;
5432     } menu_type_map[] =
5433       {
5434           { "any:",    menu_type::any       },
5435           { "inv:",    menu_type::invlist   },
5436           { "drop:",   menu_type::drop      },
5437           { "pickup:", menu_type::pickup    },
5438           { "know:",   menu_type::know      }
5439       };
5440 
5441     for (const auto &mi : menu_type_map)
5442     {
5443         const string &name = mi.mname;
5444         if (starts_with(s, name))
5445         {
5446             s = s.substr(name.length());
5447             mtype = mi.mtype;
5448             break;
5449         }
5450     }
5451 }
5452 
set_sort(string & s)5453 void menu_sort_condition::set_sort(string &s)
5454 {
5455     // Strip off the optional sort clauses and get the primary sort condition.
5456     string::size_type trail_pos = s.find(':');
5457     if (starts_with(s, "auto:"))
5458         trail_pos = s.find(':', trail_pos + 1);
5459 
5460     string sort_cond = trail_pos == string::npos? s : s.substr(0, trail_pos);
5461 
5462     trim_string(sort_cond);
5463     sort = _read_bool_or_number(sort_cond, sort, "auto:");
5464 
5465     if (trail_pos != string::npos)
5466         s = s.substr(trail_pos + 1);
5467     else
5468         s.clear();
5469 }
5470 
set_comparators(string & s)5471 void menu_sort_condition::set_comparators(string &s)
5472 {
5473     init_item_sort_comparators(
5474         cmp,
5475         s.empty()? "equipped, basename, qualname, curse, qty" : s);
5476 }
5477