#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Graphics pack management."""
import glob
import os
import shutil
from . import baselines, colors, df, log, manifest, mods, paths
from .dfraw import DFRaw
from .launcher import open_file
from .lnp import lnp
[docs]def open_graphics():
"""Opens the graphics pack folder."""
open_file(paths.get('graphics'))
[docs]def get_title(pack):
"""Returns the pack title; either per manifest or from dirname."""
title = manifest.get_cfg('graphics', pack).get_string('title')
if title:
return title
return pack
[docs]def get_folder_prefix(pack):
"""Returns the pack folder_prefix; either per manifest or from dirname."""
folder_prefix = manifest.get_cfg('graphics', pack).get_string(
'folder_prefix')
if folder_prefix:
return folder_prefix
return pack
[docs]def current_pack():
"""Returns the currently installed graphics pack.
If the pack cannot be identified, returns "FONT/GRAPHICS_FONT".
"""
p = paths.get('df', 'raw', 'installed_raws.txt')
if os.path.isfile(p):
p = logged_graphics(p)
if p:
log.i('Read installed graphics ({}) from log'.format(p))
return p
packs = read_graphics()
for p in packs:
if (lnp.settings.FONT == p[1]
and (not lnp.settings.version_has_option('GRAPHICS_FONT')
or lnp.settings.GRAPHICS_FONT == p[2])):
log.i('Installed graphics is {} by checking tilesets'.format(p[0]))
return p[0]
result = str(lnp.settings.FONT)
if lnp.settings.version_has_option('GRAPHICS_FONT'):
result += '/' + str(lnp.settings.GRAPHICS_FONT)
log.w('Could not determine installed graphics, tileset is ' + result)
return result
[docs]def logged_graphics(logfile, start='graphics/'):
"""Returns the graphics pack from an 'installed_raws.txt' file"""
if os.path.isfile(logfile):
with open(logfile, encoding="utf-8") as f:
for line in f.readlines():
if line.startswith(start):
return line.strip().replace(start, '')
return ''
[docs]def read_graphics():
"""Returns a list of tuples of (graphics dir, FONT, GRAPHICS_FONT)."""
packs = [os.path.basename(o) for o in
glob.glob(paths.get('graphics', '*')) if os.path.isdir(o)
and manifest.is_compatible('graphics', os.path.basename(o))]
result = []
for p in packs:
if not validate_pack(p):
continue
init_path = paths.get('graphics', p, 'data', 'init', 'init.txt')
font = DFRaw(init_path).get_value('FONT')
graphics = DFRaw(init_path).get_value('GRAPHICS_FONT')
result.append((p, font, graphics))
return tuple(sorted(result))
[docs]def add_tilesets():
"""Copies missing tilesets from LNP/Tilesets to the data/art folder."""
for item in glob.glob(paths.get('tilesets', '*')):
if not os.path.exists(paths.get('data', 'art', os.path.basename(item))):
if os.path.isfile(item):
shutil.copy2(item, paths.get('data', 'art'))
else:
shutil.copytree(item, paths.get('data', 'art'))
[docs]def install_graphics(pack):
"""Installs the graphics pack located in LNP/Graphics/<pack>.
Args:
pack: the name of the pack to install.
Returns:
True if successful,
False if an exception occurred
None if baseline vanilla raws are missing
"""
# pylint:disable=too-many-branches
if not baselines.find_vanilla_raws():
log.w('Cannot install graphics when baseline raws are missing!')
return None
try:
# Update raws
if not update_graphics_raws(paths.get('df', 'raw'), pack):
return 0
# Backup TwbT-specific art files
for item in ('white1px.png', 'transparent1px.png'):
if os.path.exists(paths.get('data', 'art', item)):
shutil.copy(paths.get('data', 'art', item),
paths.get('graphics', pack, 'data', 'art'))
# Copy art
shutil.rmtree(paths.get('data', 'art'))
shutil.copytree(paths.get('graphics', pack, 'data', 'art'),
paths.get('data', 'art'))
add_tilesets()
# ensure that mouse.png and font.ttf exist (required by DF)
base = baselines.find_vanilla()
if base:
for item in ('mouse.png', 'font.ttf'):
cur = paths.get('data', 'art', item)
bas = os.path.join(base, 'data', 'art', item)
if not os.path.isfile(cur) and os.path.isfile(bas):
shutil.copy2(bas, cur)
# Handle init files
patch_inits(paths.get('graphics', pack))
# Remove file with old space-prefixed "Current graphics pack" name
if os.path.isfile(paths.get('colors',
' Current graphics pack.txt')):
os.remove(paths.get('colors', ' Current graphics pack.txt'))
# Install colorscheme
if lnp.df_info.version >= '0.31.04':
colors.load_colors(paths.get('graphics', pack, 'data', 'init',
'colors.txt'))
shutil.copyfile(paths.get('graphics', pack, 'data', 'init',
'colors.txt'),
paths.get('colors', '_Current graphics pack.txt'))
else:
colors.load_colors(paths.get('graphics', pack, 'data', 'init',
'init.txt'))
if os.path.isfile(paths.get('colors',
'_Current graphics pack.txt')):
os.remove(paths.get('colors', '_Current graphics pack.txt'))
# TwbT overrides
try:
os.remove(paths.get('init', 'overrides.txt'))
except Exception:
pass
try:
shutil.copyfile(
paths.get('graphics', pack, 'data', 'init', 'overrides.txt'),
paths.get('init', 'overrides.txt'))
except Exception:
pass
# TwbT file replacements
if 'TWBT' in lnp.settings.printmode:
for folder in ['graphics', 'objects']:
twbt_folder = paths.get('graphics', pack, 'raw', 'twbt_' + folder)
target_folder = paths.get('df', 'raw', folder)
for path, _, files in os.walk(twbt_folder):
for f in files:
twbt_f = os.path.join(path, f)
target_f = os.path.join(target_folder, os.path.relpath(
twbt_f, twbt_folder))
shutil.copyfile(twbt_f, target_f)
except Exception:
log.e('Something went wrong while installing graphics', stack=True)
df.load_params()
return False
df.load_params()
return True
[docs]def validate_pack(pack, df_version=None):
"""Checks for presence of all required files for a pack install."""
if df_version is None:
df_version = lnp.df_info.version
result = True
gfx_dir = paths.get('graphics', pack)
result &= os.path.isdir(gfx_dir)
result &= os.path.isdir(os.path.join(gfx_dir, 'data', 'init'))
result &= os.path.isdir(os.path.join(gfx_dir, 'data', 'art'))
result &= os.path.isfile(os.path.join(gfx_dir, 'data', 'init', 'init.txt'))
result &= manifest.is_compatible('graphics', pack, df_version)
if df_version >= '0.31.04':
result &= os.path.isfile(os.path.join(
gfx_dir, 'data', 'init', 'd_init.txt'))
result &= os.path.isfile(os.path.join(
gfx_dir, 'data', 'init', 'colors.txt'))
return result
[docs]def patch_inits(gfx_dir):
"""Installs init files from a graphics pack by selectively changing
specific fields. All settings but the mentioned fields are preserved.
"""
d_init_fields = [
'WOUND_COLOR_NONE', 'WOUND_COLOR_MINOR',
'WOUND_COLOR_INHIBITED', 'WOUND_COLOR_FUNCTION_LOSS',
'WOUND_COLOR_BROKEN', 'WOUND_COLOR_MISSING', 'SKY', 'CHASM',
'PILLAR_TILE', 'VARIED_GROUND_TILES', 'ENGRAVINGS_START_OBSCURED',
# Tracks
'TRACK_N', 'TRACK_S', 'TRACK_E', 'TRACK_W', 'TRACK_NS',
'TRACK_NE', 'TRACK_NW', 'TRACK_SE', 'TRACK_SW', 'TRACK_EW',
'TRACK_NSE', 'TRACK_NSW', 'TRACK_NEW', 'TRACK_SEW',
'TRACK_NSEW', 'TRACK_RAMP_N', 'TRACK_RAMP_S', 'TRACK_RAMP_E',
'TRACK_RAMP_W', 'TRACK_RAMP_NS', 'TRACK_RAMP_NE',
'TRACK_RAMP_NW', 'TRACK_RAMP_SE', 'TRACK_RAMP_SW',
'TRACK_RAMP_EW', 'TRACK_RAMP_NSE', 'TRACK_RAMP_NSW',
'TRACK_RAMP_NEW', 'TRACK_RAMP_SEW', 'TRACK_RAMP_NSEW',
# Trees
'TREE_ROOT_SLOPING', 'TREE_TRUNK_SLOPING',
'TREE_ROOT_SLOPING_DEAD', 'TREE_TRUNK_SLOPING_DEAD',
'TREE_ROOTS', 'TREE_ROOTS_DEAD', 'TREE_BRANCHES',
'TREE_BRANCHES_DEAD', 'TREE_SMOOTH_BRANCHES',
'TREE_SMOOTH_BRANCHES_DEAD', 'TREE_TRUNK_PILLAR',
'TREE_TRUNK_PILLAR_DEAD', 'TREE_CAP_PILLAR',
'TREE_CAP_PILLAR_DEAD', 'TREE_TRUNK_N', 'TREE_TRUNK_S',
'TREE_TRUNK_N_DEAD', 'TREE_TRUNK_S_DEAD', 'TREE_TRUNK_EW',
'TREE_TRUNK_EW_DEAD', 'TREE_CAP_WALL_N', 'TREE_CAP_WALL_S',
'TREE_CAP_WALL_N_DEAD', 'TREE_CAP_WALL_S_DEAD', 'TREE_TRUNK_E',
'TREE_TRUNK_W', 'TREE_TRUNK_E_DEAD', 'TREE_TRUNK_W_DEAD',
'TREE_TRUNK_NS', 'TREE_TRUNK_NS_DEAD', 'TREE_CAP_WALL_E',
'TREE_CAP_WALL_W', 'TREE_CAP_WALL_E_DEAD',
'TREE_CAP_WALL_W_DEAD', 'TREE_TRUNK_NW', 'TREE_CAP_WALL_NW',
'TREE_TRUNK_NW_DEAD', 'TREE_CAP_WALL_NW_DEAD', 'TREE_TRUNK_NE',
'TREE_CAP_WALL_NE', 'TREE_TRUNK_NE_DEAD',
'TREE_CAP_WALL_NE_DEAD', 'TREE_TRUNK_SW', 'TREE_CAP_WALL_SW',
'TREE_TRUNK_SW_DEAD', 'TREE_CAP_WALL_SW_DEAD', 'TREE_TRUNK_SE',
'TREE_CAP_WALL_SE', 'TREE_TRUNK_SE_DEAD',
'TREE_CAP_WALL_SE_DEAD', 'TREE_TRUNK_NSE',
'TREE_TRUNK_NSE_DEAD', 'TREE_TRUNK_NSW', 'TREE_TRUNK_NSW_DEAD',
'TREE_TRUNK_NEW', 'TREE_TRUNK_NEW_DEAD', 'TREE_TRUNK_SEW',
'TREE_TRUNK_SEW_DEAD', 'TREE_TRUNK_NSEW',
'TREE_TRUNK_NSEW_DEAD', 'TREE_TRUNK_BRANCH_N',
'TREE_TRUNK_BRANCH_N_DEAD', 'TREE_TRUNK_BRANCH_S',
'TREE_TRUNK_BRANCH_S_DEAD', 'TREE_TRUNK_BRANCH_E',
'TREE_TRUNK_BRANCH_E_DEAD', 'TREE_TRUNK_BRANCH_W',
'TREE_TRUNK_BRANCH_W_DEAD', 'TREE_BRANCH_NS',
'TREE_BRANCH_NS_DEAD', 'TREE_BRANCH_EW', 'TREE_BRANCH_EW_DEAD',
'TREE_BRANCH_NW', 'TREE_BRANCH_NW_DEAD', 'TREE_BRANCH_NE',
'TREE_BRANCH_NE_DEAD', 'TREE_BRANCH_SW', 'TREE_BRANCH_SW_DEAD',
'TREE_BRANCH_SE', 'TREE_BRANCH_SE_DEAD', 'TREE_BRANCH_NSE',
'TREE_BRANCH_NSE_DEAD', 'TREE_BRANCH_NSW',
'TREE_BRANCH_NSW_DEAD', 'TREE_BRANCH_NEW',
'TREE_BRANCH_NEW_DEAD', 'TREE_BRANCH_SEW',
'TREE_BRANCH_SEW_DEAD', 'TREE_BRANCH_NSEW',
'TREE_BRANCH_NSEW_DEAD', 'TREE_TWIGS', 'TREE_TWIGS_DEAD',
'TREE_CAP_RAMP', 'TREE_CAP_RAMP_DEAD', 'TREE_CAP_FLOOR1',
'TREE_CAP_FLOOR2', 'TREE_CAP_FLOOR1_DEAD',
'TREE_CAP_FLOOR2_DEAD', 'TREE_CAP_FLOOR3', 'TREE_CAP_FLOOR4',
'TREE_CAP_FLOOR3_DEAD', 'TREE_CAP_FLOOR4_DEAD',
'TREE_TRUNK_INTERIOR', 'TREE_TRUNK_INTERIOR_DEAD']
init_fields = [
'FONT', 'FULLFONT', 'GRAPHICS', 'GRAPHICS_FONT',
'GRAPHICS_FULLFONT', 'TRUETYPE', 'PRINT_MODE',
'GRAPHICS_BLACK_SPACE', 'TEXTURE_PARAM', 'MOUSE_PICTURE']
init_fields = [f for f in init_fields if lnp.settings.version_has_option(f)]
d_init_fields = [
f for f in d_init_fields if lnp.settings.version_has_option(f)]
init = os.path.join(gfx_dir, 'data', 'init', 'init.txt')
if lnp.df_info.version <= '0.31.03':
d_init = init
else:
d_init = os.path.join(gfx_dir, 'data', 'init', 'd_init.txt')
lnp.settings.read_file(init, init_fields, False)
lnp.settings.read_file(d_init, d_init_fields, False)
df.save_params()
[docs]def simplify_graphics():
"""Removes unnecessary files from all graphics packs."""
for pack in read_graphics():
simplify_pack(pack[0])
[docs]def simplify_pack(pack):
"""Removes unnecessary files from one graphics pack."""
a = baselines.simplify_pack(pack, 'graphics')
b = baselines.remove_vanilla_raws_from_pack(pack, 'graphics')
c = baselines.remove_empty_dirs(pack, 'graphics')
if not all(isinstance(n, int) for n in (a, b, c)):
return False
return a + b + c
[docs]def savegames_to_update():
"""Returns a list of savegames that will be updated."""
return [o for o in glob.glob(paths.get('save', '*'))
if os.path.isdir(o) and not o.endswith('current')]
[docs]def update_graphics_raws(raw_dir, pack):
"""Updates raws in place for a new graphics pack.
Args:
raw_dir: full path to the dir to update
pack: the name of the graphics pack to add (eg 'Phoebus')
Returns:
True if successful, False if aborted
"""
if not validate_pack(pack):
log.w('Cannot update raws to an invalid graphics pack (' + pack + ')')
return None
built_log = paths.get('baselines', 'temp', 'raw', 'installed_raws.txt')
save_raws = logged_graphics(built_log, start='baselines/')
if not validate_pack(pack, save_raws.replace('df', '0').replace('_', '.')):
log.w('Save raws not compatible with ' + pack + ' graphics, aborting.')
return None
built_graphics = logged_graphics(built_log)
if mods.update_raw_dir(raw_dir, gfx=(pack, built_graphics)):
log.i('Safely updated graphics raws ' + raw_dir + ' to ' + pack)
return True
log.i('Aborted while updating raws ' + raw_dir + ' to ' + pack)
return False
[docs]def update_savegames():
"""Update save games with current raws."""
count, skipped, saves = 0, 0, savegames_to_update()
for save_raws in [paths.get('saves', s, 'raw') for s in saves]:
r = can_rebuild(os.path.join(save_raws, 'installed_raws.txt'))
if r and update_graphics_raws(save_raws, current_pack()):
count += 1
else:
skipped += 1
return count, skipped
[docs]def can_rebuild(log_file, strict=True):
"""Test if user can exactly rebuild a raw folder, returning a bool."""
if not os.path.isfile(log_file):
log.w('Cannot change graphics without log: {}'.format(log_file))
return not strict
# Graphics dirname can change as long as it begins with the folder_prefix.
names = [(pack, get_folder_prefix(pack))
for pack in [k[0] for k in read_graphics()]]
graphic_ok = any(logged_graphics(log_file) in n for n in names)
if graphic_ok and mods.can_rebuild(log_file, strict=strict):
return True
log.i('Components unavailable to rebuild raws in '
+ os.path.dirname(log_file))
return False
[docs]def open_tilesets():
"""Opens the tilesets folder."""
open_file(paths.get('tilesets'))
[docs]def read_tilesets():
"""Returns a tuple of available tileset files. Also copies missing tilesets
from LNP/Tilesets to data/art."""
add_tilesets()
files = glob.glob(paths.get('data', 'art', '*.bmp'))
if 'legacy' not in lnp.df_info.variations:
files += glob.glob(paths.get('data', 'art', '*.png'))
files = sorted(
o for o in [os.path.basename(f) for f in files] if not any(
o.startswith(a) for a in [
'transparent1px.png', 'white1px.png', 'shadows.png', 'mouse.',
'_']))
i = 0
while i < len(files): # Manual loop to allow modification during iteration
filename = os.path.splitext(files[i])
bgname = filename[0] + '-bg.png'
topname = filename[0] + '-top.png'
if bgname in files and topname in files:
files.remove(bgname)
files.remove(topname)
else:
i = i + 1
return tuple(files)
[docs]def current_tilesets():
"""Returns the current tilesets as a tuple (FONT, GRAPHICS_FONT)."""
if lnp.settings.version_has_option('GRAPHICS_FONT'):
return (lnp.settings.FONT, lnp.settings.GRAPHICS_FONT)
return (lnp.settings.FONT, None)
[docs]def install_tilesets(font, graphicsfont):
"""Installs the provided tilesets as [FULL]FONT and GRAPHICS_[FULL]FONT.
To skip either option, use None as the parameter.
"""
if font is not None and os.path.isfile(paths.get('data', 'art', font)):
df.set_option('FONT', font)
df.set_option('FULLFONT', font)
if (lnp.settings.version_has_option('GRAPHICS_FONT')
and graphicsfont is not None and os.path.isfile(
paths.get('data', 'art', graphicsfont))):
df.set_option('GRAPHICS_FONT', graphicsfont)
df.set_option('GRAPHICS_FULLFONT', graphicsfont)