Source code for core.settings

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Configuration and raw manipulation for Dwarf Fortress."""

import os
import re
import sys

from . import hacks, log
from .dfraw import DFRaw


# Markers to read certain settings correctly
# pylint:disable=too-few-public-methods
class _DisableValues(object):
    """Marker class for DFConfiguration. Value is disabled by replacing [ and ]
    with !."""


_disabled = _DisableValues()


class _NegatedBool(object):
    """Marker class for DFConfiguration. Swaps YES and NO."""


_negated_bool = _NegatedBool()


class _AnnouncementFocus(object):
    """Marker class for DFConfiguration. Value controls presence of P and R
    flags in ``announcements.txt``."""


_announcement_focus = _AnnouncementFocus()

# Format: Key = tag name, value = list of version numbers
# First value indicates first version with the tag
# Second value, if present, indicates first version WITHOUT the tag
# When adding data here, keep the list sorted using the following ordering:
# 1) First version containing the field
# 2) Last version containing the field (fields that are still present go last)
# 3) Field name
_option_version_data = {
    'EXTENDED_ASCII': ['0.21.93.19a', '0.21.104.19d'],
    'BLACK_B': ['0.21.93.19a', '0.31.04'],
    'BLACK_G': ['0.21.93.19a', '0.31.04'],
    'BLACK_R': ['0.21.93.19a', '0.31.04'],
    'BLUE_B': ['0.21.93.19a', '0.31.04'],
    'BLUE_G': ['0.21.93.19a', '0.31.04'],
    'BLUE_R': ['0.21.93.19a', '0.31.04'],
    'BROWN_B': ['0.21.93.19a', '0.31.04'],
    'BROWN_G': ['0.21.93.19a', '0.31.04'],
    'BROWN_R': ['0.21.93.19a', '0.31.04'],
    'CYAN_B': ['0.21.93.19a', '0.31.04'],
    'CYAN_G': ['0.21.93.19a', '0.31.04'],
    'CYAN_R': ['0.21.93.19a', '0.31.04'],
    'DGRAY_B': ['0.21.93.19a', '0.31.04'],
    'DGRAY_G': ['0.21.93.19a', '0.31.04'],
    'DGRAY_R': ['0.21.93.19a', '0.31.04'],
    'GREEN_B': ['0.21.93.19a', '0.31.04'],
    'GREEN_G': ['0.21.93.19a', '0.31.04'],
    'GREEN_R': ['0.21.93.19a', '0.31.04'],
    'LBLUE_B': ['0.21.93.19a', '0.31.04'],
    'LBLUE_G': ['0.21.93.19a', '0.31.04'],
    'LBLUE_R': ['0.21.93.19a', '0.31.04'],
    'LCYAN_B': ['0.21.93.19a', '0.31.04'],
    'LCYAN_G': ['0.21.93.19a', '0.31.04'],
    'LCYAN_R': ['0.21.93.19a', '0.31.04'],
    'LGRAY_B': ['0.21.93.19a', '0.31.04'],
    'LGRAY_G': ['0.21.93.19a', '0.31.04'],
    'LGRAY_R': ['0.21.93.19a', '0.31.04'],
    'LGREEN_B': ['0.21.93.19a', '0.31.04'],
    'LGREEN_G': ['0.21.93.19a', '0.31.04'],
    'LGREEN_R': ['0.21.93.19a', '0.31.04'],
    'LMAGENTA_B': ['0.21.93.19a', '0.31.04'],
    'LMAGENTA_G': ['0.21.93.19a', '0.31.04'],
    'LMAGENTA_R': ['0.21.93.19a', '0.31.04'],
    'LRED_B': ['0.21.93.19a', '0.31.04'],
    'LRED_G': ['0.21.93.19a', '0.31.04'],
    'LRED_R': ['0.21.93.19a', '0.31.04'],
    'MAGENTA_B': ['0.21.93.19a', '0.31.04'],
    'MAGENTA_G': ['0.21.93.19a', '0.31.04'],
    'MAGENTA_R': ['0.21.93.19a', '0.31.04'],
    'RED_B': ['0.21.93.19a', '0.31.04'],
    'RED_G': ['0.21.93.19a', '0.31.04'],
    'RED_R': ['0.21.93.19a', '0.31.04'],
    'WHITE_B': ['0.21.93.19a', '0.31.04'],
    'WHITE_G': ['0.21.93.19a', '0.31.04'],
    'WHITE_R': ['0.21.93.19a', '0.31.04'],
    'YELLOW_B': ['0.21.93.19a', '0.31.04'],
    'YELLOW_G': ['0.21.93.19a', '0.31.04'],
    'YELLOW_R': ['0.21.93.19a', '0.31.04'],
    'DISPLAY_LENGTH': ['0.21.93.19a', '0.47.05'],
    'MORE': ['0.21.93.19a', '0.47.05'],
    'VARIED_GROUND_TILES': ['0.21.93.19a', '0.47.05'],
    'FONT': ['0.21.93.19a'],
    'FULLFONT': ['0.21.93.19a'],
    'FULLSCREENX': ['0.21.93.19a'],
    'FULLSCREENY': ['0.21.93.19a'],
    'WINDOWEDX': ['0.21.93.19a'],
    'WINDOWEDY': ['0.21.93.19a'],
    'INTRO': ['0.21.100.19a', '0.47.05'],
    'SOUND': ['0.21.100.19a'],
    'KEY_HOLD_MS': ['0.21.101.19a'],
    'NICKNAME_ADVENTURE': ['0.21.102.19a'],
    'NICKNAME_DWARF': ['0.21.102.19a'],
    'NICKNAME_LEGENDS': ['0.21.102.19a'],
    'WINDOWED': ['0.21.102.19a'],
    'ENGRAVINGS_START_OBSCURED': ['0.21.104.19d'],
    'MOUSE': ['0.21.104.21a', '0.47.05'],
    'MOUSE_PICTURE': ['0.21.104.21a', '0.47.05'],
    'ADVENTURER_TRAPS': ['0.22.110.23a', '0.47.05'],
    'BLACK_SPACE': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_BLACK_SPACE': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_FONT': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_FULLFONT': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_FULLSCREENX': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_FULLSCREENY': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_WINDOWEDX': ['0.22.120.23a', '0.47.05'],
    'GRAPHICS_WINDOWEDY': ['0.22.120.23a', '0.47.05'],
    'FPS': ['0.22.121.23b'],
    'TEMPERATURE': ['0.22.121.23b'],
    'WEATHER': ['0.22.121.23b'],
    'FPS_CAP': ['0.23.130.23a'],
    'POPULATION_CAP': ['0.23.130.23a'],
    'ADVENTURER_ALWAYS_CENTER': ['0.27.169.32a', '0.47.05'],
    'ADVENTURER_Z_VIEWS': ['0.27.169.32a', '0.47.05'],
    'AUTOBACKUP': ['0.27.169.32a', '0.47.05'],
    'CHASM': ['0.27.169.32a', '0.47.05'],
    'COFFIN_NO_PETS_DEFAULT': ['0.27.169.32a', '0.47.05'],
    'ECONOMY': ['0.27.169.32a', '0.47.05'],
    'INVADERS': ['0.27.169.32a', '0.47.05'],
    'SKY': ['0.27.169.32a', '0.47.05'],
    'TEXTURE_PARAM': ['0.27.169.32a', '0.47.05', '50.12'],
    'TOPMOST': ['0.27.169.32a', '0.47.05'],
    'VOLUME': ['0.27.169.32a', '0.47.05'],
    'VSYNC': ['0.27.169.32a', '0.47.05'],
    'AQUIFER': ['0.27.169.32a'],
    'ARTIFACTS': ['0.27.169.32a'],
    'AUTOSAVE': ['0.27.169.32a'],
    'CAVEINS': ['0.27.169.32a'],
    'G_FPS_CAP': ['0.27.169.32a'],
    'INITIAL_SAVE': ['0.27.169.32a'],
    'LOG_MAP_REJECTS': ['0.27.169.32a'],
    'PATH_COST': ['0.27.169.32a'],
    'RECENTER_INTERFACE_SHUTDOWN_MS': ['0.27.169.32a'],
    'SHOW_FLOW_AMOUNTS': ['0.27.169.32a'],
    'SHOW_IMP_QUALITY': ['0.27.169.32a'],
    'PRIORITY': ['0.27.169.33c', '0.47.05'],
    'EMBARK_RECTANGLE': ['0.27.169.33g'],
    'PAUSE_ON_LOAD': ['0.27.169.33g'],
    'ZERO_RENT': ['0.27.176.38a', '0.47.05'],
    'BABY_CHILD_CAP': ['0.27.176.38a'],
    'AUTOSAVE_PAUSE': ['0.27.176.38b'],
    'EMBARK_WARNING_ALWAYS': ['0.27.176.38b'],
    'IDLERS': ['0.28.181.39a', '0.47.05'],
    'SHOW_ALL_HISTORY_IN_DWARF_MODE': ['0.28.181.39a'],
    'SHOW_EMBARK_CHASM': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_M_PIPE': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_M_POOL': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_OTHER': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_PIT': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_POOL': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_RIVER': ['0.28.181.39d', '0.31.01'],
    'SHOW_EMBARK_TUNNEL': ['0.28.181.39d'],
    'GRID': ['0.28.181.39f'],
    'STORE_DIST_BARREL_COMBINE': ['0.28.181.40a'],
    'STORE_DIST_BIN_COMBINE': ['0.28.181.40a'],
    'STORE_DIST_BUCKET_COMBINE': ['0.28.181.40a'],
    'STORE_DIST_ITEM_DECREASE': ['0.28.181.40a'],
    'STORE_DIST_SEED_COMBINE': ['0.28.181.40a'],
    'FULLGRID': ['0.28.181.40b'],
    'PARTIAL_PRINT': ['0.28.181.40b'],
    'WOUND_COLOR_BROKEN': ['0.31.01', '0.47.05'],
    'WOUND_COLOR_FUNCTION_LOSS': ['0.31.01', '0.47.05'],
    'WOUND_COLOR_INHIBITED': ['0.31.01', '0.47.05'],
    'WOUND_COLOR_MINOR': ['0.31.01', '0.47.05'],
    'WOUND_COLOR_MISSING': ['0.31.01', '0.47.05'],
    'WOUND_COLOR_NONE': ['0.31.01', '0.47.05'],
    'COMPRESSED_SAVES': ['0.31.01'],
    'DIG_CANCEL_DAMP': ['0.31.01'],
    'DIG_CANCEL_WARM': ['0.31.01'],
    'TESTING_ARENA': ['0.31.01'],
    'PILLAR_TILE': ['0.31.08', '0.47.05'],
    'ZOOM_SPEED': ['0.31.13', '0.47.05'],
    'ARB_SYNC': ['0.31.13', '0.47.05'],
    'PRINT_MODE': ['0.31.13', '0.47.05', '50.12'],
    'SINGLE_BUFFER': ['0.31.13', '0.47.05'],
    'TRUETYPE': ['0.31.13', '0.47.05'],
    'KEY_REPEAT_ACCEL_LIMIT': ['0.31.13'],
    'KEY_REPEAT_ACCEL_START': ['0.31.13'],
    'KEY_REPEAT_MS': ['0.31.13'],
    'MACRO_MS': ['0.31.13'],
    'RESIZABLE': ['0.31.13'],
    'WALKING_SPREADS_SPATTER_ADV': ['0.31.16'],
    'WALKING_SPREADS_SPATTER_DWF': ['0.31.16'],
    'SET_LABOR_LISTS': ['0.34.03', '0.47.05'],
    'TRACK_E': ['0.34.08', '0.47.05'],
    'TRACK_EW': ['0.34.08', '0.47.05'],
    'TRACK_N': ['0.34.08', '0.47.05'],
    'TRACK_NE': ['0.34.08', '0.47.05'],
    'TRACK_NEW': ['0.34.08', '0.47.05'],
    'TRACK_NS': ['0.34.08', '0.47.05'],
    'TRACK_NSE': ['0.34.08', '0.47.05'],
    'TRACK_NSEW': ['0.34.08', '0.47.05'],
    'TRACK_NSW': ['0.34.08', '0.47.05'],
    'TRACK_NW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_E': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_EW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_N': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NE': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NEW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NS': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NSE': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NSEW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NSW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_NW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_S': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_SE': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_SEW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_SW': ['0.34.08', '0.47.05'],
    'TRACK_RAMP_W': ['0.34.08', '0.47.05'],
    'TRACK_S': ['0.34.08', '0.47.05'],
    'TRACK_SE': ['0.34.08', '0.47.05'],
    'TRACK_SEW': ['0.34.08', '0.47.05'],
    'TRACK_SW': ['0.34.08', '0.47.05'],
    'TRACK_W': ['0.34.08', '0.47.05'],
    'TREE_BRANCH_EW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_EW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NE': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NEW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NS': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NS_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSE': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSEW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NSW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_NW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SE': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SEW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SW': ['0.40.01', '0.47.05'],
    'TREE_BRANCH_SW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_BRANCHES': ['0.40.01', '0.47.05'],
    'TREE_BRANCHES_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR1': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR1_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR2': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR2_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR3': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR3_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR4': ['0.40.01', '0.47.05'],
    'TREE_CAP_FLOOR4_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_PILLAR': ['0.40.01', '0.47.05'],
    'TREE_CAP_PILLAR_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_RAMP': ['0.40.01', '0.47.05'],
    'TREE_CAP_RAMP_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_E': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_E_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_N': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_N_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_NE': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_NE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_NW': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_NW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_S': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_S_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_SE': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_SE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_SW': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_SW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_W': ['0.40.01', '0.47.05'],
    'TREE_CAP_WALL_W_DEAD': ['0.40.01', '0.47.05'],
    'TREE_ROOT_SLOPING': ['0.40.01', '0.47.05'],
    'TREE_ROOT_SLOPING_DEAD': ['0.40.01', '0.47.05'],
    'TREE_ROOTS': ['0.40.01', '0.47.05'],
    'TREE_ROOTS_DEAD': ['0.40.01', '0.47.05'],
    'TREE_SMOOTH_BRANCHES': ['0.40.01', '0.47.05'],
    'TREE_SMOOTH_BRANCHES_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_E': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_E_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_N': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_N_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_S': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_S_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_W': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_BRANCH_W_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_E': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_E_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_EW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_EW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_INTERIOR': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_INTERIOR_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_N': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_N_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NE': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NEW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NS': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NS_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSE': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSEW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NSW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_NW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_PILLAR': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_PILLAR_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_S': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_S_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SE': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SE_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SEW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SEW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SLOPING': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SLOPING_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SW': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_SW_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_W': ['0.40.01', '0.47.05'],
    'TREE_TRUNK_W_DEAD': ['0.40.01', '0.47.05'],
    'TREE_TWIGS': ['0.40.01', '0.47.05'],
    'TREE_TWIGS_DEAD': ['0.40.01', '0.47.05'],
    'FORTRESS_SEED_CAP': ['0.40.01'],
    'SPECIFIC_SEED_CAP': ['0.40.01'],
    'STRICT_POPULATION_CAP': ['0.40.05'],
    'POST_PREPARE_EMBARK_CONFIRMATION': ['0.40.09'],
    'GRAZE_COEFFICIENT': ['0.40.13'],
    'INVASION_SOLDIER_CAP': ['0.42.01', '0.47.05'],
    'INVASION_MONSTER_CAP': ['0.42.01', '0.47.05'],
    'VISITOR_CAP': ['0.42.01'],
    'PRIESTHOOD_UNIT_COUNTS': ['0.47.01', '0.47.05'],
    'TEMPLE_VALUE_LEVELS': ['0.47.01', '0.47.05'],
    'GUILD_UNIT_COUNTS': ['0.47.01', '0.47.05'],
    'GUILDHALL_VALUE_LEVELS': ['0.47.01', '0.47.05'],
    'MAXIMUM_INTERFACE_PIXEL_WIDTH': ['50.01', '50.01'],
    'AMBIENCE_VOLUME': ['50.01'],
    'BASIC_FONT': ['50.01'],
    'INTERFACE_SCALING_DESIRED_GRID_HEIGHT': ['50.01'],
    'INTERFACE_SCALING_DESIRED_GRID_WIDTH': ['50.01'],
    'INTERFACE_SCALING_PERCENTAGE': ['50.01'],
    'INTERFACE_SCALING_TO_DESIRED_GRID': ['50.01'],
    'KEYBOARD_CURSOR': ['50.01'],
    'MASTER_VOLUME': ['50.01'],
    'MAXIMUM_EMBARK_DIM': ['50.01'],
    'MUSIC_VOLUME': ['50.01'],
    'SHOW_RAMP_ARROWS': ['50.01'],
    'NUMBER_OF_LOWER_ELEVATIONS_SHOWN': ['50.01'],
    'SFX_VOLUME': ['50.01'],
    'MAXIMUM_INTERFACE_PERCENTAGE': ['50.02'],
    'AVERAGE_TIME_BETWEEN_SONGS': ['50.04'],
    'USE_CLASSIC_ASCII': ['50.04'],
    'CULL_DEAD_UNITS_AT': ['50.06'],
    'MULTITHREADING': ['50.12'],
}


def _option_item_to_value(item):
    """Removes any validation expression from <item>."""
    if not isinstance(item, str):
        return item[0]
    return item


[docs] class DFConfiguration(object): """Reads and modifies Dwarf Fortress configuration textfiles.""" # pylint: disable=too-many-instance-attributes,too-many-statements def __init__(self, base_dir, df_info): """ Constructor for DFConfiguration. Args: base_dir: Path containing the Dwarf Fortress instance to operate on """ self.base_dir = base_dir self.settings = {} self.options = {} self.field_names = {} self.inverse_field_names = {} self.files = {} self.in_files = {} self.missing_fields = [] self.validate = {} self.df_info = df_info # init.txt boolvals = ("YES", "NO") if df_info.version >= '50.01': init = (os.path.join(base_dir, 'data', 'init', 'init_default.txt'), os.path.join(base_dir, 'prefs', 'init.txt')) else: init = (os.path.join(base_dir, 'data', 'init', 'init.txt'),) self.create_option( "truetype", "TRUETYPE", "YES", boolvals, init, 'legacy' not in df_info.variations, self.validate_truetype) self.create_option("sound", "SOUND", "YES", boolvals, init) self.create_option("volume", "VOLUME", "255", None, init) self.create_option("introMovie", "INTRO", "YES", boolvals, init) self.create_option( "startWindowed", "WINDOWED", "YES", ("YES", "NO", "PROMPT"), init) self.create_option("fpsCounter", "FPS", "NO", boolvals, init) self.create_option("fpsCap", "FPS_CAP", "100", None, init) self.create_option("gpsCap", "G_FPS_CAP", "50", None, init) self.create_option( "procPriority", "PRIORITY", "NORMAL", ( "REALTIME", "HIGH", "ABOVE_NORMAL", "NORMAL", "BELOW_NORMAL", "IDLE"), init) self.create_option( "compressSaves", "COMPRESSED_SAVES", "YES", boolvals, init) twbt_validate = hacks.is_dfhack_enabled twbt_validate_error = "DFHack is required for TWBT." printmodes = ["2D", "STANDARD"] if 'twbt' in df_info.variations: printmodes += [ ("TWBT", twbt_validate, twbt_validate_error), ("TWBT_LEGACY", twbt_validate, twbt_validate_error)] self.create_option( "printmode", "PRINT_MODE", "2D", tuple(printmodes), init, 'legacy' not in df_info.variations) # d_init.txt if df_info.version >= '50.01': dinit = (os.path.join(base_dir, 'data', 'init', 'd_init_default.txt'), os.path.join(base_dir, 'prefs', 'd_init.txt')) elif df_info.version > '0.31.03': dinit = (os.path.join(base_dir, 'data', 'init', 'd_init.txt'),) else: dinit = init self.create_option("popcap", "POPULATION_CAP", "200", None, dinit) self.create_option( "strictPopcap", "STRICT_POPULATION_CAP", "220", None, dinit) self.create_option( "childcap", "BABY_CHILD_CAP", "100:1000", None, dinit) self.create_option("invaders", "INVADERS", "YES", boolvals, dinit) self.create_option( "temperature", "TEMPERATURE", "YES", boolvals, dinit) self.create_option("weather", "WEATHER", "YES", boolvals, dinit) self.create_option("caveins", "CAVEINS", "YES", boolvals, dinit) self.create_option( "liquidDepth", "SHOW_FLOW_AMOUNTS", "YES", boolvals, dinit) self.create_option( "variedGround", "VARIED_GROUND_TILES", "YES", boolvals, dinit) self.create_option( "engravingsObscured", "ENGRAVINGS_START_OBSCURED", "NO", boolvals, dinit) self.create_option( "improvementQuality", "SHOW_IMP_QUALITY", "YES", boolvals, dinit) if df_info.version <= '0.34.06': self.create_option( "laborLists", "SET_LABOR_LISTS", "YES", boolvals, dinit) else: self.create_option("laborLists", "SET_LABOR_LISTS", "SKILLS", ( "NO", "SKILLS", "BY_UNIT_TYPE"), dinit) if df_info.version >= '50.01': autosave_opts = ("NONE", "SEASONAL", "SEMIANNUALLY", "YEARLY") else: autosave_opts = ("NONE", "SEASONAL", "YEARLY") self.create_option("autoSave", "AUTOSAVE", "SEASONAL", autosave_opts, dinit) self.create_option("autoBackup", "AUTOBACKUP", "YES", boolvals, dinit) self.create_option( "autoSavePause", "AUTOSAVE_PAUSE", "YES", boolvals, dinit) self.create_option( "initialSave", "INITIAL_SAVE", "YES", boolvals, dinit) self.create_option( "pauseOnLoad", "PAUSE_ON_LOAD", "YES", boolvals, dinit) self.create_option( "entombPets", "COFFIN_NO_PETS_DEFAULT", "NO", _negated_bool, dinit) self.create_option("artifacts", "ARTIFACTS", "YES", boolvals, dinit) self.create_option("grazeCoef", "GRAZE_COEFFICIENT", "100", None, dinit) self.create_option("visitorCap", "VISITOR_CAP", "100", None, dinit) self.create_option( "invSoldierCap", "INVASION_SOLDIER_CAP", "120", None, dinit) self.create_option( "invMonsterCap", "INVASION_MONSTER_CAP", "40", None, dinit) self.create_option( "templeCount", "PRIESTHOOD_UNIT_COUNTS", "10:25", None, dinit) self.create_option( "guildCount", "GUILD_UNIT_COUNTS", "10:25", None, dinit) # special if df_info.version < '0.31': aquifer_files = [ 'matgloss_stone_layer.txt', 'matgloss_stone_mineral.txt', 'matgloss_stone_soil.txt'] else: aquifer_files = [ 'inorganic_stone_layer.txt', 'inorganic_stone_mineral.txt', 'inorganic_stone_soil.txt'] if df_info.version >= '50.01': aquifer_dir = os.path.join(base_dir, 'data', 'vanilla', 'vanilla_materials', 'objects') else: aquifer_dir = os.path.join(base_dir, 'raw', 'objects') self.create_option("aquifers", "AQUIFER", "NO", _disabled, tuple( os.path.join(aquifer_dir, a) for a in aquifer_files)) announcements = (os.path.join( base_dir, 'data', 'init', 'announcements.txt'),) self.create_option( "focusDamp", "DIG_CANCEL_DAMP", "YES", _announcement_focus, announcements) self.create_option( "focusWarm", "DIG_CANCEL_WARM", "YES", _announcement_focus, announcements) # pylint: disable=too-many-arguments
[docs] def create_option( self, name, field_name, default, values, files, cond=True, validate=None): """ Register an option to write back for changes. If the field_name has been registered before, no changes are made. Fields that do not exist in the current DF version are simply registered with a field name mapping, but they will not be expected in the init files. Args: name: The name you want to use to refer to this field (becomes available as an attribute on this class). field_name: The field name used in the file. If this is different from the name argument, this will also become available as an attribute. default: The value to initialize this setting to. values: An iterable of valid values for this field. Used in cycle_list. Special values defined in this file: :None: Unspecified value; cycling has no effect. :_disabled: Boolean option toggled by replacing the ``[]`` around the field name with ``!!``. :force_bool: Values other than "YES" and "NO" are interpreted as "YES". files: A tuple of files this value is read from. Used for e.g. aquifer toggling, which requires editing multiple files. cond (bool): If True, the field will be treated as valid. If False, this will merely register the field name mapping. Defaults to True. validate: An optional function(x) which may be used to validate a value for this field. Will be sent the value as parameter, must return a tuple (ok, errormsg). """ # Don't allow re-registration of a known field if name in self.settings or name in self.inverse_field_names: return # Ignore registration if version doesn't have tag self.field_names[name] = field_name if not (cond and self.version_has_option(field_name)): return self.validate[name] = validate self.settings[name] = default self.options[name] = values if field_name != name: self.inverse_field_names[field_name] = name self.files[name] = files self.in_files.setdefault(files, []) self.in_files[files].append(name)
def __iter__(self): for key, value in list(self.settings.items()): yield key, value
[docs] def set_value(self, name, value): """ Sets the setting <name> to <value>. Args: name: name of the setting to alter. value: new value for the setting. """ self.settings[name] = value
[docs] def cycle_item(self, name): """ Cycle the setting <name>. Args: name: name of the setting to cycle. """ self.settings[name] = self.cycle_list( self.settings[name], self.options[name])
[docs] @staticmethod def cycle_list(current, items): """Cycles setting between a list of items. Args: current: current value. items: list of possible values (optionally a special value). Returns: If no list of values is given, returns current. If the current value is the last value in the list, or the value does not exist in the list, returns the first value in the list. Otherwise, returns the value from items immediately following the current value. """ if items is None: return current if items in (_disabled, _negated_bool, _announcement_focus): items = ("YES", "NO") if current not in items: for i in items: if not isinstance(i, str) and i[0] == current: current = i break else: # item not found result = items[0] return _option_item_to_value(result) i = 1 while i < len(items): result = items[(items.index(current) + i) % len(items)] if isinstance(result, str): break if result[1](): break i = i + 1 return _option_item_to_value(result)
[docs] def validate_truetype(self, value): """Validates the Truetype setting.""" if self.settings["printmode"] != "2D": return (True, "") if value in self.options["truetype"]: return (True, "") try: int(value) except ValueError: return (False, "Must be YES, NO or a number") return (True, "")
[docs] def validate_config(self): """ Checks if all settings are set to valid values. Returns a list of strings with error messages. """ errors = [] for f, current in self.settings.items(): fn = self.field_names[f] if self.validate[f]: ok, error = self.validate[f](current) if not ok: errors.append( "Invalid value %s for option %s: %s" % ( current, fn, error)) continue items = self.options[f] if items is None: continue if items in (_disabled, _negated_bool, _announcement_focus): items = ("YES", "NO") if current not in items: for i in items: if not isinstance(i, str) and i[0] == current: current = i break else: errors.append( "Invalid value %s for option %s: Unknown value" % ( current, fn)) continue if isinstance(current, str): continue if not current[1](): errors.append( "Invalid value %s for option %s: %s" % ( current[0], fn, current[2])) return errors
[docs] def read_settings(self): """Read settings from known filesets. If fileset only contains one file ending with "init.txt", all options will be registered automatically.""" for files, fields in self.in_files.items(): for filename in files: self.read_file( filename, fields, any((f.endswith('init.txt') for f in files)), files)
[docs] def read_file(self, filename, fields, auto_add, auto_add_key=None): """ Reads DF settings from the file <filename>. Args: filename: the file to read from. fields: an iterable containing the field names to read. auto_add: whether to automatically register all unknown fields for changes. auto_add_key: key to register the fields under (if auto_add is True). """ # pylint:disable=too-many-branches if not os.path.exists(filename): log.w('File ' + str(filename) + ' does not exist', file=sys.stderr) return text = DFRaw.read(filename) if auto_add: for match in re.findall(r'\[(.+?):(.+?)]', text): self.create_option( match[0], match[0], match[1], None, auto_add_key) for field in fields: if field in self.inverse_field_names: field = self.inverse_field_names[field] if self.options[field] is _disabled: # If there is a single match, flag the option as enabled if "[{0}]".format(self.field_names[field]) in text: self.settings[field] = "YES" else: match = re.search(r'\[{0}:(.+?)]'.format( self.field_names[field]), text) if match: value = match.group(1) if self.options[field] is _negated_bool: value = ["YES", "NO"][["NO", "YES"].index(value)] elif self.options[field] is _announcement_focus: values = value.split(':') if 'P' in values and 'R' in values: value = "YES" else: value = "NO" self.settings[field] = value else: self.missing_fields.append(self.field_names[field]) log.w( 'Field ' + str(self.field_names[field]) + ' seems to be missing from file ' + str(filename) + '!', file=sys.stderr)
[docs] @staticmethod def has_field(filename, field, num_params=-1, min_params=-1, max_params=-1): """ Returns True if <field> exists in <filename> and has the specified number of parameters. Args: filename: the file to check. field: the field to look for. num_params: the exact number of parameters for the field. -1 for no limit. min_params: the minimum number of parameters for the field. -1 for no limit. max_params: the maximum number of parameters for the field. -1 for no limit. """ try: match = re.search( r'\[' + str(field) + r'(:.+?)]', DFRaw.read(filename)) if match is None: return False params = match.group(1) param_count = params.count(":") if num_params not in (-1, param_count): return False if min_params != -1 and param_count < min_params: return False if max_params != -1 and param_count > max_params: return False return True except IOError: return False
[docs] def write_settings(self): """Write all settings to their respective files.""" for files, fields in self.in_files.items(): if any((f for f in files if f.endswith('init.txt'))): filename = [f for f in files if f.endswith('init.txt')][0] self.update_file(filename, fields) else: for filename in files: self.update_file(filename, fields)
[docs] def update_file(self, filename, fields): """ Write settings to a specific file. Args: filename: name of the file to write. fields: list of all field names to change. """ with DFRaw(filename) as raw: for field in fields: field_name = self.field_names[field] if self.options[field] is _announcement_focus: node = raw.find_first(field_name) values = node.values if "P" in values: values.remove("P") if "R" in values: values.remove("R") if self.settings[field] == "YES": values.append("P") values.append("R") node.value = values elif self.options[field] is _disabled: raw.set_all(field_name, self.settings[field] != "NO") else: value = self.settings[field] if self.options[field] is _negated_bool: value = ["YES", "NO"][["NO", "YES"].index(value)] raw.set_value(field_name, value)
[docs] def create_file(self, filename, fields): """ Create a new file containing the specified fields. Args: filename: name of the file to write. fields: list of all field names to write. """ with DFRaw.open(filename, 'wt') as newfile: for field in fields: if self.options[field] is _disabled: if self.settings[field] == "NO": text = "!{0}!" else: text = "[{0}]" newfile.write(text.format(self.field_names[field]) + '\n') else: value = self.settings[field] if self.options[field] is _negated_bool: value = ["YES", "NO"][["NO", "YES"].index(value)] elif self.options[field] is _announcement_focus: value = "P:R" if value == "YES" else "" newfile.write('[' + field + ':' + value + ']\n')
[docs] def version_has_option(self, option_name): """Returns True if <option_name> exists in the current DF version.""" if option_name in self.field_names: option_name = self.field_names[option_name] if option_name in self.missing_fields: # Field was missing when expected, pretend it doesn't exist yet return False if option_name[0] == option_name.lower()[0]: # Internal name, let it pass by return True if option_name not in _option_version_data: log.w("Unknown option: %s", option_name) # Unknown option, must be a later DF than this knows about return False option = _option_version_data[option_name] if len(option) == 2: return option[0] <= self.df_info.version < option[1] return option[0] <= self.df_info.version
def __str__(self): return ( "base_dir = {0}\nsettings = {1}\noptions = {2}\n" "field_names ={3}\ninverse_field_names = {4}\nfiles = {5}\n" "in_files = {6}").format( self.base_dir, self.settings, self.options, self.field_names, self.inverse_field_names, self.files, self.in_files) def __getattr__(self, name): """Exposes all registered options through both their internal and registered names.""" if name in self.inverse_field_names: return self.settings[self.inverse_field_names[name]] return self.settings[name]
# vim:expandtab