Skip to content

Commit d047be5

Browse files
InfernioSharlikran
authored andcommitted
cosaves.py - beta
This API was starting to become inadequate for Wrye Bash's current needs. Most importantly: 1. It was difficult to understand and maintain. 2. It did not support enough of the cosave format. 3. Pluggy support was all over the place. This commit looks like a complete rewrite, but it was actually a step-by-step refactoring comprising 100+ commits that got squashed down to this final commit, since almost every step of the way would have broken dev. For reference: cosaves.py - alpha: 0a300af. New features included in this commit are: 1. Add button for dumping Pluggy cosaves With the new cosaves API, we now have the capability to dump Pluggy cosaves as well, so this commit adds a button to do just that. 2. Correctly display save masters in SSE With SKSE 2.0.15, which adds the PLGN chunk, we can now display save master lists in SSE in the correct order (with ESLs and regular plugins interweaved).
1 parent 8d8f181 commit d047be5

9 files changed

Lines changed: 1659 additions & 545 deletions

File tree

Mopy/bash/basher/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,7 +372,7 @@ def SetFileInfo(self,fileInfo):
372372
if not fileInfo:
373373
return
374374
#--Fill data and populate
375-
for mi, masters_name in enumerate(fileInfo.header.masters):
375+
for mi, masters_name in enumerate(fileInfo.get_masters()):
376376
masterInfo = bosh.MasterInfo(masters_name)
377377
self.data_store[mi] = masterInfo
378378
self._reList()

Mopy/bash/basher/links.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ def InitSaveLinks():
650650
if bush.game.ess.canEditMore:
651651
SaveList.itemMenu.append(Save_Stats())
652652
SaveList.itemMenu.append(Save_StatObse())
653+
SaveList.itemMenu.append(Save_StatPluggy())
653654
if bush.game.ess.canEditMore:
654655
#--------------------------------------------
655656
SaveList.itemMenu.append(SeparatorLink())

Mopy/bash/basher/saves_links.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
'Save_EditCreatedEnchantmentCosts', 'Save_ImportFace',
4444
'Save_EditCreated', 'Save_ReweighPotions', 'Save_UpdateNPCLevels',
4545
'Save_ExportScreenshot', 'Save_Unbloat', 'Save_RepairAbomb',
46-
'Save_RepairHair']
46+
'Save_RepairHair', 'Save_StatPluggy']
4747

4848
#------------------------------------------------------------------------------
4949
# Saves Links -----------------------------------------------------------------
@@ -758,25 +758,51 @@ def Execute(self):
758758
#------------------------------------------------------------------------------
759759
class Save_StatObse(AppendableLink, OneItemLink):
760760
"""Dump .obse records."""
761-
_text = _(u'%s Statistics') % bush.game.se.cosave_ext.lower()
762-
_help = _(u'Dump %s records') % bush.game.se.cosave_ext.lower()
761+
_text = _(u'Dump %s Contents') % bush.game.se.cosave_ext.lower()
762+
_help = _(u'Dumps contents of associated %s cosave into a log.') % \
763+
bush.game.se.se_abbrev
763764

764765
def _append(self, window): return bool(bush.game.se.se_abbrev)
765766

766767
def _enable(self):
767768
if not super(Save_StatObse, self)._enable(): return False
768-
cosave = self._selected_info.get_se_cosave_path()
769-
return cosave.exists()
769+
return self._selected_info.get_xse_cosave_path().exists()
770770

771771
def Execute(self):
772772
with balt.BusyCursor():
773773
log = bolt.LogFile(StringIO.StringIO())
774-
cosave = self._selected_info.get_cosave()
774+
cosave = self._selected_info.get_xse_cosave()
775775
if cosave is not None:
776-
cosave.logStatObse(log, self._selected_info.header.masters)
776+
cosave.dump_to_log(log, self._selected_info.header.masters)
777777
text = log.out.getvalue()
778778
log.out.close()
779-
self._showLog(text, title=self._selected_item.s, fixedFont=False)
779+
if cosave is not None:
780+
self._showLog(text, title=cosave.cosave_path.tail.s,
781+
fixedFont=False)
782+
783+
#------------------------------------------------------------------------------
784+
class Save_StatPluggy(AppendableLink, OneItemLink):
785+
"""Dump Pluggy blocks from .pluggy files."""
786+
_text = _(u'Dump .pluggy Contents')
787+
_help = _(u'Dumps contents of associated Pluggy cosave into a log.')
788+
789+
def _append(self, window): return bush.game.has_standalone_pluggy
790+
791+
def _enable(self):
792+
if not super(Save_StatPluggy, self)._enable(): return False
793+
return self._selected_info.get_pluggy_cosave_path().exists()
794+
795+
def Execute(self):
796+
with balt.BusyCursor():
797+
log = bolt.LogFile(StringIO.StringIO())
798+
cosave = self._selected_info.get_pluggy_cosave()
799+
if cosave is not None:
800+
cosave.dump_to_log(log, self._selected_info.header.masters)
801+
text = log.out.getvalue()
802+
log.out.close()
803+
if cosave is not None:
804+
self._showLog(text, title=cosave.cosave_path.tail.s,
805+
fixedFont=False)
780806

781807
#------------------------------------------------------------------------------
782808
class Save_Unbloat(OneItemLink):

Mopy/bash/bolt.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1649,6 +1649,7 @@ def unpack_str32(ins): return ins.read(struct_unpack('I', ins.read(4))[0])
16491649
def unpack_int(ins): return struct_unpack('I', ins.read(4))[0]
16501650
def unpack_short(ins): return struct_unpack('H', ins.read(2))[0]
16511651
def unpack_float(ins): return struct_unpack('f', ins.read(4))[0]
1652+
def unpack_double(ins): return struct_unpack('d', ins.read(8))[0]
16521653
def unpack_byte(ins): return struct_unpack('B', ins.read(1))[0]
16531654
def unpack_int_signed(ins): return struct_unpack('i', ins.read(4))[0]
16541655
def unpack_int64_signed(ins): return struct_unpack('q', ins.read(8))[0]

Mopy/bash/bosh/__init__.py

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ def __init__(self, fullpath, load_cache=False):
273273

274274
def _reset_masters(self):
275275
#--Master Names/Order
276-
self.masterNames = tuple(self.header.masters)
276+
self.masterNames = tuple(self.get_masters())
277277
self.masterOrder = tuple() #--Reset to empty for now
278278

279279
def _file_changed(self, stat_tuple):
@@ -335,6 +335,18 @@ def getStatus(self):
335335
else:
336336
return status
337337

338+
def get_masters(self):
339+
"""
340+
Returns the masters of this file as a list, if that operation makes any
341+
sense. For example, the masters of a mod are its plugin masters, while
342+
the masters of a save file are the plugins listed in its plugin list.
343+
If this operation does not make sense (e.g. on an archive), an
344+
AbstractError is raised.
345+
346+
:return: A list of the masters of this file, as paths.
347+
"""
348+
raise AbstractError()
349+
338350
# Backup stuff - beta, see #292 -------------------------------------------
339351
def getFileInfos(self):
340352
"""Return one of the FileInfos singletons depending on fileInfo type.
@@ -508,6 +520,9 @@ def setmtime(self, set_time=0, crc_changed=False):
508520
else:
509521
self.calculate_crc(recalculate=True)
510522

523+
def get_masters(self):
524+
return self.header.masters
525+
511526
# Ghosting and ghosting related overrides ---------------------------------
512527
def do_update(self):
513528
self.isGhost, old_ghost = not self._abs_path.exists() and (
@@ -967,17 +982,14 @@ def listErrors(self):
967982

968983
#------------------------------------------------------------------------------
969984
from .save_headers import get_save_header_type, SaveFileHeader
970-
from ._saves import PluggyFile
985+
from .cosaves import PluggyCosave
971986
from . import cosaves
972987

973988
class SaveInfo(FileInfo):
974-
_cosave_type = None # type: cosaves.ACoSaveFile
989+
# The xSE cosave that may come with this save file. Lazily initialized.
990+
_xse_cosave = None
975991

976-
@property
977-
def cosave_type(self):
978-
if self._cosave_type is None:
979-
SaveInfo._cosave_type = cosaves.get_cosave_type(bush.game.fsName)
980-
return self._cosave_type
992+
def cosave_type(self): return cosaves.get_cosave_type(bush.game.fsName)
981993

982994
def getFileInfos(self): return saveInfos
983995

@@ -1013,26 +1025,29 @@ def write_masters(self):
10131025
oldMasters = [GPath(decode(x)) for x in oldMasters]
10141026
self.abs_path.untemp()
10151027
#--Cosaves
1016-
masterMap = dict(
1017-
(x, y) for x, y in zip(oldMasters, self.header.masters) if x != y)
1018-
#--Pluggy file?
1019-
pluggyPath = CoSaves.getPaths(self.abs_path)[0]
1020-
if masterMap and pluggyPath.exists():
1021-
pluggy = PluggyFile(pluggyPath)
1022-
pluggy.load()
1023-
pluggy.mapMasters(masterMap)
1024-
pluggy.safeSave()
1025-
#--OBSE/SKSE file?
1026-
cosave = self.get_cosave()
1027-
if cosave is not None:
1028-
cosave.map_masters(masterMap)
1029-
cosave.write_cosave_safe()
1028+
master_map = dict((x.s, y.s) for x, y in
1029+
zip(oldMasters, self.header.masters) if x != y)
1030+
if master_map:
1031+
#--Pluggy cosave?
1032+
if bush.game.has_standalone_pluggy:
1033+
pluggy_path = self.get_pluggy_cosave_path()
1034+
if pluggy_path.isfile():
1035+
pluggy_cosave = self.get_pluggy_cosave()
1036+
pluggy_cosave.remap_plugins(master_map)
1037+
pluggy_cosave.write_cosave_safe()
1038+
#--xSE cosave?
1039+
if bush.game.se.se_abbrev:
1040+
xse_path = self.get_xse_cosave_path()
1041+
if xse_path is not None and xse_path.isfile():
1042+
xse_cosave = self.get_xse_cosave()
1043+
xse_cosave.remap_plugins(master_map)
1044+
xse_cosave.write_cosave_safe()
10301045

10311046
def get_cosave_tags(self):
10321047
"""Return strings expressing whether cosaves exist and are correct."""
10331048
cPluggy, cObse = (u'', u'')
10341049
pluggy = self.name.root + u'.pluggy'
1035-
obse = self.get_se_cosave_path()
1050+
obse = self.get_xse_cosave_path()
10361051
if pluggy.exists():
10371052
cPluggy = u'XP'[abs(pluggy.mtime - self.mtime) < 10]
10381053
if obse and obse.exists():
@@ -1044,21 +1059,52 @@ def backup_paths(self, first=False):
10441059
save_paths.extend(CoSaves.get_new_paths(*save_paths[0]))
10451060
return save_paths
10461061

1047-
def get_cosave(self):
1048-
""":rtype: cosaves.ACoSaveFile"""
1049-
cosave_path = self.get_se_cosave_path()
1050-
if cosave_path is None: return None
1051-
try:
1052-
return self.cosave_type(cosave_path) # type: cosaves.ACoSaveFile
1053-
except (OSError, IOError, FileError) as e:
1054-
if isinstance(e, FileError) or (
1055-
isinstance(e, (OSError, IOError)) and e.errno != errno.ENOENT):
1056-
deprint(u'Failed to open %s' % cosave_path, traceback=True)
1057-
return None
1058-
1059-
def get_se_cosave_path(self):
1062+
# TODO(inf) Turn into a property?
1063+
def get_xse_cosave(self):
1064+
""":rtype: cosaves.xSECosave"""
1065+
if self._xse_cosave is None:
1066+
cosave_path = self.get_xse_cosave_path()
1067+
if cosave_path is None: return None
1068+
try:
1069+
cosave_constructor = self.cosave_type()
1070+
self._xse_cosave = cosave_constructor(cosave_path)
1071+
except (OSError, IOError, FileError) as e:
1072+
if isinstance(e, FileError) or (
1073+
isinstance(e, (OSError, IOError)) and e.errno != errno.ENOENT):
1074+
deprint(u'Failed to open %s' % cosave_path, traceback=True)
1075+
return None
1076+
return self._xse_cosave
1077+
1078+
def get_xse_cosave_path(self):
10601079
if self.cosave_type is None: return None
1061-
return self.getPath().root + u'.' + self.cosave_type.signature.lower()
1080+
return self.getPath().root + bush.game.se.cosave_ext
1081+
1082+
def get_pluggy_cosave(self):
1083+
cosave_path = self.get_pluggy_cosave_path()
1084+
if cosave_path is not None:
1085+
try:
1086+
return PluggyCosave(cosave_path)
1087+
except (OSError, IOError, FileError) as e:
1088+
if isinstance(e, FileError) or (
1089+
isinstance(e, (OSError, IOError)) and e.errno != errno.ENOENT):
1090+
deprint(u'Failed to open %s' % cosave_path, traceback=True)
1091+
return None
1092+
1093+
def get_pluggy_cosave_path(self):
1094+
return self.getPath().root + u'.pluggy'
1095+
1096+
def get_masters(self):
1097+
if bush.game.has_esl:
1098+
xse_cosave_path = self.get_xse_cosave_path()
1099+
if xse_cosave_path is not None and xse_cosave_path.isfile():
1100+
# Make sure the cosave's masters are actually useful
1101+
xse_cosave = self.get_xse_cosave()
1102+
if xse_cosave.has_accurate_master_list(True):
1103+
return [GPath(master) for master in
1104+
xse_cosave.get_master_list()]
1105+
# Fall back on the regular masters - either the cosave is unnecessary,
1106+
# doesn't exist or isn't accurate
1107+
return self.header.masters
10621108

10631109
#------------------------------------------------------------------------------
10641110
class DataStore(DataDict):

Mopy/bash/bosh/_saves.py

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
unpack_short, struct_pack, struct_unpack
3535
from ..brec import ModReader, MreRecord, ModWriter, getObjectIndex, \
3636
getFormIndices
37-
from ..exception import FileError, ModError, StateError
37+
from ..exception import ModError, StateError
3838
from ..parsers import LoadFactory, ModFile
3939

4040
#------------------------------------------------------------------------------
@@ -216,102 +216,6 @@ def dumpText(self,saveFile):
216216
self.skills))
217217
return buff.getvalue()
218218

219-
#------------------------------------------------------------------------------
220-
class PluggyFile:
221-
"""Represents a .pluggy cofile for saves. Used for editing masters list."""
222-
def __init__(self,path):
223-
self.path = path
224-
self.name = path.tail
225-
self.tag = None
226-
self.version = None
227-
self._plugins = None
228-
self.other = None
229-
self.valid = False
230-
231-
def mapMasters(self,masterMap):
232-
"""Update plugin names according to masterMap."""
233-
if not self.valid:
234-
raise FileError(self.path.tail, u"File not initialized.")
235-
self._plugins = [(x, y, masterMap.get(z,z)) for x,y,z in self._plugins]
236-
237-
def load(self):
238-
"""Read file."""
239-
import binascii
240-
path_size = self.path.size
241-
with self.path.open('rb') as ins:
242-
buff = ins.read(path_size-4)
243-
crc32, = struct_unpack('=i', ins.read(4))
244-
crcNew = binascii.crc32(buff)
245-
if crc32 != crcNew:
246-
raise FileError(self.path.tail,
247-
u'CRC32 file check failed. File: %X, Calc: %X' % (
248-
crc32, crcNew))
249-
#--Header
250-
with sio(buff) as ins:
251-
def _unpack(fmt, fmt_siz):
252-
return struct_unpack(fmt, ins.read(fmt_siz))
253-
if ins.read(10) != 'PluggySave':
254-
raise FileError(self.path.tail, u'File tag != "PluggySave"')
255-
self.version, = _unpack('I',4)
256-
#--Reject versions earlier than 1.02
257-
if self.version < 0x01020000:
258-
raise FileError(self.path.tail,
259-
u'Unsupported file version: %X' % self.version)
260-
#--Plugins
261-
self._plugins = []
262-
type, = _unpack('=B',1)
263-
if type != 0:
264-
raise FileError(self.path.tail,
265-
u'Expected plugins record, but got %d.' % type)
266-
count, = _unpack('=I',4)
267-
for x in range(count):
268-
espid,index,modLen = _unpack('=2BI',6)
269-
modName = GPath(decode(ins.read(modLen)))
270-
self._plugins.append((espid, index, modName))
271-
#--Other
272-
self.other = ins.getvalue()[ins.tell():]
273-
deprint(struct_unpack('I', self.other[-4:]), self.path.size-8)
274-
#--Done
275-
self.valid = True
276-
277-
def save(self,path=None,mtime=0):
278-
"""Saves."""
279-
import binascii
280-
if not self.valid:
281-
raise FileError(self.path.tail, u"File not initialized.")
282-
#--Buffer
283-
with sio() as buff:
284-
#--Save
285-
def _pack(fmt, *args):
286-
buff.write(struct_pack(fmt, *args))
287-
buff.write('PluggySave')
288-
_pack('=I',self.version)
289-
#--Plugins
290-
_pack('=B',0)
291-
_pack('=I', len(self._plugins))
292-
for (espid,index,modName) in self._plugins:
293-
modName = encode(modName.cs)
294-
_pack('=2BI',espid,index,len(modName))
295-
buff.write(modName)
296-
#--Other
297-
buff.write(self.other)
298-
#--End control
299-
buff.seek(-4,1)
300-
_pack('=I',buff.tell())
301-
#--Save
302-
path = path or self.path
303-
mtime = mtime or path.exists() and path.mtime
304-
text = buff.getvalue()
305-
with path.open('wb') as out:
306-
out.write(text)
307-
out.write(struct_pack('i', binascii.crc32(text)))
308-
path.mtime = mtime
309-
310-
def safeSave(self):
311-
"""Save data to file safely."""
312-
self.save(self.path.temp,self.path.mtime)
313-
self.path.untemp()
314-
315219
# Save File -------------------------------------------------------------------
316220
class SaveFile:
317221
"""Represents a Tes4 Save file."""

0 commit comments

Comments
 (0)