Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into 570-launchers-menu-gaz
Browse files Browse the repository at this point in the history
  • Loading branch information
BGazotti committed Mar 20, 2024
2 parents b5cf2bb + 444a36a commit 33e04d1
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 153 deletions.
71 changes: 29 additions & 42 deletions Mopy/bash/basher/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def __init__(self, parent, page_desc):
##: The restart parameters here are a smell, see usage in LanguagePage
self._request_restart = None
# Used to keep track of each setting's 'changed' state
self._setting_states = {k: False for k in self._setting_ids}
self._setting_states = dict.fromkeys(self._setting_ids, False)

def _is_changed(self, setting_id):
"""Checks if the setting with the specified ID has been changed. See
Expand Down Expand Up @@ -1381,7 +1381,6 @@ def _check_changed(self):
self._mark_changed(self, good_changed or bad_changed)
self.update_layout()

##: Here be (some) dragons, especially in the import method
def _export_lists(self):
textDir = bass.dirs[u'patches']
textDir.makedirs()
Expand All @@ -1396,25 +1395,19 @@ def _export_lists(self):
# a comment anyways, but to be sure, put a '#' in front of it
out.write(f"# {_('Exported by Wrye Bash v%(wb_version)s')}\n" % {
'wb_version': bass.AppVersion})
out.write(u'goodDlls # %s\n' % _(u'Binaries whose installation '
u'you have allowed'))
self._dump_dlls(bass.settings['bash.installers.goodDlls'], out)
out.write('\n')
out.write(u'badDlls # %s\n' % _(u'Binaries whose installation you '
u'have forbidden'))
self._dump_dlls(bass.settings['bash.installers.badDlls'], out)

@staticmethod
def _dump_dlls(dll_dict, out):
if not dll_dict:
out.write(f'# {_("None")}\n') # Treated as a comment
return
for dll, versions in dll_dict.items():
out.write(f'dll: {dll}:\n')
for i, version in enumerate(versions):
v_name, v_size, v_crc = version
out.write(f"version {i:02d}: ['{v_name}', {v_size:d}, "
f"{v_crc:d}]\n")
sect_head = [_('Binaries whose installation you have allowed'),
_('Binaries whose installation you have forbidden')]
for sect_key, sec_head in zip(['goodDlls', 'badDlls'], sect_head):
out.write(f'{sect_key} # {sect_head}\n')
dll_dict = bass.settings[f'bash.installers.{sect_key}']
if not dll_dict:
out.write(f'# {_("None")}\n')
else:
for dll, versions in dll_dict.items():
out.write(f'dll: {dll}:\n')
for i, (v_name, v_size, v_crc) in enumerate(versions):
out.write(f"version {i:02d}: ['{v_name}', "
f"{v_size:d}, {v_crc:d}]\n")

def _import_lists(self):
textDir = bass.dirs[u'patches']
Expand Down Expand Up @@ -1447,14 +1440,14 @@ def parse_int(i):
if contents.startswith(b'\xef\xbb\xbf'):
contents = contents[3:]
with io.StringIO(contents.decode(u'utf-8')) as ins:
Dlls = {u'goodDlls':{}, u'badDlls':{}}
good, bad = {}, {}
current, dll = None, None
for line in ins:
line = line.strip()
if line.startswith(u'goodDlls'):
current = Dlls[u'goodDlls']
current = good
elif line.startswith(u'badDlls'):
current = Dlls[u'badDlls']
current = bad
elif line.startswith(u'dll:'):
if current is None:
raise SyntaxError(u'Missing "goodDlls" or '
Expand All @@ -1467,27 +1460,22 @@ def parse_int(i):
raise SyntaxError(u'Missing "dll" statement '
u'before "version" statement')
ver = line.split(u':',1)[1]
# Strip off leading '[' and trailing ']' before split
ver_components = ver[1:-1].split(u',')
if len(ver_components) != 3:
try:
# strip leading '[' and trailing ']' and any
# whitespace before split - see _export_lists
inst_name, inst_size, inst_crc = map(str.strip,
ver.rstrip()[1:-1].split(','))
except ValueError:
raise SyntaxError(u'Invalid format: expected '
u'"version: [name, size, crc]"')
# Strip any spacing due to the repr used when exporting
ver_components = [c.strip() for c in ver_components]
inst_name, inst_size, inst_crc = ver_components
current[dll].append((
parse_path(inst_name), parse_int(inst_size),
parse_int(inst_crc)))
if not replace:
self._binaries_list.left_items = sorted(
set(self._binaries_list.left_items) |
set(Dlls[u'goodDlls']))
self._binaries_list.right_items = sorted(
set(self._binaries_list.right_items) |
set(Dlls[u'badDlls']))
else:
self._binaries_list.left_items = sorted(Dlls[u'goodDlls'])
self._binaries_list.right_items = sorted(Dlls[u'badDlls'])
good = good.keys() | set(self._binaries_list.left_items)
bad = bad.keys() | set(self._binaries_list.right_items)
self._binaries_list.left_items = sorted(good)
self._binaries_list.right_items = sorted(bad)
except UnicodeError:
msg = _('Wrye Bash could not load %(import_file)s, because it '
'is not saved in UTF-8 format. Please resave it in UTF-8 '
Expand Down Expand Up @@ -1517,10 +1505,9 @@ def merge_versions(dll_source, target_dict, source_dict):
bad_dlls_dict = bass.settings[u'bash.installers.badDlls']
good_dlls_dict = bass.settings[u'bash.installers.goodDlls']
# Determine which have been moved to/from the bad DLLs list
old_bad = set(bad_dlls_dict)
new_bad = set(self._binaries_list.right_items)
bad_added = new_bad - old_bad
bad_removed = old_bad - new_bad
bad_added = new_bad - bad_dlls_dict.keys()
bad_removed = bad_dlls_dict.keys() - new_bad
merge_versions(dll_source=bad_added, target_dict=bad_dlls_dict,
source_dict=good_dlls_dict)
merge_versions(dll_source=bad_removed, target_dict=good_dlls_dict,
Expand Down
96 changes: 38 additions & 58 deletions Mopy/bash/bosh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@

def data_tracking_stores() -> Iterable['_AFileInfos']:
"""Return an iterable containing all data stores that keep track of the
Data folder and which files in it are owned by which BAIN package."""
Data folder and so will get refresh calls from BAIN when files get
installed/changed/uninstalled. If they set _AFileInfos.tracks_ownership to
True, they will also get ownership updates."""
return tuple(s for s in (modInfos, iniInfos, bsaInfos, screen_infos) if
s is not None)

Expand Down Expand Up @@ -834,15 +836,13 @@ def get_ini_name(self):
exist."""
return self.fn_key.fn_body + '.ini'

def _string_files_paths(self, lang):
# type: (str) -> Iterable[str]
str_f_body = self.fn_key.fn_body
str_f_ext = self.get_extension()
def _string_files_paths(self, lang: str) -> Iterable[str]:
fmt_dict = {'body': self.fn_key.fn_body, 'ext': self.get_extension(),
'language': lang}
for str_format in bush.game.Esp.stringsFiles:
yield os.path.join(u'Strings', str_format % {
u'body': str_f_body, u'ext': str_f_ext, u'language': lang})
yield os.path.join('Strings', str_format % fmt_dict)

def getStringsPaths(self, lang=u'English'):
def getStringsPaths(self, lang):
"""If Strings Files are available as loose files, just point to
those, otherwise extract needed files from BSA if needed."""
baseDirJoin = self.info_dir.join
Expand Down Expand Up @@ -928,27 +928,20 @@ def _bsa_heuristic(binf):
ret_bsas.sort(key=lambda b: not b.fn_key.lower().startswith(plugin_prefix))
return ret_bsas

def isMissingStrings(self, cached_ini_info=(None, None, None),
ci_cached_strings_paths=None):
def isMissingStrings(self, cached_ini_info, ci_cached_strings_paths):
"""True if the mod says it has .STRINGS files, but the files are
missing.
:param cached_ini_info: Passed to get_bsa_lo, see there for docs.
:param ci_cached_strings_paths: An optional set of lower-case versions
of the paths to all strings files. They must match the format
returned by _string_files_paths (i.e. starting with 'strings/'. If
specified, no stat calls will occur to determine if loose strings
files exist."""
:param ci_cached_strings_paths: Set of lower-case versions of the paths
to all strings files. They must match the format returned by
_string_files_paths (i.e. starting with 'strings/')."""
if not getattr(self.header.flags1, 'localized', False): return False
lang = oblivionIni.get_ini_language()
bsa_infos = self._find_string_bsas(cached_ini_info)
info_dir_join = self.info_dir.join
for assetPath in self._string_files_paths(lang):
# Check loose files first
if ci_cached_strings_paths is not None:
if assetPath.lower() in ci_cached_strings_paths:
continue
elif info_dir_join(assetPath).is_file():
if assetPath.lower() in ci_cached_strings_paths:
continue
# Check in BSA's next
for bsa_info in bsa_infos:
Expand Down Expand Up @@ -1571,14 +1564,16 @@ class _AFileInfos(DataStore):
file_pattern = None # subclasses must define this !
_rdata_type = RefrData
factory: type[AFile]
# Whether these file infos track ownership in a table
tracks_ownership = False

def __init__(self, dir_, factory):
"""Init with specified directory and specified factory type."""
self.corrupted: FNDict[FName, Corrupted] = FNDict()
super().__init__(self._initDB(dir_))
self.factory = factory

def _initDB(self, dir_):
self.corrupted: FNDict[FName, Corrupted] = FNDict()
self.store_dir = dir_ #--Path
deprint(f'Initializing {self.__class__.__name__}')
deprint(f' store_dir: {self.store_dir}')
Expand Down Expand Up @@ -1750,6 +1745,7 @@ def _do_copy(self, cp_file_info, cp_dest_path):
cp_file_info.abs_path.copyTo(cp_dest_path)

class TableFileInfos(_AFileInfos):
tracks_ownership = True

def _initDB(self, dir_):
"""Load pickled data for mods, saves, inis and bsas."""
Expand Down Expand Up @@ -2416,7 +2412,7 @@ def _refresh_mod_inis_and_strings(self):
cached_ini_info=cached_ini_info,
ci_cached_strings_paths=ci_cached_strings_paths)}
self.new_missing_strings = self.missing_strings - oldBad
return self.new_missing_strings ^ oldBad
return self.missing_strings ^ oldBad

def _refresh_active_no_cp1252(self):
"""Refresh which filenames cannot be saved to plugins.txt - active
Expand Down Expand Up @@ -3097,39 +3093,28 @@ def get_bsas_from_inis(self):
##: This will need caching in the future - invalidation will be *hard*.
# Prerequisite for a fully functional BSA tab though (see #233), especially
# for Morrowind
def get_bsa_lo(self, for_plugins=None, cached_ini_info=(None, None, None)):
def get_bsa_lo(self, for_plugins, cached_ini_info=None):
"""Returns the full BSA load order for this game, mapping each BSA to
the position of its activator mods. Also returns a dict mapping each
BSA to a string describing the reason it was loaded. If a mod activates
more than one bsa, their relative order is undefined.
:param for_plugins: If not None, only returns plugin-name-specific BSAs
for those plugins. Otherwise, returns it for all plugins.
:param for_plugins: the plugins to return plugin-name-specific BSAs for
:param cached_ini_info: Can contain the result of calling
get_bsas_from_inis, in which case calling that (fairly expensive)
method will be skipped."""
fetch_ini_info = any(c is None for c in cached_ini_info)
if fetch_ini_info:
# At least one part of the cached INI info we were passed in is
# None, which means we need to fetch the info from disk
available_bsas, bsa_lo, bsa_cause = self.get_bsas_from_inis()
else:
# We can use the cached INI info
try:
available_bsas, bsa_lo, bsa_cause = cached_ini_info
except TypeError: # cached_ini_info is None - fetch it from disk
available_bsas, bsa_lo, bsa_cause = self.get_bsas_from_inis()
# BSAs loaded based on plugin name load in the middle of the pack
if for_plugins is None: for_plugins = list(self)
for i, p in enumerate(for_plugins):
for binf in self[p].mod_bsas(available_bsas):
bsa_lo[binf] = i
bsa_cause[binf] = p
del available_bsas[binf.fn_key]
return bsa_lo, bsa_cause

def get_active_bsas(self):
"""Returns the load order of all active BSAs. See get_bsa_lo for more
information."""
return self.get_bsa_lo(for_plugins=load_order.cached_active_tuple())

@staticmethod
def plugin_wildcard(file_str=_('Plugins')):
joinstar = ';*'.join(bush.game.espm_extensions)
Expand Down Expand Up @@ -3348,17 +3333,18 @@ def _setLocalSaveFromIni(self):
"""Read the current save profile from the oblivion.ini file and set
local save attribute to that value."""
# saveInfos singleton is constructed in InitData after bosh.oblivionIni
self.localSave = oblivionIni.getSetting(
*bush.game.Ini.save_profiles_key,
default=bush.game.Ini.save_prefix)
# Hopefully will solve issues with unicode usernames # TODO(ut) test
self.localSave = decoder(self.localSave.rstrip('\\')) ##: use cp1252?
prev = getattr(self, 'localSave', None)
save_dir = oblivionIni.getSetting(*bush.game.Ini.save_profiles_key,
default=bush.game.Ini.save_prefix).rstrip('\\')
self.localSave = save_dir
if prev is not None and prev != save_dir:
self.table.save()
self.__init_db()
return prev != save_dir

def __init__(self):
self.localSave = bush.game.Ini.save_prefix
self._setLocalSaveFromIni()
super().__init__(dirs['saveBase'].join(
env.convert_separators(self.localSave)), SaveInfo)
super().__init__(self.__saves_dir(), SaveInfo)
# Save Profiles database
self.profiles = bolt.PickleDict(
dirs[u'saveBase'].join(u'BashProfiles.dat'), load_pickle=True)
Expand Down Expand Up @@ -3427,7 +3413,8 @@ def _parse_save_path(cls, save_name: FName | str) -> tuple[
def bash_dir(self): return self.store_dir.join(u'Bash')

def refresh(self, refresh_infos=True, booting=False):
if not booting: self._refreshLocalSave() # otherwise we just did this
if not booting:
self._setLocalSaveFromIni()
return super().refresh(booting=booting) if refresh_infos else \
self._rdata_type()

Expand Down Expand Up @@ -3474,18 +3461,11 @@ def move_infos(self, sources, destinations, window, bash_frame):
return moved

#--Local Saves ------------------------------------------------------------
def _refreshLocalSave(self):
"""Refreshes self.localSave."""
#--self.localSave is NOT a Path object.
localSave = self.localSave
self._setLocalSaveFromIni()
if localSave == self.localSave: return # no change
self.table.save()
self.__init_db()

def __init_db(self):
self._initDB(dirs['saveBase'].join(
env.convert_separators(self.localSave))) # always has backslashes
self._initDB(self.__saves_dir())

def __saves_dir(self): # always has backslashes
return dirs['saveBase'].join(env.convert_separators(self.localSave))

def setLocalSave(self, localSave: str, refreshSaveInfos=True):
"""Sets SLocalSavePath in Oblivion.ini. The latter must exist."""
Expand Down
2 changes: 1 addition & 1 deletion Mopy/bash/bosh/_mergeability.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def _pbash_mergeable_no_load(modInfo, minfos, reasons):
f'\n - {dir_list}') % {'blocking_plugin_name': modInfo.fn_key}):
return False
#--Missing Strings Files?
if modInfo.isMissingStrings():
if modInfo.fn_key in minfos.missing_strings:
if not verbose: return False
from . import oblivionIni
strings_example = (f'{os.path.join("Strings", modInfo.fn_key.fn_body)}'
Expand Down
12 changes: 8 additions & 4 deletions Mopy/bash/bosh/bain.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

from . import DataStore, InstallerConverter, ModInfos, bain_image_exts, \
best_ini_files, data_tracking_stores, RefrData, Corrupted
from .. import archives, bass, bolt, bush, env
from .. import archives, bass, bolt, bush, env, load_order
from ..archives import compress7z, defaultExt, extract7z, list_archive, \
readExts
from ..bass import Store
Expand Down Expand Up @@ -1992,6 +1992,7 @@ def rename_operation(self, member_info, name_new):
# Update the ownership information for relevant data stores
owned_per_store = []
for store in data_tracking_stores():
if not store.tracks_ownership: continue
storet = store.table
owned = [x for x in storet.getColumn('installer') if str(
storet[x]['installer']) == old_key] # str due to Paths
Expand Down Expand Up @@ -2820,7 +2821,9 @@ def _remove_restore(self, removes, restores, refresh_ui, cede_ownership,
for owned_path in owned_files:
for store in stores:
if store_info := store.data_path_to_info(owned_path):
store_info.set_table_prop('installer', f'{ikey}')
if store.tracks_ownership:
store_info.set_table_prop(
'installer', f'{ikey}')
refresh_ui[store.unique_store_key] = True
# Each file may only belong to one data store
break
Expand Down Expand Up @@ -3091,8 +3094,9 @@ def getConflictReport(self, srcInstaller, mode, modInfos):
include_bsas = bass.settings[
u'bash.installers.conflictsReport.showBSAConflicts']
##: Add support for showing inactive & excluding lower BSAs
if include_bsas:
active_bsas, bsa_cause = modInfos.get_active_bsas()
if include_bsas: # get the load order of all active BSAs
active_bsas, bsa_cause = modInfos.get_bsa_lo(
load_order.cached_active_tuple())
else:
active_bsas, bsa_cause = None, None
lower_loose, higher_loose, lower_bsa, higher_bsa = self.find_conflicts(
Expand Down
Loading

0 comments on commit 33e04d1

Please sign in to comment.