Skyrim Mod:SkyProc/LLI

< Skyrim Mod:SkyProc
To meet our site's higher standard of quality, this article or section may require cleanup. The user who placed this here had the following concern:
Page needs re-written in an encyclopedic style rather than first-person
To leave a message about the cleanup for this article, please add it to this article's talk page.

Skyproc's documentation is a little sparse and a little enigmatic and since Leviathan mentioned some others were looking to learn I figured I would post just what I wanted to find when I was just starting: a walkthrough of a patcher so I could get a feel for how it all fit together (and a refresher on Java). So here is my LLI patcher. Its open source so feel free to use any bit you find useful. The full project is available at http://code.google.com/p/lootification/ I'm going to trim it a bit for coherency but you might want the real deal. Its also the first serious bit of programming I've done in like 6 years so I'm not sure if I should be proud or embarrassed by the code quality. This program has grown organically as I got bits working and wanted to do more with it. This is generally bad practice as it ends up with a project turning into a hideous nightmare beast with razor sharp tentacles for eyes and cabbage for feet (just look at dwarf fortress). On the other hand I didn't have to spend ages planning things out and you get to see different ways of doing things as I remembered/figured out java and skyproc.

The Main fileEdit

There are only a handful of 'interesting' parts to LeveledListInjector.java. First are the import requests:

   GRUP_TYPE[] importRequests = new GRUP_TYPE[]{
       GRUP_TYPE.LVLI, GRUP_TYPE.ARMO, GRUP_TYPE.WEAP, GRUP_TYPE.FLST, GRUP_TYPE.KYWD, GRUP_TYPE.OTFT
   };

This tells skyproc what parts of the data files we want. Every time my project grew I forgot to add the new record types to the import request. Every. Time. Don't do what I did and waste your life trying to figure out why getWeapons() is giving you nulls and remember to keep your import requests up to date.

Now we skip lots of important stuff handling the gui and go right to sausage making of the patch. The reason is twofold: the UI is I think the least interesting bit of LLI and it doesn't make much sense without know what else is going on. Plus I wrote it last.

Now we go down to the bottom with

   @Override
   public void runChangesToPatch() throws Exception {
       Mod patch = SPGlobal.getGlobalPatch();
       Mod merger = new Mod(getName() + "Merger", false);
       merger.addAsOverrides(SPGlobal.getDB());
       merger.addAsOverrides(global, GRUP_TYPE.ARMO, GRUP_TYPE.WEAP);
       FLST baseArmorKeysFLST = (FLST) merger.getMajor("LLI_BASE_ARMOR_KEYS", GRUP_TYPE.FLST);
       FLST variantArmorKeysFLST = (FLST) merger.getMajor("LLI_VAR_ARMOR_KEYS", GRUP_TYPE.FLST);
       FLST baseWeaponKeysFLST = (FLST) merger.getMajor("LLI_BASE_WEAPON_KEYS", GRUP_TYPE.FLST);
       FLST variantWeaponKeysFLST = (FLST) merger.getMajor("LLI_VAR_WEAPON_KEYS", GRUP_TYPE.FLST);

The first bits you saw in the tutorial, they declare two objects we'll be seeing a lot of:patch and merger. Then we add the things the gui did to out working dataset. Then there are some magic formlists I made in the creation kit to hold important information. Below that we have the action parts.

       if (save.getBool(Settings.PROCESS_ARMORS)) {
           ArmorTools.setupArmorMatches(baseArmorKeysFLST, variantArmorKeysFLST, merger);
           ArmorTools.buildArmorVariants(merger, patch, baseArmorKeysFLST, variantArmorKeysFLST);
           if (save.getBool(Settings.PROCESS_OUTFITS)) {
               ArmorTools.buildOutfitsArmors(baseArmorKeysFLST, merger, patch);
           }
           ArmorTools.linkLVLIArmors(baseArmorKeysFLST, merger, patch);
       }

And the same for weapons. That there is the basic flow of the program and the order we'll be looking at the code. The (save.getBool(Settings.PROCESS_ARMORS)) comes from the gui, if they checked the box we first do some legwork setting things up we'll use later. Then we build the variants. Then we put the variants in the outfits and linked lists. Below that is the weapons bit, its a little different because I got bored. I probably shouldn't have made made those static functions and instead done declared an instance of the class and handled some of the work in the constructor but for some reason I didn't.

ArmorToolsEdit

Setup MatchesEdit

I should probably mention that this patcher takes modded weapons and armor and enchants them and distributes the lot around the game in what I consider a reasonable manner. I figured the easiest way to do this would be to match each modded item with an existing item and then match the enchantments and placements in the leveled lists and outfits.

   static void setupArmorMatches(FLST base, FLST var, Mod merger) {
       armorMatches = new ArrayList<>();
       ArrayList<FormID> bases = base.getFormIDEntries();
       ArrayList<FormID> vars = var.getFormIDEntries();
       for (int i = 0; i < bases.size(); i++) {
           KYWD newBase = (KYWD) merger.getMajor(bases.get(i), GRUP_TYPE.KYWD);
           KYWD newVar = (KYWD) merger.getMajor(vars.get(i), GRUP_TYPE.KYWD);
           SPGlobal.log("Armor pair", newBase.getEDID() + " " + newVar.getEDID());
           Pair<KYWD, KYWD> p = new Pair(newBase, newVar);
           armorMatches.add(p);
           SPGlobal.log("Armor pair", p.getBase().getEDID() + " " + p.getVar().getEDID());
       }
   }

There are a few things I'll point out here. First are the ArrayLists, these are handy objects that you can add and remove things from without worrying about them getting full. the <FormID> is what type of things we are putting in the ArrayList. ArrayLists are ordered, they remember what order you added things in and you can use that to get a specific thing back out. That's just what we do here: we fill the arraylists with the keywords in the magic formlists and then get them out in order. Pair<> is a tiny little class that associates two keywords, just a way to keep track of them. Then we add the Pair of keywords to another arraylist so we can get at them later. You might notice the log entries, they are all over the place. That is because I forgot how much easier it is to set breakpoints and use a debugger. Still they are useful to have around.

Building armor variantsEdit

This function really does two things. It builds a reference of the 'base' armors that I setup in the creation kit. Then it actually enchants the modded armors.

       for (ARMO armor : merger.getArmors()) {
           KYWD baseKey = armorHasAnyKeyword(armor, baseKeys, merger);
           if (baseKey != null) {
               SPGlobal.log(armor.getEDID(), "is base armor");
               ArrayList<FormID> alts = new ArrayList<>(0);
               alts.add(0, armor.getForm());
               armorVariants.add(alts);
           }
       }

Now we're getting to some real action. First you should know armorVariants is declared at the top of the file as a member variable, this lets the various functions all use it without the annoying passing back and forth I do with the formlists. private static ArrayList<ArrayList<FormID>> armorVariants = new ArrayList<>(0); That's it right there. An ArrayList of ArrayLists of FormIDs. This lets us keep track of as many lists of armors as we want. The for loop goes through each armor in the loaded data files and checks if it has any of the keywords on my formlist. Computers are pretty fast these days so even with lots of weapons or armor it doesn't take long at all to go through each one of them. If the armor has any keyword we make a new list, put the armor in it, and add that to the list of lists. In the CK I made some new keywords and put them on all the unenchanted armor pieces. Because the enchanted versions use templates they end up getting the new keywords too and it saves me a whole lot of time. So anything that ended up having one of my 'base' keywords is now on a list in the list of lists. We'll use that later.

       for (ARMO armor : merger.getArmors()) {
           KYWD variantKey = armorHasAnyKeyword(armor, varKeys, merger);
           if (variantKey != null) {
               for (int j = 0; j < armorVariants.size(); j++) {
                   ArrayList<FormID> a2 = armorVariants.get(j);
                   ARMO form = (ARMO) merger.getMajor((FormID) a2.get(0), GRUP_TYPE.ARMO);

We're going to run through each armor in the data files again so we use another for loop. This time we check if they have any of my 'variant' keywords. If they do we then we have to find what base armor they match so we look through the list of lists and get the first entry since the base armors were the first things we put in. I'll take a minute to point out the two main variable types Skyproc uses: FormIDs and MajorRecords. A formID is the number that represents and entry in the esm or esp. A MajorRecord is the actual entry in the esm or esp. Its kinda like this the FormID is the name Spot, it refers to a particular dog. Almost all the time you want to refer to that dog you use the name Spot. The MajorRecord is the dog itself, its not the name Spot that chews up your slippers but the dog itself. You can go from the FormID to the MajorRecord by using Mod.getMajor(FormID, GRUP_TYPEs) or from the major record to the FormID with MajorRecord.getForm(). You should also know that things like ARMOs and KYWDs are kinds of MajorRecords. Anything a MajorRecord can do a ARMO can do too, but not always the other way around. When we call getMajor() we have to tell java what kind of major record we'll get back before we can do any of the ARMO specific things with it which is what the (ARMO) part does.

To refresh now we are looking at a piece of armor with one of the variant keywords on it and each entry from the list of base armors we setup before.

                   if (armorHasKeyword(form, getBaseArmor(variantKey), merger)) {
                       ARMO replace = form;
                       FormID tmp = replace.getTemplate();
                       if (!tmp.isNull()) {
                           replace = (ARMO) merger.getMajor(tmp, GRUP_TYPE.ARMO);
                       }
                       for (BodyTemplate.FirstPersonFlags c : BodyTemplate.FirstPersonFlags.values()) {
                           boolean armorFlag = armor.getBodyTemplate().get(BodyTemplate.BodyTemplateType.Biped, c);
                           boolean formFlag = replace.getBodyTemplate().get(BodyTemplate.BodyTemplateType.Biped, c);
                           boolean flagMatch = (armorFlag == formFlag);
                           if (flagMatch == false) {
                               passed = false;
                           }
                       }

Now come some tricky bits, but first we make sure the base armor's keyword actually matches the variant's keyword. We don't want mix leather armor in with steel. Next we check if the base armor uses a template in the CK. Skyproc doesn't fully process templated records so we need to look at the item in the template instead. Then we look at the biped object slots on the two pieces of armor, if they are all the same we keep going, otherwise we move onto the next armor to check.

                       if (passed) {
                           //SPGlobal.log("variant found", armor.getEDID() + " is variant of " + form.getEDID());
                           FormID template = form.getTemplate();
                           //SPGlobal.log("template", template.getFormStr());
                           if (template.isNull()) {
                               a2.add(armor.getForm());
                               //SPGlobal.log("variant added", a2.contains(armor.getForm()) + " " + a2.size());
                           } else {
                               //SPGlobal.log("Enchant found", armor.getEDID() + "  " + form.getEDID());
                               String name = generateArmorName(armor, form, merger);
                               String newEdid = generateArmorEDID(armor, form, merger);
                               ARMO armorDupe = (ARMO) patch.makeCopy(armor, "DienesARMO" + newEdid);
                               //SPGlobal.log("armor copied", armorDupe.getEDID());
                               armorDupe.setEnchantment(form.getEnchantment());
                               armorDupe.setName(name);
                               a2.add(armorDupe.getForm());
                               patch.addRecord(armorDupe);

I'll be back in the morning to finish up.