Package skyproc

Source Code of skyproc.NiftyFunc

/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package skyproc;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
import lev.LInChannel;
import lev.Ln;
import skyproc.gui.SUMGUI;

/**
* A class to hold many common/useful functions.
*
* @author Justin Swanson
*/
public class NiftyFunc {

    static String[] validationSkip = {"DIAL"};
    static String recordLengths = "Record Lengths";
    static Map<FormID, MajorRecord> deepSubrecordCopyDB = new HashMap<>();

    /**
     * A common way to attach scripts to NPCs that normally cannot have scripts
     * attached<br> (Any NPC that is referenced by a LVLN)<br> is to give a
     * racial spell to them that has a magic effect that has the desired
     * script.<br><br> This function streamlines the process and gives you a
     * SPEL/MGEF setup that will attach the desired script.<br> Simply give this
     * SPEL to the NPC's race.<br><br> NOTE: Attaching a script attachment spell
     * to an NPCs race will affect ALL NPCs with that same race.<br> If you do
     * not want this, then consider using genSafeScriptAttachingRace().
     *
     * @param script Script to have the SPEL attach
     * @param uniqueID A unique string to differentiate the records from any
     * other SkyProc user's setups.<br> (using your mod's name is usually
     * sufficient)
     * @return The generated SPEL record that can be attached to any RACE to
     * have it attach the desired script.
     */
    public static SPEL genScriptAttachingSpel(ScriptRef script, String uniqueID) {
        String name = "SP_" + uniqueID + "_" + script.name.data + "_attacher";
        MGEF mgef = new MGEF(name + "_MGEF", name + "_MGEF");
        mgef.getScriptPackage().addScript(script);
        mgef.set(MGEF.SpellEffectFlag.HideInUI, true);
        SPEL spel = new SPEL(name + "_SPEL");
        spel.setSpellType(SPEL.SPELType.Ability);
        spel.addMagicEffect(mgef);
        return spel;
    }

    /**
     * A common way to attach scripts to NPCs that normally cannot have scripts
     * attached<br> (Any NPC that is referenced by a LVLN)<br> is to give a
     * racial spell to them that has a magic effect that has the desired
     * script.<br><br> This function streamlines the process and gives you a
     * duplicate race which will attach the desired script.<br> You can then set
     * it to be the target NPCs race. Since it is a duplicate, it will only
     * affect NPCs you explicitly attach it to, and not ALL NPCs that shared the
     * original race. <br> It is a full duplicate and will retain any settings
     * of the original race.
     *
     * @param script Script to have the SPEL attach
     * @param uniqueID A unique string to differentiate the records from any
     * other SkyProc user's setups.<br> (using your mod's name is usually
     * sufficient)
     * @param raceToDup Original race that you wish to duplicate.
     * @return A duplicate of the input race, with the only difference being it
     * has a script attachment racial spell.
     */
    public static RACE genSafeScriptAttachingRace(ScriptRef script, RACE raceToDup, String uniqueID) {
        SPEL attachmentSpel = genScriptAttachingSpel(script, uniqueID);
        RACE attachmentRace = (RACE) SPGlobal.getGlobalPatch().makeCopy(raceToDup);
        attachmentRace.addSpell(attachmentSpel.getForm());
        return attachmentRace;
    }

    /**
     * Checks the given template flag "chains" to see if the NPC is templated to
     * a Leveled List at any point. If it is, that Leveled List is returned;
     * Null if not.
     *
     * @param npc NPC formID to investigate.
     * @param templateFlagsToCheck Template flags to consider.
     * @return LVLN that it is templated to, or null.
     */
    public static LVLN isTemplatedToLList(FormID npc, NPC_.TemplateFlag... templateFlagsToCheck) {
        return isTemplatedToLList(npc, templateFlagsToCheck, 0);
    }

    /**
     * Checks the given template flag "chains" to see if the NPC is templated to
     * a Leveled List at any point. If it is, that Leveled List is returned;
     * Null if not.
     *
     * @param npc NPC to investigate.
     * @param templateFlagsToCheck Template flags to consider.
     * @return LVLN that it is templated to, or null.
     */
    public static LVLN isTemplatedToLList(NPC_ npc, NPC_.TemplateFlag... templateFlagsToCheck) {
        return isTemplatedToLList(npc.getForm(), templateFlagsToCheck);
    }

    static LVLN isTemplatedToLList(FormID npc, NPC_.TemplateFlag[] templateFlagsToCheck, int depth) {
        if (depth > 100) {
            return null; // avoid circular template overflows
        }

        if (templateFlagsToCheck.length == 0) {
            templateFlagsToCheck = NPC_.TemplateFlag.values();
        }

        NPC_ npcSrc = (NPC_) SPDatabase.getMajor(npc, GRUP_TYPE.NPC_);

        if (npcSrc != null && !npcSrc.getTemplate().equals(FormID.NULL)) {
            boolean hasTargetTemplate = false;
            for (NPC_.TemplateFlag flag : templateFlagsToCheck) {
                if (npcSrc.get(flag)) {
                    hasTargetTemplate = true;
                    break;
                }
            }
            if (!hasTargetTemplate) {
                return null;
            }

            NPC_ templateN = (NPC_) SPDatabase.getMajor(npcSrc.getTemplate(), GRUP_TYPE.NPC_);
            if (templateN != null) { // If template is an NPC, recursively chain the check
                return isTemplatedToLList(templateN.getForm(), templateFlagsToCheck, depth + 1);
            } else {
                return (LVLN) SPDatabase.getMajor(npcSrc.getTemplate(), GRUP_TYPE.LVLN);
            }
        }
        return null;
    }

    /**
     * Makes a new quest that starts immediately in-game, that has this script
     * attached to it.
     *
     * @param script The script to add to the quest.
     * @return
     */
    public static QUST makeScriptQuest(ScriptRef script) {
        QUST quest = new QUST(script.getName() + "_qust");
        quest.getScriptPackage().addScript(script);
        quest.setName(script.getName() + " Quest");
        return quest;
    }

    /**
     * A function that starts a new java program with more memory. Use this to
     * allocate more memory for your SkyProc program by simply putting the name
     * of your jar file as the jarpath. This function will automatically open a
     * second instance of your program, giving it more memory, and close the
     * first program if the second is opened properly.
     *
     * @param startingMem Memory to start the new program with.
     * @param maxMem Max amount of memory to allow the new program to use.
     * @param jarPath Path to the jar file to open. Usually, just put the name
     * of your jar.
     * @param args Any special main function args you want to give to the second
     * program.
     * @throws IOException
     * @throws InterruptedException
     */
    public static void allocateMoreMemory(String startingMem, String maxMem, String jarPath, String... args) throws IOException, InterruptedException {
        String[] argsInternal = new String[args.length + 5];
        argsInternal[0] = "java";
        argsInternal[1] = "-jar";
        argsInternal[2] = "-Xms" + startingMem;
        argsInternal[3] = "-Xmx" + maxMem;
        argsInternal[4] = jarPath;
        for (int i = 5; i < args.length + 5; i++) {
            argsInternal[i] = args[i - 5];
        }
        ProcessBuilder proc = new ProcessBuilder(argsInternal);
        Process start = proc.start();
        InputStream shellIn = start.getInputStream();
        int exitStatus = start.waitFor();
        String response = Ln.convertStreamToStr(shellIn);
        if (exitStatus != 0) {
            JOptionPane.showMessageDialog(null, "Error allocating " + maxMem + " of memory:\n"
                    + response
                    + "\nMemory defaulted to lowest levels.  Please lower your\n"
                    + "allocated memory in Other Settings and start the program again.");
        } else {
            System.exit(0);
        }
    }

    /**
     * Replaces all "naughty" characters with "" or "_"
     *
     * @param origEDID
     * @return
     */
    public static String EDIDtrimmer(String origEDID) {
        origEDID = origEDID.replaceAll(" ", "");
        origEDID = origEDID.replaceAll(":", "_");
        origEDID = origEDID.replaceAll("-", "_");
        return origEDID;
    }

    /**
     * Converts a string version to a unique number<br> Only supports
     * XX.XX.XX.XX with numbers for X
     *
     * @param version
     * @return
     */
    public static int versionToNum(String version) {
        String tmp = "";
        for (int i = 0; i < version.length(); i++) {
            if (Character.isDigit(version.charAt(i))
                    || version.charAt(i) == '.') {
                tmp += version.charAt(i);
            } else {
                break;
            }
        }
        version = tmp;
        String[] split = version.split("\\.");
        int out = 0;
        for (int i = 0; i < split.length && i < 4; i++) {
            int next = Integer.valueOf(split[i]) * 1000000;
            if (i != 0) {
                next /= Math.pow(100, i);
            }
            out += next;
        }
        return out;
    }

    /**
     * Reads in the file and confirms that all GRUPs and Major Records have
     * correct lengths. It does not explicitly check subrecord lengths, but due
     * to the recursive nature of SkyProc, these will be implicitly checked as
     * well by confirming Major Record length.
     *
     * This will bug out if strings inside records contain major record types
     * (ex. "SomeEDIDforMGEF", would fail because of "MGEF")
     *
     * @param testFile File to test.
     * @param numErrorsToPrint Number of error messages to print before
     * stopping.
     * @return True if the file had correct record lengths.
     */
    public static boolean validateRecordLengths(File testFile, int numErrorsToPrint) {
        return validateRecordLengths(testFile.getPath(), numErrorsToPrint);
    }

    /**
     * Reads in the file and confirms that all GRUPs and Major Records have
     * correct lengths. It does not explicitly check subrecord lengths, but due
     * to the recursive nature of SkyProc, these will be implicitly checked as
     * well by confirming Major Record length.
     *
     * This will bug out if strings inside records contain major record types
     * (ex. "SomeEDIDforMGEF", would fail because of "MGEF")
     *
     * @param testFilePath Path to the file to test.
     * @param numErrorsToPrint Number of error messages to print before
     * stopping.
     * @return True if the file had correct record lengths.
     */
    public static boolean validateRecordLengths(String testFilePath, int numErrorsToPrint) {
        boolean correct = true;
        int numErrors = 0;
        ArrayList<String> skip = new ArrayList<>(Arrays.asList(validationSkip));
        try {
            File file = new File(testFilePath);
            if (file.isFile()) {
                SPGlobal.log("Validate", "Target file exists: " + file);
            } else {
                SPGlobal.log("Validate", "Target file does NOT exist: " + file);
            }
            LInChannel input = new LInChannel(testFilePath);

            correct = testHeaderLength(input);

            String inputStr;
            //Test GRUPs
            String majorRecordType = "NULL";
            int grupLength = 0;
            long grupPos = input.pos();
            int length;
            long start = 0;
            String EDID = "";
            Map<Integer, String> formids = new HashMap<>();
            Map<Integer, String> dupIds = new HashMap<>();
            while (input.available() >= 4 && (numErrors < numErrorsToPrint || numErrorsToPrint == 0)) {

                inputStr = input.extractString(0, 4);
                if (inputStr.equals("GRUP")) {
                    long inputPos = input.pos();
                    if (inputPos - grupPos - 4 != grupLength) {
                        SPGlobal.logError(recordLengths, "GRUP " + majorRecordType + " is wrong. (" + Ln.prettyPrintHex(grupPos) + ")");
                        numErrors++;
                        correct = false;
                    }
                    grupPos = input.pos() - 4;
                    grupLength = input.extractInt(0, 4);
                    majorRecordType = input.extractString(0, 4);
                    if (skip.contains(majorRecordType)) {
                        input.skip(grupLength - 12);
                    } else {
                        input.skip(12);
                    }
                } else if (inputStr.equals(majorRecordType)) {
                    start = input.pos() - 4;
                    length = input.extractInt(0, 4);
                    input.skip(4);
                    int formID = input.extractInt(4);
                    input.skip(8);
                    String subRecordType = input.extractString(0, 4);
                    if (subRecordType.equalsIgnoreCase("EDID")) {
                        int edidLength = input.extractInt(0, 2);
                        EDID = input.extractString(0, edidLength - 1);
                        input.skip(length - 6 - EDID.length()); // 4 from subrecord type 'EDID' + 2 from length of EDID subrecord
                        if (formids.containsKey(formID)) {
                            dupIds.put(formID, EDID);
                        } else {
                            formids.put(formID, EDID);
                        }
                    } else {
                        EDID = "No EDID subrecord";
                        input.skip(length - 4); // 4 from subrecord type
                    }
                } else {
                    SPGlobal.logError(recordLengths, "Major Record: " + majorRecordType + " | " + EDID + " is wrong. (" + Ln.prettyPrintHex(start) + ")");
                    numErrors++;
                    correct = false;
                }
            }

            if (!dupIds.isEmpty()) {
                SPGlobal.logError(recordLengths, "Duplicate FormIDs: ");
                for (int id : dupIds.keySet()) {
                    SPGlobal.logError(recordLengths, Ln.printHex(id) + ", EDIDS: " + dupIds.get(id) + ", and " + formids.get(id));
                }
                correct = false;
            }

            input.close();

        } catch (FileNotFoundException ex) {
            SPGlobal.logError(recordLengths, "File could not be found.");
            SPGlobal.logException(ex);
        } catch (IOException ex) {
            SPGlobal.logError(recordLengths, "File I/O error.");
            SPGlobal.logException(ex);
        }

        if (correct) {
            SPGlobal.log(recordLengths, "Validated.");
        } else {
            SPGlobal.logError(recordLengths, "NOT Validated.");
        }
        return correct;
    }

    static boolean testHeaderLength(LInChannel input) throws IOException {
        // Check header
        String inputStr;
        boolean correct = true;
        int length = input.extractInt(4, 4);
        input.skip(length + 16);
        if (input.available() > 0) {
            // Next should be a GRUP
            inputStr = input.extractString(0, 4);
            if (!"GRUP".equals(inputStr)) {
                SPGlobal.logError("Mod Header", "Header length is wrong.");
                correct = false;
            }
            input.skip(-4);
        } else if (input.available() < 0) {
            SPGlobal.logError("Mod Header", "Header length is wrong.");
            correct = false;
        } else {
            SPGlobal.logError("Mod Header", "File header was correct, but there were no GRUPS.  Validated.");
            return true;
        }
        return correct;
    }

    /**
     * Starts a process as if from the command line, with the given working
     * directory.
     *
     * @param directory
     * @param args
     * @return
     */
    static public boolean startProcess(File directory, String... args) {
        try {
            ProcessBuilder proc = new ProcessBuilder(args);
            if (directory != null) {
                proc.directory(directory);
            }
            Process start = proc.start();
            InputStream shellIn = start.getInputStream();
            int exitStatus = start.waitFor();
            String response = Ln.convertStreamToStr(shellIn);
            if (exitStatus != 0) {
                String tmp = "";
                for (String arg : args) {
                    tmp += " " + arg;
                }
                SPGlobal.logError("StartProcess", "Process with args " + tmp + " Failed to run: " + response);
                return false;
            }
        } catch (IOException | InterruptedException ex) {
            SPGlobal.logException(ex);
            return false;
        }
        return true;
    }

    /**
     * Starts a process as if from the command line.
     *
     * @param args
     * @return
     */
    static public boolean startProcess(String... args) {
        return startProcess(null, args);
    }

    /**
     * Trims or expands a string (with _) to have exactly four characters.
     *
     * @param in
     * @return
     */
    static public String trimToFour(String in) {
        if (in.length() > 4) {
            return in.substring(0, 3);
        } else if (in.length() < 4) {
            return Ln.spaceRight(4, '_', in);
        } else {
            return in;
        }
    }

    /**
     * Replaces formIDs in arraylist.
     *
     * @param src Target arraylist to look in
     * @param target FormID to replace
     * @param with FormIDs to substitute in place of target
     * @return Number of targets replaced
     */
    static public int replaceAll(ArrayList<FormID> src, FormID target, FormID... with) {
        ArrayList<FormID> tmp = new ArrayList<>(src);
        int numChanges = 0;
        for (int i = tmp.size() - 1; i >= 0; i--) {
            if (tmp.get(i).equals(target)) {
                numChanges++;
                src.remove(i);
                for (FormID id : with) {
                    src.add(i, id);
                }
            }
        }
        return numChanges;
    }

    static public int replaceAll(ArrayList<FormID> src, FormID target, ArrayList<FormID> with) {
        return replaceAll(src, target, with.toArray(new FormID[0]));
    }

    static public Map<FormID, Integer> replaceIDs(ArrayList<FormID> src, Map<FormID, FormID> replacements) {
        Map<FormID, Integer> out = new HashMap<>(replacements.size());
        for (FormID id : replacements.keySet()) {
            out.put(id, 0);
        }
        for (FormID id : src) {
            FormID replace = replacements.get(id);
            if (replace != null) {
                out.put(replace, out.get(replace) + 1);
                id.setTo(id);
            }
        }
        return out;
    }

    static public Map<FormID, Integer> replaceMajors(ArrayList<FormID> src, Map<FormID, MajorRecord> replacements) {
        Map<FormID, Integer> out = new HashMap<>(replacements.size());
        for (FormID id : replacements.keySet()) {
            out.put(id, new Integer(0));
        }
        for (FormID id : src) {
            MajorRecord replace = replacements.get(id);
            if (replace != null) {
                Integer i = out.get(id);
                out.put(id, i + 1);
                id.setTo(id);
            }
        }
        return out;
    }

    /**
     * Creates empty files for non existent mods.
     *
     * @param mods
     * @throws IOException
     */
    public static void setupMissingPatchFiles(ArrayList<Mod> mods) throws IOException {
        setupMissingPatchFiles(mods.toArray(new Mod[0]));
    }

    /**
     * Creates empty files for non existent mods.
     *
     * @param mods
     * @throws IOException
     */
    public static void setupMissingPatchFiles(Mod... mods) throws IOException {
        // Handle non-existant patchers
        for (Mod newPatcher : mods) {
            File path = new File(SPGlobal.pathToData + newPatcher.getName());
            // Export tmp patch as a placeholder
            if (!path.isFile()) {
                BufferedWriter placeholder = new BufferedWriter(new FileWriter(SPGlobal.pathToData + newPatcher.getName()));
                placeholder.close();
            }
        }
    }

    /**
     * Adds and removes desired mods from the plugins list.
     *
     * @param add Mods to add if they don't already exist on the list
     * @param remove Mods to remove if they do exist.
     * @throws IOException
     */
    public static void modifyPluginsTxt(ArrayList<Mod> add, ArrayList<Mod> remove) throws IOException {
        //Read in plugins.txt
        ArrayList<String> pluginsLines = Ln.loadFileToStrings(SPGlobal.getPluginsTxt(), false);

        // Remove unwanted mods
        if (remove != null) {
            for (Mod r : remove) {
                Ln.removeIgnoreCase(pluginsLines, r.getName());
            }
        }

        // Find missing lines on plugins.txt
        if (add != null) {
            for (Mod newPatcher : add) {
                if (!Ln.containsEqualsIgnoreCase(pluginsLines, newPatcher.getName())) {
                    // Add listing to plugins.txt
                    pluginsLines.add(newPatcher.getName());
                }
            }
        }

        // Write out new plugins.txt
        BufferedWriter pluginsOut = new BufferedWriter(new FileWriter(SPGlobal.getPluginsTxt()));
        for (String line : pluginsLines) {
            pluginsOut.write(line + "\n");
        }

        pluginsOut.close();
    }

    /**
     * Adds a mod to the plugins list if it doesn't exist.
     *
     * @param add
     * @throws IOException
     */
    public static void modifyPluginsTxt(Mod add) throws IOException {
        ArrayList<Mod> addL = new ArrayList<>(1);
        addL.add(add);
        modifyPluginsTxt(addL, null);
    }

    /**
     * Runs BOSS and sorts the load order. Does not update BOSS before running
     * it.
     *
     * @param errorMessages
     */
    public static void runBOSS(boolean errorMessages) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                SUMGUI.progress.setStatusNumbered("Running BOSS");
            }
        });
        // Find BOSS
        SPGlobal.logMain("BOSS", "Looking for BOSS.");
        int response = JOptionPane.YES_OPTION;
        String bossPath = WinRegistry.WinRegistry.getRegistryEntry("BOSS", "Installed Path");
        File bossExe = new File(".");
        if (bossPath != null) {
            bossExe = new File(bossPath + "\\BOSS.exe");
        }
        if (!bossExe.isFile()) {
            try {
                bossExe = Ln.manualFindFile("BOSS.exe", new File(SPGlobal.pathToInternalFiles + "BOSS location"));
            } catch (IOException ex) {
                SPGlobal.logException(ex);
            }
        }

        // Run BOSS
        if (bossExe != null && bossExe.isFile()) {
            SPGlobal.logMain("BOSS", "Running BOSS.");
            if (!NiftyFunc.startProcess(bossExe.getParentFile(), new String[]{bossExe.getPath(), "-s", "-U", "-g", "Skyrim"})) {
                SPGlobal.logMain("BOSS", "BOSS failed to run.");
                if (errorMessages) {
                    response = JOptionPane.showConfirmDialog(null, "BOSS failed to run. Do you want to continue?", "BOSS failed", JOptionPane.YES_NO_OPTION);
                }
            }
        } else if (errorMessages) {
            SPGlobal.logMain("BOSS", "BOSS could not be found.");
            response = JOptionPane.showConfirmDialog(null, "BOSS could not be located.\n"
                    + "It is highly recommended you download BOSS so that it can be used.\n\n"
                    + "Do you want to continue patching without BOSS?", "Cannot locate BOSS", JOptionPane.YES_NO_OPTION);
        }
        if (response == JOptionPane.NO_OPTION) {
            SPGlobal.logMain("BOSS", "Exiting program due to BOSS failure.");
            SUMGUI.exitProgram(false, true);
        }
        SPGlobal.logMain("BOSS", "BOSS complete.");
    }

    /**
     * Copies each major record from the target mod that is referenced in the
     * major record. This makes the major record "self contained" from the
     * target mod.
     *
     * @param in
     * @param targetMod
     * @return
     */
    public static ArrayList<MajorRecord> deepCopySubRecords(MajorRecord in, ModListing targetMod) {
        ArrayList<MajorRecord> out = new ArrayList<>();
        ArrayList<FormID> allIDs = in.allFormIDs();
        allIDs.remove(in.getForm());
        for (FormID id : allIDs) {
            MajorRecord m = SPDatabase.getMajor(id);
            if (m != null
                    && (id.getMaster().equals(targetMod) // From target mod
                    || !SPDatabase.getMod(id.getMaster()).contains(id) // Or missing "insert"
                    )) {
                MajorRecord copy = deepSubrecordCopyDB.get(id);
                if (copy == null) {
                    String edid = m.getEDID();
                    if (!m.getType().equals("KYWD") && !m.getType().equals("GMST")) {
                        edid += "_deepCopy";
                    }
                    copy = m.copy(edid);
                    deepSubrecordCopyDB.put(id, copy);
                    deepCopySubRecords(copy, targetMod);
                }
                id.setTo(copy.getForm());
                out.add(copy);
            }
        }
        return out;
    }

    /**
     * Checks global path for duplicate record. If none is found, in is
     * returned. If a duplicate is found, in is removed from the global patch
     * and the duplicate is returned.<br><br>
     *
     * deepEquals is used to determine if two records are equal.
     *
     * @param in Major Record to check for duplicates.
     * @return A record from global patch that is unique.
     */
    public static MajorRecord mergeDuplicate(MajorRecord in) {
        GRUP_TYPE g = GRUP_TYPE.valueOf(in.getType());
        ArrayList<MajorRecord> grup = new ArrayList<>(SPGlobal.getGlobalPatch().getGRUPs().get(g).getRecords());
        grup.remove(in);
        for (MajorRecord existing : grup) {
            if (in.deepEquals(existing)) {
                SPGlobal.getGlobalPatch().remove(in.getForm());
                if (SPGlobal.logging()) {
                    SPGlobal.log("NiftyFunc", in + " duplicate of " + existing);
                }
                return existing;
            }
        }
        return in;
    }
}
TOP

Related Classes of skyproc.NiftyFunc

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.