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