omw_shuffle_ingredients.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. #!/usr/bin/env python3
  2. from struct import pack, unpack
  3. from datetime import date
  4. from pathlib import Path
  5. from random import shuffle
  6. import os.path
  7. import argparse
  8. import sys
  9. import re
  10. configFilename = 'openmw.cfg'
  11. configPaths = { 'linux': '~/.config/openmw',
  12. 'freebsd': '~/.config/openmw',
  13. 'darwin': '~/Library/Preferences/openmw' }
  14. modPaths = { 'linux': '~/.local/share/openmw/data',
  15. 'freebsd': '~/.local/share/openmw/data',
  16. 'darwin': '~/Library/Application Support/openmw/data' }
  17. def packLong(i):
  18. # little-endian, "standard" 4-bytes (old 32-bit systems)
  19. return pack('<l', i)
  20. def packFloat(i):
  21. return pack('<f', i)
  22. def packString(s):
  23. return bytes(s, 'ascii')
  24. def packPaddedString(s, l):
  25. bs = bytes(s, 'ascii')
  26. if len(bs) > l:
  27. # still need to null-terminate
  28. return bs[:(l-1)] + bytes(1)
  29. else:
  30. return bs + bytes(l - len(bs))
  31. def parseString(ba):
  32. i = ba.find(0)
  33. return ba[:i].decode(encoding='ascii', errors='ignore')
  34. def parseNum(ba):
  35. return int.from_bytes(ba, 'little', signed=True)
  36. def parseFloat(ba):
  37. return unpack('f', ba)[0]
  38. def parseTES3(rec):
  39. tesrec = {}
  40. sr = rec['subrecords']
  41. tesrec['version'] = parseFloat(sr[0]['data'][0:4])
  42. tesrec['filetype'] = parseNum(sr[0]['data'][4:8])
  43. tesrec['author'] = parseString(sr[0]['data'][8:40])
  44. tesrec['desc'] = parseString(sr[0]['data'][40:296])
  45. tesrec['numrecords'] = parseNum(sr[0]['data'][296:300])
  46. masters = []
  47. for i in range(1, len(sr), 2):
  48. mastfile = parseString(sr[i]['data'])
  49. mastsize = parseNum(sr[i+1]['data'])
  50. masters.append((mastfile, mastsize))
  51. tesrec['masters'] = masters
  52. return tesrec
  53. def parseINGR(rec):
  54. ingrrec = {}
  55. srs = rec['subrecords']
  56. for sr in srs:
  57. if sr['type'] == 'NAME':
  58. ingrrec['id'] = parseString(sr['data'])
  59. elif sr['type'] == 'MODL':
  60. ingrrec['model'] = parseString(sr['data'])
  61. elif sr['type'] == 'FNAM':
  62. ingrrec['name'] = parseString(sr['data'])
  63. elif sr['type'] == 'ITEX':
  64. ingrrec['icon'] = parseString(sr['data'])
  65. elif sr['type'] == 'SCRI':
  66. ingrrec['script'] = parseString(sr['data'])
  67. elif sr['type'] == 'IRDT':
  68. attr_struct = sr['data']
  69. ingrrec['weight'] = parseFloat(attr_struct[0:4])
  70. ingrrec['value'] = parseNum(attr_struct[4:8])
  71. effect_tuples = []
  72. for i in range(0,4):
  73. effect = parseNum(attr_struct[(8+i*4):(12+i*4)])
  74. skill = parseNum(attr_struct[(24+i*4):(28+i*4)])
  75. attribute = parseNum(attr_struct[(40+i*4):(44+i*4)])
  76. effect_tuples.append((effect, skill, attribute))
  77. ingrrec['effects'] = effect_tuples
  78. else:
  79. print("unknown subrecord type '%s'" % sr['type'])
  80. ppRecord(rec)
  81. ingrrec['file'] = os.path.basename(rec['fullpath'])
  82. return ingrrec
  83. def parseLEVC(rec):
  84. # we're not writing these back out, and we're only
  85. # interested in the ID and the list of items, so
  86. # disregard the other info
  87. levrec = {}
  88. sr = rec['subrecords']
  89. levrec['name'] = parseString(sr[0]['data'])
  90. if len(sr) > 3:
  91. listcount = parseNum(sr[3]['data'])
  92. listitems = []
  93. for i in range(0,listcount*2,2):
  94. itemid = parseString(sr[4+i]['data'])
  95. listitems.append(itemid)
  96. levrec['items'] = listitems
  97. else:
  98. levrec['items'] = []
  99. return levrec
  100. def pullSubs(rec, subtype):
  101. return [ s for s in rec['subrecords'] if s['type'] == subtype ]
  102. def readHeader(ba):
  103. header = {}
  104. header['type'] = ba[0:4].decode()
  105. header['length'] = int.from_bytes(ba[4:8], 'little')
  106. return header
  107. def readSubRecord(ba):
  108. sr = {}
  109. sr['type'] = ba[0:4].decode()
  110. sr['length'] = int.from_bytes(ba[4:8], 'little')
  111. endbyte = 8 + sr['length']
  112. sr['data'] = ba[8:endbyte]
  113. return (sr, ba[endbyte:])
  114. def readRecords(filename):
  115. fh = open(filename, 'rb')
  116. while True:
  117. headerba = fh.read(16)
  118. if headerba is None or len(headerba) < 16:
  119. return None
  120. record = {}
  121. header = readHeader(headerba)
  122. record['type'] = header['type']
  123. record['length'] = header['length']
  124. record['subrecords'] = []
  125. # stash the filename here (a bit hacky, but useful)
  126. record['fullpath'] = filename
  127. remains = fh.read(header['length'])
  128. while len(remains) > 0:
  129. (subrecord, restofbytes) = readSubRecord(remains)
  130. record['subrecords'].append(subrecord)
  131. remains = restofbytes
  132. yield record
  133. def oldGetRecords(filename, rectype):
  134. return ( r for r in readRecords(filename) if r['type'] == rectype )
  135. def getRecords(filename, rectypes):
  136. numtypes = len(rectypes)
  137. retval = [ [] for x in range(numtypes) ]
  138. for r in readRecords(filename):
  139. if r['type'] in rectypes:
  140. for i in range(numtypes):
  141. if r['type'] == rectypes[i]:
  142. retval[i].append(r)
  143. return retval
  144. def packStringSubRecord(lbl, strval):
  145. str_bs = packString(strval) + bytes(1)
  146. l = packLong(len(str_bs))
  147. return packString(lbl) + l + str_bs
  148. def packIntSubRecord(lbl, num, numsize=4):
  149. # This is interesting. The 'pack' function from struct works fine like this:
  150. #
  151. # >>> pack('<l', 200)
  152. # b'\xc8\x00\x00\x00'
  153. #
  154. # but breaks if you make that format string a non-literal:
  155. #
  156. # >>> fs = '<l'
  157. # >>> pack(fs, 200)
  158. # Traceback (most recent call last):
  159. # File "<stdin>", line 1, in <module>
  160. # struct.error: repeat count given without format specifier
  161. #
  162. # This is as of Python 3.5.2
  163. num_bs = b''
  164. if numsize == 4:
  165. # "standard" 4-byte longs, little-endian
  166. num_bs = pack('<l', num)
  167. elif numsize == 2:
  168. num_bs = pack('<h', num)
  169. elif numsize == 1:
  170. # don't think endian-ness matters for bytes, but consistency
  171. num_bs = pack('<b', num)
  172. elif numsize == 8:
  173. num_bs = pack('<q', num)
  174. return packString(lbl) + packLong(numsize) + num_bs
  175. def packTES3(desc, numrecs, masters):
  176. start_bs = b'TES3'
  177. headerflags_bs = bytes(8)
  178. hedr_bs = b'HEDR' + packLong(300)
  179. version_bs = pack('<f', 1.0)
  180. # .esp == 0, .esm == 1, .ess == 32
  181. # suprisingly, .omwaddon == 0, also -- figured it would have its own
  182. ftype_bs = bytes(4)
  183. author_bs = packPaddedString('code copyright 2020, jmelesky', 32)
  184. desc_bs = packPaddedString(desc, 256)
  185. numrecs_bs = packLong(numrecs)
  186. masters_bs = b''
  187. for (m, s) in masters:
  188. masters_bs += packStringSubRecord('MAST', m)
  189. masters_bs += packIntSubRecord('DATA', s, 8)
  190. reclen = len(hedr_bs) + len(version_bs) + len(ftype_bs) + len(author_bs) +\
  191. len(desc_bs) + len(numrecs_bs) + len(masters_bs)
  192. reclen_bs = packLong(reclen)
  193. return start_bs + reclen_bs + headerflags_bs + \
  194. hedr_bs + version_bs + ftype_bs + author_bs + \
  195. desc_bs + numrecs_bs + masters_bs
  196. def packINGR(rec):
  197. start_bs = b'INGR'
  198. headerflags_bs = bytes(8)
  199. id_bs = packStringSubRecord('NAME', rec['id'])
  200. modl_bs = packStringSubRecord('MODL', rec['model'])
  201. name_bs = packStringSubRecord('FNAM', rec['name'])
  202. irdt_bs = b'IRDT'
  203. irdt_bs += packLong(56) # this subrecord is always length 56
  204. irdt_bs += packFloat(rec['weight'])
  205. irdt_bs += packLong(rec['value'])
  206. for i in range(0,4):
  207. irdt_bs += packLong(rec['effects'][i][0])
  208. for i in range(0,4):
  209. irdt_bs += packLong(rec['effects'][i][1])
  210. for i in range(0,4):
  211. irdt_bs += packLong(rec['effects'][i][2])
  212. icon_bs = packStringSubRecord('ITEX', rec['icon'])
  213. script_bs = b''
  214. if 'script' in rec:
  215. script_bs = packStringSubRecord('SCRI', rec['script'])
  216. reclen = len(id_bs) + len(modl_bs) + len(name_bs) + \
  217. len(irdt_bs) + len(icon_bs) + len(script_bs)
  218. reclen_bs = packLong(reclen)
  219. return start_bs + reclen_bs + headerflags_bs + id_bs + \
  220. modl_bs + name_bs + irdt_bs + icon_bs + script_bs
  221. def ppSubRecord(sr):
  222. if sr['type'] in ['NAME', 'INAM', 'CNAM', 'FNAM', 'MODL', 'TEXT', 'SCRI']:
  223. print(" %s, length %d, value '%s'" % (sr['type'], sr['length'], parseString(sr['data'])))
  224. elif sr['type'] in ['DATA', 'NNAM', 'INDX', 'INTV']:
  225. print(" %s, length %d, value '%s'" % (sr['type'], sr['length'], parseNum(sr['data'])))
  226. else:
  227. print(" %s, length %d" % (sr['type'], sr['length']))
  228. def ppRecord(rec):
  229. print("%s, length %d" % (rec['type'], rec['length']))
  230. for sr in rec['subrecords']:
  231. ppSubRecord(sr)
  232. def ppINGR(rec):
  233. print("Ingredient name: '%s'" % (rec['name']))
  234. print(" ID: '%s', file: '%s'" % (rec['id'], rec['file']))
  235. print(" Model: '%s', Icon: '%s'" % (rec['model'], rec['icon']))
  236. if 'script' in rec:
  237. print(" Script: '%s'" % (rec['script']))
  238. print(" %10s%10s%10s" % ("effect", "skill", "attribute"))
  239. for i in range(0,4):
  240. print(" %10d%10d%10d" % rec['effects'][i])
  241. def ppTES3(rec):
  242. print("TES3 record, type %d, version %f" % (rec['filetype'], rec['version']))
  243. print("author: %s" % rec['author'])
  244. print("description: %s" % rec['desc'])
  245. for (mfile, msize) in rec['masters']:
  246. print(" master %s, size %d" % (mfile, msize))
  247. print()
  248. def readCfg(cfg):
  249. # first, open the file and pull all 'data' and 'content' lines, in order
  250. data_dirs = []
  251. mods = []
  252. with open(cfg, 'r') as f:
  253. for l in f.readlines():
  254. # match of form "blah=blahblah"
  255. m = re.search(r'^(.*)=(.*)$', l)
  256. if m:
  257. varname = m.group(1).strip()
  258. # get rid of not only whitespace, but also surrounding quotes
  259. varvalue = m.group(2).strip().strip('\'"')
  260. if varname == 'data':
  261. data_dirs.append(varvalue)
  262. elif varname == 'content':
  263. mods.append(varvalue)
  264. # we've got the basenames of the mods, but not the full paths
  265. # and we have to search through the data_dirs to find them
  266. fp_mods = []
  267. for m in mods:
  268. for p in data_dirs:
  269. full_path = os.path.join(p, m)
  270. if os.path.exists(full_path):
  271. fp_mods.append(full_path)
  272. break
  273. print("Config file parsed...")
  274. return fp_mods
  275. def dumpalchs(cfg):
  276. alchs = []
  277. fp_mods = readCfg(cfg)
  278. for f in fp_mods:
  279. [ ppTES3(parseTES3(x)) for x in oldGetRecords(f, 'TES3') ]
  280. for f in fp_mods:
  281. ingrs = [ parseINGR(x) for x in oldGetRecords(f, 'INGR') ]
  282. [ ppINGR(x) for x in ingrs ]
  283. def shuffle_ingredients(ingredients):
  284. # Okay, here's what we're doing.
  285. #
  286. # First, let's take out all the ingredients that
  287. # don't have any effects. They're likely unused
  288. # or singular quest items.
  289. final_ingredients = []
  290. for ingr in ingredients:
  291. if ingr['effects'][0][0] < 0 \
  292. and ingr['effects'][1][0] < 0 \
  293. and ingr['effects'][2][0] < 0 \
  294. and ingr['effects'][3][0] < 0:
  295. final_ingredients.append(ingr)
  296. for ingr in final_ingredients:
  297. ingredients.remove(ingr)
  298. # Next, we're going to build four lists, one
  299. # each for the first, second, third, and fourth
  300. # effects.
  301. #
  302. # Why?
  303. #
  304. # We want to maintain proportions of different
  305. # effects. For example, in Vanilla, Restore
  306. # Fatigue is common as a first effect, and only
  307. # shows up once as a second effect. Likewise,
  308. # in Vanilla, some effects only appear in one
  309. # ingredient. We want to keep those
  310. # characteristics
  311. effect_lists = [[],[],[],[]]
  312. for i in range(0,4):
  313. for ingr in ingredients:
  314. if ingr['effects'][i][0] > 0:
  315. effect_lists[i].append(ingr['effects'][i])
  316. # Next, we shuffle the ingredients, then go
  317. # through each effect, assigning it to an
  318. # ingredient. At the end, move any remaining
  319. # ingredients to the final list. Repeat
  320. # until we assign all four levels of effect
  321. for i in range(0,4):
  322. shuffle(ingredients)
  323. total_effects = len(effect_lists[i])
  324. for j in range(0,total_effects):
  325. ingredients[j]['effects'][i] = effect_lists[i][j]
  326. if len(ingredients) > total_effects:
  327. final_ingredients += ingredients[total_effects:]
  328. del ingredients[total_effects:]
  329. # and then slap the rest in
  330. final_ingredients += ingredients
  331. print("first effects: %s" % len(effect_lists[0]))
  332. print("second effects: %s" % len(effect_lists[1]))
  333. print("third effects: %s" % len(effect_lists[2]))
  334. print("fourth effects: %s" % len(effect_lists[3]))
  335. print("total ingredients shuffled: %s" % len(final_ingredients))
  336. return final_ingredients
  337. def main(cfg, outmoddir, outmod):
  338. fp_mods = readCfg(cfg)
  339. # first, let's grab the "raw" records from the files
  340. (rtes3, rlevc, ringr) = ([], [], [])
  341. for f in fp_mods:
  342. print("Parsing '%s' for relevant records" % f)
  343. (rtes3t, rlevct, ringrt) = getRecords(f, ('TES3', 'LEVC', 'INGR'))
  344. rtes3 += rtes3t
  345. rlevc += rlevct
  346. ringr += ringrt
  347. # next, parse the tes3 records so we can get a list
  348. # of master files required by all our mods
  349. tes3list = [ parseTES3(x) for x in rtes3 ]
  350. masters = {}
  351. for t in tes3list:
  352. for m in t['masters']:
  353. masters[m[0]] = m[1]
  354. master_list = [ (k,v) for (k,v) in masters.items() ]
  355. # parse the levc records -- we want to sort things
  356. # as food, if the appear in a food leveled list
  357. levclist = [ parseLEVC(x) for x in rlevc ]
  358. # get a list of items that appear in lists of "food"
  359. foodset = set()
  360. for ll in levclist:
  361. if 'food' in ll['name'] or 'Food' in ll['name']:
  362. for ingr in ll['items']:
  363. foodset.add(ingr)
  364. # now parse the ingredients entries.
  365. ilist = [ parseINGR(x) for x in ringr ]
  366. # we need to uniquify the list -- mods may alter
  367. # Vanilla ingredients by replacing them
  368. idict = {}
  369. for ingr in ilist:
  370. idict[ingr['id']] = ingr
  371. new_ilist = [ x for x in idict.values() ]
  372. # now sort the ingredients into food and non-food
  373. food_ilist = []
  374. nonfood_ilist = []
  375. for ingr in new_ilist:
  376. if ingr['id'] in foodset or 'food' in ingr['id'] \
  377. or 'Food' in ingr['id']:
  378. food_ilist.append(ingr)
  379. else:
  380. nonfood_ilist.append(ingr)
  381. print("total ingredient records: %s" % (len(ilist)))
  382. print("total ingredients: %s" % (len(new_ilist)))
  383. print("total food: %s, total nonfood: %s" % (len(food_ilist), len(nonfood_ilist)))
  384. # now we build new lists with shuffled ingredient effects
  385. shuffled_foods = shuffle_ingredients(food_ilist)
  386. shuffled_nonfoods = shuffle_ingredients(nonfood_ilist)
  387. # now turn those ingredients back into INGR records
  388. #
  389. # along the way, build up the module
  390. # description for the new merged mod, out
  391. # of the names of mods that had ingredients
  392. ilist_bin = b''
  393. plugins = set()
  394. for x in shuffled_foods:
  395. ilist_bin += packINGR(x)
  396. plugins.add(x['file'])
  397. for x in shuffled_nonfoods:
  398. ilist_bin += packINGR(x)
  399. plugins.add(x['file'])
  400. moddesc = "Shuffled ingredients from: %s" % ', '.join(plugins)
  401. # finally, build the binary form of the
  402. # TES3 record, and write the whole thing
  403. # out to disk
  404. if not os.path.exists(outmoddir):
  405. p = Path(outmoddir)
  406. p.mkdir(parents=True)
  407. with open(outmod, 'wb') as f:
  408. f.write(packTES3(moddesc, len(shuffled_foods) +
  409. len(shuffled_nonfoods), master_list))
  410. f.write(ilist_bin)
  411. # And give some hopefully-useful instructions
  412. modShortName = os.path.basename(outmod)
  413. print("\n\n****************************************")
  414. 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)
  415. 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)
  416. print(" 2. %s is checked (enabled)" % modShortName)
  417. print(" 3. Any other OMW ingredient shuffler mods are *un*checked. Loading them might not cause problems, but probably will")
  418. print("\n")
  419. print(" Then, go ahead and start the game! All alchemy ingredients from all your mods should now have shuffled effects.")
  420. print("\n")
  421. if __name__ == '__main__':
  422. parser = argparse.ArgumentParser()
  423. parser.add_argument('-c', '--conffile', type = str, default = None,
  424. action = 'store', required = False,
  425. help = 'Conf file to use. Optional. By default, attempts to use the default conf file location.')
  426. parser.add_argument('-d', '--moddir', type = str, default = None,
  427. action = 'store', required = False,
  428. help = 'Directory to store the new module in. By default, attempts to use the default work directory for OpenMW-CS')
  429. parser.add_argument('-m', '--modname', type = str, default = None,
  430. action = 'store', required = False,
  431. help = 'Name of the new module to create. By default, this is "Shuffled Ingredients - <today\'s date>.omwaddon.')
  432. parser.add_argument('--dumpalchs', default = False,
  433. action = 'store_true', required = False,
  434. help = 'Instead of generating merged lists, dump all alchemy ingredients in the conf mods. Used for debugging')
  435. p = parser.parse_args()
  436. # determine the conf file to use
  437. confFile = ''
  438. if p.conffile:
  439. confFile = p.conffile
  440. else:
  441. pl = sys.platform
  442. if pl in configPaths:
  443. baseDir = os.path.expanduser(configPaths[pl])
  444. confFile = os.path.join(baseDir, configFilename)
  445. elif pl == 'win32':
  446. # this is ugly. first, imports that only work properly on windows
  447. from ctypes import *
  448. import ctypes.wintypes
  449. buf = create_unicode_buffer(ctypes.wintypes.MAX_PATH)
  450. # opaque arguments. they are, roughly, for our purposes:
  451. # - an indicator of folder owner (0 == current user)
  452. # - an id for the type of folder (5 == 'My Documents')
  453. # - an indicator for user to call from (0 same as above)
  454. # - a bunch of flags for different things
  455. # (if you want, for example, to get the default path
  456. # instead of the actual path, or whatnot)
  457. # 0 == current stuff
  458. # - the variable to hold the return value
  459. windll.shell32.SHGetFolderPathW(0, 5, 0, 0, buf)
  460. # pull out the return value and construct the rest
  461. baseDir = os.path.join(buf.value, 'My Games', 'OpenMW')
  462. confFile = os.path.join(baseDir, configFilename)
  463. else:
  464. print("Sorry, I don't recognize the platform '%s'. You can try specifying the conf file using the '-c' flag." % p)
  465. sys.exit(1)
  466. baseModDir = ''
  467. if p.moddir:
  468. baseModDir = p.moddir
  469. else:
  470. pl = sys.platform
  471. if pl in configPaths:
  472. baseModDir = os.path.expanduser(modPaths[pl])
  473. elif pl == 'win32':
  474. # this is ugly in exactly the same ways as above.
  475. # see there for more information
  476. from ctypes import *
  477. import ctypes.wintypes
  478. buf = create_unicode_buffer(ctypes.wintypes.MAX_PATH)
  479. windll.shell32.SHGetFolderPathW(0, 5, 0, 0, buf)
  480. baseDir = os.path.join(buf.value, 'My Games', 'OpenMW')
  481. baseModDir = os.path.join(baseDir, 'data')
  482. else:
  483. print("Sorry, I don't recognize the platform '%s'. You can try specifying the conf file using the '-c' flag." % p)
  484. sys.exit(1)
  485. if not os.path.exists(confFile):
  486. print("Sorry, the conf file '%s' doesn't seem to exist." % confFile)
  487. sys.exit(1)
  488. modName = ''
  489. if p.modname:
  490. modName = p.modname
  491. else:
  492. modName = 'Shuffled Ingredients - %s.omwaddon' % date.today().strftime('%Y-%m-%d')
  493. modFullPath = os.path.join(baseModDir, modName)
  494. if p.dumpalchs:
  495. dumpalchs(confFile)
  496. else:
  497. main(confFile, baseModDir, modFullPath)
  498. # regarding the windows path detection:
  499. #
  500. # "SHGetFolderPath" is deprecated in favor of "SHGetKnownFolderPath", but
  501. # >>> windll.shell32.SHGetKnownFolderPath('{FDD39AD0-238F-46AF-ADB4-6C85480369C7}', 0, 0, buf2)
  502. # -2147024894