-
Notifications
You must be signed in to change notification settings - Fork 79
/
preservers.py
624 lines (587 loc) · 29.8 KB
/
preservers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
# -*- coding: utf-8 -*-
#
# GPL License and Copyright Notice ============================================
# This file is part of Wrye Bash.
#
# Wrye Bash is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation, either version 3
# of the License, or (at your option) any later version.
#
# Wrye Bash is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Wrye Bash. If not, see <https://www.gnu.org/licenses/>.
#
# Wrye Bash copyright (C) 2005-2009 Wrye, 2010-2022 Wrye Bash Team
# https://github.com/wrye-bash
#
# =============================================================================
"""This module houses preservers. A preserver is an import patcher that simply
carries forward changes from the last tagged plugin. The goal is to eventually
absorb all of them under the _APreserver base class."""
from __future__ import annotations
from collections import defaultdict, Counter
from itertools import chain
# Internal
from .. import getPatchesPath
from ..base import ImportPatcher
from ... import bush, load_order, parsers
from ...bolt import attrgetter_cache, deprint, setattr_deep
from ...brec import RecordType
from ...exception import ModSigMismatchError
#------------------------------------------------------------------------------
class APreserver(ImportPatcher):
"""Fairly mature base class for preservers. Some parts could (read should)
be moved to ImportPatcher and used to eliminate duplication with
_AMerger."""
# The record attributes to patch. Can be either a straight
# signature-to-tuple mapping or a more complex mapping for multi-tag
# importers (see _multi_tag below). Each of the tuples may contain
# regular attributes (represented as strings), which get carried forward
# one by one, and fused attributes (represented as tuples of strings),
# which get carried forward as a 'block' - if one of the attributes in a
# fused attribute differs, then all attributes in that 'block' get carried
# forward, even if the others are unchanged. See for example the handling
# of level_offset and pcLevelOffset
rec_attrs: dict[bytes, tuple] | dict[bytes, dict[str, tuple]] = {}
# Record attributes that are FormIDs. These will be checked to see if their
# FormID is valid before being imported
##: set for more patchers?
_fid_rec_attrs = {}
# True if this importer is a multi-tag importer. That means its rec_attrs
# must map record signatures to dicts mapping the tags to a tuple of the
# subrecords to import, instead of just mapping the record signatures
# directly to the tuples
_multi_tag = False
_csv_parser = None
# A bash tag to force the import of all relevant data from a tagged mod,
# without it being checked against the masters first. None means no such
# tag exists for this patcher
_force_full_import_tag = None
def __init__(self, p_name, p_file, p_sources):
super(APreserver, self).__init__(p_name, p_file, p_sources)
#--(attribute-> value) dicts keyed by long fid.
self.id_data = defaultdict(dict)
self.srcs_sigs = set() #--Record signatures actually provided by src
# mods/files.
#--Type Fields
self._fid_rec_attrs_class = (defaultdict(dict) if self._multi_tag
else defaultdict(tuple))
self._fid_rec_attrs_class.update(self._fid_rec_attrs)
# We want FormID attrs in the full recAttrs as well. They're only
# separate for checking before we import
if self._multi_tag: ##: This is hideous
def collect_attrs(r, tag_dict):
return {t: a + self._fid_rec_attrs.get(r, {}).get(t, ())
for t, a in tag_dict.items()}
else:
def collect_attrs(r, a):
return a + self._fid_rec_attrs.get(r, ())
self.rec_type_attrs = {r: collect_attrs(r, a)
for r, a in self.rec_attrs.items()}
# Check if we need to use setattr_deep to set attributes
if self._multi_tag:
all_attrs = chain.from_iterable(
v for d in self.rec_type_attrs.values()
for v in d.values())
else:
all_attrs = chain.from_iterable(self.rec_type_attrs.values())
self._deep_attrs = any(u'.' in a for a in all_attrs)
# Split srcs based on CSV extension ##: move somewhere else?
self.csv_srcs = [s for s in p_sources if s.fn_ext == '.csv']
self.srcs = [s for s in p_sources if s.fn_ext != '.csv']
self.loadFactory = self._patcher_read_fact(by_sig=self.rec_type_attrs)
# CSV helpers
def _parse_csv_sources(self, progress):
"""Parses CSV files. Only called if _csv_parser is set."""
parser_instance = self._csv_parser(self.patchFile.pfile_aliases,
called_from_patcher=True)
for src_path in self.csv_srcs:
try:
parser_instance.read_csv(getPatchesPath(src_path))
except OSError:
deprint(u'%s is no longer in patches set' % src_path,
traceback=True)
except UnicodeError:
deprint(u'%s is not saved in UTF-8 format' % src_path,
traceback=True)
progress.plus()
parsed_sources = parser_instance.id_stored_data
# Filter out any entries that don't actually have data or don't
# actually exist (for this game at least)
##: make sure k is always bytes and drop encode below
filtered_dict = {k.encode(u'ascii') if isinstance(k, str) else k: v
for k, v in parsed_sources.items()
if v and k in RecordType.sig_to_class}
self.srcs_sigs.update(filtered_dict)
for src_data in filtered_dict.values():
self.id_data.update(src_data)
@property
def _read_sigs(self):
return self.srcs_sigs
def _init_data_loop(self, top_grup_sig, srcFile, srcMod, mod_id_data,
mod_tags, loaded_mods, __attrgetters=attrgetter_cache):
rec_attrs = self.rec_type_attrs[top_grup_sig]
fid_attrs = self._fid_rec_attrs_class[top_grup_sig]
if self._multi_tag:
# For multi-tag importers, we need to look up the applied bash tags
# and use those to find all applicable attributes
def _merge_attrs(to_merge):
"""Helper to concatenate all applicable tags' attributes into a
final list while preserving order."""
merged_attrs = []
merged_attrs_set = set()
for t, m_attrs in to_merge.items():
if t in mod_tags:
for a in m_attrs:
if a not in merged_attrs_set:
merged_attrs_set.add(a)
merged_attrs.append(a)
return merged_attrs
rec_attrs = _merge_attrs(rec_attrs)
fid_attrs = _merge_attrs(fid_attrs)
# Faster than a dict since we save the items() call in the (very hot)
# loop below
ra_getters = [(a, __attrgetters[a]) for a in rec_attrs]
fa_getters = [__attrgetters[a] for a in fid_attrs]
src_top = srcFile.tops[top_grup_sig]
# If we have FormID attributes, check those before importing - since
# this is constant for the entire loop, duplicate the loop to save the
# overhead in the no-FormIDs case
if fa_getters:
for rfid, record in src_top.iter_present_records():
fid_attr_values = [getter(record) for getter in fa_getters]
if any(f and (f.mod_fn not in loaded_mods) for f in
fid_attr_values):
# Ignore the record. Another option would be to just ignore
# the fid_attr_values result
self.patchFile.patcher_mod_skipcount[
self._patcher_name][srcMod] += 1
continue
mod_id_data[rfid] = {attr: getter(record) for attr, getter in
ra_getters}
else:
for rfid, record in src_top.iter_present_records():
mod_id_data[rfid] = {attr: getter(record) for attr, getter in
ra_getters}
def initData(self, progress, __attrgetters=attrgetter_cache):
if not self.isActive: return
id_data = self.id_data
progress.setFull(len(self.srcs) + len(self.csv_srcs))
cachedMasters = {}
minfs = self.patchFile.p_file_minfos
loaded_mods = self.patchFile.loadSet
for srcMod in self.srcs:
mod_id_data = {}
if srcMod not in minfs: continue
srcInfo = minfs[srcMod]
srcFile = self._mod_file_read(srcInfo)
mod_sigs = set()
mod_tags = srcFile.fileInfo.getBashTags() if self._multi_tag else None
for rsig in self.rec_type_attrs:
if rsig not in srcFile.tops: continue
self.srcs_sigs.add(rsig)
mod_sigs.add(rsig)
self._init_data_loop(rsig, srcFile, srcMod, mod_id_data,
mod_tags, loaded_mods, __attrgetters)
if (self._force_full_import_tag and
self._force_full_import_tag in srcInfo.getBashTags()):
# We want to force-import - copy the temp data without
# filtering by masters, then move on to the next mod
id_data.update(mod_id_data)
continue
for master in srcInfo.masterNames:
if master not in minfs: continue # or break filter mods
if master in cachedMasters:
masterFile = cachedMasters[master]
else:
masterFile = self._mod_file_read(minfs[master])
cachedMasters[master] = masterFile
for rsig in self.rec_type_attrs:
if rsig not in masterFile.tops or rsig not in mod_sigs:
continue
for rfid, record in masterFile.tops[rsig].iter_present_records():
if rfid not in mod_id_data: continue
for attr, val in mod_id_data[rfid].items():
try:
if val == __attrgetters[attr](record):
continue
else:
id_data[rfid][attr] = val
except AttributeError:
raise ModSigMismatchError(master, record)
progress.plus()
if self._csv_parser:
self._parse_csv_sources(progress)
self.isActive = bool(self.srcs_sigs)
def scanModFile(self, modFile, progress, __attrgetters=attrgetter_cache):
id_data = self.id_data
for rsig in self.srcs_sigs:
if rsig not in modFile.tops: continue
patchBlock = self.patchFile.tops[rsig]
# Records that have been copied into the BP once will automatically
# be updated by update_patch_records_from_mod/mergeModFile
copied_records = patchBlock.id_records.copy()
for rfid, record in modFile.tops[rsig].iter_present_records():
# Skip if we've already copied this record or if we're not
# interested in it
if rfid in copied_records or rfid not in id_data: continue
for attr, val in id_data[rfid].items():
if __attrgetters[attr](record) != val:
patchBlock.setRecord(record.getTypeCopy())
break
def _inner_loop(self, keep, records, top_mod_rec, type_count,
__attrgetters=attrgetter_cache):
loop_setattr = setattr_deep if self._deep_attrs else setattr
id_data = self.id_data
for rfid, record in records:
if rfid not in id_data: continue
for attr, val in id_data[rfid].items():
if __attrgetters[attr](record) != val: break
else: continue
for attr, val in id_data[rfid].items():
if isinstance(attr, tuple):
# This is a fused attribute, so unpack the attrs and assign
# each value to each matching attr
for f_a, f_v in zip(attr, val):
loop_setattr(record, f_a, f_v)
else:
# This is a regular attribute, so we just need to assign it
loop_setattr(record, attr, val)
keep(rfid)
type_count[top_mod_rec] += 1
def buildPatch(self, log, progress):
if not self.isActive: return
modFileTops = self.patchFile.tops
keep = self.patchFile.getKeeper()
type_count = Counter()
for rsig in self.srcs_sigs:
if rsig not in modFileTops: continue
present_recs = modFileTops[rsig].iter_present_records(
include_ignored=True) ##: why include_ignored?
self._inner_loop(keep, present_recs, rsig, type_count)
self.id_data.clear() # cleanup to save memory
# Log
self._patchLog(log, type_count)
def _srcMods(self,log):
log(self.__class__.srcsHeader)
all_srcs = self.srcs + self.csv_srcs
if not all_srcs:
log(u". ~~%s~~" % _(u'None'))
else:
for srcFile in all_srcs:
log(u"* %s" % srcFile)
#------------------------------------------------------------------------------
# Absorbed patchers -----------------------------------------------------------
#------------------------------------------------------------------------------
class ImportActorsPatcher(APreserver):
rec_attrs = bush.game.actor_importer_attrs
_multi_tag = True
#------------------------------------------------------------------------------
class ImportActorsFacesPatcher(APreserver):
logMsg = u'\n=== '+_(u'Faces Patched')
rec_attrs = {b'NPC_': {
u'NPC.Eyes': (),
u'NPC.FaceGen': (u'fggs_p', u'fgga_p', u'fgts_p'),
u'NPC.Hair': (u'hairLength', u'hairRed', u'hairBlue', u'hairGreen'),
u'NpcFacesForceFullImport': (u'fggs_p', u'fgga_p', u'fgts_p',
u'hairLength', u'hairRed', u'hairBlue',
u'hairGreen'),
}}
_fid_rec_attrs = {b'NPC_': {
u'NPC.Eyes': (u'eye',),
u'NPC.FaceGen': (),
u'NPC.Hair': (u'hair',),
u'NpcFacesForceFullImport': (u'eye', u'hair'),
}}
_multi_tag = True
_force_full_import_tag = u'NpcFacesForceFullImport'
#------------------------------------------------------------------------------
class ImportActorsFactionsPatcher(APreserver):
logMsg = u'\n=== ' + _(u'Refactioned Actors')
srcsHeader = u'=== ' + _(u'Source Mods/Files')
rec_attrs = {x: (u'factions',) for x in bush.game.actor_types}
_csv_parser = parsers.ActorFactions
#------------------------------------------------------------------------------
class ImportDestructiblePatcher(APreserver):
"""Merges changes to destructible records."""
rec_attrs = {x: (u'destructible',) for x in bush.game.destructible_types}
#------------------------------------------------------------------------------
class ImportEffectsStatsPatcher(APreserver):
"""Preserves changes to MGEF stats."""
rec_attrs = {b'MGEF': bush.game.mgef_stats_attrs}
#------------------------------------------------------------------------------
class ImportEnchantmentsPatcher(APreserver):
"""Preserves changes to EITM (enchantment/object effect) subrecords."""
rec_attrs = {x: ('enchantment',) for x in bush.game.enchantment_types}
#------------------------------------------------------------------------------
class ImportEnchantmentStatsPatcher(APreserver):
"""Preserves changes to ENCH stats."""
rec_attrs = {b'ENCH': bush.game.ench_stats_attrs}
#------------------------------------------------------------------------------
class ImportKeywordsPatcher(APreserver):
rec_attrs = {x: (u'keywords',) for x in bush.game.keywords_types}
#------------------------------------------------------------------------------
class ImportNamesPatcher(APreserver):
"""Import names from source mods/files."""
logMsg = u'\n=== ' + _(u'Renamed Items')
srcsHeader = u'=== ' + _(u'Source Mods/Files')
rec_attrs = {x: (u'full',) for x in bush.game.namesTypes}
_csv_parser = parsers.FullNames
#------------------------------------------------------------------------------
class ImportObjectBoundsPatcher(APreserver):
rec_attrs = {x: (u'bounds',) for x in bush.game.object_bounds_types}
#------------------------------------------------------------------------------
class ImportScriptsPatcher(APreserver):
rec_attrs = {x: (u'script_fid',) for x in bush.game.scripts_types}
#------------------------------------------------------------------------------
class ImportSoundsPatcher(APreserver):
"""Imports sounds from source mods into patch."""
rec_attrs = bush.game.soundsTypes
#------------------------------------------------------------------------------
class ImportSpellStatsPatcher(APreserver):
"""Import spell changes from mod files."""
srcsHeader = u'=== ' + _(u'Source Mods/Files')
rec_attrs = {x: bush.game.spell_stats_attrs
for x in bush.game.spell_stats_types}
_csv_parser = parsers.SpellRecords if bush.game.fsName == 'Oblivion' \
else None
#------------------------------------------------------------------------------
class ImportStatsPatcher(APreserver):
"""Import stats from mod file."""
patcher_order = 28 # Run ahead of Bow Reach Fix ##: This seems unneeded
logMsg = u'\n=== ' + _(u'Imported Stats')
srcsHeader = u'=== ' + _(u'Source Mods/Files')
# Don't patch Editor IDs - those are only in statsTypes for the
# Export/Import links
rec_attrs = {r: tuple(x for x in a if x != u'eid')
for r, a in bush.game.statsTypes.items()}
_csv_parser = parsers.ItemStats
#------------------------------------------------------------------------------
class ImportTextPatcher(APreserver):
rec_attrs = bush.game.text_types
#------------------------------------------------------------------------------
# Patchers to absorb ----------------------------------------------------------
#------------------------------------------------------------------------------
##: absorbing this one will be hard - hint: getActiveRecords only exists on
# MobObjects, iter_records works for all Mob* classes, so attack that part of
# _APreserver
class ImportCellsPatcher(ImportPatcher):
logMsg = u'\n=== ' + _(u'Cells/Worlds Patched')
_read_sigs = (b'CELL', b'WRLD')
def __init__(self, p_name, p_file, p_sources):
super(ImportCellsPatcher, self).__init__(p_name, p_file, p_sources)
self.cellData = defaultdict(dict)
self.recAttrs = bush.game.cellRecAttrs # dict[str, tuple[str]]
self.loadFactory = self._patcher_read_fact()
def initData(self, progress, __attrgetters=attrgetter_cache):
"""Get cells from source files."""
if not self.isActive: return
cellData = self.cellData
def importCellBlockData(cellBlock):
"""
Add attribute values from source mods to a temporary cache.
These are used to filter for required records by formID and
to update the attribute values taken from the master files
when creating cell_data.
"""
if not cellBlock.cell.flags1.ignored:
cfid = cellBlock.cell.fid
# If we're in an interior, see if we have to ignore any attrs
actual_attrs = ((attrs - bush.game.cell_skip_interior_attrs)
if cellBlock.cell.flags.isInterior else attrs)
for attr in actual_attrs:
tempCellData[cfid][attr] = __attrgetters[attr](
cellBlock.cell)
def checkMasterCellBlockData(cellBlock):
"""
Add attribute values from record(s) in master file(s).
Only adds records where a matching formID is found in temp
cell data.
The attribute values in temp cell data are then used to
update these records where the value is different.
"""
if not cellBlock.cell.flags1.ignored:
cfid = cellBlock.cell.fid
if cfid not in tempCellData: return
# If we're in an interior, see if we have to ignore any attrs
actual_attrs = ((attrs - bush.game.cell_skip_interior_attrs)
if cellBlock.cell.flags.isInterior else attrs)
for attr in actual_attrs:
master_attr = __attrgetters[attr](cellBlock.cell)
if tempCellData[cfid][attr] != master_attr:
cellData[cfid][attr] = tempCellData[cfid][attr]
progress.setFull(len(self.srcs))
cachedMasters = {}
minfs = self.patchFile.p_file_minfos
for srcMod in self.srcs:
if srcMod not in minfs: continue
# tempCellData maps long fids for cells in srcMod to dicts of
# (attributes (among attrs) -> their values for this mod). It is
# used to update cellData with cells that change those attributes'
# values from the value in any of srcMod's masters.
tempCellData = defaultdict(dict)
srcInfo = minfs[srcMod]
bashTags = srcInfo.getBashTags()
tags = bashTags & set(self.recAttrs)
if not tags: continue
srcFile = self._mod_file_read(srcInfo)
cachedMasters[srcMod] = srcFile
attrs = set(chain.from_iterable(
self.recAttrs[bashKey] for bashKey in tags))
if b'CELL' in srcFile.tops:
for cellBlock in srcFile.tops[b'CELL'].id_cellBlock.values():
importCellBlockData(cellBlock)
if b'WRLD' in srcFile.tops:
for worldBlock in srcFile.tops[b'WRLD'].id_worldBlocks.values():
for cellBlock in worldBlock.id_cellBlock.values():
importCellBlockData(cellBlock)
if worldBlock.worldCellBlock:
importCellBlockData(worldBlock.worldCellBlock)
for master in srcInfo.masterNames:
if master not in minfs: continue # or break filter mods
if master in cachedMasters:
masterFile = cachedMasters[master]
else:
masterFile = self._mod_file_read(minfs[master])
cachedMasters[master] = masterFile
if b'CELL' in masterFile.tops:
for cellBlock in masterFile.tops[b'CELL'].id_cellBlock.values():
checkMasterCellBlockData(cellBlock)
if b'WRLD' in masterFile.tops:
for worldBlock in masterFile.tops[b'WRLD'].id_worldBlocks.values():
for cellBlock in worldBlock.id_cellBlock.values():
checkMasterCellBlockData(cellBlock)
if worldBlock.worldCellBlock:
checkMasterCellBlockData(worldBlock.worldCellBlock)
progress.plus()
def scanModFile(self, modFile, progress): # scanModFile0
"""Add lists from modFile."""
if not (b'CELL' in modFile.tops or b'WRLD' in modFile.tops):
return
cellData = self.cellData
patchCells = self.patchFile.tops[b'CELL']
patchWorlds = self.patchFile.tops[b'WRLD']
if b'CELL' in modFile.tops:
for cfid, cellBlock in modFile.tops[b'CELL'].id_cellBlock.items():
if cfid in cellData:
patchCells.setCell(cellBlock.cell)
if b'WRLD' in modFile.tops:
for wfid, worldBlock in modFile.tops[b'WRLD'].id_worldBlocks.items():
patchWorlds.setWorld(worldBlock.world)
curr_pworld = patchWorlds.id_worldBlocks[wfid]
for cfid, cellBlock in worldBlock.id_cellBlock.items():
if cfid in cellData:
curr_pworld.setCell(cellBlock.cell)
pers_cell_block = worldBlock.worldCellBlock
if pers_cell_block and pers_cell_block.cell.fid in cellData:
curr_pworld.set_persistent_cell(pers_cell_block.cell)
def buildPatch(self, log, progress, __attrgetters=attrgetter_cache):
"""Adds merged lists to patchfile."""
def handlePatchCellBlock(patchCellBlock):
"""This function checks if an attribute or flag in CellData has
a value which is different to the corresponding value in the
bash patch file.
The Patch file will contain the last corresponding record
found when it is created regardless of tags.
If the CellData value is different, then the value is copied
to the bash patch, and the cell is flagged as modified.
Modified cell Blocks are kept, the other are discarded."""
cell_modified = False
patch_cell = patchCellBlock.cell
patch_cell_fid = patch_cell.fid
for attr, val in cellData[patch_cell_fid].items():
if val != __attrgetters[attr](patch_cell):
setattr_deep(patch_cell, attr, val)
cell_modified = True
if cell_modified:
patch_cell.setChanged()
keep(patch_cell_fid)
return cell_modified
if not self.isActive: return
keep = self.patchFile.getKeeper()
cellData, count = self.cellData, Counter()
for cell_fid, cellBlock in self.patchFile.tops[b'CELL'].id_cellBlock.items():
if cell_fid in cellData and handlePatchCellBlock(cellBlock):
count[cell_fid.mod_fn] += 1
for worldId, worldBlock in self.patchFile.tops[
b'WRLD'].id_worldBlocks.items():
keepWorld = False
for cell_fid, cellBlock in worldBlock.id_cellBlock.items():
if cell_fid in cellData and handlePatchCellBlock(cellBlock):
count[cell_fid.mod_fn] += 1
keepWorld = True
if worldBlock.worldCellBlock:
cell_fid = worldBlock.worldCellBlock.cell.fid
if cell_fid in cellData and handlePatchCellBlock(
worldBlock.worldCellBlock):
count[cell_fid.mod_fn] += 1
keepWorld = True
if keepWorld:
keep(worldId)
self.cellData.clear()
self._patchLog(log, count)
def _plog(self,log,count): # type 1 but for logMsg % sum(...)
log(self.__class__.logMsg)
for srcMod in load_order.get_ordered(count):
log(u'* %s: %d' % (srcMod,count[srcMod]))
#------------------------------------------------------------------------------
class ImportGraphicsPatcher(APreserver):
rec_attrs = bush.game.graphicsTypes
_fid_rec_attrs = bush.game.graphicsFidTypes
def _inner_loop(self, keep, records, top_mod_rec, type_count,
__attrgetters=attrgetter_cache):
id_data = self.id_data
for rfid, record in records:
if rfid not in id_data: continue
for attr, val in id_data[rfid].items():
rec_attr = __attrgetters[attr](record)
if isinstance(rec_attr, str) and isinstance(val, str):
if rec_attr.lower() != val.lower():
break
continue
elif attr in bush.game.graphicsModelAttrs:
try:
if rec_attr.modPath.lower() != val.modPath.lower():
break
continue
except AttributeError:
if rec_attr is val is None: continue
if rec_attr is None or val is None: # not both
break
if rec_attr.modPath is val.modPath is None: continue
break
if rec_attr != val: break
else: continue
for attr, val in id_data[rfid].items():
setattr(record, attr, val)
keep(rfid)
type_count[top_mod_rec] += 1
#------------------------------------------------------------------------------
class ImportRacesPatcher(APreserver):
rec_attrs = bush.game.import_races_attrs
_multi_tag = True
def _inner_loop(self, keep, records, top_mod_rec, type_count,
__attrgetters=attrgetter_cache):
loop_setattr = setattr_deep if self._deep_attrs else setattr
id_data = self.id_data
for rfid, record in records:
if rfid not in id_data: continue
for attr, val in id_data[rfid].items():
record_val = __attrgetters[attr](record)
if attr in (u'eyes', u'hairs'):
if set(record_val) != set(val): break
else:
if attr in (u'leftEye', u'rightEye') and not record_val:
deprint(u'Very odd race %s found - %s is None' % (
record.full, attr))
elif record_val != val: break
else: continue
for attr, val in id_data[rfid].items():
loop_setattr(record, attr, val)
keep(rfid)
type_count[top_mod_rec] += 1