omw_shuffle_ingredients.py 22 KB

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