#!/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