123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599 |
- #!/usr/bin/env python3
- from struct import pack, unpack
- from datetime import date
- from pathlib import Path
- import os.path
- import argparse
- import sys
- import re
- configFilename = 'openmw.cfg'
- configPaths = { 'linux': '~/.config/openmw',
- 'freebsd': '~/.config/openmw',
- 'darwin': '~/Library/Preferences/openmw' }
- modPaths = { 'linux': '~/.local/share/openmw/data',
- 'freebsd': '~/.local/share/openmw/data',
- 'darwin': '~/Library/Application Support/openmw/data' }
-
- def packLong(i):
- # little-endian, "standard" 4-bytes (old 32-bit systems)
- return pack('<l', i)
- def packString(s):
- return bytes(s, 'ascii')
- def packPaddedString(s, l):
- bs = bytes(s, 'ascii')
- if len(bs) > l:
- # still need to null-terminate
- return bs[:(l-1)] + bytes(1)
- else:
- return bs + bytes(l - len(bs))
- def parseString(ba):
- i = ba.find(0)
- return ba[:i].decode(encoding='ascii', errors='ignore')
- def parseNum(ba):
- return int.from_bytes(ba, 'little', signed=True)
- def parseFloat(ba):
- return unpack('f', ba)[0]
- def parseTES3(rec):
- tesrec = {}
- sr = rec['subrecords']
- tesrec['version'] = parseFloat(sr[0]['data'][0:4])
- tesrec['filetype'] = parseNum(sr[0]['data'][4:8])
- tesrec['author'] = parseString(sr[0]['data'][8:40])
- tesrec['desc'] = parseString(sr[0]['data'][40:296])
- tesrec['numrecords'] = parseNum(sr[0]['data'][296:300])
- masters = []
- for i in range(1, len(sr), 2):
- mastfile = parseString(sr[i]['data'])
- mastsize = parseNum(sr[i+1]['data'])
- masters.append((mastfile, mastsize))
- tesrec['masters'] = masters
- return tesrec
- def parseINGR(rec):
- ingrrec = {}
- sr = rec['subrecords']
- ingrrec['id'] = parseString(sr[0]['data'])
- ingrrec['model'] = parseString(sr[1]['data'])
- ingrrec['name'] = parseString(sr[2]['data'])
- ingrrec['weight'] = parseFloat(sr[3]['data'][0:4])
- ingrrec['value'] = parseNum(sr[3]['data'][4:8])
- effect_ids = []
- skill_ids = []
- attr_ids = []
- for ind in range(8, 21, 4):
- effect_ids.append(parseNum(sr[3]['data'][ind:ind+4]))
- for ind in range(24, 37, 4):
- skill_ids.append(parseNum(sr[3]['data'][ind:ind+4]))
- for ind in range(40, 53, 4):
- attr_ids.append(parseNum(sr[3]['data'][ind:ind+4]))
- ingrrec['effect_ids'] = effect_ids
- ingrrec['skill_ids'] = skill_ids
- ingrrec['attr_ids'] = attr_ids
- ingrrec['icon'] = parseString(sr[4]['data'])
- if len(sr) > 5:
- ingrrec['script'] = parseString(sr[5]['data'])
- return ingrrec
- def pullSubs(rec, subtype):
- return [ s for s in rec['subrecords'] if s['type'] == subtype ]
- def readHeader(ba):
- header = {}
- header['type'] = ba[0:4].decode()
- header['length'] = int.from_bytes(ba[4:8], 'little')
- return header
- def readSubRecord(ba):
- sr = {}
- sr['type'] = ba[0:4].decode()
- sr['length'] = int.from_bytes(ba[4:8], 'little')
- endbyte = 8 + sr['length']
- sr['data'] = ba[8:endbyte]
- return (sr, ba[endbyte:])
- def readRecords(filename):
- fh = open(filename, 'rb')
- while True:
- headerba = fh.read(16)
- if headerba is None or len(headerba) < 16:
- return None
- record = {}
- header = readHeader(headerba)
- record['type'] = header['type']
- record['length'] = header['length']
- record['subrecords'] = []
- # stash the filename here (a bit hacky, but useful)
- record['fullpath'] = filename
- remains = fh.read(header['length'])
- while len(remains) > 0:
- (subrecord, restofbytes) = readSubRecord(remains)
- record['subrecords'].append(subrecord)
- remains = restofbytes
- yield record
- def oldGetRecords(filename, rectype):
- return ( r for r in readRecords(filename) if r['type'] == rectype )
- def getRecords(filename, rectypes):
- numtypes = len(rectypes)
- retval = [ [] for x in range(numtypes) ]
- for r in readRecords(filename):
- if r['type'] in rectypes:
- for i in range(numtypes):
- if r['type'] == rectypes[i]:
- retval[i].append(r)
- return retval
- def packStringSubRecord(lbl, strval):
- str_bs = packString(strval) + bytes(1)
- l = packLong(len(str_bs))
- return packString(lbl) + l + str_bs
- def packIntSubRecord(lbl, num, numsize=4):
- # This is interesting. The 'pack' function from struct works fine like this:
- #
- # >>> pack('<l', 200)
- # b'\xc8\x00\x00\x00'
- #
- # but breaks if you make that format string a non-literal:
- #
- # >>> fs = '<l'
- # >>> pack(fs, 200)
- # Traceback (most recent call last):
- # File "<stdin>", line 1, in <module>
- # struct.error: repeat count given without format specifier
- #
- # This is as of Python 3.5.2
- num_bs = b''
- if numsize == 4:
- # "standard" 4-byte longs, little-endian
- num_bs = pack('<l', num)
- elif numsize == 2:
- num_bs = pack('<h', num)
- elif numsize == 1:
- # don't think endian-ness matters for bytes, but consistency
- num_bs = pack('<b', num)
- elif numsize == 8:
- num_bs = pack('<q', num)
- return packString(lbl) + packLong(numsize) + num_bs
- def packLEV(rec):
- start_bs = b''
- id_bs = b''
- if rec['type'] == 'LEVC':
- start_bs += b'LEVC'
- id_bs = 'CNAM'
- else:
- start_bs += b'LEVI'
- id_bs = 'INAM'
- headerflags_bs = bytes(8)
- name_bs = packStringSubRecord('NAME', rec['name'])
- calcfrom_bs = packIntSubRecord('DATA', rec['calcfrom'])
- chance_bs = packIntSubRecord('NNAM', rec['chancenone'], 1)
- subrec_bs = packIntSubRecord('INDX', len(rec['items']))
- for (lvl, lid) in rec['items']:
- subrec_bs += packStringSubRecord(id_bs, lid)
- subrec_bs += packIntSubRecord('INTV', lvl, 2)
- reclen = len(name_bs) + len(calcfrom_bs) + len(chance_bs) + len(subrec_bs)
- reclen_bs = packLong(reclen)
- return start_bs + reclen_bs + headerflags_bs + \
- name_bs + calcfrom_bs + chance_bs + subrec_bs
- def packTES3(desc, numrecs, masters):
- start_bs = b'TES3'
- headerflags_bs = bytes(8)
- hedr_bs = b'HEDR' + packLong(300)
- version_bs = pack('<f', 1.0)
- # .esp == 0, .esm == 1, .ess == 32
- # suprisingly, .omwaddon == 0, also -- figured it would have its own
- ftype_bs = bytes(4)
- author_bs = packPaddedString('omwllf, copyright 2017, jmelesky', 32)
- desc_bs = packPaddedString(desc, 256)
- numrecs_bs = packLong(numrecs)
- masters_bs = b''
- for (m, s) in masters:
- masters_bs += packStringSubRecord('MAST', m)
- masters_bs += packIntSubRecord('DATA', s, 8)
- reclen = len(hedr_bs) + len(version_bs) + len(ftype_bs) + len(author_bs) +\
- len(desc_bs) + len(numrecs_bs) + len(masters_bs)
- reclen_bs = packLong(reclen)
- return start_bs + reclen_bs + headerflags_bs + \
- hedr_bs + version_bs + ftype_bs + author_bs + \
- desc_bs + numrecs_bs + masters_bs
- def ppSubRecord(sr):
- if sr['type'] in ['NAME', 'INAM', 'CNAM', 'FNAM', 'MODL', 'TEXT', 'SCRI']:
- print(" %s, length %d, value '%s'" % (sr['type'], sr['length'], parseString(sr['data'])))
- elif sr['type'] in ['DATA', 'NNAM', 'INDX', 'INTV']:
- print(" %s, length %d, value '%s'" % (sr['type'], sr['length'], parseNum(sr['data'])))
- else:
- print(" %s, length %d" % (sr['type'], sr['length']))
- def ppRecord(rec):
- print("%s, length %d" % (rec['type'], rec['length']))
- for sr in rec['subrecords']:
- ppSubRecord(sr)
- def ppLEV(rec):
- if rec['type'] == 'LEVC':
- print("Creature list '%s' from '%s':" % (rec['name'], rec['file']))
- else:
- print("Item list '%s' from '%s':" % (rec['name'], rec['file']))
- print("flags: %d, chance of none: %d" % (rec['calcfrom'], rec['chancenone']))
- for (lvl, lid) in rec['items']:
- print(" %2d - %s" % (lvl, lid))
- def ppINGR(rec):
- print("Ingredient name: '%s'" % (rec['name']))
- print(" ID: '%s'" % rec['id'])
- print(" Model: '%s', Icon: '%s'" % (rec['model'], rec['icon']))
- if 'script' in rec:
- print(" Script: '%s'" % (rec['script']))
- print(" %10s%10s%10s" % ("effect", "skill", "attribute"))
- for i in range(0,4):
- print(" %10d%10d%10d" % (rec['effect_ids'][i], rec['skill_ids'][i], rec['attr_ids'][i]))
- def ppTES3(rec):
- print("TES3 record, type %d, version %f" % (rec['filetype'], rec['version']))
- print("author: %s" % rec['author'])
- print("description: %s" % rec['desc'])
- for (mfile, msize) in rec['masters']:
- print(" master %s, size %d" % (mfile, msize))
- print()
- def mergeableLists(alllists):
- candidates = {}
- for l in alllists:
- lid = l['name']
- if lid in candidates:
- candidates[lid].append(l)
- else:
- candidates[lid] = [l]
- mergeables = {}
- for k in candidates:
- if len(candidates[k]) > 1:
- mergeables[k] = candidates[k]
- return mergeables
- def mergeLists(lls):
- # last one gets priority for list-level attributes
- last = lls[-1]
- newLev = { 'type': last['type'],
- 'name': last['name'],
- 'calcfrom': last['calcfrom'],
- 'chancenone': last['chancenone'] }
- allItems = []
- for l in lls:
- allItems += l['items']
- newLev['files'] = [ x['file'] for x in lls ]
- newLev['file'] = ', '.join(newLev['files'])
- # This ends up being a bit tricky, but it prevents us
- # from overloading lists with the same stuff.
- #
- # This is needed, because the original leveled lists
- # contain multiple entries for some creatures/items, and
- # that gets reproduced in many plugins.
- #
- # If we just added and sorted, then the more plugins you
- # have, the less often you'd see plugin content. This
- # method prevents the core game content from overwhelming
- # plugin contents.
- allUniques = [ x for x in set(allItems) ]
- allUniques.sort()
- newList = []
- for i in allUniques:
- newCount = max([ x['items'].count(i) for x in lls ])
- newList += [i] * newCount
- newLev['items'] = newList
- return newLev
- def mergeAllLists(alllists):
- mergeables = mergeableLists(alllists)
- merged = []
- for k in mergeables:
- merged.append(mergeLists(mergeables[k]))
- return merged
- def readCfg(cfg):
- # first, open the file and pull all 'data' and 'content' lines, in order
- data_dirs = []
- mods = []
- with open(cfg, 'r') as f:
- for l in f.readlines():
- # match of form "blah=blahblah"
- m = re.search(r'^(.*)=(.*)$', l)
- if m:
- varname = m.group(1).strip()
- # get rid of not only whitespace, but also surrounding quotes
- varvalue = m.group(2).strip().strip('\'"')
- if varname == 'data':
- data_dirs.append(varvalue)
- elif varname == 'content':
- mods.append(varvalue)
- # we've got the basenames of the mods, but not the full paths
- # and we have to search through the data_dirs to find them
- fp_mods = []
- for m in mods:
- for p in data_dirs:
- full_path = os.path.join(p, m)
- if os.path.exists(full_path):
- fp_mods.append(full_path)
- break
- print("Config file parsed...")
- return fp_mods
- def dumplists(cfg):
- llists = []
- fp_mods = readCfg(cfg)
- for f in fp_mods:
- [ ppTES3(parseTES3(x)) for x in oldGetRecords(f, 'TES3') ]
- for f in fp_mods:
- llists += [ parseLEV(x) for x in oldGetRecords(f, 'LEVI') ]
- for f in fp_mods:
- llists += [ parseLEV(x) for x in oldGetRecords(f, 'LEVC') ]
- for l in llists:
- ppLEV(l)
- def dumpalchs(cfg):
- alchs = []
- fp_mods = readCfg(cfg)
- for f in fp_mods:
- [ ppTES3(parseTES3(x)) for x in oldGetRecords(f, 'TES3') ]
- for f in fp_mods:
- [ ppINGR(parseINGR(x)) for x in oldGetRecords(f, 'INGR') ]
- def main(cfg, outmoddir, outmod):
- fp_mods = readCfg(cfg)
- # first, let's grab the "raw" records from the files
- (rtes3, rlevi, rlevc) = ([], [], [])
- for f in fp_mods:
- print("Parsing '%s' for relevant records" % f)
- (rtes3t, rlevit, rlevct) = getRecords(f, ('TES3', 'LEVI', 'LEVC'))
- rtes3 += rtes3t
- rlevi += rlevit
- rlevc += rlevct
- # next, parse the tes3 records so we can get a list
- # of master files required by all our mods
- tes3list = [ parseTES3(x) for x in rtes3 ]
- masters = {}
- for t in tes3list:
- for m in t['masters']:
- masters[m[0]] = m[1]
- master_list = [ (k,v) for (k,v) in masters.items() ]
- # now, let's parse the levi and levc records into
- # mergeable lists, then merge them
- # creature lists
- clist = [ parseLEV(x) for x in rlevc ]
- levc = mergeAllLists(clist)
- # item lists
- ilist = [ parseLEV(x) for x in rlevi ]
- levi = mergeAllLists(ilist)
- # now build the binary representation of
- # the merged lists.
- # along the way, build up the module
- # description for the new merged mod, out
- # of the names of mods that had lists
- llist_bc = b''
- pluginlist = []
- for x in levi + levc:
- # ppLEV(x)
- llist_bc += packLEV(x)
- pluginlist += x['files']
- plugins = set(pluginlist)
- moddesc = "Merged leveled lists from: %s" % ', '.join(plugins)
- # finally, build the binary form of the
- # TES3 record, and write the whole thing
- # out to disk
- if not os.path.exists(outmoddir):
- p = Path(outmoddir)
- p.mkdir(parents=True)
- with open(outmod, 'wb') as f:
- f.write(packTES3(moddesc, len(levi + levc), master_list))
- f.write(llist_bc)
- # And give some hopefully-useful instructions
- modShortName = os.path.basename(outmod)
- print("\n\n****************************************")
- print(" Great! I think that worked. When you next start the OpenMW Launcher, look for a module named %s. Make sure of the following things:" % modShortName)
- print(" 1. %s is at the bottom of the list. Drag it to the bottom if it's not. It needs to load last." % modShortName)
- print(" 2. %s is checked (enabled)" % modShortName)
- print(" 3. Any other OMWLLF mods are *un*checked. Loading them might not cause problems, but probably will")
- print("\n")
- print(" Then, go ahead and start the game! Your leveled lists should include adjustmemts from all relevants enabled mods")
- print("\n")
- if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('-c', '--conffile', type = str, default = None,
- action = 'store', required = False,
- help = 'Conf file to use. Optional. By default, attempts to use the default conf file location.')
- parser.add_argument('-d', '--moddir', type = str, default = None,
- action = 'store', required = False,
- help = 'Directory to store the new module in. By default, attempts to use the default work directory for OpenMW-CS')
- parser.add_argument('-m', '--modname', type = str, default = None,
- action = 'store', required = False,
- help = 'Name of the new module to create. By default, this is "OMWLLF Mod - <today\'s date>.omwaddon.')
- parser.add_argument('--dumpalchs', default = True,
- action = 'store_true', required = False,
- help = 'Instead of generating merged lists, dump all leveled lists in the conf mods. Used for debugging')
- p = parser.parse_args()
- # determine the conf file to use
- confFile = ''
- if p.conffile:
- confFile = p.conffile
- else:
- pl = sys.platform
- if pl in configPaths:
- baseDir = os.path.expanduser(configPaths[pl])
- confFile = os.path.join(baseDir, configFilename)
- elif pl == 'win32':
- # this is ugly. first, imports that only work properly on windows
- from ctypes import *
- import ctypes.wintypes
- buf = create_unicode_buffer(ctypes.wintypes.MAX_PATH)
- # opaque arguments. they are, roughly, for our purposes:
- # - an indicator of folder owner (0 == current user)
- # - an id for the type of folder (5 == 'My Documents')
- # - an indicator for user to call from (0 same as above)
- # - a bunch of flags for different things
- # (if you want, for example, to get the default path
- # instead of the actual path, or whatnot)
- # 0 == current stuff
- # - the variable to hold the return value
- windll.shell32.SHGetFolderPathW(0, 5, 0, 0, buf)
- # pull out the return value and construct the rest
- baseDir = os.path.join(buf.value, 'My Games', 'OpenMW')
- confFile = os.path.join(baseDir, configFilename)
- else:
- print("Sorry, I don't recognize the platform '%s'. You can try specifying the conf file using the '-c' flag." % p)
- sys.exit(1)
- baseModDir = ''
- if p.moddir:
- baseModDir = p.moddir
- else:
- pl = sys.platform
- if pl in configPaths:
- baseModDir = os.path.expanduser(modPaths[pl])
- elif pl == 'win32':
- # this is ugly in exactly the same ways as above.
- # see there for more information
- from ctypes import *
- import ctypes.wintypes
- buf = create_unicode_buffer(ctypes.wintypes.MAX_PATH)
- windll.shell32.SHGetFolderPathW(0, 5, 0, 0, buf)
- baseDir = os.path.join(buf.value, 'My Games', 'OpenMW')
- baseModDir = os.path.join(baseDir, 'data')
- else:
- print("Sorry, I don't recognize the platform '%s'. You can try specifying the conf file using the '-c' flag." % p)
- sys.exit(1)
- if not os.path.exists(confFile):
- print("Sorry, the conf file '%s' doesn't seem to exist." % confFile)
- sys.exit(1)
- modName = ''
- if p.modname:
- modName = p.modname
- else:
- modName = 'OMWLLF Mod - %s.omwaddon' % date.today().strftime('%Y-%m-%d')
- modFullPath = os.path.join(baseModDir, modName)
- if p.dumpalchs:
- dumpalchs(confFile)
- else:
- main(confFile, baseModDir, modFullPath)
- # regarding the windows path detection:
- #
- # "SHGetFolderPath" is deprecated in favor of "SHGetKnownFolderPath", but
- # >>> windll.shell32.SHGetKnownFolderPath('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}', 0, 0, buf2)
- # -2147024894
|