Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patcher modules layout #124

Closed
Utumno opened this issue Jul 28, 2014 · 31 comments
Closed

Patcher modules layout #124

Utumno opened this issue Jul 28, 2014 · 31 comments
Assignees
Labels
A-patchers Area: Patchers (Everything in the patcher package) C-goal Category: Long term goal. May be code-related or a meta goal.
Milestone

Comments

@Utumno
Copy link
Member

Utumno commented Jul 28, 2014

@lojack5 said:

bash\
    game\
        skyrim\
            __init__.py  # brings in all the files
            patcher.py  # defines/modifies the base patchers
            records.py  # defines the records used by skyrim
            ...etc...
    patcher\
        __init__.py # brings in all the files
        ...etc...

Since I am about to actually move the patchers out I want us to discuss the layout in detail - that is in class level. Chime in!

Edit: I mean to clarify, among others:

  • how much is game independent in the patchers - in other words what is the patchers API
  • which classes need to go with them (I already ripped the utilitties classes - but had to import them in basher too)
  • how are the pacthers themselves going to be split in files. Of course not a file per class a la Java (yak) but are we going to split them in correspondence to the UI ? To the scan orders (notice patchers 10,20,30,40 in bosh) ?

This is of course under #3

@Utumno Utumno added this to the 306 milestone Jul 28, 2014
@Utumno Utumno added the TODO label Jul 28, 2014
@Utumno Utumno self-assigned this Jul 28, 2014
@Utumno Utumno changed the title Patcher modules lauyout Patcher modules layout Jul 28, 2014
@Utumno
Copy link
Member Author

Utumno commented Jul 30, 2014

Discuss in code - last 3 commits ([layout]):

https://github.com/wrye-bash/wrye-bash/commits/utumno-refactoring-master

Questions:

  • utilities.py ? Better name. Better docs in __init__.py - could use your help
  • should this be oblivion/patcher/utilities.py, oblivion/patcher/patchers/XXXPatcher.py ? I think that these "utilities" are not for the patcher only
  • Am I right to think that the classes I put in the bash/patcher/base.py module are game independent and actually comprise the patcher API ? What other classes should go in there ?
  • the RecordsGroups.py are indeed oblivion specific ? Should we extract an API ? What should the module name be (lowercase) ?

@Sharlikran
Copy link
Contributor

I looked at the utilities.py and RecordGroups.py and I don't think they are Oblivion specific. Those would probably have to be updated for Skyrim and FO3/FNV at some point.

I don't know what to call the file either. They are basically helpers for the records like CELL, WRLD, DIAL. I'm not sure all of them are callback routines either nor would you want to be that strict. They are not really common routines and even then, common routines like MreHasEffects will be different for each game mode.

I say leave it the name it is and just specify in a comment at the top what the file is for.

Utumno referenced this issue Jul 30, 2014
Otherwise, when building a patch twice without restarting, races_data
finds itself with fids in short format from the previous build(s),
when fids are expected to be in long format, which causes Bash to choke.
@Sharlikran
Copy link
Contributor

I might be able to shed some light on the utilities.py file but it won't really answer your question. While working on Wrye Mash I discovered there is a "Utilities" tab where things like Mlox show up. Which was later moved to the bottom bar where the icons show up now for BOSS and such.

Those could be moved to another file specific for the game mode so that they can be updated as needed to suit the needs of that game and how Wrye reads and writes to the Bash Patch to fit that game's plugin format.

While you are doing this please look for the byte definition for the header and record sizes.

FO3/FNV/TES4/TES5 : [4 byte Signature] [ 2 byte size ]

ESCE [ 10 00 ] - 2 Bytes for the size

Morrowind : [4 byte Signature] [ 4 byte size ]

ESCE [ 10 00 00 00 ] - 4 Bytes for the size

Please refactor the record and header size so that it becomes game specific.

@Sharlikran
Copy link
Contributor

@lojack5 in regards to my previous request, would class RecordHeader(brec.BaseRecordHeader): already handle that if changed correctly? Or would that be handled in ModReader?

@lojack5
Copy link
Member

lojack5 commented Aug 27, 2014

No, you'd have to update the subrecord unpacking code. RecordHeader only handles the record headers. Relevant section here - you'd want to generalize it so it can be derived and implemented per-game.

Utumno referenced this issue Sep 2, 2014
Last one:

Error! Unable to start Wrye Bash.

Please ensure Wrye Bash is correctly installed.

Traceback (most recent call last):
  File "bash\bash.py", line 454, in main
    import bosh
  File "Wrye Bash Launcher.pyw", line 63, in load_module
    mod = imp.load_source(fullname,filename+ext,fp)
  File "bash\bosh.py", line 69, in <module>
    from game.oblivion.RecordGroups import MobWorlds, MobDials, MobICells, \
ImportError: No module named RecordGroups

Well turns out that game/oblivion.py and game/oblivion was confusing python
which spit out the quite unhelpful "No module named RecordGroups"

Moving oblivion.py to game/oblivion/ completely ruins the game detection
algorithm though
@Utumno
Copy link
Member Author

Utumno commented Sep 2, 2014

I resumed work on this finally on my https://github.com/wrye-bash/wrye-bash/compare/utumno-refactoring-master branch - need feedback - see there

Among others - shouldn't we move the record classes out of skyrim/oblivion.py ?

Another idea - a new module bash/records with submodules for each game and base record classes on top level - I actually like this - what you say ?

@Sharlikran
Copy link
Contributor

I am fine with whatever you feel is appropriate.

        for recClass in (MreRecord.type_class[x] for x in ('NPC_',)):
            self.recAttrs_class[recClass] = {
                u'Actors.AIData': ('aggression','confidence','energyLevel','responsibility','services','trainSkill','trainLevel'),

One thing I am going to do in the next few days on a test branch I have is move the patcher routines to skyrim.py, oblivion.py, fallout3.py, falloutnv.py. This way I can make side by side comparasons of the constants used for the patchers as shown in the above code. It eliminates the need for 4 if/fsName statements by just saying self.recAttrs_class[recClass] = bosh.game.thePatcherSetName. It would be one decleration and connect to the set for each game mode.

So if you want to move those to another file that's fine also. Whatever you do I'll just follow suit.

Utumno referenced this issue Sep 2, 2014
@Utumno
Copy link
Member Author

Utumno commented Sep 2, 2014

Stay tuned - I already started the rip - I disagree with patchers routines in gamename.py files - we aim at 2000-3000 lines at most per file (excluding files with constants) - not 20.000.

See the commits at my branch - started moving the patcher routines out of bosh and into patcher/oblivion/

Similar structure for other games

But I also believe we should go for records/gamename modules

@lojack5
Copy link
Member

lojack5 commented Sep 2, 2014

My personal opinion on how to divvy up the game specific code:

bush/game/<game_name> has every bit of specific code for games (further divided into specialized sub-packages). Then if you want to split up stuff into bash/records, have the __init__.py part of that module pull in the applicable information form bush/game/<game_name>.

Reasoning: makes maintenance of the files easier, makes it easier to understand the code for those unfamiliar with it, makes it easier to add new games. Because all the game information is in one place, rather than piece-meal scattered around.

Obviously not the only way to do it, but my personal choice. And we don't have to have everything in oblivion.py as is the case now, for example, just change to somehting similar to this;

bash\
    game\
        oblivion\
            __init__.py
            records.py
            constants.py
            ...etc...

@Sharlikran
Copy link
Contributor

bash\
    game\
        oblivion\
            __init__.py
            records.py
            constants.py
            patcher.py
            oblivion.py
            ...etc...

I like Lojack's take on the structure. The constants.py could include the code I mentioned earlier. Is that what you already had in mind Utumno, maybe something like that?

I also like his take on what __init__.py should have so there doesn't have to be declarations to things like from patcher.oblivion.utilities import ActorLevels, CBash_ActorLevels. The code would know where to pull the information because of the game mode, or fsName or whatever you chose to use.

@Utumno
Copy link
Member Author

Utumno commented Sep 2, 2014

This is not really about the games - it is mainly about the patchers
Have a look at my branch - I need to know what is game independent in there:

  • in the patchers (AListPatcher should be game independent but ListPatcher (as it is now) is not)
  • in the records (MelBlah)
  • in the "RecordGroups" (whatever this is) - see RecordGroups.py

The most important thing is to extract a patcher API - that is Abstract Base Classes.
Please have a look at patcher/base.py - is it anywhere near the mark ?

NB: the way it is now is because I do not want to mess with bush.GetGame() (or whatever)
I am certainly open to Lo's suggestion - but even then the details must be specified - in class level.

Another option I consider is

bash/
  patcher/
    base.py
    oblivion/
    skytim/
    ...
  records/
    base.py
    oblivion/
    skytim/
  game/
    game.template
    oblivion.py
    skytim.py

But I most importantly want some feedback on the patchers API (game refactoring is another issue in this tracker)

@Sharlikran
Copy link
Contributor

If Lojack likes the above option, I like that format too, I approve of this messege. 👍

@Arthmoor
Copy link
Contributor

Arthmoor commented Sep 3, 2014

As an outsider looking in, I'd prefer Lojack's structure simply because I view things based on each supported game and it seems less disorganized that way.

@Sharlikran
Copy link
Contributor

From a programming standpoint for someone unfamiliar with the code Utumno's idea has merit. I actually like that idea because then I know where the patcher routines are going to be and I don't have to guess. Either way works for me I don't care as long as the patcher API is factored out. Even though this is more about the patchers and less about the games themselves, without the changes it will be hard to continue support for future games such as FO4/TES6 if they ever happen.

@lojack5
Copy link
Member

lojack5 commented Sep 3, 2014

@Utumno: I undestand you're specifically referring to the patchers part of refactoring at the moment, but it is still game related (otherwise it wouldn't need to be refactored). What I'm saying my opinion is, is the game specific code should all be in one module. The patchers should import from there.

So

bash\
    game\
        oblivion\
            __init__.py
            records.py
            patcher.py
            ...etc...
        skyrim\ 
            __init__.py
            records.py
            patcher.py
            ...etc...
patcher\
    __init__.py
    base.py
    ...etc...

And what happens is, all the code is in one location for each game, easier to find, easier to implement a new game mode. Then, the patchers module will import the applicable sections from the currently loaded game module.

So instead of using say:
bash.patcher.oblivion.<insert function here>
You just do:
bash.patcher.<insert function here>
Because the patcher module on import already imports from the correct game module. Another good side effect of this is that we never have to explicitly import from a module with a game mode name in it. For example, say each game mode has a slightly different implementation of the RacePatcher. You would just do bash.patcher.RacePatcher, because bash.patcher has already pulled in the correct one via doing a from ..game.patcher import RacePatcher. And the bush.game module has already been set up at load time to be set to the current game mode.

The biggest reason for doing it all this way: the game switching/detection only has to happen once. bush.game will always be the current game mode, and that's set up in bush at startup. Splitting game specific files out of bush.game means we now have to setup up other modules in a similar manner so they import the correct versions depending on the game mode, ie: we'd need code like this:

if bush.game.name == 'Oblivion':
    import patcher.oblivion as patcher
elif bush.game.name == 'Skyrim':
    import patcher.skyrim as patcher
elif bush.game.name == 'Fallout 3':
    import patcher.fallout3 as patcher
# etc

Keeping all game specific files in the bush.game package negates the need for that.

@Sharlikran
Copy link
Contributor

With the changes from #144 will the above code work? Because bush.game.fsName replaced bush.game.name.

if bush.game.fsName == u'Oblivion':
    import patcher.oblivion as patcher
elif bush.game.fsName == u'Skyrim':
    import patcher.skyrim as patcher
elif bush.game.fsName == u'Fallout3':
    import patcher.fallout3 as patcher
elif bush.game.fsName == u'FalloutNV':
    import patcher.falloutnv as patcher
else:
    raise BoltError( [insert error message here] )

Would you use that instead? Bacause of __init__.py will fsName be available that early in initialization?

@lojack5
Copy link
Member

lojack5 commented Sep 3, 2014

That's my point though, by splitting up the way I propose, you don't need that code at all. You just need one simple line in the patcher init file

bash\patcher\__init__.py:

from ..game import patcher

file referencing oblivion.patcher, skyrim.patcher, etc:

import patcher  #automatically is oblivion.patcher, skyrim.patcher, etc as determined by the game mode

You can customize the exact logical layout of where stuff gets imported exactly, this is just a very simple example.

#144 doesn't really have much to do with this issue. That's just adding an additional variable inside the bush.game module. Ideally though, rather than doing comparisons on games, we should be adding a boolean variable, so wherever you have code like:

if bush.game.fsname == 'Oblivion':
    # do something
elif bush.game.fsname == 'Skyrim':
   # do something else
else:
   # do something else

Say for example, those are for adding a specific menu item, instead of doing bush.game.fsname comparisons, we should add a new variable bush.game.<menuItemBoolean> to each game file, or maybe an int instead if it's more than one option, then the code becomes:

if bush.game.<menuItemInt> == 1:
    # Do one thing
elif bush.game.<menuItemInt> == 2:
    # Do something else
else:
    # Do another something

The idea behind this is, the main code of Bash doesn't need to know which game it's managing. It just needs to know certain properties. It makes it more generic, and simpler to add new games. In the above example, say TES6 comes out, you'd have to find every place that has those compound if statements and and in a check for bush.game.fsname == 'TES6Name', rather than simply making the variable the appropriate value in the game file.

@lojack5
Copy link
Member

lojack5 commented Sep 3, 2014

As a side note: this does require that bush is imported and the game set before any modules that import from it are imported. That, however is already taken care of in bash.py. The only modules imported before the game mode is set are:

  • bass - constants
  • barg - command line parsing
  • bolt - utility functions and classes, is and should be kept game generic

every other sub-module of bash are safe to use imports from bush.game in.

@Utumno
Copy link
Member Author

Utumno commented Sep 3, 2014

@lojack5: makes sense indeed - I will have to look into the bush game handling code - will ask here at #65 when I start looking into it.
We need to change things anyway as now the oblivion/skyrim/etc.py will become packages instead of modules (and the patcher will be a package also ofc - no huge files anymore !)

Till then I continue the ripping - and I would like you (when you have time) to have a look at my branch cause we need to discuss what the patchers API will be.

  • Is it really possible to have the same patchers for all games though (RacePatcher...) ?
  • We need some base classes (have a look at patcher\base.py)

EDIT: I just saw your second post - will have a thorough look and post back

@Utumno Utumno mentioned this issue Sep 3, 2014
5 tasks
Utumno added a commit that referenced this issue Sep 19, 2014
- Layout is _temporary_ - now that bush can detect modules (`oblivion/` as
 opposed to `oblivion.py`) the patcher/oblivion/* modules must be moved
 to game/ovlivion/* ones. Will do this in a final commit though as most of the
 work on this and later branches are based on this layout plus I need the import
 fixes that come later on. See fc37bf6 (here we
 follow solution 1) and issues 65 and 124 for a discussion.
- modInfos is None if I import it directly so I need to import bash.bosh.
 Performance concerns. Also does the same hold for bosh.dirs ?
- the patchers class hierarchy is still pretty much at stake (see #124). Nothing
 is final in patcher/base.py or in patcher/oblivion/patchers/base.py.
- also if reecord_groups.py belongs to oblivion/ is not clear to me
- note to self: http://stackoverflow.com/a/25699674/281545

Base classes moved from bosh.py to patcher/base.py:

_Abstract_Patcher
Patcher
CBash_Patcher
AListPatcher
AMultiTweaker
AAliasesPatcher [bosh need not import _Abstract_Patcher any more]
AMultiTweakItem: notice this is not an _Abstract_Patcher subclass !

File moves:

Mopy/bash/patcher/RecordGroups.py -> Mopy/bash/game/oblivion/record_groups.py
@Utumno
Copy link
Member Author

Utumno commented Sep 25, 2014

Two questions : record_groups.py and utilities.py - how game specific are those (apart from CBash) ?

record_groups is also used in bosh and utilities in basher - are they even patcher specific ?

@Sharlikran
Copy link
Contributor

Those should be in a common file because they are not patcher or game specific. As for how you should organize it no idea. However, Oblivion, Skyrim, FO3/FNV all use routines from both of those.

I am going to be re-installing windows so if I don't respond for a few days now you know why.

Utumno added a commit that referenced this issue Oct 9, 2014
- Layout is _temporary_ - since bush can detect packages (`oblivion/` as
 opposed to `oblivion.py`) the patcher/oblivion/* modules must be moved
 to game/ovlivion/* ones. Will do this in a final commit though as most of the
 work on this and later branches are based on this layout plus I need the import
 fixes that come later on. See fc37bf6 (here we
 follow solution 1) and issues #65 and #124 for a discussion.
- modInfos is None if I import it directly so I need to import bash.bosh.
 Performance concerns. Also does the same hold for bosh.dirs ?
- the patchers class hierarchy is still pretty much at stake (see #124). Nothing
 is final in patcher/base.py or in patcher/oblivion/patchers/base.py.
- also if record_groups.py belongs to oblivion/ is not clear to me
- note to self: http://stackoverflow.com/a/25699674/281545

Base classes moved from bosh.py to patcher/base.py:

_Abstract_Patcher
Patcher
CBash_Patcher
AListPatcher
AMultiTweaker
AAliasesPatcher [bosh need not import _Abstract_Patcher any more]
AMultiTweakItem: notice this is not an _Abstract_Patcher subclass !

File moves:

Mopy/bash/patcher/RecordGroups.py -> Mopy/bash/patcher/oblivion/record_groups.py
Utumno added a commit that referenced this issue Oct 9, 2014
Under #124

At some point I got (notice bush imported many times):

Wrye Bash starting
Using Wrye Bash Version 306
Python version: 2.7.8
wxPython version: 2.8.12.1 (msw-unicode)
input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
Searching for game to manage:
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  144 setGame: No preferred game specified.
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
testing UAC
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
fail bash\game\oblivion\patchers\special.py
fail bash\basher.py
Traceback (most recent call last):
  File "Wrye Bash Launcher.pyw", line 87, in <module>
    bash.main()
  File "bash\bash.py", line 484, in main
    raise e
bash.bolt.BoltError: Error creating required Wrye Bash directories.  Please check the settings for the following paths in your bash.ini, the drive does not exist:

[General] sOblivionMods
    F:\Oblivion Mods2

I had to rename the module to 'patcher'

    Wrye Bash starting
    Using Wrye Bash Version 306
    Python version: 2.7.8
    wxPython version: 2.8.12.1 (msw-unicode)
    input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
    Searching for game to manage:
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  144 setGame: No preferred game specified.
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
    testing UAC
    bosh.py 5542 __init__: Using libbsa API version: 2.0.0
    bosh.py 5548 __init__: Using libloadorder API version: 4.0.1
    bosh.py 5554 __init__: Using LOOT API version: 0.6.0
    C:\GAMES\TESIV\Oblivion
    basher.py  139 <module>: Comtypes is missing, features utilizing HTML will be disabled
    Traceback (most recent call last):
      File "Wrye Bash Launcher.pyw", line 87, in <module>
        bash.main()
      File "bash\bash.py", line 573, in main
        basher.InitSettings()
      File "bash\basher.py", line 17746, in InitSettings
        initPatchers()
      File "bash\basher.py", line 8129, in initPatchers
        globals()[x]() for x in bush.game.patchers
    TypeError: 'module' object is not iterable

----------------------------- Mopy/bash/basher.py -----------------------------
index c5d22e8..b85165b 100644
@@ -42,7 +42,7 @@
 import bush
 import bosh
 import bolt
-from game.oblivion.patchers.special import AlchemicalCatalogs, \
+from game.oblivion.patcher.special import AlchemicalCatalogs, \
     CBash_AlchemicalCatalogs
 import loot
 import barb

----------------- Mopy/bash/game/oblivion/patcher/__init__.py -----------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/__init__.py
rename to Mopy/bash/game/oblivion/patcher/__init__.py

------------------ Mopy/bash/game/oblivion/patcher/special.py ------------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/special.py
rename to Mopy/bash/game/oblivion/patcher/special.py
@Utumno
Copy link
Member Author

Utumno commented Oct 9, 2014

Well I am reinstalling myself in another country - gotta find a house (and a proper computer)
I kept working on the layout though - I think that the last big issue lies in basher.
In commit d2d6925 as you see I move one of the Cobl patchers to the game/oblivion/patcher/special.py - this results in game specific imports in basher - which is a nono. Actually those were always there - just made explicit by the refactoring

The solution is to dynamically create the basher patcher classes (using type())- but I would appreciate some help/advice on this by @lojack5.

I would rather close #124 and #3 together - #3 is ready but the patchers are not in their final place there.

@Utumno Utumno added the M-review Misc: Review from another developer is requested label Oct 9, 2014
@Utumno
Copy link
Member Author

Utumno commented Oct 25, 2014

Update: proposed solution for the basher game specific imports: 592b84f f73b37b 6f56335

Promoting this to a goal

EDIT: @lojack5 tried to PM you but your box is full

@Utumno Utumno added C-goal Category: Long term goal. May be code-related or a meta goal. and removed C-todo Category: TODO, specific item that needs to be accomplished in working towards a goal labels Oct 25, 2014
Utumno added a commit that referenced this issue Oct 26, 2014
Under #124

At some point I got (notice bush imported many times):

Wrye Bash starting
Using Wrye Bash Version 306
Python version: 2.7.8
wxPython version: 2.8.12.1 (msw-unicode)
input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
Searching for game to manage:
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  144 setGame: No preferred game specified.
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
testing UAC
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
fail bash\game\oblivion\patchers\special.py
fail bash\basher.py
Traceback (most recent call last):
  File "Wrye Bash Launcher.pyw", line 87, in <module>
    bash.main()
  File "bash\bash.py", line 484, in main
    raise e
bash.bolt.BoltError: Error creating required Wrye Bash directories.  Please check the settings for the following paths in your bash.ini, the drive does not exist:

[General] sOblivionMods
    F:\Oblivion Mods2

I had to rename the module to 'patcher'

    Wrye Bash starting
    Using Wrye Bash Version 306
    Python version: 2.7.8
    wxPython version: 2.8.12.1 (msw-unicode)
    input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
    Searching for game to manage:
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  144 setGame: No preferred game specified.
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
    testing UAC
    bosh.py 5542 __init__: Using libbsa API version: 2.0.0
    bosh.py 5548 __init__: Using libloadorder API version: 4.0.1
    bosh.py 5554 __init__: Using LOOT API version: 0.6.0
    C:\GAMES\TESIV\Oblivion
    basher.py  139 <module>: Comtypes is missing, features utilizing HTML will be disabled
    Traceback (most recent call last):
      File "Wrye Bash Launcher.pyw", line 87, in <module>
        bash.main()
      File "bash\bash.py", line 573, in main
        basher.InitSettings()
      File "bash\basher.py", line 17746, in InitSettings
        initPatchers()
      File "bash\basher.py", line 8129, in initPatchers
        globals()[x]() for x in bush.game.patchers
    TypeError: 'module' object is not iterable

----------------------------- Mopy/bash/basher.py -----------------------------
index c5d22e8..b85165b 100644
@@ -42,7 +42,7 @@
 import bush
 import bosh
 import bolt
-from game.oblivion.patchers.special import AlchemicalCatalogs, \
+from game.oblivion.patcher.special import AlchemicalCatalogs, \
     CBash_AlchemicalCatalogs
 import loot
 import barb

----------------- Mopy/bash/game/oblivion/patcher/__init__.py -----------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/__init__.py
rename to Mopy/bash/game/oblivion/patcher/__init__.py

------------------ Mopy/bash/game/oblivion/patcher/special.py ------------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/special.py
rename to Mopy/bash/game/oblivion/patcher/special.py
Utumno added a commit that referenced this issue Oct 28, 2014
Under #124

At some point I got (notice bush imported many times):

Wrye Bash starting
Using Wrye Bash Version 306
Python version: 2.7.8
wxPython version: 2.8.12.1 (msw-unicode)
input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
Searching for game to manage:
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  144 setGame: No preferred game specified.
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
testing UAC
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
fail bash\game\oblivion\patchers\special.py
fail bash\basher.py
Traceback (most recent call last):
  File "Wrye Bash Launcher.pyw", line 87, in <module>
    bash.main()
  File "bash\bash.py", line 484, in main
    raise e
bash.bolt.BoltError: Error creating required Wrye Bash directories.  Please check the settings for the following paths in your bash.ini, the drive does not exist:

[General] sOblivionMods
    F:\Oblivion Mods2

I had to rename the module to 'patcher'

    Wrye Bash starting
    Using Wrye Bash Version 306
    Python version: 2.7.8
    wxPython version: 2.8.12.1 (msw-unicode)
    input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
    Searching for game to manage:
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  144 setGame: No preferred game specified.
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
    testing UAC
    bosh.py 5542 __init__: Using libbsa API version: 2.0.0
    bosh.py 5548 __init__: Using libloadorder API version: 4.0.1
    bosh.py 5554 __init__: Using LOOT API version: 0.6.0
    C:\GAMES\TESIV\Oblivion
    basher.py  139 <module>: Comtypes is missing, features utilizing HTML will be disabled
    Traceback (most recent call last):
      File "Wrye Bash Launcher.pyw", line 87, in <module>
        bash.main()
      File "bash\bash.py", line 573, in main
        basher.InitSettings()
      File "bash\basher.py", line 17746, in InitSettings
        initPatchers()
      File "bash\basher.py", line 8129, in initPatchers
        globals()[x]() for x in bush.game.patchers
    TypeError: 'module' object is not iterable

----------------------------- Mopy/bash/basher.py -----------------------------
index c5d22e8..b85165b 100644
@@ -42,7 +42,7 @@
 import bush
 import bosh
 import bolt
-from game.oblivion.patchers.special import AlchemicalCatalogs, \
+from game.oblivion.patcher.special import AlchemicalCatalogs, \
     CBash_AlchemicalCatalogs
 import loot
 import barb

----------------- Mopy/bash/game/oblivion/patcher/__init__.py -----------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/__init__.py
rename to Mopy/bash/game/oblivion/patcher/__init__.py

------------------ Mopy/bash/game/oblivion/patcher/special.py ------------------
similarity index 100%
rename from Mopy/bash/game/oblivion/patchers/special.py
rename to Mopy/bash/game/oblivion/patcher/special.py
Utumno added a commit that referenced this issue Oct 31, 2014
- utilities.py and record_groups.py:
      Mopy/bash/patcher/oblivion/utilities.py -> Mopy/bash/patcher/utilities.py
      Mopy/bash/patcher/oblivion/record_groups.py -> Mopy/bash/patcher/record_groups.py

 Those two may be moved further up - not patcher specific (see #124).

- Mopy/bash/patcher/oblivion/patchers/* -> Mopy/bash/patcher/patchers/*
  Final ?

modified:   Mopy/bash/basher.py
modified:   Mopy/bash/bish.py
modified:   Mopy/bash/bosh.py
renamed:    Mopy/bash/patcher/oblivion/patchers/__init__.py -> Mopy/bash/patcher/patchers/__init__.py
renamed:    Mopy/bash/patcher/oblivion/patchers/base.py -> Mopy/bash/patcher/patchers/base.py
renamed:    Mopy/bash/patcher/oblivion/patchers/importers.py -> Mopy/bash/patcher/patchers/importers.py
renamed:    Mopy/bash/patcher/oblivion/patchers/multitweak_actors.py -> Mopy/bash/patcher/patchers/multitweak_actors.py
renamed:    Mopy/bash/patcher/oblivion/patchers/multitweak_assorted.py -> Mopy/bash/patcher/patchers/multitweak_assorted.py
renamed:    Mopy/bash/patcher/oblivion/patchers/multitweak_clothes.py -> Mopy/bash/patcher/patchers/multitweak_clothes.py
renamed:    Mopy/bash/patcher/oblivion/patchers/multitweak_names.py -> Mopy/bash/patcher/patchers/multitweak_names.py
renamed:    Mopy/bash/patcher/oblivion/patchers/multitweak_settings.py -> Mopy/bash/patcher/patchers/multitweak_settings.py
renamed:    Mopy/bash/patcher/oblivion/patchers/races_multitweaks.py -> Mopy/bash/patcher/patchers/races_multitweaks.py
renamed:    Mopy/bash/patcher/oblivion/patchers/special.py -> Mopy/bash/patcher/patchers/special.py
renamed:    Mopy/bash/patcher/oblivion/record_groups.py -> Mopy/bash/patcher/record_groups.py
renamed:    Mopy/bash/patcher/oblivion/utilities.py -> Mopy/bash/patcher/utilities.py
Utumno added a commit that referenced this issue Oct 31, 2014
Under #124

I had to rename the module to 'patcher' - good name ?

    Wrye Bash starting
    Using Wrye Bash Version 306
    Python version: 2.7.8
    wxPython version: 2.8.12.1 (msw-unicode)
    input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
    Searching for game to manage:
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  144 setGame: No preferred game specified.
    bush.py   81 detectGames: Detected the following supported games via Windows Registry:
    bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
    bush.py   95 detectGames: Detecting games via relative path and the -o argument:
    bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
    testing UAC
    bosh.py 5542 __init__: Using libbsa API version: 2.0.0
    bosh.py 5548 __init__: Using libloadorder API version: 4.0.1
    bosh.py 5554 __init__: Using LOOT API version: 0.6.0
    C:\GAMES\TESIV\Oblivion
    basher.py  139 <module>: Comtypes is missing, features utilizing HTML will be disabled
    Traceback (most recent call last):
      File "Wrye Bash Launcher.pyw", line 87, in <module>
        bash.main()
      File "bash\bash.py", line 573, in main
        basher.InitSettings()
      File "bash\basher.py", line 17746, in InitSettings
        initPatchers()
      File "bash\basher.py", line 8129, in initPatchers
        globals()[x]() for x in bush.game.patchers
    TypeError: 'module' object is not iterable

--------------------------------------------------------------------------------
Notice: bush detection goes in cycles - at some point I got:

Wrye Bash starting
Using Wrye Bash Version 306
Python version: 2.7.8
wxPython version: 2.8.12.1 (msw-unicode)
input encoding: None; output encoding: None; locale: ('el_GR', 'cp1253')
Searching for game to manage:
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  144 setGame: No preferred game specified.
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  133 setGame: Specified game "oblivion" was found: C:\GAMES\TESIV\Oblivion
testing UAC
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
bush.py   81 detectGames: Detected the following supported games via Windows Registry:
bush.py   83 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py   95 detectGames: Detecting games via relative path and the -o argument:
bush.py  101 detectGames:  oblivion: C:\GAMES\TESIV\Oblivion
bush.py  144 setGame: No preferred game specified.
bush.py  151 setGame:  Using oblivion game: C:\GAMES\TESIV\Oblivion
fail bash\game\oblivion\patchers\special.py
fail bash\basher.py
Traceback (most recent call last):
  File "Wrye Bash Launcher.pyw", line 87, in <module>
    bash.main()
  File "bash\bash.py", line 484, in main
    raise e
bash.bolt.BoltError: Error creating required Wrye Bash directories.  Please check the settings for the following paths in your bash.ini, the drive does not exist:

[General] sOblivionMods
    F:\Oblivion Mods2
Utumno added a commit that referenced this issue Oct 31, 2014
This merge is an attempt to define the patcher layout as discussed in
#124. Also makes SpecialPatcher an object subclass and moves oblivion
specific patchers in a game.oblivion.patcher package. Review:

091bd59: layout commit
10ca05f: SpecialPatcher(object)
fd73d18: bush game init cycles
6f56335: dynamically create UI patchers
@Utumno
Copy link
Member Author

Utumno commented Dec 30, 2014

review e6b2e0e and links there

306:

  • Move utilities and rec groups out of patcher module
    • bash/patcher/utilities.py -> bash/parsers.py (not patcher specific)
    • bash/patcher/record_groups.py -> bash/record_groups.py (not patcher specific)
  • Move PatchFile, CBash_PatchFile outta bosh - hard - they use many bosh classes and globals (modInfos) - more importantly bosh is using them - this would lead to cyclic imports

307:

@Sharlikran: can I proceed with those ? Can you help me with rewriting the game.template?

Utumno added a commit that referenced this issue Jan 29, 2015
Under #3, #124.

utilities also renamed to parsers.py - they are certainly no mere
"utilities" now that I looked closer.
Note: import bosh in place of from . import bosh - seems ok at last.
@Utumno Utumno modified the milestones: 306, Code Quality Jun 11, 2015
@Utumno Utumno added the A-patchers Area: Patchers (Everything in the patcher package) label Sep 15, 2015
Utumno referenced this issue Oct 13, 2015
This closes #122

TODOs:

- decide on the final place for those (see #123)
- docs
- eliminate duplicated code (writeToText especially) - see #1
Utumno added a commit that referenced this issue Nov 7, 2015
This release features an almost complete refactoring of the codebase,
fixing many bugs and opening the way to extend Bash functionality.
Bash, being a community work, has over the years become an
assortment of hacks, patches and cruft and the program was just about to
become non-operable. Example issues - in no particular order (although
probably the worst was the horrible performance):

- deleting an item in the lists displayed by Bash ran different code
depending on whether the user hit delete or right clicked and chose
delete - most of those pieces of code were buggy
- start up took more than 15 seconds on large Oblivion Load Orders and
alt tabbing out and back into Bash would take 6-7 seconds - last one
should take no time in the usual case
- as seen from the (user visible) debug prints the game detection
algorithm run 3-5 times
- many, many broken things - including performance in general (most
simple operations would hang Bash for some seconds), display glitches
and general UI inconsistencies and internal data structures corruption
(see #176)

Those bugs reflected the state of the code - again in no particular
order:

- bosh.py (the data model) in 305 was 24635 lines (reduced from
30550 lines in 304.4 - see bosh section below)
- basher.py (the UI lists/menus etc) was 18936 lines
- the seven tabs that Bash featured were backed up by Tank (2) and List
(5) instances - so implementing a feature (say delete) needed edits to
at least two classes (usually 7)
- the wx library was intermingled with the basher code - the menu items
(Links) defined 215 AppendToMenu methods with nearly identical wx code
- readability suffered - most attributes/variables were named `data` or
`items`, globals hacks crippled the IDE static analysis, copy paste (aka
ravioli) code was everywhere.

When I started in it I had no idea if refactoring the code was even
possible, core Bash is 80k lines of code. It was clear that bosh needed
splitting, that candidate number one was the patchers code (well defined
and functional) and that basher should follow - but was not at all
clear if those were possible, let alone how exactly should the code be
structured and how much time would this take. Turns out that it was
possible: Bash was indeed coded with modularity in mind (by Wrye ?)
but later additions bloated the initial modules beyond recognition
and broke the (non enforced) encapsulation - however it wasn't too late.
Also turns out that refactoring needed 2 full work years of a single
engineer (learning python).

The huge performance boost that this release features is the measurable
effect of the increase in the code quality. In the items below I try to
describe in a quantitative manner what "code quality" means and how it
was increased. That's meant to be read from an app that links to the
commits - both github and gitk do but additionally github links to the
issues and files.


### bosh

This release features splitting the patchers' code out of bosh
to a dedicated package (whose final structure is still at stake). This
reduced bosh to 10516 lines while naturally exposing class
dependencies issues, hidden by the "one file program" nature of Bash.
The attempt to split bosh had already started in 305, involving:

- an effort to eliminate code duplication in C and P patchers using
multiple inheritance (chopped off around 1500 lines of pure copy paste
code) - see commits in 3c40989
- splitting out what was to become `record_groups.py` - see
c3e3543, but don't
imitate it - lots has changed since then, including import conventions
and commit style - for instance commits on dev that do not launch are
strictly forbidden.
- splitting out what was to become `parsers.py` - see commits in
08ab6e2

Final (`record_groups.py`, `parsers.py`):
6ae30fc

With those out of the way it was finally possible to rip the patchers
out of bosh - starting here:
983f259
That was not a mere copy paste rip - warnings were squashed, long lines
were wrapped, and most importantly a lot of tricky refactoring was
applied to eliminate code duplication - see for ex. commits in:
72fc8a0 for ripping
and commits in (note the negative line count):
5d21377,
fdab163,
78a85e0 for refactoring

See #3 for commit links (finally closed in
b6b743b) and #124 for the (still open)
package layout issue. Be aware that a lot of things changed since,
including import style and the overall package layout - see:
e6b2e0e

The remaining bosh.py code had some preliminary refactoring applied to
it (archives handling, BAIN error handling and refresh etc - see
6ddd4a8) but core bosh
refactoring will take place in 307. Major goals are splitting bosh into
a number of modules, getting rid of old style classes (DataDict
hierarchy, that is the data store singletons) and refactoring settings
to efficiently support profiles.


### .data

There are 544 occurrences of `.data` in 306 (1081 of `data`) as opposed
to 1071 in 305 (2221 `data`) - baring string literals and comments
(Pycharm only recently made these scopes available in IDE search, they
were much needed). Practically one had to run the debugger to see the
type of the object one had at hand, since the variable/attribute name
was always 'data':

- data is the wrapped dictionary of `DataDict`. Most accesses to this
dict were not via the wrappers the DataDict provides. This took
many commits to fix - ex
56198be. Before even
_thinking_ "performance" read the performance section below (no, none of
those modInfos.data.keys() calls was in the middle of a tight loop).
- data also was an attribute of the Link hierarchy which had _different
meaning based on an isinstance check_:
92e50cd. It could either mean a
DataDict subclass (so in Links code there were `self.data.data`
accesses) _or_ the list of selected items in the UI, leading to a
complete chaos - see the commit above up till the final removal in
ffae59e.
- UIList subclasses have a `data` attribute that points to the
DataDict singletons in bosh. That was dealt with throughout
coding 306 as it is closely related to a deeper issue namely the
intermingling of UI and model/business logic code. This
stems from the 'one file program' nature of Bash so solving this
properly means refactoring is at 100%.
- to somehow improve readability I introduced idata and pdata
attributes for the Installers and People data links -
eba53f9 - Those are transitory and
meant to also help in eliminating Tank.
The links of List subclasses were using self.window.data, whose uses
I strived to encapsulate - random example:
0934f1f
- all kind of different classes attributes were named `data`. Those
required dedicated commits (hey, I couldn't just do a search and replace
for "data") - ex. 9839c47. An IDE
really helps here.
- finally data local variables were renamed - ex.
8e77283

The war with data was going on in parallel with smaller scale wars with
`items` (e87dca7 and its parents),
`refresh` (4d872ab), etc.


### basher (#163)

This is the part of refactoring that I consider almost done - and it was
a big pleasure coding it. Since I first tried coding for Bash the
dichotomy between balt.Tank and basher.List clearly stood out as a major
issue, as did the omnipresence of the wx library and of course the sheer
size of basher.py (the UI module).
Basher became a package and then the UI API went through a complete
rewrite. Basher globals served as a guide in the splitting process,
and a metric of class dependencies. Eliminating those globals was an
ongoing effort throughout 306 coding - see for instance:
892a19c (screenlist, statusBar),
5852328 (gInstallers),
faea771 (modList)
till final elimination in bab7c00
Guest star: bosh.links (353be43)
Globals served as a link also between Bash's modules (so they evince
class and module coupling issues) - eventually encapsulated as static
fields of the balt.Link.Frame singleton - see
0690cf3
Link.Frame is an interesting binding of the UI (basher) and the rest of
bash that will serve as guide to a later refactoring phase. Especially
Link.Frame.modList use in bosh must be eliminated but this is related
to the RefreshUI/delete API finalization.
The items below detail the UI refactoring.


#### wx

wx usages went down by an impressive 1148:

305: 2260 usages (balt: 381, basher: 1586)
306: 1112 usages (balt: 418, basher/: 494)

Note balt barely going up. This is because, as was stressed in the
relevant issue (#190), decoupling the wx library from the code
does not simply mean "create wrappers for wx classes/constants and
drop them into balt - it means that balt must export an API", so:

 - balt should not return wx items on which wx methods will be called -
 see: 9856c69 for example fix.
 - the code outside of balt should be agnostic of wx ids. Bash code was
far form agnostic of wx ids - on the contrary it used them extensively:

  - balt was exporting it as a function parameter - this mostly resulted
  in client code calling it with `wx.ID_ANY` (adding wx usages) - see
  for ex. a555160,
  18cf2b1 up till finally dropping
  the last one of them in 370bef3.
  - event handlers were using `event.GetId()` - for example fix see
  081bfa6.
  - Link.Execute() has an event parameter which was mostly used with the
  IdList hack to define lists of menus - this was taken care by the
  ChoiceLink subclass, introduced in
  6fbec32 till finally the last uses of
  IdList were binned: 000f320 and
  IdList was removed (967b921) - note
  that this permitted dropping the `_id` parameter from `Link.__init__`.
  ItemLink.Execute() `event` parameter is in fact _never used_ in the
  306 Link API iteration and will be removed. That's "decoupling wx from
  Bash code".

For commits that combine both fixes see:
  46599bb,
  9a77bfc

The bulk of what remains is to hash up a clever interface for sizers -
needs thought as it must accommodate all usages while being powerful
enough to cater for new ones. That being said, another
thought is incorporating some basher wx dependent stuff back to balt
which anyway should be split to a package - at least its bosh depended
classes should be clearly separated from its "abstract" wx wrapping API.


#### Link API

In a gigantic merge (054970e - avoid)
the Link subclasses were ripped from basher to per-tab modules - this
is not final but it was a necessary step to start splitting basher. As
with the rip of patchers this was not a copy paste rip - a new Link
class API was introduced.
Unlike the patchers rip however (where package structure and the class
hierarchy is still at stake) the Link API has few rough edges left in.
The guide for designing the Link hierarchy was `AppendToMenu()` calls
that featured boilerplate wx code - for wip boilerplate elimination
commits see: 6cf40c1
where `EnabledLink` is introduced and
658fcac where my favourite
`TransLink` is introduced (AppendToMenu could become quite _complex_ -
see also `ChoiceLink`: 6fbec32).
The merge chopped off 785 wx uses and AppendToMenu was confined in balt,
and reduced to 8 different implementations vs 215 (!) in 305.

The Link API significantly evolved since - compare the 306 (semi final)
form with the 305 implementation:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/balt.py#L2098-L2114
306: https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/balt.py#L2392-L2446

Previous (old style) Link class explicitly defined _different_
attributes based on an isinstance check, checking if the underlying
window was a List or Tank instance. This made the code plain unreadable
(given also the names used, see .data section above) while enforcing the
Tank/List dichotomy. The isinstance check was finally removed here:
6d260f0.

Here is `INI_ListErrors` Link before and after the refactoring:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/basher.py#L11168-L11191
306: https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/basher/ini_links.py#L71-L90

Note AppendToMenu code is encapsulated in EnabledLink, wx wrapper
introduced for the clipboard, text and help are now class variables,
"data" is now "selected" and the lines are wrapped for more readable,
declarative and concise code. For a more complicated example see:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/basher.py#L11029-L11122
306 :https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/basher/mods_links.py#L84-L161

Previous implementation directly manipulated wx IDs (by global ID_***
complex hacks) while the new one declares classes and lets ChoiceLink
superclass take care of creating and appending the menu items.
This permitted focusing on the Execute code which as seen got improved
while refactored to remove duplication. This class still has rough
edges (\_self...) which will be corrected in 307 along with rough edges
in the API - for one dropping ItemLink.Execute() `event` parameter.
Of note that the Link subclasses featured a lot of duplicate code apart
from the wx related stuff - see for instance:
1a9a29e for duplicate code in
Mod_Export Links.


#### UIList

After the Links were ripped from basher it became obvious that
refactoring could not progress while balt.Tank and basher.List were
both still around. That lead, one fine morning, to a new class - UIList:
1462227. Both Tank and List had an
attribute (glist and list (!) respectively) pointing to a ListControl
instance (the items displayed in the UI). The first UIList merge:
abd5f24 (see commits there for how
UIList was engineered from common Tank/List code) has List.list removed
in favour of gList: fc7bde8, to be
finally encapsulated as _gList in
c48ce7d (note there is a completely
unrelated `gList` patcher attribute).

##### UIList.SortItems()

The first real snug though was unifying the sorting API of Tank and
List. ListControl does not support sorting, one has to introduce a hacky
dictionary mapping indexes to displayed items' ids (not wx ids) - that's
what Tank was doing (undocumented !) while List was using no less than
PopulateItems - resulting in bad performance most apparent in the ini
tab, where sorting would stat the file system (so clicking on a column
header Bash would hang couple seconds).
I proceeded unifying the List.SortItems() overrides
(6f0adc9), then moving the Tank
mechanism to the ListControl class where it belongs
(52a4a22) and finally moving SortItems
to base: 42d213a - note OnColumnClick
callback to base and ignore the isinstance(self, Tank) - corrected
later. This made also various other fixups around the Sort API possible,
namely adding sort indicators in all Bash lists (only mod tabs
had them), fixing master lists sorting (and display) and the beginning
of TankData param API deprecation:
7a3b872.

##### UIList.RefreshUI()

Second snug and a blocker for fixing performance was centralizing (and
cleaning up) the RefreshUI family and underlying PopulateItem(s)
(UpdateItem(s) in Tank). Some commits:

- Moving RefreshUI to List: 39e1e60
- List.PopulateItem(): f390ab7,
ecf30e9
- UIList.PopulateItems(): 350075a
- Tank and List `__init__()` eliminated:
a510b23

Unifying the RefreshUI API made finally possible to remove List itself:
d94a09c. More work is done - for
instance see some work on refreshing the details displayed in:
3ff6cf2.

##### UIList.DeleteItems()

Once List was no more and Tank a relic of itself it was finally possible
to tackle the delete API. First relevant merge is:
02a5891 but the API is yet in alpha
due to calling UI code from bosh - see:
6452bcb and its parent merge containing
3da60e6 (the ugly
DataDict.delete_Refresh()). This API will hit beta once the data refresh
methods return deleted, modified and added - see how this precious info
is currently discarded:

	return bool(_added) or bool(_updated) or bool(_deleted)

https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/bosh.py#L3887


### Performance (#123)

As seen by profiling, the performance bottleneck was the
libloadorder32.dll we bundled with bash. Given the central place load
order operations have in a mod manager application, the introduction of
a laggy dll had taken Bash's performance down on its knees since
51f99f2.
But this was far from being the only problem. Due to the spaghetti
nature of the code (throw some event programming in) Bash was very
generous with its refreshes:

- it would refresh its data three (3) times on boot
- RefreshData callback was triggered whenever Bash took focus -
_including_ after Bash itself displayed a dialog to the user
- on Oblivion, while Bash had refreshed its mod infos it was _always_
requesting also a refresh from libloadorder, to be sure - so,
for instance, when just tabbing out and back in to Bash (with absolutely
no changes in the Data dir) libloadorder was triggered and went through
its refresh cycle - see ad05f44 for the
fix - refresh in BAIN, say, install operations was called twice, along
with a double call of modList RefreshUI
- refreshing the mods panel would result in also refreshing the saves
panel - even when the change that triggered the refresh did not affect
saves - see commits in: 63d8dec

Now, given that most of those refreshes ended up triggering a refresh
from the libloadorder32.dll, Bash would take of the order of 10 seconds
for all but the most trivial operations. But even if libloadorder was
lightning fast the double and triple refreshes were as much a bug as
anything - so the dll lag was even a blessing in disguise as it made
apparent the underlying chaos.

To solve the dll issue one alternative would be to reimplement
it in python. But given that the same dll is used by other mod managers
and related apps I anyway had to know what's in there. So I ended up
forking libloadorder and going through the C++ ~~pain~~ code:
https://github.com/Utumno/libloadorder/
I realized that some of the operations performed by the dll are
inherently costly and would actually need a complete rewrite
using caches - for instance the mod validity checks (see
Utumno/libloadorder#3), which by the way
involves yet another library and is not clear what is checked (as
is not clear what Bash checks and if those checks are basically
performed twice - Utumno/libloadorder#6).
As the situation was critical in Oblivion mainly (due to
stating the filesystem for mod times) I explicitly bypassed the dll
because I anyway have all the data needed to determine the load order -
7cd6533 - while I still use the dll for
saving it.

Liblo 7.6.0 commit (f5de6c8) apart from
performance fixups fixes some other issues with liblo 4 and 6 like
writing loadorder.txt when not there, return active plugins in order,
adding files to libloadorder.txt that are added to Data, etc - see:
Utumno/libloadorder@7bb5cfb

But rewriting the dll was not enough - on the Python side of things the
load order internal API was a giant hack, complete with event handlers
and absolutely no encapsulation, let alone proper caches. I introduced
a new module `load_order.py` (for first iteration and design decisions
see 98ed549) to centralize load_order
handling - will be finalized in 307 but already Plugins is again
encapsulated in ModInfos which serves as the basic internal load order
API for the rest of Bash. Load order API will be final when implementing
features such as "undo load order change" is a breeze, but already makes
implementing minor load order related features much easier:
14ecafc.

Using the caches I introduced I was able to fix performance of
getOrdered() - was O(n^3\*logn) instead of O(n\*logn):
5d7ed0b

The double and triple refreshes were a deeper issue - the "one file
program" thing - so rewriting refreshes (RefreshUI, the DataDicts
refresh, RefreshData etc) - was an ongoing effort throughout 306
development, going hand to hand with the UI refactoring (see #163 for
RefreshUI and #123 for links to some of the most relevant commits).
In short:

- the refreshes on bash dialogs were taken care using a decorator to
unbind RefreshData(): a71f3f2
- the BAIN issues were addressed finally here:
8107f7b - BAIN however needs to be
split in a dedicated package to start properly fixing it.

Goals for the next few releases:

- cache ini reads
- finalize then profile the load order handling API
- profile the refresh of data on boot and optimize it
- centralize refresh of data and UI so we can eventually
- switch to an event based model of runtime refresh (watchdog ?)


### Warnings

Pycharm code inspection emits 2228 warnings in 306 as opposed to 4229
in 305. Metrics are from Pycharm 4.5.4 - using this inspection:
https://github.com/Utumno/wrye-bash-pycharm/blob/168253bca81313a3cc7dc85ee53747b116e984fb/.idea/inspectionProfiles/default_copy_no_typos.xml
on this scope:
https://github.com/Utumno/wrye-bash-pycharm/blob/168253bca81313a3cc7dc85ee53747b116e984fb/.idea/scopes/wrye_bash_all_no_cint.xml
Warnings come in various sizes and shapes but in general show
low code quality.
Some I batch fixed (like 'Comparison to None performed with
equality operator') but in general I was fixing warnings when visiting
nearby code so the remaining ones mostly mark code areas that need
attention. On the other end of the spectrum there are warnings that
deserve issues of their own (like 'Too broad exception clause' -
nicknamed the most diabolical python anti-pattern - see #200).
Other interesting cases were:

- "Function 'close' doesn't return anything" led to a rewrite of the
archives handling code: c29d7b8
- copy paste unused variables were kind of a guide to otherwise heavily
undocumented code - but also often pointed to bugs
- "method may be static" (264 in 305, went down to 72 in 306) is a
classic example of a warning one fixes while editing the code - the
remaining ones should be revisited for 307, along with their classes.
- unused imports are on their way to extinction - the remaining cases
involve `import *` patterns and normalizing the import style, which
shall be done when package layout is frozen - and the ugly `__all__`
directives one has to update on adding a menu item:
6a2e4a9 - UILists and their links
belong together.


### Cruft (#173)

Cruft removal got a dedicated issue so returning developers could
readily track when and why code they were familiar with got removed.
Cruft is not just unused classes, commented out code etc - it is also
compatibility with ancient Bash settings (will be removed in 307),
non working tabs (like the PM tab also to be removed in 307) and
compatibility code with pre 2.7 python (see
6d0a944). One major piece of cruft
removed in 306 was BALO: b520591.
As you can see removing BALO was needed for tackling refresh - as it
was a complex, obsolete piece of code that greatly complicated both
refresh and load order handling - and even the Link API (see notoriously
complex Mod_BaloGroups(ChoiceLink) subclass here:
aa5b89a).


### Formatting & wrapping

After some discussion it was clear that PEP8'ing the code would be
a pointless effort at the state it was in - however word wrapping is
a _functional_ requirement and 79 chars is well thought of, even if you
use a wall projector for programming.

305:
Total number of non-blank lines:  82463 in 29 files
96.22 lines less than 80 characters (3120 lines exceed)
98.63 lines less than 100 characters (1130 lines exceed)
99.44 lines less than 120 characters (462 lines exceed)

306:
Total number of non-blank lines:  85366 in 63 files
97.89 lines less than 80 characters (1803 lines exceed)
99.26 lines less than 100 characters (631 lines exceed)
99.71 lines less than 120 characters (248 lines exceed)

Note:
- I do not include `cint.py` (and the `chardet` package which should
become a submodule)
- a big part of the line wrapping was made by manually correcting
Pycharm's formatter - so have a close look
- the increase of line count is mainly due to
21de440 which adds 5130 lines most
(4974) of it being static data in Mopy/bash/game/skyrim.py.
Considering there were 34 files added for 34 * 23 = 782 lines of licence
text and that wrapping should have produced another 1k lines at least
_core Bash code was actually reduced by ~4000 lines_. That is more than
welcome and should be imitated - _less code more features_ is a great
motto.


### Boot

Booting process got a facelift to be finalized in 307. This involves
game loading (see 97fc9cf), fixes to
the game detection algorithm (e19237c)
which used to run 3-5 times on boot, a fix for our custom importer
(43e359a) compliments of @mjpieters,
and finally a reworking of boot error messages
(8dbdd49).


### WINE

As reported in #203 "WryeBash v304.2 works beautifully in Wine" - the
windows only shell UI stuff broke it though. One of the last things
I did for 306 was fixing this - now Bash runs again on WINE:
124f314. Still making it run correctly
and in a second phase making Bash run _on bare Linux_ is a code quality
goal that got dedicated issues (#240, #241, #243)

------------------------------------------------------------------------

This release is dedicated to Wrye.

Special thanks to:
- @Sharlikran, @Supierce, @lojack5, @Arthmoor, @alt3rn1ty, @psilocide
(aka lefenger), @leandor for testing, support and sharing their
knowledge,
- @jfpelland, @wduda, @Isenr and @valda for patches,
- @mjpieters for saving my sanity at SO a couple times as well as @zed
for helping me rewrite the archives handling code.


------------------------------------------------------------------------
# Conflicts:
#	Mopy/bash/bass.py

@@@ -21,9 -21,42 +21,46 @@@
  #  https://github.com/wrye-bash
  #
  # =============================================================================

  """This module just stores some data that all modules have to be able to access
- without worrying about circular imports."""
+ without worrying about circular imports. Currently used to expose layout
+ and environment issues - do not modify or imitate (ut)."""
+ import os as _os
+ import ConfigParser as _cp

  language = None
  <<<<<<< HEAD
 +AppVersion = u"304.4"
  =======
+ AppVersion = u"306"
+ bashIni = None
+
+ #--Null strings (for default empty byte arrays)
+ null1 = '\x00'
+ null2 = null1*2
+ null3 = null1*3
+ null4 = null1*4
+
+ def GetBashIni(iniPath=None, reload_=False): ##: needs work
+     iniPath = iniPath or u'bash.ini'
+     global bashIni
+     if reload_ or bashIni is None:
+         if _os.path.exists(iniPath):
+             bashIni = _cp.ConfigParser()
+             bashIni.read(iniPath)
+     return bashIni
+
+ class Resources: # this belongs to basher but leads to cyclic imports, so...
+     fonts = None
+     #--Icon Bundles
+     bashRed = None
+     bashBlue = None
+     bashDocBrowser = None
+     bashMonkey = None
+
+ # move with its uses to a cross platform 'env.py' module - maybe add bashIni
+ try:
+     import _winreg as winreg
+ except ImportError:
+     winreg = None
  >>>>>>> rel-306


------------------------------------------------------------------------

Build with `2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)]`

Closes #187.
Utumno added a commit that referenced this issue Nov 7, 2015
This release features an almost complete refactoring of the codebase,
fixing many bugs and opening the way to extend Bash functionality.
Bash, being a community work, has over the years become an
assortment of hacks, patches and cruft and the program was just about to
become non-operable. Example issues - in no particular order (although
probably the worst was the horrible performance):

- deleting an item in the lists displayed by Bash ran different code
depending on whether the user hit delete or right clicked and chose
delete - most of those pieces of code were buggy
- start up took more than 15 seconds on large Oblivion Load Orders and
alt tabbing out and back into Bash would take 6-7 seconds - last one
should take no time in the usual case
- as seen from the (user visible) debug prints the game detection
algorithm run 3-5 times
- many, many broken things - including performance in general (most
simple operations would hang Bash for some seconds), display glitches
and general UI inconsistencies and internal data structures corruption
(see #176)

Those bugs reflected the state of the code - again in no particular
order:

- bosh.py (the data model) in 305 was 24635 lines (reduced from
30550 lines in 304.4 - see bosh section below)
- basher.py (the UI lists/menus etc) was 18936 lines
- the seven tabs that Bash featured were backed up by Tank (2) and List
(5) instances - so implementing a feature (say delete) needed edits to
at least two classes (usually 7)
- the wx library was intermingled with the basher code - the menu items
(Links) defined 215 AppendToMenu methods with nearly identical wx code
- readability suffered - most attributes/variables were named `data` or
`items`, globals hacks crippled the IDE static analysis, copy paste (aka
ravioli) code was everywhere.

When I started in it I had no idea if refactoring the code was even
possible, core Bash is 80k lines of code. It was clear that bosh needed
splitting, that candidate number one was the patchers code (well defined
and functional) and that basher should follow - but was not at all
clear if those were possible, let alone how exactly should the code be
structured and how much time would this take. Turns out that it was
possible: Bash was indeed coded with modularity in mind (by Wrye ?)
but later additions bloated the initial modules beyond recognition
and broke the (non enforced) encapsulation - however it wasn't too late.
Also turns out that refactoring needed 2 full work years of a single
engineer (learning python).

The huge performance boost that this release features is the measurable
effect of the increase in the code quality. In the items below I try to
describe in a quantitative manner what "code quality" means and how it
was increased. That's meant to be read from an app that links to the
commits - both github and gitk do but additionally github links to the
issues and files.

This release features splitting the patchers' code out of bosh
to a dedicated package (whose final structure is still at stake). This
reduced bosh to 10516 lines while naturally exposing class
dependencies issues, hidden by the "one file program" nature of Bash.
The attempt to split bosh had already started in 305, involving:

- an effort to eliminate code duplication in C and P patchers using
multiple inheritance (chopped off around 1500 lines of pure copy paste
code) - see commits in 3c40989
- splitting out what was to become `record_groups.py` - see
c3e3543, but don't
imitate it - lots has changed since then, including import conventions
and commit style - for instance commits on dev that do not launch are
strictly forbidden.
- splitting out what was to become `parsers.py` - see commits in
08ab6e2

Final (`record_groups.py`, `parsers.py`):
6ae30fc

With those out of the way it was finally possible to rip the patchers
out of bosh - starting here:
983f259
That was not a mere copy paste rip - warnings were squashed, long lines
were wrapped, and most importantly a lot of tricky refactoring was
applied to eliminate code duplication - see for ex. commits in:
72fc8a0 for ripping
and commits in (note the negative line count):
5d21377,
fdab163,
78a85e0 for refactoring

See #3 for commit links (finally closed in
b6b743b) and #124 for the (still open)
package layout issue. Be aware that a lot of things changed since,
including import style and the overall package layout - see:
e6b2e0e

The remaining bosh.py code had some preliminary refactoring applied to
it (archives handling, BAIN error handling and refresh etc - see
6ddd4a8) but core bosh
refactoring will take place in 307. Major goals are splitting bosh into
a number of modules, getting rid of old style classes (DataDict
hierarchy, that is the data store singletons) and refactoring settings
to efficiently support profiles.

There are 544 occurrences of `.data` in 306 (1081 of `data`) as opposed
to 1071 in 305 (2221 `data`) - baring string literals and comments
(Pycharm only recently made these scopes available in IDE search, they
were much needed). Practically one had to run the debugger to see the
type of the object one had at hand, since the variable/attribute name
was always 'data':

- data is the wrapped dictionary of `DataDict`. Most accesses to this
dict were not via the wrappers the DataDict provides. This took
many commits to fix - ex
56198be. Before even
_thinking_ "performance" read the performance section below (no, none of
those modInfos.data.keys() calls was in the middle of a tight loop).
- data also was an attribute of the Link hierarchy which had _different
meaning based on an isinstance check_:
92e50cd. It could either mean a
DataDict subclass (so in Links code there were `self.data.data`
accesses) _or_ the list of selected items in the UI, leading to a
complete chaos - see the commit above up till the final removal in
ffae59e.
- UIList subclasses have a `data` attribute that points to the
DataDict singletons in bosh. That was dealt with throughout
coding 306 as it is closely related to a deeper issue namely the
intermingling of UI and model/business logic code. This
stems from the 'one file program' nature of Bash so solving this
properly means refactoring is at 100%.
- to somehow improve readability I introduced idata and pdata
attributes for the Installers and People data links -
eba53f9 - Those are transitory and
meant to also help in eliminating Tank.
The links of List subclasses were using self.window.data, whose uses
I strived to encapsulate - random example:
0934f1f
- all kind of different classes attributes were named `data`. Those
required dedicated commits (hey, I couldn't just do a search and replace
for "data") - ex. 9839c47. An IDE
really helps here.
- finally data local variables were renamed - ex.
8e77283

The war with data was going on in parallel with smaller scale wars with
`items` (e87dca7 and its parents),
`refresh` (4d872ab), etc.

This is the part of refactoring that I consider almost done - and it was
a big pleasure coding it. Since I first tried coding for Bash the
dichotomy between balt.Tank and basher.List clearly stood out as a major
issue, as did the omnipresence of the wx library and of course the sheer
size of basher.py (the UI module).
Basher became a package and then the UI API went through a complete
rewrite. Basher globals served as a guide in the splitting process,
and a metric of class dependencies. Eliminating those globals was an
ongoing effort throughout 306 coding - see for instance:
892a19c (screenlist, statusBar),
5852328 (gInstallers),
faea771 (modList)
till final elimination in bab7c00
Guest star: bosh.links (353be43)
Globals served as a link also between Bash's modules (so they evince
class and module coupling issues) - eventually encapsulated as static
fields of the balt.Link.Frame singleton - see
0690cf3
Link.Frame is an interesting binding of the UI (basher) and the rest of
bash that will serve as guide to a later refactoring phase. Especially
Link.Frame.modList use in bosh must be eliminated but this is related
to the RefreshUI/delete API finalization.
The items below detail the UI refactoring.

wx usages went down by an impressive 1148:

305: 2260 usages (balt: 381, basher: 1586)
306: 1112 usages (balt: 418, basher/: 494)

Note balt barely going up. This is because, as was stressed in the
relevant issue (#190), decoupling the wx library from the code
does not simply mean "create wrappers for wx classes/constants and
drop them into balt - it means that balt must export an API", so:

 - balt should not return wx items on which wx methods will be called -
 see: 9856c69 for example fix.
 - the code outside of balt should be agnostic of wx ids. Bash code was
far form agnostic of wx ids - on the contrary it used them extensively:

  - balt was exporting it as a function parameter - this mostly resulted
  in client code calling it with `wx.ID_ANY` (adding wx usages) - see
  for ex. a555160,
  18cf2b1 up till finally dropping
  the last one of them in 370bef3.
  - event handlers were using `event.GetId()` - for example fix see
  081bfa6.
  - Link.Execute() has an event parameter which was mostly used with the
  IdList hack to define lists of menus - this was taken care by the
  ChoiceLink subclass, introduced in
  6fbec32 till finally the last uses of
  IdList were binned: 000f320 and
  IdList was removed (967b921) - note
  that this permitted dropping the `_id` parameter from `Link.__init__`.
  ItemLink.Execute() `event` parameter is in fact _never used_ in the
  306 Link API iteration and will be removed. That's "decoupling wx from
  Bash code".

For commits that combine both fixes see:
  46599bb,
  9a77bfc

The bulk of what remains is to hash up a clever interface for sizers -
needs thought as it must accommodate all usages while being powerful
enough to cater for new ones. That being said, another
thought is incorporating some basher wx dependent stuff back to balt
which anyway should be split to a package - at least its bosh depended
classes should be clearly separated from its "abstract" wx wrapping API.

In a gigantic merge (054970e - avoid)
the Link subclasses were ripped from basher to per-tab modules - this
is not final but it was a necessary step to start splitting basher. As
with the rip of patchers this was not a copy paste rip - a new Link
class API was introduced.
Unlike the patchers rip however (where package structure and the class
hierarchy is still at stake) the Link API has few rough edges left in.
The guide for designing the Link hierarchy was `AppendToMenu()` calls
that featured boilerplate wx code - for wip boilerplate elimination
commits see: 6cf40c1
where `EnabledLink` is introduced and
658fcac where my favourite
`TransLink` is introduced (AppendToMenu could become quite _complex_ -
see also `ChoiceLink`: 6fbec32).
The merge chopped off 785 wx uses and AppendToMenu was confined in balt,
and reduced to 8 different implementations vs 215 (!) in 305.

The Link API significantly evolved since - compare the 306 (semi final)
form with the 305 implementation:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/balt.py#L2098-L2114
306: https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/balt.py#L2392-L2446

Previous (old style) Link class explicitly defined _different_
attributes based on an isinstance check, checking if the underlying
window was a List or Tank instance. This made the code plain unreadable
(given also the names used, see .data section above) while enforcing the
Tank/List dichotomy. The isinstance check was finally removed here:
6d260f0.

Here is `INI_ListErrors` Link before and after the refactoring:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/basher.py#L11168-L11191
306: https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/basher/ini_links.py#L71-L90

Note AppendToMenu code is encapsulated in EnabledLink, wx wrapper
introduced for the clipboard, text and help are now class variables,
"data" is now "selected" and the lines are wrapped for more readable,
declarative and concise code. For a more complicated example see:

305: https://github.com/wrye-bash/wrye-bash/blob/d8f05202e6485a3bef0d92bb55a731b8040eb94e/Mopy/bash/basher.py#L11029-L11122
306 :https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/basher/mods_links.py#L84-L161

Previous implementation directly manipulated wx IDs (by global ID_***
complex hacks) while the new one declares classes and lets ChoiceLink
superclass take care of creating and appending the menu items.
This permitted focusing on the Execute code which as seen got improved
while refactored to remove duplication. This class still has rough
edges (\_self...) which will be corrected in 307 along with rough edges
in the API - for one dropping ItemLink.Execute() `event` parameter.
Of note that the Link subclasses featured a lot of duplicate code apart
from the wx related stuff - see for instance:
1a9a29e for duplicate code in
Mod_Export Links.

After the Links were ripped from basher it became obvious that
refactoring could not progress while balt.Tank and basher.List were
both still around. That lead, one fine morning, to a new class - UIList:
1462227. Both Tank and List had an
attribute (glist and list (!) respectively) pointing to a ListControl
instance (the items displayed in the UI). The first UIList merge:
abd5f24 (see commits there for how
UIList was engineered from common Tank/List code) has List.list removed
in favour of gList: fc7bde8, to be
finally encapsulated as _gList in
c48ce7d (note there is a completely
unrelated `gList` patcher attribute).

The first real snug though was unifying the sorting API of Tank and
List. ListControl does not support sorting, one has to introduce a hacky
dictionary mapping indexes to displayed items' ids (not wx ids) - that's
what Tank was doing (undocumented !) while List was using no less than
PopulateItems - resulting in bad performance most apparent in the ini
tab, where sorting would stat the file system (so clicking on a column
header Bash would hang couple seconds).
I proceeded unifying the List.SortItems() overrides
(6f0adc9), then moving the Tank
mechanism to the ListControl class where it belongs
(52a4a22) and finally moving SortItems
to base: 42d213a - note OnColumnClick
callback to base and ignore the isinstance(self, Tank) - corrected
later. This made also various other fixups around the Sort API possible,
namely adding sort indicators in all Bash lists (only mod tabs
had them), fixing master lists sorting (and display) and the beginning
of TankData param API deprecation:
7a3b872.

Second snug and a blocker for fixing performance was centralizing (and
cleaning up) the RefreshUI family and underlying PopulateItem(s)
(UpdateItem(s) in Tank). Some commits:

- Moving RefreshUI to List: 39e1e60
- List.PopulateItem(): f390ab7,
ecf30e9
- UIList.PopulateItems(): 350075a
- Tank and List `__init__()` eliminated:
a510b23

Unifying the RefreshUI API made finally possible to remove List itself:
d94a09c. More work is done - for
instance see some work on refreshing the details displayed in:
3ff6cf2.

Once List was no more and Tank a relic of itself it was finally possible
to tackle the delete API. First relevant merge is:
02a5891 but the API is yet in alpha
due to calling UI code from bosh - see:
6452bcb and its parent merge containing
3da60e6 (the ugly
DataDict.delete_Refresh()). This API will hit beta once the data refresh
methods return deleted, modified and added - see how this precious info
is currently discarded:

	return bool(_added) or bool(_updated) or bool(_deleted)

https://github.com/wrye-bash/wrye-bash/blob/b40b1a10ff414dd41dd412c8484ca253e42ca92c/Mopy/bash/bosh.py#L3887

As seen by profiling, the performance bottleneck was the
libloadorder32.dll we bundled with bash. Given the central place load
order operations have in a mod manager application, the introduction of
a laggy dll had taken Bash's performance down on its knees since
51f99f2.
But this was far from being the only problem. Due to the spaghetti
nature of the code (throw some event programming in) Bash was very
generous with its refreshes:

- it would refresh its data three (3) times on boot
- RefreshData callback was triggered whenever Bash took focus -
_including_ after Bash itself displayed a dialog to the user
- on Oblivion, while Bash had refreshed its mod infos it was _always_
requesting also a refresh from libloadorder, to be sure - so,
for instance, when just tabbing out and back in to Bash (with absolutely
no changes in the Data dir) libloadorder was triggered and went through
its refresh cycle - see ad05f44 for the
fix - refresh in BAIN, say, install operations was called twice, along
with a double call of modList RefreshUI
- refreshing the mods panel would result in also refreshing the saves
panel - even when the change that triggered the refresh did not affect
saves - see commits in: 63d8dec

Now, given that most of those refreshes ended up triggering a refresh
from the libloadorder32.dll, Bash would take of the order of 10 seconds
for all but the most trivial operations. But even if libloadorder was
lightning fast the double and triple refreshes were as much a bug as
anything - so the dll lag was even a blessing in disguise as it made
apparent the underlying chaos.

To solve the dll issue one alternative would be to reimplement
it in python. But given that the same dll is used by other mod managers
and related apps I anyway had to know what's in there. So I ended up
forking libloadorder and going through the C++ ~~pain~~ code:
https://github.com/Utumno/libloadorder/
I realized that some of the operations performed by the dll are
inherently costly and would actually need a complete rewrite
using caches - for instance the mod validity checks (see
Utumno/libloadorder#3), which by the way
involves yet another library and is not clear what is checked (as
is not clear what Bash checks and if those checks are basically
performed twice - Utumno/libloadorder#6).
As the situation was critical in Oblivion mainly (due to
stating the filesystem for mod times) I explicitly bypassed the dll
because I anyway have all the data needed to determine the load order -
7cd6533 - while I still use the dll for
saving it.

Liblo 7.6.0 commit (f5de6c8) apart from
performance fixups fixes some other issues with liblo 4 and 6 like
writing loadorder.txt when not there, return active plugins in order,
adding files to libloadorder.txt that are added to Data, etc - see:
Utumno/libloadorder@7bb5cfb

But rewriting the dll was not enough - on the Python side of things the
load order internal API was a giant hack, complete with event handlers
and absolutely no encapsulation, let alone proper caches. I introduced
a new module `load_order.py` (for first iteration and design decisions
see 98ed549) to centralize load_order
handling - will be finalized in 307 but already Plugins is again
encapsulated in ModInfos which serves as the basic internal load order
API for the rest of Bash. Load order API will be final when implementing
features such as "undo load order change" is a breeze, but already makes
implementing minor load order related features much easier:
14ecafc.

Using the caches I introduced I was able to fix performance of
getOrdered() - was O(n^3\*logn) instead of O(n\*logn):
5d7ed0b

The double and triple refreshes were a deeper issue - the "one file
program" thing - so rewriting refreshes (RefreshUI, the DataDicts
refresh, RefreshData etc) - was an ongoing effort throughout 306
development, going hand to hand with the UI refactoring (see #163 for
RefreshUI and #123 for links to some of the most relevant commits).
In short:

- the refreshes on bash dialogs were taken care using a decorator to
unbind RefreshData(): a71f3f2
- the BAIN issues were addressed finally here:
8107f7b - BAIN however needs to be
split in a dedicated package to start properly fixing it.

Goals for the next few releases:

- cache ini reads
- finalize then profile the load order handling API
- profile the refresh of data on boot and optimize it
- centralize refresh of data and UI so we can eventually
- switch to an event based model of runtime refresh (watchdog ?)

Pycharm code inspection emits 2228 warnings in 306 as opposed to 4229
in 305. Metrics are from Pycharm 4.5.4 - using this inspection:
https://github.com/Utumno/wrye-bash-pycharm/blob/168253bca81313a3cc7dc85ee53747b116e984fb/.idea/inspectionProfiles/default_copy_no_typos.xml
on this scope:
https://github.com/Utumno/wrye-bash-pycharm/blob/168253bca81313a3cc7dc85ee53747b116e984fb/.idea/scopes/wrye_bash_all_no_cint.xml
Warnings come in various sizes and shapes but in general show
low code quality.
Some I batch fixed (like 'Comparison to None performed with
equality operator') but in general I was fixing warnings when visiting
nearby code so the remaining ones mostly mark code areas that need
attention. On the other end of the spectrum there are warnings that
deserve issues of their own (like 'Too broad exception clause' -
nicknamed the most diabolical python anti-pattern - see #200).
Other interesting cases were:

- "Function 'close' doesn't return anything" led to a rewrite of the
archives handling code: c29d7b8
- copy paste unused variables were kind of a guide to otherwise heavily
undocumented code - but also often pointed to bugs
- "method may be static" (264 in 305, went down to 72 in 306) is a
classic example of a warning one fixes while editing the code - the
remaining ones should be revisited for 307, along with their classes.
- unused imports are on their way to extinction - the remaining cases
involve `import *` patterns and normalizing the import style, which
shall be done when package layout is frozen - and the ugly `__all__`
directives one has to update on adding a menu item:
6a2e4a9 - UILists and their links
belong together.

Cruft removal got a dedicated issue so returning developers could
readily track when and why code they were familiar with got removed.
Cruft is not just unused classes, commented out code etc - it is also
compatibility with ancient Bash settings (will be removed in 307),
non working tabs (like the PM tab also to be removed in 307) and
compatibility code with pre 2.7 python (see
6d0a944). One major piece of cruft
removed in 306 was BALO: b520591.
As you can see removing BALO was needed for tackling refresh - as it
was a complex, obsolete piece of code that greatly complicated both
refresh and load order handling - and even the Link API (see notoriously
complex Mod_BaloGroups(ChoiceLink) subclass here:
aa5b89a).

After some discussion it was clear that PEP8'ing the code would be
a pointless effort at the state it was in - however word wrapping is
a _functional_ requirement and 79 chars is well thought of, even if you
use a wall projector for programming.

305:
Total number of non-blank lines:  82463 in 29 files
96.22 lines less than 80 characters (3120 lines exceed)
98.63 lines less than 100 characters (1130 lines exceed)
99.44 lines less than 120 characters (462 lines exceed)

306:
Total number of non-blank lines:  85366 in 63 files
97.89 lines less than 80 characters (1803 lines exceed)
99.26 lines less than 100 characters (631 lines exceed)
99.71 lines less than 120 characters (248 lines exceed)

Note:
- I do not include `cint.py` (and the `chardet` package which should
become a submodule)
- a big part of the line wrapping was made by manually correcting
Pycharm's formatter - so have a close look
- the increase of line count is mainly due to
21de440 which adds 5130 lines most
(4974) of it being static data in Mopy/bash/game/skyrim.py.
Considering there were 34 files added for 34 * 23 = 782 lines of licence
text and that wrapping should have produced another 1k lines at least
_core Bash code was actually reduced by ~4000 lines_. That is more than
welcome and should be imitated - _less code more features_ is a great
motto.

Booting process got a facelift to be finalized in 307. This involves
game loading (see 97fc9cf), fixes to
the game detection algorithm (e19237c)
which used to run 3-5 times on boot, a fix for our custom importer
(43e359a) compliments of mjpieters,
and finally a reworking of boot error messages
(8dbdd49).

As reported in #203 "WryeBash v304.2 works beautifully in Wine" - the
windows only shell UI stuff broke it though. One of the last things
I did for 306 was fixing this - now Bash runs again on WINE:
124f314. Still making it run correctly
and in a second phase making Bash run _on bare Linux_ is a code quality
goal that got dedicated issues (#240, #241, #243)

------------------------------------------------------------------------

This release is dedicated to Wrye.

Special thanks to:
- Sharlikran, Supierce, lojack5, Arthmoor, alt3rn1ty, psilocide
(aka lefenger), leandor for testing, support and sharing their
knowledge,
- jfpelland, wduda, Isenr and valda for patches,
- mjpieters for saving my sanity at SO a couple times as well as zed
for helping me rewrite the archives handling code.

------------------------------------------------------------------------

Build with `2.7.10 (default, May 23 2015, 09:40:32) [MSC v.1500 32 bit (Intel)]`

Closes #187.
This was referenced Nov 8, 2015
Closed
@Infernio Infernio removed the M-review Misc: Review from another developer is requested label Sep 20, 2019
Infernio added a commit that referenced this issue Aug 5, 2020
Important merge splitting importers.py into four files:
 - preservers.py houses preservers, which are importers that forward
   changes to certain subrecords from the last mod with a tag relevant
   to the patcher.
 - mergers.py houses mergers, which target list subrecords and merge
   additions, deletions and changes into a final version of the list,
   allowing several tagged mods to have their changes to a subrecord
   unified.
 - _cbash_importers.py and _shared.py are temporary and will be dropped
   once CBash support is dropped.

This split allows us to absorb a lot of patchers into base classes:
 25 files changed, 2743 insertions(+), 3047 deletions(-)

Not a massive reduction, but a lot of overrides have disappeared and
adding new patchers is more doable than ever before.

_APreserver is very mature. The last few TODOs here are:
 - CellImporter, due to how different it is from the other preservers. I
   started an attempt and it *is* possible to absorb it, but will need
   lots of careful thinking.
 - GraphicsPatcher, due to its half-broken case-insensitivity handling.
   Will be addressed in 480-pt3.
 - RoadImporter, same reason as CellImporter. Even more specialized, but
   if we can do CellImporter we can probably do this one as well.

_AMerger is much more WIP. See the code for some of its TODOs, but the
most notable one is that I've already begun rewriting it to work with
unsorted subrecords (ref #497).

Also introduces a new patcher that was requested and a new tag that was
requested on Discord.

Paves the way for a ton of open issues:

Under #124 - what a throwback :)
Under #151, #312, #468, #482, #492, #494, #497, #512, #513
Closes #304 by dropping the weird logic entirely
Closes #314
Closes #521
@Utumno Utumno closed this as completed in a7d7300 Mar 13, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-patchers Area: Patchers (Everything in the patcher package) C-goal Category: Long term goal. May be code-related or a meta goal.
Projects
None yet
Development

No branches or pull requests

5 participants