Wrye Bash Programming

Post » Mon Dec 28, 2009 7:15 am

Ashemanu has asked me if Bash could be used to do something like batch edit a bunch of npc files to quickly add spells to them. The answer is yes and no. Bash itself? No. But bash has a command line counterpart (bish.py) which with a little modification could do the job pretty easily.

Unfortunately, I'm only one who knows it that well, I'm too busy to write custom scripts for folks. So instead, I'm going to give a brief primer on what's possible. This will most likely sink into the sands of Oblivion, but here it is...

Overview
While Bash is well known as an end user tool for manipulating mod and save files in various ways, it can also be useful for repetitive modding tasks -- if you don't mind doing a little python programming and working from the command line. When developing my Rational Names mod, I used it quite a bit to write short little functions to do a lot of the work for me. Since I've been asked about using Bash to copy spells to a bunch of new NPCs, I thought that I'd explain how to do this a little bit.

I'll start off with a nice simple program. After that, I'll scare the pants off you with horrible details that will make you love the construction set.

Sample Program
Here's a brief program that you can add to bish.py and run from the command line to print out the formids of all spells of all npcs in a given mod (I actually haven't tested this, but it should work):
def npcTweak(fileName=None):	"""Tweak npcs in misc. ways."""	init(3)	loadFactory= bosh.LoadFactory(True,bosh.NPC)	modInfo = bosh.modInfos[fileName]	modFile = bosh.ModFile(modInfo,loadFactory)	modFile.load(True)	for npc in modFile.NPC_.records:		print npc.eid, npc.full		for spell in npc.spells:			print ' ', spell	modFile.safeSave()callables.add(npcTweak)

Once you've added that to bish.py, open the Windows command line tool, chdir to the mopy directory, and run it like so: bish npcTweak Oblivion.esm

Wasn't that easy? Now onto scary details...

Possibilities
* Change object names in a systematic way.
* Tweak npcs in just about any way you want (change faction, spells, race, face data, etc.)
* Tweak leveled lists in any way.
* Tweak/access string type properties (editor id, full name, model, icon) of most objects.

Difficulties
* You need to be able to program to some degree in python. Python is a great programming language, but it is programming. If you don't know at least a little about programming, this is not for you.
* bosh.py has 4300 lines of code. Granted, you don't have to understand all of it, but it's not nothing.
* Mod files are fairly complex, with a fair amount of cruft in them. Bash hides a fair amount of that mess from you, but it makes code more obscure to understand.
* Bash is pretty well documented, but I'm the only one who has used it so far, so I'm sure there's still plenty of obscurity.
* Bash uses some advanced python techniques in places. These can be obscure to people unfamiliar with the language. E.g. "[x for x in (1,2,3,4,5) if x % 2]" produces "[1,3,5]". If you're not familiar with advanced python, you probably just said "Huh?"
* Bash code is built to skip over most data in mod files. It actually only has a deep understanding of a few types of records (NPCs, leveled lists, Books). More types can be added with moderate ease, but you need to understand the records for that type of data (and apparently, modfiles have a moderate amount of cruft in them -- Oblivion seems to have changed record the record file formats as it developed. And sometimes the seem to have added stuff and then not used it.)
* Bash does not understand structured content (Dialogue and World Cell blocks) at all. Adding support for that would require a fair amount of work. (The problem here is that these types of records are combined in complex block structures which have to be handled correctly.)

Bash Files
Bash's python code comes in a few modules...
* bush.py: Definines miscelleaneous chunks of data. Don't worry about this.
* bosh.py: The "library" does most of heavy lifting of application. No GUI code.
* bish.py: A miscellaneous collection of command line programs/functions.
* basher.py: Almost all of the GUI code. 99% of Bash application is defined here.
* bash.py: A thin wrapper around basher.py. Basically takes stuff defined in basher.py and presses "Start".

Getting Started...
Hello, hello? Okay, I've scared the non-programmers away, but I think there's a few of you left... Anyway, to get started:
* copy bish.py to mybish.py (Don't want next Bash release to overwrite your code!)
* Mess around. Try not to break Oblivion.esm.

More hints:
* Get a decent, code understanding text editor. I use EditPlus, but there are tons of them around. In fact in a pinch, you can use Notepad.
* Have your python docs ready. Note that bash code is fairly documented with doc strings. There are also tools which will read python modules and spit out the docs for them in html format. Might be useful.

Wanted: Bash Coding Experts?
There's a lot of batch stuff that I can do very rapidly, but I'm not going to do it for everyone. I think that large projects might find that ability useful, so having a couple of extra people knowing it well enough to knock little functions out would be good. More hands/eyes would probably also mean that Bash's understanding of Mod file structure would be improved, which would be useful for everyone.
User avatar
Alex Vincent
 
Posts: 3514
Joined: Thu Jun 28, 2007 9:31 pm

Post » Mon Dec 28, 2009 6:12 am

And here's some more pain for you... Going to Ashemanu's case (adding spells) you could do something like this...
def npcSpellCopy(fileName=None):	"""Copies spells from template npc to other npcs."""	init(3)	loadFactory= bosh.LoadFactory(True,bosh.NPC)	modInfo = bosh.modInfos[fileName]	modFile = bosh.ModFile(modInfo,loadFactory)	modFile.load(True)	#--Get source spells from template npc	sourceNpc = modFile.NPC_.getRecord(0x1000CAE) #--Formid of template npc	sourceSpells = sourceNPC.spells	#--Loop over other npcs	for npc in modFile.NPC_.records:		print npc.eid, npc.full		npcSpells = npc.spells		for spell in sourceSpells:			if spell not in npcSpells:				npcSpells.append(spell)				print ' ', spell	modFile.safeSave()callables.add(npcSpellCopy)

Now, that copies the spells to everyone, which is probably not what is desired. Some sort of filtration would be nice. This is left an an exercise for the student.

The code above is no doubt a bit confusing, so I'll give another example with lots of commenting... For my Rational Names mod, I wanted to rename the sigil stones. There's a bunch of them, so first thing for me to do was to copy the soulgems records from Oblivion.esm to my "Rational Names.esp" file. You'll find the original code for this in bish.py, but heres the same thing with lots of comments...
def importRecs(fileName=None):	"""Imports records from Oblivion into a mod. Used for Rational Names."""	#--Initializes some data arrays by scanning mods and savegames.	init(3) 	#--Mod	#--Oblvion.esm and "Rational Names.esp" will both be represented as ModFile	#  objects. But I need to tell the ModFile objects to only break down SGST 	#  records, and for those, just treat them as generic records (i.e. just as 	#  a collection of subrecords, with no real understanding of what those subrecords	#  mean. Also I want to be able to save file when I'm done. So I create a 	#  LoadFactory object and tell it canSave == True and records to anolyze == 'SGST'	loadFactory= bosh.LoadFactory(True,'SGST')	#--Now I go to my bosh.modInfos database and get the modInfo for the 	#  "Rational Names.esp" mod. A modinfo is just a chunk of data with a	#  summary of info about the mod (directory, file name, masters, etc.). The	#  point of it here is that I don't have to specify the full path, and modFile	#  needs a modInfo rather than just the filename as it's argument.	modInfo = bosh.modInfos[fileName]	#--Create the ModFile representation...	modFile = bosh.ModFile(modInfo,loadFactory)	#--And tell it to load (i.e., actually read and anolyze the mod file).	modFile.load(True)	#--Now modfile is full of data, mostly just large chunks of unanolyzed data, 	#  except for SGST records, which are broken down somewhat.	#--Source (Oblivion)	#--Now I do the same thing for Oblivion.esm...	srcInfo = bosh.modInfos['Oblivion.esm']	srcFile = bosh.ModFile(srcInfo,loadFactory)	srcFile.load(True)	#--Import All Sigil stones	modSGST = modFile.SGST #--Just a shortcut to save a little typing.	#--Now, loop over all sigil stone records in Oblivion.esm...	for record in srcFile.SGST.records:		#--Now, get the formid from oblivion.esm record and see if that's		#  already in "Rational Names.esp". If not, then add it.		if not modSGST.getRecord(record.formid):			#--Okay, adding it...			modSGST.setRecord(record.formid,record)			#--And print a little message to command line showing formid and 			#  in game name of sigil stone.			print 'importing',hex(record.formid),record.getSubString('FULL')	#--Finally, save the changes.	modFile.safeSave()#--This is just a little glue that allows me to call the function from the #  command line.callables.add(importRecs)

To run this command, I open comand line tool, go to the Mopy directory and type: bish importRecs "Rational Names.esp"
User avatar
Lilit Ager
 
Posts: 3444
Joined: Thu Nov 23, 2006 9:06 pm

Post » Mon Dec 28, 2009 6:33 am

This is left an an exercise for the student.

That phrase is the bane of all students. ;)
But Python looks like a fun language.
User avatar
alyssa ALYSSA
 
Posts: 3382
Joined: Mon Sep 25, 2006 8:36 pm

Post » Mon Dec 28, 2009 10:21 pm

Excellent! I can't wait to get my hands dirty with this. It will probably make a couple things a little easier for me - plus OBSE doesn't know anything about mod files yet (we only deal with the in-memory representations.) I've got some ideas for this... :D
User avatar
Julie Ann
 
Posts: 3383
Joined: Thu Aug 23, 2007 5:17 am

Post » Mon Dec 28, 2009 9:21 am

Keep in mind that I'd be glad to merge changes back into bash.

Also, for file format record info (e.g. for records that aren't represented strongly in Bash), the place to go would be UESP. But it's having server problems while Dave Humphrey is out of town. I've had some luck pulling stuff from Google's cache (e.g., search for tes4mod:mod_file_format) however that doesn't always work (and right now is definitely having a problem with wiki subpages (which is most of the specific record data. Dang.)
User avatar
lexy
 
Posts: 3439
Joined: Tue Jul 11, 2006 6:37 pm

Post » Mon Dec 28, 2009 2:56 pm

I've taken a look at the format pages in the past - Dave (I believe) suggested they might be useful for decoding purposes.

I've wanted to try adding a new Magic Effect by creating a new entry in an esm and see if it is usable. The idea would be to try and mark it as needing a script effect. If this works we could add multiple new scriptable magic effects which could then have their own icons (say one for each current school at a minimum) so that people weren't stuck with the same icon for all scripted effects. Another use would be to create more conjuration functions. For some reason Bethesda coded multiple conjuration effects and stored the refid of the item to be summoned in the magic effect itself, rather than as a single summon effect with an override on the summoned object. Adding new effects could allow other objects to be summoned without scripting it.

I could probably create a new effect in memory in OBSE, but without getting it into an esm or esp, others couldn't use it.

There is also a long dormant python version of obse that will allow scripting more or less completely in python, which would allow significant improvements in script complexity and power.
User avatar
Ross Thomas
 
Posts: 3371
Joined: Sat Jul 21, 2007 12:06 am

Post » Mon Dec 28, 2009 11:30 am

This will most likely sink into the sands of Oblivion, but here it is...


Hmmm... Having just got back into Herbert's 'Duniverse' some sand delving or providing some protection from the elements sits well with me. I don't know Python (only used Java, please forgive me), and had alot of trouble with getting it onto my comp... but this is just the sort of thing to provide an avenue for me to get into it... after Hogmanay of course.

Thanks Wrye.
User avatar
casey macmillan
 
Posts: 3474
Joined: Fri Feb 09, 2007 7:37 pm

Post » Mon Dec 28, 2009 1:07 pm

Hmmm... Having just got back into Herbert's 'Duniverse' some sand delving or providing some protection from the elements sits well with me. I don't know Python (only used Java, please forgive me), and had alot of trouble with getting it onto my comp... but this is just the sort of thing to provide an avenue for me to get into it... after Hogmanay of course.

Thanks Wrye.

I've programmed in quite a few languages, but python is easily my favorite -- in fact (with a few exceptions, it's the only language I've been using recently). So, I went back and checked on a couple of python vs... references. Here's a couple:
* http://www.ferg.org/projects/python_java_side-by-side.html
* http://www.linuxjournal.com/article/3882

behippo: New mgef. I'd be surprised, but you never know. (I'd be surprised because they seem to be built in. E.g, the RSWD has a formid of 0, which is a sign of built-in-ness AFAIK.) But, definitely worth a try. Of course, messing with the exe in memory gives you more options.

OBSE and python: I remember reading about that -- I wonder about performance though. (However, I've been surprised by python's performance in the past, so I may be underestimating it again.)
User avatar
Josee Leach
 
Posts: 3371
Joined: Tue Dec 26, 2006 10:50 pm

Post » Mon Dec 28, 2009 8:50 am

behippo: New mgef. I'd be surprised, but you never know. (I'd be surprised because they seem to be built in. E.g, the RSWD has a formid of 0, which is a sign of built-in-ness AFAIK.) But, definitely worth a try. Of course, messing with the exe in memory gives you more options.

yeah - and DUMY as well. DUMY is available in the Mehrunes Razor expansion - so I bet that esp has a non-0 formid. The other effects all have calid ids. So, I have hopes.
OBSE and python: I remember reading about that -- I wonder about performance though. (However, I've been surprised by python's performance in the past, so I may be underestimating it again.)

Ian says it shold be pretty fast. I am new to python myself - being a C++ programmer by trade. Looks like fun. Also, plenty of other games use python exclusively for scripting (Civ IV, Temple of Elemental Evil, V:TM Bloodlines) I imagine it will be on par with OBScript. We'll see.
User avatar
Jessica Thomson
 
Posts: 3337
Joined: Fri Jul 21, 2006 5:10 am

Post » Mon Dec 28, 2009 12:44 pm

Good stuff! I need to bone up on my python anyway. ;)
User avatar
joeK
 
Posts: 3370
Joined: Tue Jul 10, 2007 10:22 am

Post » Mon Dec 28, 2009 1:53 pm

Since, there has been some interest... I'll bump with some minor notes.

String Translation: One of the things that's been on my to do list for a while (like a year and half) is improved support for non-English speaking users of Bash is better language support. Going through Bash code, you'll see almost all information strings are surrounded by _(). This is actually a setup to access a language string translating facility. Essentially the idea is that _() is supposed to be a function that takes the string argument and returns a equivalent string for the locale. This is actually pretty easy to do -- the main deal is that I just need to make it a little easier for foreign speakers to build up the string translation libraries. I know what I want to do and it should be too hard, just gotta find the time.

Path Class: A related issue is better path handling on non-English systems. Also something I'm overdue to fix is case-insensitive file name comparisons. To deal with both problems, I'm adding a Path class to represent all paths/file names. The problem here is that it's a pretty substantial refactoring of the code (paths and filenames are pervasive in Bash, not to mention stuff like directory listings, mtime access, file copy, etc.) So, I've done a fair amount of work on that, but there's still a fair amount to go. This refactoring will be included in next release of Bash.

Customizability: I kept customizability in mind while designing Bash (and Mash), however there wasn't a lot of interest, so it could probably be developed further. Rencently, I heard from Gez, who has customized the bottom launch bar to launch additional utilities. So I'll probably devote some further thought to this when I have time.

One way to customize right now would be: Copy bash.py to mybash.py. In my bash.py: import * from basher. Then define any new menu items, or launcher icons you want. Then define your updated versions of final assembly functions (e.g., InitLinks). Then tweak the final application launch code accordingly.
Note that for Bash's final assembly is done in the final functions.

That could probably be improved. I'll think about it when I get a chance, if there's enough interest.
User avatar
CHangohh BOyy
 
Posts: 3462
Joined: Mon Aug 20, 2007 12:12 pm

Post » Mon Dec 28, 2009 9:37 am

More info, so it's not really a "bump", is it?? :D

The COBL work has bumped me into refactoring code some more. So, next release of bash will be improved in a number of ways:
* Flags fields are handled better now -- you'll be able to define and access flags by name very easily thanks to a new Flags class.
* Run under either python 2.4 or 2.5.
* Better class names.
* And best of all, greater ease in adding support for more record types. I'm trying to do something like Enchanted Editor where you define class through a fairly simple definition, and all the basic read, write, mangle functions come automatically from that. Unlike EE, there isn't an external template file, but of course, python code itself is editable with fairly simple text editors, so it's pretty much the same thing.

To give an example, the MISC record type is defined as:
class MreMisc(MelRecord):	"""MISC (miscellaneous item) record."""	type = 'MISC' #--Used by LoadFactory	melSet = MelSet(		MelString('EDID','eid'),		MelString('FULL','full'),		MelString('MODL','model'),		MelBase('MODB','modb'),		MelBase('MODT','modt'),		MelString('ICON','icon'),		MelFormid('SCRI','script'),		MelStruct('DATA','if',('value','weight'),(0,0)),		)

That's 13 lines. The prefactor version is 50 lines. Granted MISC is a pretty simple structure, but most of the others should be similarly reducible.

You may notice that this class actually doesn't have a single member function. Instead the work is done by the parent MelRecord class working in combination with the class constant "melSet" and its MelXXX element subrecords.

To decode a little... "MelSet" is an ordered set of Morrowind Record Elements. The elements of the set define the type of record, what object variable it maps to, and what the corresponding file subrecords are. Strings are for string subrecords, Base is for raw (usually unknown subrecords). The pick the complicated case the last one is a structure in which the 'DATA' subrecord is read as an intger and float ('if') which map respectively to instance members value and weight, which have respectively default values of 0 and 0.
User avatar
Sharra Llenos
 
Posts: 3399
Joined: Wed Jan 17, 2007 1:09 pm

Post » Mon Dec 28, 2009 10:23 pm

New Version 0.41. Major Refactoring, Support for Many New Types
After a heck of a lot of work and testing, I've got the new version out. For the general user, it doesn't do much different then before, but the new refactoring of the code supports the relatively easy addition of most record types. (Note: Version bumped to 0.41 after fixing a Python 2.5 compatibility problem.)

With this release, the following classes provide definition for their corresponding types:
* MreActi: Activator
* MreAppa: Alchemical Apparatus
* MreBook: Book
* MreBsgn: Birthsign
* MreCont: Container
* MreFact: Faction
* MreGlob: Global
* MreGmst: GMST
* MreHair: Hair
* MreIngr: Ingredient
* MreLvlc: Leveled Creature List
* MreLvli: Leveled Item List
* MreLvsp: Leveled Spell List
* MreMisc: Miscellaneous Item
* MreKeym: Key
* MreNpc: NPC
* MreSlgm: Soulgem
* MreStat: Static

The other record types should be fairly easy to add since all types are now defined through structure elements (similar to Enchanted Editor templates, but much more flexible and structured). Note that though several types are not explained at UESP, I've found it quite simple to guess/test/model them using the bash's new structures/tools. If you're interested in seeing a particular type, just let me know and I'll take a shot when I have time.

NOTE: I don't yet have support for complex block types (cells, world, dialog). Dialogs probably wouldn't be too bad, but cell/world grouping is pretty complex and will take a bit of figuring.

To give you another taste, here's the full class definition for a book record:
class MreBook(MelRecord):	"""BOOK record."""	type = 'BOOK' 	flags = Flags(0,Flags.getNames('isScroll','isFixed'))	melSet = MelSet(		MelString('EDID','eid'),		MelString('FULL','full'),		MelModel(),		MelString('ICON','icon'),		MelString('DESC','text'),		MelFormid('SCRI','script'),		MelFormid('ENAM','enchantment'),		MelStruct('ANAM','H','enchantPoints'),		MelStruct('DATA', '=BBif',(flags,'flags',0L),('teaches',0xFF),'value','weight'),		)

To clarify a bit: MelModel defines a group (essentially a subobject) which maps to attribute 'model' and which has attributes 'path', 'modb' and 'modt'. The MelStruct at the end is unpacked into two bytes, and int and a float (the '=' is for bit alignment), the first btye is interpreted as a flags object instance (these some trickiness here, which I won't bore you with), and which has a default value of 0L (long integer). The second byte maps to 'teaches', and is given a default value of 0xFF (teaches nothing). The int and the float map to value and weight -- which have no special handling, and have defaults of zero, so I don't need to specify them.

And here's some code where I use that. Note how object fields match to definition above. (This function is used by Bash's merge data list to build the update versions of the alchemical catalogs. It's actually defined within a member function -- that's where the "self" comes from. ("self" is Python's version of "this".))
def getBook(objectId,eid,full,value,icon,modelPath,modb):	#--New book and set some management fields for it.	book = MreBook(('BOOK',0,0,0,0))	book.longFormIds = True	book.changed = True	#--Book 	book.eid = eid	book.full = full	book.value = http://forums.bethsoft.com/index.php?/topic/612047-wrye-bash-programming/value	book.weight = 0.2	book.formid = (Path('Cobl Main.esm'),objectId) #--"Long" formid. Converted to hex later in code.	book.text = _("Salan's Catalog of %s\r\n\r\n") % (full,) #--More text added later in code.	book.icon = icon	book.model = book.getDefault('model')	book.model.path = modelPath	book.model.modb = modb	book.modb = book	self.BOOK.setRecord(book.formid,book) #--Save book record in BOOK type data block.	return book

So, that's it for now, if you were waiting until after the refactoring, it's done!
User avatar
Joie Perez
 
Posts: 3410
Joined: Fri Sep 15, 2006 3:25 pm

Post » Mon Dec 28, 2009 4:15 pm

A faction relationship merger would be a good thing, though I don't think I could make one.

I see you have a class for hair, there could be one for eyes too. Then it would be possible to just make a simple code that would add all hair styles and all eyes to all races, allowing unwanted combinations but putting an end to the chore of adapting things like cosmetic compilation to a half-dozen new races...
User avatar
Lyndsey Bird
 
Posts: 3539
Joined: Sun Oct 22, 2006 2:57 am

Post » Mon Dec 28, 2009 1:36 pm

A faction relationship merger would be a good thing, though I don't think I could make one.

This would actually be very simple to do and could be included in the Bash merge process. I still need to check back on Martigen's objections to it (I still haven't had time to review the COBL factions topic), but implementation is pretty simple.

I see you have a class for hair, there could be one for eyes too. Then it would be possible to just make a simple code that would add all hair styles and all eyes to all races, allowing unwanted combinations but putting an end to the chore of adapting things like cosmetic compilation to a half-dozen new races...

Well I added MreEyes (pretty trivial) but that's actually not relevant. I need an MreRace class to do cosmetics merging. I started anolyzing the RACE record last night, but it's fairly complicated and it seems Bethesda programmers found yet another way to encode data.

In general, I have been thinking about more complicated merging. Not as vanilla as TesTool's object merging for Morrowind, but maybe something more directed. No promises on any of thes, but potential merging might include:
* NPC levels (Bash already has this in one form, of course)
* Face data (for the that mod that tweaks lots of faces to add more character)
* Names? (already done to some degree, but something to keep in mind)

Note that there's a limit to what can be automatically merged. For example you can merge new weapons into the leveled lists so that they show up along OOO changes. But OOO changes the nature of weapons too (weight/speed balances, etc.), so if you're really doing a OOO merge, you need to tweak those as well, and that pretty much has to be done manually. Of course, if you could define an algorithm, a programmatic tweak might be possible.

Started a new topic for this: http://www.elderscrolls.com/forums/index.php?showtopic=622764 (oops, typo -- was supposd to be "Bashed RaceS". Ah well...
User avatar
Emma
 
Posts: 3287
Joined: Mon Aug 28, 2006 12:51 am

Post » Mon Dec 28, 2009 11:50 am

As of version 0.42 (just released) Bash has support for even more record types (including RACE, which was true pain in the butt to do). The inclusion of RACE means that merging in eyes and hair would be pretty trivial with a command line script.
User avatar
Tinkerbells
 
Posts: 3432
Joined: Sat Jun 24, 2006 10:22 pm

Post » Mon Dec 28, 2009 4:40 pm

The inclusion of RACE means that merging in eyes and hair would be pretty trivial with a command line script.

That's great news!
User avatar
OnlyDumazzapplyhere
 
Posts: 3445
Joined: Wed Jan 24, 2007 12:43 am

Post » Mon Dec 28, 2009 8:50 am

That's great news!

Continued on http://www.elderscrolls.com/forums/index.php?s=&showtopic=622764 topic.
User avatar
Kat Stewart
 
Posts: 3355
Joined: Sun Feb 04, 2007 12:30 am

Post » Mon Dec 28, 2009 10:43 am

A few people (Gez, Abot) have been extending bash a little bit on their own by editing the files as released. This actually wasn't necessary, you can extend the application quite easily without modifying any of them. However, with current release (0.44), I've made that just a bit easier by tweaking menu creation slighly.

So here's an example in which a "Hello World" message is added under the "Version 0.8" menu item for mod list items...

First thing to do is duplicate bash.py to something like "mybash.py". There's very little code it in and it's very rarely updated, so you'll rarely have to adapt it to newer bash releases. Then in mybash.py, you add a new menu item definition, and then you throw in a little code to add the new menu item in under one of the existing menu items. Like so...
# Imports ---------------------------------------------------------------------import getoptimport osimport sysif sys.version[:3] == '2.4':	import wxversion	wxversion.select("2.5.3.1")import bosh, basher#------------------------------------------------------------------------------from basher import _, wxclass HelloWorld(basher.Link):	"""Hello World link."""	def __init__(self,menuText,text):		basher.Link.__init__(self)		self.menuText = menuText		self.text = text	def AppendToMenu(self,menu,window,data):		basher.Link.AppendToMenu(self,menu,window,data)		menu.AppendItem(wx.MenuItem(menu,self.id,self.menuText))	def Do(self,event):		basher.Message(self.window,self.text)# Main ------------------------------------------------------------------------if __name__ == '__main__':	#--Parse arguments	optlist,args = getopt.getopt(sys.argv[1:],'u:')	optdict = dict(optlist)	if '-u' in optdict:		drive,path = os.path.splitdrive(optdict['-u'])		os.environ['HOMEDRIVE'] = drive		os.environ['HOMEPATH'] = path	#--Initialize Directories	#os.environ['HOMEPATH'] = r'\Documents and Settings\Wrye' #--In case of registry problems.	bosh.initDirs()	#--More Initialization	basher.InitSettings()	basher.InitLinks()	basher.InitImages()	#--My stuff	point = basher.modsItemMenu.getClassPoint(basher.Mod_SetVersion)	point.append(HelloWorld('Hello World','Hello Wonderful World!'))	#--Start application	if args and args[0] == '0':		app = basher.BashApp(0)	else:		app = basher.BashApp()	app.MainLoop()

The new parts here are the HelloWold class, and the two lines under "My Stuff" in the main function.

What's been tweaked in 0.44 is:
* menus are now more easily accessible (e.g. basher.modsItemMenu)
* You can use the getClassPoint function to get a point in the current menu, by finding a class in it (most menu items are unique classes). You can then do things like:
point.remove()
point.replace(newItem)
point.append(newItem)

If you want to do something complicated, generally you'll want to do it as a command line function first, and then, if you want, convert that to a menu command. Understanding menu commands is a little complicated, but usually you can do pretty well by following an existing menu item's code.

When testing your new command, you'll almost certainly need to be ready to run from command line (e.g., "mbash 0") for debugging purposes.
User avatar
Fluffer
 
Posts: 3489
Joined: Thu Jul 05, 2007 6:29 am

Post » Mon Dec 28, 2009 7:46 pm

Interesting. Well, all I want to do is adding new launch buttons. So, for instance, basher.py contains the following code:

# App Links -------------------------------------------------------------------#------------------------------------------------------------------------------class App_Oblivion(Link):	"""Launch Oblivion."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['oblivion'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Launch Oblivion")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		cwd = os.getcwd()		os.chdir(bosh.dirs['app'])		os.spawnl(os.P_NOWAIT,os.path.join(bosh.dirs['app'],'Oblivion.exe'))		os.chdir(cwd)#------------------------------------------------------------------------------class App_ObMM(Link):	"""Launch Oblivion Mod Manager."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['obmm'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Launch Oblivion Mod Manager")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		cwd = os.getcwd()		os.chdir(bosh.dirs['app'])		os.spawnl(os.P_NOWAIT,os.path.join(bosh.dirs['app'],'OblivionModManager.exe'))		os.chdir(cwd)#------------------------------------------------------------------------------class App_TESCS(Link):	"""Launch TES Construction Set."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['tescs'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Launch TES Construction Set")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		cwd = os.getcwd()		os.chdir(bosh.dirs['app'])		os.spawnl(os.P_NOWAIT,os.path.join(bosh.dirs['app'],'TESConstructionSet.exe'))		os.chdir(cwd)#------------------------------------------------------------------------------class App_Help(Link):	"""Show help browser."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['help'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Help File")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		"""Handle menu selection."""		if not helpBrowser: 			HelpBrowser().Show()			settings['bash.help.show'] = True		helpBrowser.Raise()# Initialization --------------------------------------------------------------def InitSettings():	bosh.initSettings()	global settings	settings = bosh.settings	settings.loadDefaults(settingDefaults)def InitImages():	#--Standard	images['save.on'] = Image(r'images\save_on.png',wx.BITMAP_TYPE_PNG)	images['save.off'] = Image(r'images\save_off.png',wx.BITMAP_TYPE_PNG)	#--Misc	images['oblivion'] = Image(r'images\oblivion.png',wx.BITMAP_TYPE_PNG)	images['obmm'] = Image(r'images\obmm.png',wx.BITMAP_TYPE_PNG)	images['tescs'] = Image(r'images\tescs.png',wx.BITMAP_TYPE_PNG)	images['help'] = Image(r'images\help.png',wx.BITMAP_TYPE_PNG)	#--Tools	images['doc.on'] = Image(r'images\doc_on.png',wx.BITMAP_TYPE_PNG)	#--Checkboxes	images['bash.checkboxes'] = Checkboxes()	images['checkbox.green.on.32'] = (		Image(r'images\checkbox_green_on_32.png',wx.BITMAP_TYPE_PNG))	images['checkbox.blue.on.32'] = (		Image(r'images\checkbox_blue_on_32.png',wx.BITMAP_TYPE_PNG))	#--Bash	images['bash.16'] = Image(r'images\bash_16.png',wx.BITMAP_TYPE_PNG)	images['bash.32'] = Image(r'images\bash_32.png',wx.BITMAP_TYPE_PNG)	images['bash.16.blue'] = Image(r'images\bash_16_blue.png',wx.BITMAP_TYPE_PNG)	images['bash.32.blue'] = Image(r'images\bash_32_blue.png',wx.BITMAP_TYPE_PNG)	#--Applications Icons	wryeBashIcons = ImageBundle()	wryeBashIcons.Add(images['bash.16'])	wryeBashIcons.Add(images['bash.32'])	images['bash.icons'] = wryeBashIcons	#--Application Subwindow Icons	wryeBashIcons2 = ImageBundle()	wryeBashIcons2.Add(images['bash.16.blue'])	wryeBashIcons2.Add(images['bash.32.blue'])	images['bash.icons2'] = wryeBashIcons2def InitLinks():	#--Bash Status/LinkBar	BashStatusBar.links.append(App_Oblivion())	BashStatusBar.links.append(App_ObMM())	BashStatusBar.links.append(App_TESCS())	BashStatusBar.links.append(App_Help())


I'm not sure how I'd achieve the same result by editing bash instead.

I'm not a python monkey, so I'm just coding by copy/pasting stuff and modifying thingies. Just the most obvious stuff. I'm not sure if it's really simpler to use points and indirections.
User avatar
Katie Samuel
 
Posts: 3384
Joined: Tue Oct 10, 2006 5:20 am

Post » Mon Dec 28, 2009 8:19 am

Here's how you would do it. So, create your gezbash.py and use this for its code:
# Imports ---------------------------------------------------------------------import getoptimport osimport sysif sys.version[:3] == '2.4':	import wxversion	wxversion.select("2.5.3.1")import bosh, basher# Gez -------------------------------------------------------------------------from basher import *#------------------------------------------------------------------------------class App_ObMM(Link):	"""Launch Oblivion Mod Manager."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['obmm'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Launch Oblivion Mod Manager")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		cwd = os.getcwd()		os.chdir(bosh.dirs['app'])		os.spawnl(os.P_NOWAIT,os.path.join(bosh.dirs['app'],'OblivionModManager.exe'))		os.chdir(cwd)#------------------------------------------------------------------------------class App_TESCS(Link):	"""Launch TES Construction Set."""	def GetBitmapButton(self,window,style=0):		if not self.id: self.id = wx.NewId()		button = wx.BitmapButton(window,self.id,images['tescs'].GetBitmap(),style=style)		button.SetToolTip(wx.ToolTip(_("Launch TES Construction Set")))		wx.EVT_BUTTON(button,self.id,self.Do)		return button	def Do(self,event):		cwd = os.getcwd()		os.chdir(bosh.dirs['app'])		os.spawnl(os.P_NOWAIT,os.path.join(bosh.dirs['app'],'TESConstructionSet.exe'))		os.chdir(cwd)# Initialization --------------------------------------------------------------def InitGez():	#--Images	images['obmm'] = Image(r'images\obmm.png',wx.BITMAP_TYPE_PNG)	images['tescs'] = Image(r'images\tescs.png',wx.BITMAP_TYPE_PNG)	#--Bash Status/LinkBar	point = bashStatusBar.getClassPoint(App_Oblivion)	point.append(App_ObMM())	point.append(App_TESCS())# Main ------------------------------------------------------------------------if __name__ == '__main__':	#--Parse arguments	optlist,args = getopt.getopt(sys.argv[1:],'u:')	optdict = dict(optlist)	if '-u' in optdict:		drive,path = os.path.splitdrive(optdict['-u'])		os.environ['HOMEDRIVE'] = drive		os.environ['HOMEPATH'] = path	#--Initialize Directories	#os.environ['HOMEPATH'] = r'\Documents and Settings\Wrye' #--In case of registry problems.	bosh.initDirs()	#--More Initialization	basher.InitSettings()	basher.InitLinks()	basher.InitImages()	#--My stuff	InitGez()	#--Start application	if args and args[0] == '0':		app = basher.BashApp(0)	else:		app = basher.BashApp()	app.MainLoop()

I haven't tested this, so there might be a bug, but it's pretty simple. Essentially the only changes
1) Add the gez section with your two new button classes and the InitGez function.
2) Edit the main bash loop to run InitGez() just after running the regular bash init code.

If you were making a lot of additions, you might want to isolate your own code into a separate file. In that case, just take the whole Gez section and stick in in your own code file (gezzer.py, I guess). Then the only change you need to make to bash.py is add:
import gezzer 

with the rest of the imports. And then do the initialization with
gezzer.InitGez()

in the same place as above.
User avatar
Marquis deVille
 
Posts: 3409
Joined: Thu Jul 26, 2007 8:24 am

Post » Mon Dec 28, 2009 7:07 am

* menus are now more easily accessible (e.g. basher.modsItemMenu)
* You can use the getClassPoint function to get a point in the current menu, by finding a class in it (most menu items are unique classes). You can then do things like:
point.remove()
point.replace(newItem)
point.append(newItem)


Awesome! This looks very useful. Need to start playing with this stuff.
User avatar
Jade Payton
 
Posts: 3417
Joined: Mon Sep 11, 2006 1:01 pm

Post » Mon Dec 28, 2009 9:54 pm

There are two Batch functions I really need for "Lore Dialogue 300" the first one is a double-clickable Windows bats file that will copy a silent.mp3 and create all of the folders used by the .esp so that all of the Dialogue will stay on screen long enough to be read.

Here is an example of what I mean, but this only includes very little:
COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CE9_2.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CE9_3.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CE9_4.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEA_1.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEA_2.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEA_3.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEA_4.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEB_1.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEB_2.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEB_3.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CEC_1.mp3"COPY "Silence.mp3" ".\Sound\Voice\300_Lore Dialogue.esp\Argonian\F\LoreDialogueQuest_LoreBackground_00000CED_1.mp3"


I also need a way to batch-rename Dialogue Topics. When they are made the Dialogue Topic's Name is automaitically its FormID which looks very "yucky" in-game. So for the Background Topic, it will show up as LoreBackground, in-game. I jiust need something that can search+replace the Dialogue Topics to remoave the wrod "Lore" form the beginning of Each.
User avatar
N Only WhiTe girl
 
Posts: 3353
Joined: Mon Oct 30, 2006 2:30 pm

Post » Mon Dec 28, 2009 4:44 pm

I also need a way to batch-rename Dialogue Topics. When they are made the Dialogue Topic's Name is automaitically its FormID which looks very "yucky" in-game. So for the Background Topic, it will show up as LoreBackground, in-game. I jiust need something that can search+replace the Dialogue Topics to remoave the wrod "Lore" form the beginning of Each.


Did you already find a way around this problem?
User avatar
Noely Ulloa
 
Posts: 3596
Joined: Tue Jul 04, 2006 1:33 am

Post » Mon Dec 28, 2009 1:59 pm

Did you already find a way around this problem?


No, the only thing I could do is manually change the name of each topic one by one. Which I did for the race topics.
User avatar
Alexandra Ryan
 
Posts: 3438
Joined: Mon Jul 31, 2006 9:01 am

Next

Return to IV - Oblivion