omw_shuffle_ingredients.py 20 KB

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