Skip to content

Commit 863e145

Browse files
committed
Move timestamp conflicts caching to ModInfo: RRR TTT
This is to fix a bug reported by @ RRR sibir-ine: > When two plugins in a timestamp-based LO game have the same timestamp > & you attempt to resolve it by redating one of them via its Modified > field, it only updates the duplicate timestamp status of that plugin, > not both. This regressed in 8163746 The regression was due to passing the rdata instance to propagate_refresh (rather than True). Note that for a pure redate (that does not affect load_order) we need to manually add the info to redraw in ModDetails._extra_changes. That change in info status is only needed for the UI to detect the conflict, but moving it there raised several issues (like needing to pass caches into _set_icon_text). The solution adopted here was to build the cache alongside the other load order caches, ensuring correctness and simplifying the model. games_lo no longer builds its own _mtime_mods cache, which it never used and which added unnecessary complexity (cf the todo comment). Finally the conflict detection logic is now centralized in lo_cache rather than split across two modules. Took the opportunity to cleanup refresh a bit. Under # RRR 353, # RRR 578
1 parent 49b0af0 commit 863e145

File tree

3 files changed

+32
-40
lines changed

3 files changed

+32
-40
lines changed

Mopy/bash/bosh/__init__.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -796,11 +796,11 @@ def txt_status(self, *, __st_names={ST_ACTIVE: _('Active'),
796796

797797
def hasTimeConflict(self):
798798
"""True if there is another mod with the same ftime."""
799-
return load_order.has_load_order_conflict(self.fn_key)
799+
return self.fn_key in self._store().lo_conflicts
800800

801801
def hasActiveTimeConflict(self):
802802
"""True if it has an active mtime conflict with another mod."""
803-
return load_order.has_load_order_conflict_active(self.fn_key)
803+
return self.fn_key in self._store().act_lo_conflicts
804804

805805
def hasBadMasterNames(self): # used in status calculation
806806
"""True if has a master with un unencodable name in cp1252."""
@@ -2131,10 +2131,32 @@ def _modinfos_cache_wrapper(self: ModInfos, *args, ldiff=None,
21312131
self[k].get_table_prop('allowGhosting', True)})
21322132
ldiff.affected.update(mod for mod, ghost_it in ghostify.items()
21332133
if self[mod].setGhost(ghost_it))
2134+
# check for load order conflicts - if ldiff is empty we should keep
2135+
# it empty (for refresh to check if it needs the refreshes above),
2136+
# but we should notify the UI to redraw items that changed status
2137+
mt_conflicts_changes = set()
2138+
if bush.game.mtime_lo:
2139+
mtime_mods = defaultdict(set)
2140+
for mod, info in self.items():
2141+
mtime_mods[int(info.ftime)].add(mod)
2142+
mtime_mods = {frozenset(v) for v in mtime_mods.values() if
2143+
len(v) > 1} # keep conflicting sets of mods
2144+
if mtime_mods:
2145+
lo_conflicts, act_lo_conflicts = set(), set()
2146+
activ = {*load_order.cached_active_tuple()}
2147+
for confls in mtime_mods:
2148+
lo_conflicts |= confls
2149+
if len(confls_act := confls & activ) > 1:
2150+
act_lo_conflicts |= confls_act
2151+
# mods that started/stopped conflicting
2152+
mt_conflicts_changes |= (self.lo_conflicts ^ lo_conflicts |
2153+
act_lo_conflicts ^ self.act_lo_conflicts)
2154+
self.lo_conflicts = lo_conflicts
2155+
self.act_lo_conflicts = act_lo_conflicts
21342156
# note we ignore missing/added here - this is the responsibility of
21352157
# refresh - if we are not called from refresh those should be empty
21362158
return RefrData(ldiff.reordered | ldiff.affected |
2137-
ldiff.act_ord_status())
2159+
ldiff.act_ord_status() | mt_conflicts_changes)
21382160
finally:
21392161
self._lo_wip = list(load_order.cached_lo_tuple())
21402162
self._active_wip = list(load_order.cached_active_tuple())
@@ -2219,6 +2241,8 @@ def __init__(self):
22192241
global modInfos
22202242
modInfos = self ##: hack needed in ModInfo.readHeader
22212243
super().__init__(ModInfo)
2244+
# lo conflicts cache only used in _ModsUIList.set_item_format
2245+
self.lo_conflicts, self.act_lo_conflicts = set(), set()
22222246

22232247
# Refresh - not quite surprisingly this is super complex - therefore define
22242248
# refresh satellite methods before even defining the DataStore overrides
@@ -2237,26 +2261,25 @@ def refresh(self, refresh_in, *, booting=False, unlock_lo=False,
22372261
bt_contents = set() # No BashTags folder -> no BashTags files
22382262
rdata = super().refresh(refresh_in, booting=booting,
22392263
bt_contents=bt_contents)
2240-
mods_changes = bool(rdata)
22412264
ldiff = LordDiff()
22422265
if insert_after:
22432266
lordata = self._lo_insert_after(insert_after, save_wip_lo=True,
22442267
ldiff=ldiff)
22452268
else: # if refresh_infos is False but mods are added force refresh
22462269
lordata = self.refreshLoadOrder(ldiff=ldiff,
2247-
forceRefresh=mods_changes or unlock_lo,
2270+
forceRefresh=bool(rdata) or unlock_lo,
22482271
forceActive=bool(rdata.to_del), unlock_lo=unlock_lo)
22492272
if not unlock_lo and ldiff.missing: # unlock_lo=True in delete/BAIN
22502273
self.warn_missing_lo_act.update(ldiff.missing)
2251-
rdata |= lordata
22522274
# if load order did not change, we must perform the refreshes below
22532275
if not ldiff:
22542276
# in case ini files were deleted or modified or maybe string files
22552277
# were deleted... we need a load order below: in skyrim we read
22562278
# inis in active order - we then need to redraw what changed status
22572279
rdata.redraw |= self._refresh_mod_inis_and_strings()
2258-
if mods_changes:
2280+
if rdata:
22592281
rdata.redraw |= self._file_or_active_updates()
2282+
rdata |= lordata
22602283
self._voAvailable, self.voCurrent = bush.game.modding_esms(self)
22612284
return rdata
22622285

Mopy/bash/games_lo.py

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,6 @@ def set_load_order(self, lord, active, previous_lord=None,
369369
previous_lord)
370370
return lord, active # return what we set or was previously set
371371

372-
# Conflicts - only for timestamp games
373-
def has_load_order_conflict(self, mod_name): return False
374-
def has_load_order_conflict_active(self, mod_name, active): return False
375-
376372
@classmethod
377373
def _must_update_active(cls, deleted_plugins, reord_plugins):
378374
raise NotImplementedError
@@ -714,25 +710,15 @@ def _must_update_active(cls, deleted_plugins, reord_plugins):
714710
class TimestampGame(LoGame):
715711
"""Oblivion and other games where load order is set using modification
716712
times."""
717-
# Intentionally imprecise mtime cache
718-
_mtime_mods: defaultdict[int, set[Path]] = defaultdict(set)
719713

720714
@staticmethod
721715
def _check_active_order(acti, lord):
722716
super(TimestampGame, TimestampGame)._check_active_order(acti, lord)
723717
return [] # no need to reorder plugins.txt - fix_lo.act_reordered False
724718

725719
@classmethod
726-
def _must_update_active(cls, deleted_plugins, reord_plugins): return deleted_plugins
727-
728-
def has_load_order_conflict(self, mod_name):
729-
ti = int(self._mod_infos[mod_name].ftime)
730-
return ti in self._mtime_mods and len(self._mtime_mods[ti]) > 1
731-
732-
def has_load_order_conflict_active(self, mod_name, active):
733-
ti = int(self._mod_infos[mod_name].ftime)
734-
return self.has_load_order_conflict(mod_name) and bool(
735-
(self._mtime_mods[ti] - {mod_name}) & active)
720+
def _must_update_active(cls, deleted_plugins, reord_plugins):
721+
return deleted_plugins
736722

737723
# Abstract overrides ------------------------------------------------------
738724
def __calculate_mtime_order(self, mods=None): # excludes mods in corrupted
@@ -742,7 +728,6 @@ def __calculate_mtime_order(self, mods=None): # excludes mods in corrupted
742728
return sorted(self._mod_infos if mods is None else mods, key=is_m)
743729

744730
def _fetch_load_order(self, cached_load_order, cached_active):
745-
self._rebuild_mtimes_cache() ##: will need that tweaked for lock load order
746731
return self.__calculate_mtime_order()
747732

748733
def _persist_load_order(self, lord, active):
@@ -767,13 +752,6 @@ def _persist_load_order(self, lord, active):
767752
restamp.append((ordered, self._mod_infos[mod].ftime))
768753
for ordered, modification_time in restamp:
769754
self._mod_infos[ordered].setmtime(modification_time)
770-
# rebuild our cache
771-
self._rebuild_mtimes_cache()
772-
773-
def _rebuild_mtimes_cache(self):
774-
self._mtime_mods.clear()
775-
for mod, info in self._mod_infos.items():
776-
self._mtime_mods[int(info.ftime)].add(mod)
777755

778756
def _persist_if_changed(self, active, lord, previous_active,
779757
previous_lord):

Mopy/bash/load_order.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -427,15 +427,6 @@ def get_lo_files() -> set[bolt.Path]:
427427
# implementations
428428
return set(_lo_handler.get_lo_files())
429429

430-
# Timestamp games helpers
431-
def has_load_order_conflict(mod_name):
432-
return _lo_handler.has_load_order_conflict(mod_name)
433-
434-
def has_load_order_conflict_active(mod_name):
435-
if not cached_is_active(mod_name): return False
436-
return _lo_handler.has_load_order_conflict_active(mod_name,
437-
_cached_lord.active)
438-
439430
# Lock load order -------------------------------------------------------------
440431
def toggle_lock_load_order(user_warning_callback):
441432
global locked

0 commit comments

Comments
 (0)