diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..63ea6b66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ + +buildSrc/.gradle +buildSrc/build +distribution +meta.ini +Java/*/build +Java/*/nbproject/private/ +Java/*/dist/ +Java/*/store +.gradle +.idea +*iml +*ipr +*iws +PapyrusLogs +userSetup.gradle +Scripts/*.pex +distribution/* +Java/Reqtificator/src/main/resources/version.properties +documentation/**.aux +documentation/**.log +documentation/*/*.pdf +documentation/*/*.png +Interface/Translations/Requiem_CZECH.txt +Interface/Translations/Requiem_ENGLISH.txt +Interface/Translations/Requiem_FRENCH.txt +Interface/Translations/Requiem_GERMAN.txt +Interface/Translations/Requiem_ITALIAN.txt +Interface/Translations/Requiem_JAPANESE.txt +Interface/Translations/Requiem_POLISH.txt +Interface/Translations/Requiem_RUSSIAN.txt +Interface/Translations/Requiem_SPANISH.txt +.kotlintest +fomod/info.xml +SkyProc Patchers/Requiem/app +SkyProc Patchers/Requiem/legacy/Reqtificator.jar +build/ \ No newline at end of file diff --git a/Interface/Requiem/MCM_background.dds b/Interface/Requiem/MCM_background.dds new file mode 100644 index 00000000..50abcae0 Binary files /dev/null and b/Interface/Requiem/MCM_background.dds differ diff --git a/Interface/Requiem/MCM_background.xcf b/Interface/Requiem/MCM_background.xcf new file mode 100644 index 00000000..94923a17 Binary files /dev/null and b/Interface/Requiem/MCM_background.xcf differ diff --git a/Interface/Requiem/MCM_background_warning.dds b/Interface/Requiem/MCM_background_warning.dds new file mode 100644 index 00000000..72d74762 Binary files /dev/null and b/Interface/Requiem/MCM_background_warning.dds differ diff --git a/Interface/Requiem/MCM_background_warning.xcf b/Interface/Requiem/MCM_background_warning.xcf new file mode 100644 index 00000000..9de7bbc2 Binary files /dev/null and b/Interface/Requiem/MCM_background_warning.xcf differ diff --git a/Interface/Requiem/MCM_skills_background.dds b/Interface/Requiem/MCM_skills_background.dds new file mode 100644 index 00000000..b31f23c4 Binary files /dev/null and b/Interface/Requiem/MCM_skills_background.dds differ diff --git a/Interface/Requiem/MCM_skills_background.xcf b/Interface/Requiem/MCM_skills_background.xcf new file mode 100644 index 00000000..0ef15fdc Binary files /dev/null and b/Interface/Requiem/MCM_skills_background.xcf differ diff --git a/Interface/Translations/Requiem_TEMPLATE.txt b/Interface/Translations/Requiem_TEMPLATE.txt new file mode 100644 index 00000000..25b7aad7 Binary files /dev/null and b/Interface/Translations/Requiem_TEMPLATE.txt differ diff --git a/Java/Reqtificator/build.gradle.kts b/Java/Reqtificator/build.gradle.kts new file mode 100644 index 00000000..2e3e7b8c --- /dev/null +++ b/Java/Reqtificator/build.gradle.kts @@ -0,0 +1,102 @@ +import skyrim.requiem.build.VersionFileTask + +plugins { + application + java + kotlin("jvm") + id("org.jlleitschuh.gradle.ktlint") + id("org.beryx.jlink") +} + +val reqtificatorDir = objects.directoryProperty() +reqtificatorDir.set(file("$rootDir/SkyProc Patchers/Requiem/app")) + +dependencies { + implementation(kotlin("stdlib-jdk8")) + implementation("com.typesafe:config:1.3.1") + implementation("org.apache.logging.log4j:log4j-api:2.13.0") + implementation("org.apache.logging.log4j:log4j-core:2.13.0") + implementation(project(":Java:SkyProc")) + testImplementation("io.kotlintest:kotlintest-runner-junit5:3.3.2") + testImplementation("io.mockk:mockk:1.9.3") + testImplementation("net.bytebuddy:byte-buddy:1.10.6") +} + +val createVersionFile = tasks.register("createVersionFile") { + val mercurialRevision: String by rootProject.extra + val mercurialBranch: String by rootProject.extra + val mercurialBookmarks: String by rootProject.extra + + group = "build" + description = "store Mercurial revision information in a properties file" + + revision = mercurialRevision + branch = mercurialBranch + tags = mercurialBookmarks + versionFile = file("file:/$projectDir/src/main/resources/version.properties") +} + +tasks.processResources { + dependsOn(createVersionFile) +} + +tasks.jar { + manifest { + attributes( + mapOf( + "Implementation-Title" to "Reqtificator - SkyProc Patcher for the Skyrim mod 'Requiem'", + "Implementation-Version" to archiveVersion + ) + ) + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +val cleanReqtificator = tasks.register("cleanReqtificator") { + group = "build" + description = "remove the deployed Reqtificator" + + delete(reqtificatorDir) + delete(file("file:/$projectDir/src/main/resources/version.properties")) +} + +tasks.clean { + dependsOn(cleanReqtificator) +} + +tasks.compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } + destinationDir = tasks.compileJava.map { it.destinationDir }.get() +} +tasks.compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +tasks.test { + useJUnitPlatform() +} + +val moduleName by project.extra("skyrim.requiem") + +tasks.compileJava { + inputs.property("moduleName", moduleName) + doFirst { + options.compilerArgs = listOf("--module-path", classpath.asPath) + classpath = files() + } +} + +jlink { + setProperty("mainClass", "Reqtificator.Reqtificator") + setProperty("options", listOf("--compress", "2", "--no-header-files", "--no-man-pages")) + setProperty("imageDir", reqtificatorDir) + forceMerge("log4j-api", "config") + + launcher { + name = "launcher_template" + } +} \ No newline at end of file diff --git a/Java/Reqtificator/src/main/java/Reqtificator/ConsistencyManager.java b/Java/Reqtificator/src/main/java/Reqtificator/ConsistencyManager.java new file mode 100644 index 00000000..b312521e --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/ConsistencyManager.java @@ -0,0 +1,243 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator; + +import Reqtificator.exceptions.SetupException; +import Reqtificator.exceptions.SilentException; +import Reqtificator.logging_and_gui.TextManager; +import skyproc.ModListing; +import skyproc.SPDatabase; +import skyproc.SPGlobal; +import skyrim.requiem.fptools.Option; +import skyrim.requiem.gui.PopupTools; +import skyrim.requiem.gui.PopupTools.Companion.PopupType; +import skyrim.requiem.localization.Translatable; +import skyrim.requiem.localization.TextReference; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.text.MessageFormat; +import java.util.Calendar; + +/** + * A management class for assigning FormIDs in a consistent way. + * This helper + * class takes care of managing SkyProc's internal consistency files and tries + * to prevent the user from accidentally deleting mandatory auxiliary files to + * continue existing save games when updating Requiem. + * + * @author Ogerboss + */ +public class ConsistencyManager { + + private TextManager texts; + private final PopupTools popupManager; + private final File backuppath; + private final String consistencyFileName; + private final String MetaFileName = "ConsistencyMetaData.txt"; + + private enum UserReaction implements Translatable { + Cancel { + @Override + public TextReference getText() { + return new TextReference("gui.consistency.cancel"); + } + }, + FreshInstall { + @Override + public TextReference getText() { + return new TextReference("gui.consistency.fresh"); + } + }; + } + + /** + * Create a new ConsistencyManager. + *

+ * In addition to loading the required strings, this constructor will also + * create the backup directory in My Documents/Skyrim if it doesn't exist + * yet. + * + * @throws SetupException if the My Documents/Skyrim folder was not readable + */ + public ConsistencyManager(TextManager texts, PopupTools popupManager, String consistencyFileName) + throws SetupException { + this.texts = texts; + this.popupManager = popupManager; + this.consistencyFileName = consistencyFileName; + try { + backuppath = new File(SPGlobal.getMyDocumentsSkyrimFolder(), + "Requiem/"); + if (!backuppath.exists()) { + Files.createDirectory(backuppath.toPath()); + } + } catch (IOException e) { + String message = texts.format( + "patch.consistency.my_documents_not_found", + "My Documents/Skyrim", e.toString()); + throw new SetupException("My Documents not found", message, e); + } + } + + /** + * Find the Consistency file to use and warn user if it is missing. + *

+ * If the default location doesn't contain any usable consistency file, the + * user is informed about the situation and whether a backup has been found. + * The user can then either abort the process to recover the backup or start + * from scratch to patch for a new game. + * + * @throws SilentException if default location provides no match and the + * user decides to abort the patch generation + */ + public void checkConsistencyFile() throws SilentException { + File standard = new File(".", consistencyFileName); + File backup = new File(backuppath, consistencyFileName); + + if (!standard.exists()) { + TextReference message; + UserReaction defaultChoice; + if (backup.exists()) { + message = new TextReference("patch.consistency.backup_found", standard.getAbsolutePath(), + backup.getAbsolutePath()); + defaultChoice = UserReaction.Cancel; + } else { + message = new TextReference("patch.consistency.no_backup", consistencyFileName); + defaultChoice = UserReaction.FreshInstall; + } + Option choice = popupManager.showPopupQuestion( + new TextReference("patch.consistency.backup_title"), + message, + UserReaction.values(), + defaultChoice, + PopupType.Warning); + if (choice.getOrElse(() -> UserReaction.Cancel).equals(UserReaction.Cancel)) { + throw new SilentException("aborted to recover backup consistency file"); + } + } + } + + + /** + * Write consistency file meta data and update external backups. + * + * @throws SetupException if any file operation fails + */ + public void backupConsistencyData() throws SetupException { + File tempMeta = new File(MessageFormat.format("{0}_tmp", MetaFileName)); + File fileMeta = new File(MetaFileName); + + writeTempMetaFile(tempMeta); + try { + Files.move(tempMeta.toPath(), fileMeta.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + String message = texts.format("patch.consistency.not_writable", + texts.format("urls.security_settings", true), + fileMeta.getAbsolutePath()); + throw new SetupException("cannot write consistency file", message, + e); + } + updateBackups(); + } + + private void writeTempMetaFile(File temppath) + throws SetupException { + try (FileWriter fstream = new FileWriter(temppath); + BufferedWriter writer = new BufferedWriter(fstream)) { + String end = System.getProperty("line.separator"); + writer.write("This file contains the meta data for the " + + "consistency file used by the Reqtificator, " + + "Requiem's SkyProc Patcher." + end); + writer.write("It is intended to help you to recover the correct " + + "backup file if you accidentally deleted the " + + "original version." + end); + writer.write(MessageFormat.format("Creation Date: " + + "{0,date,full} {0,time,full}{1}", + Calendar.getInstance().getTime(), end)); + writer.write("Imported Load Order this Consistency File was " + + "based on:" + end); + for (ModListing mod : SPDatabase.getImportedModListings()) { + writer.write(MessageFormat.format( + "ModIndex: {0,number,###} - {1}{2}", + SPDatabase.modIndex(mod), mod, end)); + } + } catch (IOException e) { + String message = texts.format("patch.consistency.not_writable", + texts.format("urls.security_settings", true), + temppath.getAbsolutePath()); + throw new SetupException("consistency metafile not writable", + message, e); + } + } + + private void updateBackups() + throws SetupException { + File metaFile = new File(backuppath, MetaFileName + "_5"); + File consistencyFile = new File(backuppath, consistencyFileName + "_5"); + + File copySource = new File("."); + File copyTarget = new File("."); + try { + for (int i = 4; i >= 2; i--) { + copyTarget = metaFile; + copySource = new File(backuppath, + MetaFileName + "_" + i); + if (copySource.exists()) { + Files.move(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + metaFile = copySource; + + copyTarget = consistencyFile; + copySource = new File(backuppath, + consistencyFileName + "_" + i); + if (copySource.exists()) { + Files.move(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + consistencyFile = copySource; + } + + copyTarget = metaFile; + copySource = new File(backuppath, MetaFileName); + if (copySource.exists()) { + Files.move(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + metaFile = copySource; + + copyTarget = consistencyFile; + copySource = new File(backuppath, consistencyFileName); + if (copySource.exists()) { + Files.move(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } + consistencyFile = copySource; + + copyTarget = consistencyFile; + copySource = new File(consistencyFileName); + Files.copy(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + + copyTarget = metaFile; + copySource = new File(MetaFileName); + Files.copy(copySource.toPath(), copyTarget.toPath(), + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + String message = texts.format("patch.consistency.copy_failure", + texts.format("urls.security_settings", true), + copySource.getAbsolutePath(), copyTarget.getAbsolutePath()); + throw new SetupException("backup file rotation failed", message, e); + } + } + +} \ No newline at end of file diff --git a/Java/Reqtificator/src/main/java/Reqtificator/ExceptionManager.java b/Java/Reqtificator/src/main/java/Reqtificator/ExceptionManager.java new file mode 100644 index 00000000..69776935 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/ExceptionManager.java @@ -0,0 +1,112 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator; + +import Reqtificator.components.Component; +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.SetupException; +import Reqtificator.exceptions.SilentException; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import skyproc.ModListing; +import skyproc.SPDefaultExceptionHandler; +import skyproc.exceptions.BadMod; +import skyproc.exceptions.MissingMaster; +import skyproc.gui.SUMGUI; +import skyrim.requiem.exceptions.LoadOrderIssueDetectedException; +import skyrim.requiem.exceptions.ReqtificatorException; +import skyrim.requiem.exceptions.SetupRequirementFailedException; +import skyrim.requiem.gui.PopupTools; +import skyrim.requiem.gui.PopupTools.Companion.PopupType; +import skyrim.requiem.localization.TextFormatter; + +/** + * @author Ogerboss + */ +public class ExceptionManager extends SPDefaultExceptionHandler { + + private final static Logger logger = LogManager.getLogger(); + + private final TextFormatter formatter; + private final TextManager texts; + private final PopupTools popupTools; + + /** + * Create a new ExceptionManager. + * + * @param texts the TextManager instance to load GUI text from + */ + public ExceptionManager(TextManager texts, TextFormatter formatter, PopupTools popupTools) { + this.texts = texts; + this.formatter = formatter; + this.popupTools = popupTools; + } + + @Override + public void handleCriticalException(Exception error) { + String title = texts.format("error_handling.popup_title"); + if (!(error instanceof UnexpectedException || error instanceof SilentException + || error instanceof LoadOrderIssueDetectedException)) { + logger.error("critical exception encountered", error); + } + if (error instanceof SilentException) { + SilentException convert = (SilentException) error; + logger.info("user canceled patch: " + convert.log); + } else if (error instanceof LoadOrderIssueDetectedException) { + LoadOrderIssueDetectedException convert = (LoadOrderIssueDetectedException) error; + logger.info("user canceled patch: " + formatter.format(convert.getMessageTemplate())); + } else if (error instanceof SetupException) { + popupTools.showPopupMessage(title, ((SetupException) error).gui, PopupType.Error); + } else if (error instanceof SetupRequirementFailedException) { + SetupRequirementFailedException convert = (SetupRequirementFailedException) error; + popupTools.showPopupMessage(title, formatter.format(convert.getMessageTemplate()), PopupType.Error); + } else if (error instanceof ReqtificatorException) { + ReqtificatorException convert = (ReqtificatorException) error; + popupTools.showPopupMessage(title, formatter.format(convert.getMessageTemplate()), PopupType.Error); + } else if (error instanceof PatchingException) { + popupTools.showPopupMessage(title, ((PatchingException) error).gui, PopupType.Error); + } else if (error instanceof UnexpectedException) { + UnexpectedException convert = (UnexpectedException) error; + String message = texts.format("error_handling.unexpected_error", + Component.formatRecordPretty(convert.failedRecord), + convert.getCause().toString(), + texts.format("urls.service_desk", true)); + popupTools.showPopupMessage(title, message, PopupType.Error); + } else if (error instanceof MissingMaster) { + MissingMaster convert = (MissingMaster) error; + StringBuilder sB = new StringBuilder(); + for (ModListing master : convert.notinstalled) { + sB.append(texts.format("error_handling.masters_missing", + master.print())); + } + for (ModListing master : convert.inactive) { + sB.append(texts.format("error_handling.masters_inactive", + master.print())); + } + for (ModListing master : convert.loadedafter) { + sB.append(texts.format("error_handling.masters_loadafter", + master.print())); + } + String message = texts.format("error_handling.masters_error", + convert.failedmod.print(), sB.toString()); + popupTools.showPopupMessage(title, message, PopupType.Error); + } else if (error instanceof BadMod) { + BadMod convert = (BadMod) error; + String message = texts.format("error_handling.corrupt_mod", + convert.failedmod.print()); + popupTools.showPopupMessage(title, message, PopupType.Error); + } else { + String message = texts.format("error_handling.general_error", + texts.format("urls.service_desk", true), + error.toString()); + popupTools.showPopupMessage(title, message, PopupType.Error); + } + SUMGUI.exitProgram(false, true); + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/FormIDStash.java b/Java/Reqtificator/src/main/java/Reqtificator/FormIDStash.java new file mode 100644 index 00000000..677e1425 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/FormIDStash.java @@ -0,0 +1,40 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator; + +import skyproc.FormID; +import skyproc.GRUP_TYPE; +import skyproc.MajorRecord; +import skyproc.Mod; +import skyproc.ModListing; + +/** + * This class is simply a centralized connection of all special FormIDs that are + * hardcoded into the patcher. + * @author Ogerboss + */ +public class FormIDStash { + public static final ModListing SKY = new ModListing("Skyrim.esm"); + public static final ModListing REQ = new ModListing("Requiem.esp"); + + public static final FormID formlist_ME_races = new FormID("A3C6DD", REQ); + public static final FormID formlist_GM_perks = new FormID("8F57EA", REQ); + public static final FormID formlist_closedzones = new FormID("A46546", REQ); + public static final FormID formlist_LLmerge_highpriority + = new FormID("AD36E7", REQ); + public static final FormID formlist_LLmerge_mediumpriority + = new FormID("AD36E6", REQ); + public static final FormID formlistHelpTopcicsPC = new FormID("000163", SKY); + public static final FormID formlistHelpTopcicsXbox = new FormID("000165", SKY); + public static final FormID formlistHelpTopcicsRequiem = new FormID("AD3A1A", REQ); + + public static final FormID spell_ME_base = new FormID("7E76F4", REQ); + public static final FormID spell_ME_npc = new FormID("82CC14", REQ); + + public static final FormID PROTOTYPE_CONTAINER_LOCKED = new FormID("003E38", REQ); + public static final FormID PROTOTYPE_DOOR_LOCKED = new FormID("AD38BA", REQ); +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/OldReqtificatorConfiguration.java b/Java/Reqtificator/src/main/java/Reqtificator/OldReqtificatorConfiguration.java new file mode 100644 index 00000000..6643d03e --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/OldReqtificatorConfiguration.java @@ -0,0 +1,194 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator; + +import Reqtificator.enums.ConfigSections; +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.SetupException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.Mod; +import skyproc.ModListing; +import skyproc.SPDatabase; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/**This class extracts Reqtificator-specific data from the setup. + * This class does two things when created: First, it parses the + * Reqtificator.ini to identify the mods that qualify as visual templates. + * Afterwards it goes through the imported plugins and checks all Requiem- + * dependent plugins for REQ-Tags. The results are then stored in public fields. + * + * @author Ogerboss + */ +public class OldReqtificatorConfiguration { + + private final static Logger logger = LogManager.getLogger(); + private TextManager texts; + + /**Mods registered for Tempered Lists. + */ + public final Set modsWithTemperedItems; + /**Mods registered for Compact Leveled Lists. + */ + public final Set modsWithCompactLists; + /** Mods registered for ActorVariations. + */ + public final Set modsWithActorVariations; + + /**The settings from the parsed configuration file. + * This map contains the known sections of the config-file and maps them to + * the set of mods the user has specified for this category. + */ + public final Map> settings; + + /** + * Creates a new ReqtificatorConfiguration instance. During setup, it will parse the + * given file for mods that qualify as visual templates for automated visual + * merging. Furthermore it will process the list of imported mods for + * REQ-Tags in Requiem-patches to identify patches that qualify for special + * treatment. + * + * @param configfile path to the file used to set up visual templates + * @throws PatchingException if the Reqtificator.ini contains inconsistent + * data or a mod has wrong REQ-Tags + * @throws SetupException if the Reqtificator.ini cannot be read + */ + public OldReqtificatorConfiguration(String configfile, TextManager texts) + throws PatchingException, SetupException { + this.texts = texts; + modsWithActorVariations = new HashSet<>(); + modsWithTemperedItems = new HashSet<>(); + modsWithCompactLists = new HashSet<>(); + settings = new HashMap<>(); + settings.put(ConfigSections.NPCVisuals, new HashSet<>()); + settings.put(ConfigSections.RaceVisuals, new HashSet<>()); + readConfig(configfile); + extractPatchTags(); + } + + /**Read the given configuration file. + * This function will parse the given configuration file and store the + * parsed user-input in the settings field. + * + * @param configfile the file to parse + * @throws PatchingException if the file contains inconsistent data + * @throws SetupException if the file is missing or could not be read from + */ + private void readConfig(String configfile) throws PatchingException, + SetupException { + ArrayList imported = SPDatabase.getImportedModListings(); + try { + FileReader fstream = new FileReader(configfile); + BufferedReader reader = new BufferedReader(fstream); + String line = reader.readLine(); + ConfigSections category = null; + ThreadContext.put("context", "Configuration"); + logger.info("begin dump of Reqtificator.ini"); + while (line != null) { + logger.info("|" + line); + line = line.trim(); + if (line.length() == 0 || line.charAt(0) == '#') { + line = reader.readLine(); + continue; + } else if (line.charAt(0) == '[') { + try { + category = ConfigSections.valueOf(line.substring(1, + line.length() - 1)); + } catch (EnumConstantNotPresentException | + IllegalArgumentException err) { + String message = texts.format( + "patch.configuration.unknown_category", line); + throw new PatchingException("unknown section", message, + err); + } + } else { + ModListing overwrite = new ModListing(line); + if (!imported.contains(overwrite)) { + String message = texts.format( + "patch.configuration.mod_missing", + overwrite.print()); + throw new PatchingException("missing mod", message); + } + settings.get(category).add(overwrite); + } + line = reader.readLine(); + } + logger.info("finished dump of Reqtificator.ini"); + for (ModListing entry: settings.get(ConfigSections.NPCVisuals)) { + logger.info("'" + entry.print() + "' registered as NPC visual template provide"); + } + for (ModListing entry: settings.get(ConfigSections.RaceVisuals)) { + logger.info("'" + entry.print() + "' registered as race visual template provide"); + } + ThreadContext.remove("context"); + } catch (FileNotFoundException e) { + throw new SetupException("Reqtificator.ini not found", + texts.format("patch.configuration.ini_not_found"), e); + } catch (IOException e) { + throw new SetupException("Reqtificator.ini not readable", + texts.format("patch.configuration.ini_io_error"), e); + } + } + + /**Extract the REQ-Tags from Requiem-dependent mods in the load order. + * Iterate through the load order and identify Requiem-dependent mods that + * specify REQ-Tags. The extracted tags are stored in the public fields + * modsWithTemperedItems, modsWithCompactLists and modsWithActorVariations of the instance. + * + * @throws PatchingException if any of the mods specified a REQ-Tag pattern + * that could not be parsed. + */ + private void extractPatchTags() throws PatchingException { + Pattern reqtag = Pattern.compile("<<(REQ:\"[^>\"]+\" ?;)?([^>]+)>>", + Pattern.CASE_INSENSITIVE); + modsWithActorVariations.add(FormIDStash.REQ); + modsWithTemperedItems.add(FormIDStash.REQ); + modsWithCompactLists.add(FormIDStash.REQ); + for (Mod imported : SPDatabase.getImportedMods()) { + if (imported.getMasters().contains(FormIDStash.REQ)) { + Matcher match = reqtag.matcher(imported.getDescription()); + if (!match.find()) { + continue; + } + if (match.group(1) != null) { + logger.warn("DEPRECATED FEATURE USED: '" + imported.getName() + + "' specifies a REQ-TAG prefix. This is " + + "no longer used and will be removed in a future release."); + } + for (String command: match.group(2).split(";")) { + command = command.trim(); + if (command.equalsIgnoreCase("REQ:MUTATE")) { + modsWithActorVariations.add(imported.getInfo()); + logger.info("'" + imported.getName() + "' registered for Actor Variations"); + } else if (command.equalsIgnoreCase("REQ:UNROLL")) { + modsWithCompactLists.add(imported.getInfo()); + logger.info("'" + imported.getName() + + "' registered for Compact Leveled Lists"); + } else if (command.equalsIgnoreCase("REQ:TEMPER")) { + modsWithTemperedItems.add(imported.getInfo()); + logger.info("'" + imported.getName() + + "' registered for Tempered Item Lists"); + } else { + String message = texts.format( + "patch.configuration.invalid_tags", imported.getName(), + command, texts.format("urls.req_tags", true)); + throw new PatchingException("invalid tags", message); + } + } + } + } + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/YourSaveFile.java b/Java/Reqtificator/src/main/java/Reqtificator/YourSaveFile.java new file mode 100644 index 00000000..2df79ba7 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/YourSaveFile.java @@ -0,0 +1,51 @@ +/* + * To change this template, choose Tools | Templates + * and open the template in the editor. + */ +package Reqtificator; + +import Reqtificator.logging_and_gui.TextManager; +import skyproc.SkyProcSave; + +/** + * + * @author Justin Swanson + */ +public class YourSaveFile extends SkyProcSave { + private final TextManager texts; + + public YourSaveFile(TextManager texts) { + this.texts = texts; + } + + @Override + protected void initSettings() { + // The Setting, The default value, Whether or not it changing means a new patch should be made + Add(Settings.LCHAR_MERGE, false, true); + Add(Settings.LITEM_MERGE, false, true); + Add(Settings.ADDALLMASTERS, true, true); + Add(Settings.OpenEncounterZones, true, true); + Add(Settings.LOG_DEBUG, false, true); + } + + @Override + protected void initHelp() { + helpInfo.put(Settings.LCHAR_MERGE, texts.format("gui.settings.info_lchar_merge")); + helpInfo.put(Settings.LITEM_MERGE, texts.format("gui.settings.info_litem_merge")); + helpInfo.put(Settings.ADDALLMASTERS, texts.format("gui.settings.info_all_masters")); + helpInfo.put(Settings.OpenEncounterZones, texts.format("gui.settings.info_open_encounter_zones")); + helpInfo.put(Settings.LOG_DEBUG, texts.format("gui.settings.info_debug_logging")); + helpInfo.put(Settings.OTHER_SETTINGS, texts.format("gui.settings.info_other_settings")); + } + + // Note that some settings just have help info, and no actual values in + // initSettings(). + public enum Settings { + LCHAR_MERGE, + LITEM_MERGE, + OTHER_SETTINGS, + OpenEncounterZones, + ADDALLMASTERS, + LOG_DEBUG + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/ActorFusion.java b/Java/Reqtificator/src/main/java/Reqtificator/components/ActorFusion.java new file mode 100644 index 00000000..ef1bbecf --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/ActorFusion.java @@ -0,0 +1,413 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.exceptions.PatchingException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; +import skyproc.NPC_.NPCFlag; +import skyproc.NPC_.NPCStat; +import skyproc.NPC_.TemplateFlag; +import skyproc.genenums.Skill; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/**The ActorFusion component is a helper class to copy data between NPCs. + * It can either be used to copy an individual data section between NPCs or it + * can be used to link a given actor to a new template and different inheritance + * flags without loosing any data from the previous template. + * + * @author Ogerboss + */ +class ActorFusion extends Component { + + private final static Logger logger = LogManager.getLogger(); + + final private TextManager texts; + + /**Create a new ActorFusion instance. + * + */ + protected ActorFusion(TextManager texts) { + super(); + this.texts = texts; + } + + /**Fusion two actors. + * This function creates a clone of the given actor and assigns the given + * EditorID to it. Afterwards, it assigns a new template to the clone and + * sets the inheritance flags as specified in the arguments. Any data that + * was previously inherited from the old template, is copied explicitly to + * ensure that no data is lost. If the actor inherited any data from a + * leveled character, an exception will be thrown because this inheritance + * cannot be resolved at compile time. + * + * @param actor the NPC to manipulate + * @param newtemplate the FormID of the new template to use + * @param eid the editorID to be assigned to the clone + * @param toinherit the inheritance flags for the clone + * @return the cloned actor inheriting the specified data from the new + * template + * @throws PatchingException if inheritance from a leveled character was + * encountered + */ + protected NPC_ fuse_actors(NPC_ actor, FormID newtemplate, String eid, + TemplateFlag... toinherit) throws PatchingException { + int stackDepth = ThreadContext.getDepth(); + ThreadContext.push(formatRecord(actor)); + NPC_ fusion = (NPC_) actor.copy(eid); + ThreadContext.push(formatRecord(fusion)); + List flaglist = Arrays.asList(toinherit); + for (TemplateFlag flag : TemplateFlag.values()) { + if (flaglist.contains(flag)) { + fusion.set(flag, true); + } else if (fusion.get(flag)) { + fusion.set(flag, false); + NPC_ source = find_ancestor(actor, flag); + ThreadContext.push(formatRecord(source)); + switch (flag) { + case USE_AI_DATA: + copy_AIdata(fusion, source); + break; + case USE_AI_PACKAGES: + copy_AIpackages(fusion, source); + break; + case USE_ATTACK_DATA: + copy_attackdata(fusion, source); + break; + case USE_BASE_DATA: + copy_basedata(fusion, source); + break; + case USE_DEF_PACK_LIST: + copy_defaultpackages(fusion, source); + break; + case USE_FACTIONS: + copy_factions(fusion, source); + break; + case USE_INVENTORY: + copy_inventory(fusion, source); + break; + case USE_KEYWORDS: + copy_keywords(fusion, source); + break; + case USE_SCRIPTS: + copy_scripts(fusion, source); + break; + case USE_SPELL_LIST: + copy_spells_perks(fusion, source); + break; + case USE_STATS: + copy_stats(fusion, source); + break; + case USE_TRAITS: + copy_traits(fusion, source); + break; + default: + throw new UnsupportedOperationException( + "Inheritance flag " + flag + + " not implemented!"); + } + ThreadContext.pop(); + } + } + fusion.setTemplate(newtemplate); + ThreadContext.trim(stackDepth); + return fusion; + } + + /**Find the actor from which the given NPC inherits the specified data. + * This functions traverses the inheritance chain of the given NPC until + * it either finds the root template (has no template on its own) or an + * intermediate actor which does not inherit the specified data from his + * template. If a LeveledCharacter is found along the way, an exception + * is raised because determining a unique ancestor is not possible in + * this case. + * + * @param actor the actor to find the ancestor for + * @param flag the inheritance flag for which the inheritance shall be + * analyzed + * @return the template defining the data (or the actor itself if the + * flag is not checked) + * @throws PatchingException if the inheritance chain leads to a leveled + * Character, these cannot be resolved at compile-time + */ + private NPC_ find_ancestor(NPC_ actor, TemplateFlag flag) + throws PatchingException { + NPC_ current = actor; + ArrayList inheritance = new ArrayList<>(); + while (current.get(flag) && !current.getTemplate().isNull()) { + MajorRecord template = SPDatabase.getMajor(current.getTemplate(), + GRUP_TYPE.NPC_, GRUP_TYPE.LVLN); + inheritance.add(template); + if (template instanceof LVLN) { + StringBuilder sb = new StringBuilder(); + for (MajorRecord ancestor : inheritance) { + sb.append(texts.format( + "patch.actor_fusion.error_leveled_template_entry", + formatRecordPretty(ancestor))); + } + String message = texts.format( + "patch.actor_fusion.error_leveled_template", + formatRecordPretty(actor), flag.name(), sb.toString()); + throw new PatchingException( + "ambiguous inheritance cannot be resolved", message); + } + current = (NPC_) template; + } + return current; + } + + /**Copy faction data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_factions(NPC_ target, NPC_ source) { + target.set(NPC_.TemplateFlag.USE_FACTIONS, false); + target.clearFactions(); + for (SubFormInt group : source.getFactions()) { + target.addFaction(group.getForm(), group.getNum()); + } + target.setCrimeFaction(source.getCrimeFaction()); + } + + /**Copy script data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_scripts(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_SCRIPTS, false); + ScriptPackage scriptlist = target.getScriptPackage(); + for (ScriptRef script: scriptlist.getScripts()) { + scriptlist.removeScript(script); + } + for (ScriptRef script: source.getScriptPackage().getScripts()) { + scriptlist.addScript(script); + } + } + + /**Copy keyword data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_keywords(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_KEYWORDS, false); + target.getKeywordSet().clearKeywordRefs(); + for (FormID keyword : source.getKeywordSet().getKeywordRefs()) { + target.getKeywordSet().addKeywordRef(keyword); + } + } + + /**Copy AI Data data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_AIdata(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_AI_DATA, false); + target.setAggression(source.getAggression()); + target.setConfidence(source.getConfidence()); + target.setAssistance(source.getAssistance()); + target.setMood(source.getMood()); + target.setEnergy(source.getEnergy()); + target.setMorality(source.getMorality()); + target.set(NPCFlag.AggroRadiusBehavior, + source.get(NPCFlag.AggroRadiusBehavior)); + target.setAggroWarn(source.getAggroWarn()); + target.setAggroWarnAttack(source.getAggroWarnAttack()); + target.setAggroAttack(source.getAggroAttack()); + target.setCombatStyle(source.getCombatStyle()); + //TODO: copy gift-filter once fixed in SkyProc library +// target.setGiftFilter(source.getGiftFilter()); + } + + /**Copy AI Package data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_AIpackages(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_AI_PACKAGES, false); + target.clearAIPackages(); + for (FormID fid_package : source.getAIPackages()) { + target.addAIPackage(fid_package); + } + } + + /**Copy default packages data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_defaultpackages(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_DEF_PACK_LIST, false); + target.setDefaultPackageList(source.getDefaultPackageList()); + target.setSpectatorOverride(source.getSpectatorOverride()); + target.setObserveDeadOverride(source.getObserveDeadOverride()); + target.setGuardWornOverride(source.getGuardWornOverride()); + target.setCombatOverride(source.getCombatOverride()); + } + + /**Copy stats data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_stats(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_STATS, false); + target.set(NPCStat.LEVEL, target.get(NPCStat.LEVEL)); + target.set(NPCStat.MIN_CALC_LEVEL, target.get(NPCStat.MIN_CALC_LEVEL)); + target.set(NPCStat.MAX_CALC_LEVEL, target.get(NPCStat.MAX_CALC_LEVEL)); + target.set(NPCFlag.PCLevelMult, source.get(NPCFlag.PCLevelMult)); + target.set(NPCFlag.AutoCalcStats, source.get(NPCFlag.AutoCalcStats)); + target.setHealthOffset(source.getHealthOffset()); + target.setMagickaOffset(source.getMagickaOffset()); + target.setFatigueOffset(source.getFatigueOffset()); + for (Skill stat: Skill.NPC_Skills()) { + target.set(stat, source.get(stat)); + } + target.set(NPCStat.SPEED_MULT, source.get(NPCStat.SPEED_MULT)); + target.set(NPCFlag.BleedoutOverride, + source.get(NPCFlag.BleedoutOverride)); + target.setNPCClass(source.getNPCClass()); + } + + /**Copy spells and perks data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_spells_perks(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_SPELL_LIST, false); + target.clearSpells(); + for (FormID spell: source.getSpells()) { + target.addSpell(spell); + } + target.clearPerks(); + for (SubFormInt perk: source.getPerks()) { + target.addPerk(perk.getForm(), perk.getNum()); + } + } + + /**Copy base data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_basedata(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_BASE_DATA, false); + target.setName(source.getName()); + //copy short name, once fixed in SkyProc + //target.setShortName(source.getShortName()); + target.set(NPCFlag.IsCharGenFacePreset, + source.get(NPCFlag.IsCharGenFacePreset)); + target.set(NPCFlag.Essential, source.get(NPCFlag.Essential)); + target.set(NPCFlag.Protected, source.get(NPCFlag.Protected)); + target.set(NPCFlag.Respawn, source.get(NPCFlag.Respawn)); + target.set(NPCFlag.Unique, source.get(NPCFlag.Unique)); + target.set(NPCFlag.Summonable, source.get(NPCFlag.Summonable)); + target.set(NPCFlag.IsGhost, source.get(NPCFlag.IsGhost)); + target.set(NPCFlag.Invulnerable, source.get(NPCFlag.Invulnerable)); + target.set(NPCFlag.DoesntBleed, source.get(NPCFlag.DoesntBleed)); + target.set(NPCFlag.SimpleActor, source.get(NPCFlag.SimpleActor)); + target.set(NPCFlag.DoesntAffectStealthMeter, + source.get(NPCFlag.DoesntAffectStealthMeter)); + } + + /**Copy attack data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_attackdata(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_ATTACK_DATA, false); + target.setAttackDataRace(source.getAttackDataRace()); + target.clearAttackPackages(); + for (NPC_.AttackPackage pack: source.getAttackPackages()) { + target.addAttackPackage(pack); + } + } + + /**Copy traits data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_traits(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_TRAITS, false); + //Traits tab + target.setRace(source.getRace()); + target.set(NPCFlag.Female, source.get(NPCFlag.Female)); + target.setSkin(source.getSkin()); + target.setHeight(source.getHeight()); + target.setWeight(source.getWeight()); + target.setFarAwayModelSkin(source.getFarAwayModelSkin()); + target.setFarAwayModelDistance(source.getFarAwayModelDistance()); + target.setVoiceType(source.getVoiceType()); + target.set(NPCStat.DISPOSITION_BASE, + source.get(NPCStat.DISPOSITION_BASE)); + target.setDeathItem(source.getDeathItem()); + target.set(NPCFlag.OppositeGenderAnims, + source.get(NPCFlag.OppositeGenderAnims)); + //Sounds tab + target.setSoundVolume(source.getSoundVolume()); + target.setAudioTemplate(source.getAudioTemplate()); + target.clearSoundPackages(); + for (NPC_.SoundPackage pack: source.getSounds()) { + target.addSoundPackage(pack); + } + //Character Gen Parts tab + target.setFeatureSet(source.getFeatureSet()); + target.clearHeadParts(); + target.setHairColor(source.getHairColor()); + for (FormID part: source.getHeadParts()) { + target.addHeadPart(part); + } + target.clearTinting(); + for (NPC_.TintLayer tint: source.getTinting()) { + NPC_.TintLayer clone = new NPC_.TintLayer(tint.getIndex()); + for (RGBA color: RGBA.values()) { + clone.setColor(color, tint.getColor(color)); + } + clone.setInterpolation(tint.getInterpolation()); + clone.setPreset(tint.getPreset()); + target.addTinting(clone); + } + for (RGB color : RGB.values()) { + target.setFaceTint(color, source.getFaceTint(color)); + } + //Character Gen Morphs tab + target.setEyePreset(source.getEyePreset()); + target.setNosePreset(source.getNosePreset()); + target.setMouthPreset(source.getMouthPreset()); + for (NPC_.FacePart part: NPC_.FacePart.values()) { + target.setFaceValue(part, source.getFaceValue(part)); + } + } + + /**Copy inventory data from source to target actor. + * + * @param target NPC to copy to + * @param source NPC to copy from + */ + protected void copy_inventory(NPC_ target, NPC_ source) { + target.set(TemplateFlag.USE_INVENTORY, false); + target.setDefaultOutfit(source.getDefaultOutfit()); + target.setSleepingOutfit(source.getSleepingOutfit()); + target.clearItems(); + for (ItemListing entry: source.getItems()) { + target.addItem(entry.getForm(), entry.getCount()); + } + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/ActorVariations.java b/Java/Reqtificator/src/main/java/Reqtificator/components/ActorVariations.java new file mode 100644 index 00000000..96cf38df --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/ActorVariations.java @@ -0,0 +1,303 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; +import skyproc.NPC_.TemplateFlag; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.TreeMap; + +/**The patching component for ActorVariations. + * This component creates the ActorVariations for Requiem, which improve the + * visual variety of generic actors like guards and bandits. Instead of defining + * an actor with gameplay and visual data, Requiem splits actors in two distinct + * parts: the skills and the visuals. By encoding meta-data into Leveled + * Characters, the Reqtificator can combine each skill template with a large + * number of visual templates to create a variety in race, gender and look for + * each skill template. + * + * @author Ogerboss + */ +public class ActorVariations extends Component{ + + /**Maps the keys of already processed skill-look pairs to their result. + */ + private final TreeMap variationmap; + + /**The ActorMerger to use for relinking actor templates. + */ + private final ActorFusion fusion; + + private final static Logger logger = LogManager.getLogger(); + + final private TextManager texts; + + public ActorVariations(TextManager texts) { + super(); + this.texts = texts; + this.fusion = new ActorFusion(texts); + variationmap = new TreeMap<>(); + } + + /**Test the correctness of the metadata in the ActorVariations. + * The ActorVariations contain meta-data in the form of the count and level + * fields in the LeveledCharacter's entries. Since there are unleveling + * scripts for Tes5Edit flying around, these might destroy this metadata + * if applied before the Reqtificator. + * + * @param workload the list of ActorVariations to check + * @param merger the import mod to use for "is Actor" tests + * @throws PatchingException if none of the tested ActorVariations has any + * leveled actors + */ + private void test_metadata(ArrayList workload, Mod merger) + throws PatchingException{ + for (LVLN list: workload) { + ThreadContext.push(formatRecord(list)); + for (LeveledEntry entry: list) { + FormID fid = entry.getForm(); + NPC_ test = (NPC_) merger.getMajor(fid, GRUP_TYPE.NPC_); + if (test != null && entry.getLevel() > 1) { + return; + } + } + ThreadContext.pop(); + } + String msg = texts.format( + "patch.actor_variations.error_unleveled_data"); + throw new PatchingException("invalid actor variation meta data", msg); + } + + /** + * Generate all valid actor variations from the leveled lists in the + * workload and export them to the patch. Creating NPC variations consists + * of two steps: first create a new LeveledCharacter for each Actor-LChar + * combination in the processed LeveledCharacter and fill it with new actors + * that have the skills of the single actors and the visuals of each actor + * in the LChar. Then add the new LChars to the processed LChar and use the + * count of the replaced LChar to determine the number of instances to add + * to the processed list. + * At the end, the original FormList thus will contain a list of LChars with + * the skills of the original actors, but all the possible visuals + * variations that were contained in the original LChars. + * In addition, symbolic or hard links (user's choice) will be created for + * each new actor's FaceGen data to overcome Skyrim's grey face bug. + * @param merger the merged import + * @param patch the patch for exporting modified records + * @param workload the list LeveledCharacters to process + * @throws PatchingException if the reference FaceGen-data is missing + */ + public void reqtifyActorVariations(Mod merger, Mod patch, + ArrayList workload) + throws PatchingException, UnexpectedException { + int maxRecords = workload.size(); + startCategory(maxRecords, "Actor Variations", + texts.format("patch.gui_titles.actor_variations")); + test_metadata(workload, merger); + TreeMap templates_skill = new TreeMap<>( + new FID_Comparator()); + TreeMap templates_look = new TreeMap<>( + new FID_Comparator()); + ArrayList spawnlist = new ArrayList<>(); + for (LVLN variation: workload) { + ThreadContext.put(contextRecord, formatRecord(variation)); + try { + templates_look.clear(); + templates_skill.clear(); + partition_variation(variation, merger, templates_skill, + templates_look); + LVLN container; + spawnlist.clear(); + for (NPC_ skill : templates_skill.keySet()) { + ThreadContext.push(formatRecord(skill)); + for (LVLN look : templates_look.keySet()) { + ThreadContext.push(formatRecord(look)); + VariationIndex key = new VariationIndex(skill, look); + if (variationmap.containsKey(key)) { + container = variationmap.get(key); + } else { + container = create_variation(skill, look, merger); + variationmap.put(key, container); + } + int totalcount = templates_skill.get(skill) + * templates_look.get(look); + for (int k = 0; k < totalcount; k++) { + spawnlist.add(container); + } + ThreadContext.pop(); + } + ThreadContext.pop(); + } + variation.clearEntries(); + for (LVLN spawn : spawnlist) { + variation.addEntry(spawn.getForm(), 1, 1); + } + logger.debug("created actor variations"); + patch.addRecord(variation); + updateProgressBar(); + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), variation, ex); + } + } + endCategory(); + } + + /** Create a new varied Leveled Character. + * This function will at first create a new actor for each actor in the + * look LChar. These actors are the visuals from the LChar entry merged with + * the stats and skills of the specified skill actor. Afterwards, the + * resulting actors are collected in a new leveled character. The usage of + * the FormIDManager ensures that already existing combinations keep their + * FormID in consecutive patches. + * @param skill the actor whose skills and stats should be used + * @param look the LChar with all actors whose visuals should be used + * @param merger the global importer, needed to extract NPCs from the LChar + * @return the leveled Character with all the new actors + * @throws PatchingException if inheritances are not defined uniquely + */ + private LVLN create_variation(NPC_ skill, LVLN look, Mod merger) + throws PatchingException { + String eid = "REQI_LChar_Variations_" + skill.getFormStr() + "_" + + look.getFormStr(); + LVLN container = new LVLN(eid); + for (FormID fid_look: look.getEntryForms()) { + NPC_ lookactor = (NPC_) merger.getMajor(fid_look, GRUP_TYPE.NPC_); + ThreadContext.push(formatRecord(lookactor)); + eid = "REQI_Actor_Variations_" + skill.getFormStr() + "_" + + lookactor.getFormStr(); + NPC_ newactor = fusion.fuse_actors(skill, lookactor.getForm(), + eid, TemplateFlag.USE_TRAITS, TemplateFlag.USE_ATTACK_DATA); + ThreadContext.push(formatRecord(newactor)); + container.addEntry(newactor.getForm(), 1, 1); + ThreadContext.pop(); + ThreadContext.pop(); + } + return container; + } + + /**Partition an ActorVariation LeveledCharacter into its components. + * This method will separate the contained actors (skill templates) and + * leveled characters (look template lists) and also extract their spawn + * likeliness. + * @param template the ActorVariation to partition + * @param merger the global importer, used to translate the FormIDs in the + * template into either actors or LChars + * @param templates_skill data container for skill templates + * @param templates_look data container for look templates + * @throws PatchingException if the ActorVariation contains unexpected data + */ + private void partition_variation(LVLN template, Mod merger, + TreeMap templates_skill, + TreeMap templates_look) throws PatchingException { + String detail = null; + for (LeveledEntry entry: template.getEntries()) { + FormID fid = entry.getForm(); + NPC_ skill = (NPC_) merger.getMajor(fid, GRUP_TYPE.NPC_); + LVLN look = (LVLN) merger.getMajor(fid, GRUP_TYPE.LVLN); + if (skill != null) { + ThreadContext.push(formatRecord(skill)); + if (templates_skill.containsKey(skill)) { + detail = texts.format( + "patch.actor_variations.error_multiskill", + formatRecord(skill)); + break; + } else if (entry.getCount() != 1) { + detail = texts.format( + "patch.actor_variations.error_factors_skill", + formatRecord(skill)); + break; + } + templates_skill.put(skill, entry.getLevel()); + } + else if (look != null) { + ThreadContext.push(formatRecord(look)); + if (templates_look.containsKey(look)) { + detail = texts.format( + "patch.actor_variations.error_multi_looks", + formatRecord(look)); + break; + } else if (entry.getLevel() != 1) { + detail = texts.format( + "patch.actor_variations.error_factors_look", + formatRecord(look)); + break; + } + templates_look.put(look, entry.getCount()); + } else { + detail = texts.format( + "patch.actor_variations.error_unknown_record", + fid.toString()); + break; + } + ThreadContext.pop(); + } + if (detail != null) { + String msg = texts.format( + "patch.actor_variations.error_variations", + formatRecordPretty(template), detail, + texts.format("urls.actor_variations", true)); + throw new PatchingException("invalid actor variation data", msg); + } + } + + /**Helper class for defining a unique order of FormIDs. + * To ensure that the FormIDs in the treeMaps are well-ordered, we define + * a custom comparison here, which is just a string comparison of their + * FormStrings. + */ + private class FID_Comparator implements Comparator { + + @Override + public int compare(MajorRecord o1, MajorRecord o2) { + return o1.getFormStr().compareTo(o2.getFormStr()); + } + + } + + /**Index class, represents a skill+look combination. + * This is a helper class used as a map index. It represents the + * combination of a skill actor with a look leveled list. + */ + private class VariationIndex implements Comparable{ + + /**The Actor which provides the skills. + */ + NPC_ target; + + /**The LeveledLists that provides the looks to be used. + */ + LVLN variations; + + /**Create a new map key for the given skill/look pair. + * + * @param target the NPC providing the skills + * @param variations the LeveledCharacter providing the looks + */ + private VariationIndex(NPC_ target, LVLN variations) { + this.target = target; + this.variations = variations; + } + + @Override + public int compareTo(VariationIndex other) { + int comp = target.getEDID().compareTo(other.target.getEDID()); + if (comp == 0) { + comp = variations.getEDID().compareTo(other.variations.getEDID()); + } + return comp; + } + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/Component.java b/Java/Reqtificator/src/main/java/Reqtificator/components/Component.java new file mode 100644 index 00000000..4545810b --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/Component.java @@ -0,0 +1,76 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.FormIDStash; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; +import skyproc.gui.SUMGUI; + +/**This is the abstract base for all Components. + * This abstract class defines a general-purpose constructor which defines all + * the central quantities used by all individual components, e.g. logging and + * internationalization helpers and often used mod-references. + * + * @author Ogerboss + */ +public abstract class Component { + + private final static Logger logger = LogManager.getLogger(); + + final ModListing requiem_ml; + protected final Mod requiem; + + protected final String contextRecord = "record"; + private final String context = "context"; + + Component() { + requiem_ml = FormIDStash.REQ; + requiem = SPDatabase.getMod(requiem_ml); + } + + void startCategory(int maxRecords, String category, String guiTitle) { + SUMGUI.progress.setMax(maxRecords, guiTitle); + SUMGUI.progress.setBar(0); + ThreadContext.put(context, category); + logger.info("beginning patch operations"); + } + + void updateProgressBar() { + SUMGUI.progress.incrementBar(); + } + + void endCategory() { + ThreadContext.remove(contextRecord); + logger.info("finished patch operations"); + ThreadContext.remove(context); + } + + public static String formatRecord(MajorRecord record) { + if (record == null) { + return ""; + } else { + return "[" + record.getType() + + "|" + record.getEDID() + + "|" + record.getFormStr() + + "|" + record.getModImportedFrom().print() + + "]"; + } + } + + public static String formatRecordPretty(MajorRecord record) { + if (record == null) { + return ""; + } else { + return " \"" + record.getEDID() + "\" - " + record.getFormStr() + + " [type: " + record.getType() + ", last overwrite: " + + record.getModImportedFrom().print() + "]"; + } + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/Containers.java b/Java/Reqtificator/src/main/java/Reqtificator/components/Containers.java new file mode 100644 index 00000000..08517e08 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/Containers.java @@ -0,0 +1,79 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package Reqtificator.components; + +import Reqtificator.FormIDStash; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; + +import java.util.List; + +/** + *

+ */ +public class Containers extends Component { + + private final static Logger logger = LogManager.getLogger(); + + final private TextManager texts; + + public Containers(TextManager texts) { + super(); + this.texts = texts; + } + + /** + * Reqtify imported container records. + * + * @param merger the merged import mod with the armors to process + * @param patch the global patch for exporting modified records + */ + public void reqtifyContainers(Mod merger, Mod patch) + throws UnexpectedException { + GRUP containers = merger.getContainers(); + int maxRecords = containers.numRecords(); + startCategory(maxRecords, "Containers", + texts.format("patch.gui_titles.containers")); + ThreadContext.push(FormIDStash.PROTOTYPE_CONTAINER_LOCKED.toString()); + CONT template = (CONT) merger.getMajor( + FormIDStash.PROTOTYPE_CONTAINER_LOCKED, GRUP_TYPE.CONT); + ThreadContext.push(formatRecord(template)); + List prototypes = template.getScriptPackage().getScripts(); + ThreadContext.clearStack(); + for (CONT container : containers) { + ThreadContext.put(contextRecord, formatRecord(container)); + try { + ScriptPackage myScripts = container.getScriptPackage(); + boolean edited = false; + for (ScriptRef toAdd : prototypes) { + ThreadContext.push(toAdd.getName()); + if (!myScripts.hasScript(toAdd)) { + myScripts.addScript(toAdd); + logger.debug("added script: {}", toAdd.getName()); + edited = true; + } + ThreadContext.pop(); + } + if (edited) { + patch.addRecord(container); + } + updateProgressBar(); + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), container, ex); + } + + } + endCategory(); + } + +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/Doors.java b/Java/Reqtificator/src/main/java/Reqtificator/components/Doors.java new file mode 100644 index 00000000..e4de0aba --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/Doors.java @@ -0,0 +1,81 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package Reqtificator.components; + +import Reqtificator.FormIDStash; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; + +import java.util.List; + +/**Updates door records for Requiem game play. + *
    + *
  • add a script that handles lockpicking by player and + * followers
+ * + * @author ogerboss + */ +public class Doors extends Component { + + private final static Logger logger = LogManager.getLogger(); + + final private TextManager texts; + + public Doors(TextManager texts) { + super(); + this.texts = texts; + } + + /** + * Reqtify imported door records. + * + * @param merger the merged import mod with the armors to process + * @param patch the global patch for exporting modified records + */ + public void reqtifyDoors(Mod merger, Mod patch) throws UnexpectedException { + GRUP doors = merger.getDoors(); + int maxRecords = doors.numRecords(); + startCategory(maxRecords, "Doors", + texts.format("patch.gui_titles.doors")); + ThreadContext.push(FormIDStash.PROTOTYPE_DOOR_LOCKED.toString()); + DOOR template = (DOOR) merger.getMajor( + FormIDStash.PROTOTYPE_DOOR_LOCKED, GRUP_TYPE.DOOR); + ThreadContext.push(formatRecord(template)); + List prototypes = template.getScriptPackage().getScripts(); + ThreadContext.clearStack(); + for (DOOR door: doors) { + ThreadContext.put(contextRecord, formatRecord(door)); + try { + ScriptPackage myScripts = door.getScriptPackage(); + boolean edited = false; + for (ScriptRef toAdd : prototypes) { + ThreadContext.push(toAdd.getName()); + if (!myScripts.hasScript(toAdd)) { + myScripts.addScript(toAdd); + logger.debug("added script: {}", + toAdd.getName()); + edited = true; + } + ThreadContext.pop(); + } + if (edited) { + patch.addRecord(door); + } + updateProgressBar(); + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), door, ex); + } + + } + endCategory(); + } + +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/EncounterZones.java b/Java/Reqtificator/src/main/java/Reqtificator/components/EncounterZones.java new file mode 100644 index 00000000..2d3b0ba0 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/EncounterZones.java @@ -0,0 +1,95 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.FormIDStash; +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; + +import java.util.HashSet; + +/**Processing class for EncounterZones. + * This component will set all EncounterZones to have open borders, which means + * that NPCs can pursue you beyond the boundaries of their home cell/location. + * Optionally, specific EncounterZones can be excluded from this treatment, e.g. + * Requiem itself excludes all Vampire lairs to prevent their inhabitants from + * rushing into the sunlight. Another example are the zones of the Civil War + * quests, which are excluded to improve the stability of these. + * Patch authors can simply add their special EncounterZones to the FormList + * that contains the Requiem-exceptions. The Reqtificator will resolve + * conflicts between several patches on its own. + * + * @author Ogerboss + */ +public class EncounterZones extends Component{ + + private final static Logger logger = LogManager.getLogger(); + + final private TextManager texts; + + public EncounterZones(TextManager texts) { + super(); + this.texts = texts; + } + + /**Reqtify EncounterZones. + * + * @param merger merged version of the imported mods + * @param patch the global patch to be created + * @throws PatchingException if the exception list contains invalid data + * @throws UnexpectedException wrapper around any unexpected errors + */ + public void reqtifyZones(Mod merger, Mod patch, boolean openZones) throws PatchingException, + UnexpectedException { + HashSet exceptions = new HashSet<>(); + FLST exceptionList = (FLST) merger.getMajor( + FormIDStash.formlist_closedzones, GRUP_TYPE.FLST); + String key = "patch.encounter_zones.wrong_type_in_exception_list"; + for (MajorRecord version: exceptionList.getRecordHistory()) { + FLST overwrite = (FLST) version; + for (FormID fid: overwrite.getFormIDEntries()) { + if (merger.getMajor(fid, GRUP_TYPE.ECZN) == null) { + String msg = texts.format(key, fid.toString(), + version.getModImportedFrom().print(), + texts.format("urls.encounter_zones", true)); + throw new PatchingException("invalid exception list entry", + msg); + } + exceptions.add(fid); + } + } + if (openZones) { + GRUP zones = merger.getEncounterZones(); + int maxRecords = zones.numRecords(); + startCategory(maxRecords, "Encounters", + texts.format("patch.gui_titles.encounter_zones")); + for (ECZN zone : zones) { + ThreadContext.put(contextRecord, formatRecord(zone)); + try { + if (exceptions.contains(zone.getForm())) { + } else { + zone.set(ECZN.ECZNFlags.DisableCombatBoundary, true); + patch.addRecord(zone); + logger.debug("opened combat boundary for NPCs"); + } + updateProgressBar(); + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), zone, ex); + } + + } + } + endCategory(); + } + +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/IngameManual.java b/Java/Reqtificator/src/main/java/Reqtificator/components/IngameManual.java new file mode 100644 index 00000000..abb7f098 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/IngameManual.java @@ -0,0 +1,20 @@ +package Reqtificator.components; + +import Reqtificator.FormIDStash; +import skyproc.FLST; +import skyproc.GRUP_TYPE; +import skyproc.Mod; + +public class IngameManual { + + public static void patchIngameManual(Mod merger, Mod patch) { + FLST pcHelp = (FLST) merger.getMajor(FormIDStash.formlistHelpTopcicsPC, GRUP_TYPE.FLST); + FLST xboxHelp = (FLST) merger.getMajor(FormIDStash.formlistHelpTopcicsXbox, GRUP_TYPE.FLST); + FLST requiemHelp = (FLST) merger.getMajor(FormIDStash.formlistHelpTopcicsRequiem, GRUP_TYPE.FLST); + + pcHelp.addAll(requiemHelp.getFormIDEntries()); + xboxHelp.addAll(requiemHelp.getFormIDEntries()); + patch.addRecord(pcHelp); + patch.addRecord(xboxHelp); + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledCharacters.java b/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledCharacters.java new file mode 100644 index 00000000..dd1408e8 --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledCharacters.java @@ -0,0 +1,161 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.GRUP; +import skyproc.LVLN; +import skyproc.Mod; +import skyproc.ModListing; + +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/**Processing class for Leveled Characters. + * This class processes the Leveled Characters. The performed operations are the + * unrolling of compact leveled lists, merging changes from Requiem patches and + * the identification of ActorVariations. Due to the complexity of + * ActorVariations their treatment is deferred to a specialized class + * ActorVariations. + * @author Ogerboss + */ +public class LeveledCharacters extends LeveledLists { + + private final static Logger logger = LogManager.getLogger(); + final private TextManager texts; + + /**The allowed origin mods for unrolling Leveled Characters. + */ + private final Set modsWithCompactLists; + /**The allowed origin mods for ActorVariaton Leveled Characters. + */ + private final Set modsWithActorVariations; + /**RegEx pattern for compact Leveled Characters. + * If the EditorID of a record matches this pattern and the extracted prefix + * is found in modsWithCompactLists, it is identified as compact + * Leveled Character. + */ + private final Pattern re_unroll; + /**RegEx pattern for ActorVariation Leveled Characters. + * If the EditorID of a record matches this pattern and the extracted prefix + * is found in modsWithActorVariations, it is identified as an + * ActorVariation type of Leveled Character. + */ + private final Pattern re_variation; + + /**Create a new LeveledCharacter processing instance. While processing + * LeveledItems, it will merge changes made by Requiem patches and it will + * unroll suitable LeveledItems given in compact notation into a + * Skyrim-understandable version. + * + * @param modsWithCompactLists list of mods containing compact leveled lists + * @param modsWithActorVariations list of mods containing ActorVariations + * @param merger the merged import mod to fetch the exclusive lists + * @throws PatchingException if a patch requested exclusive edit rights on + * a form it does not edit + */ + public LeveledCharacters(TextManager texts, Set modsWithCompactLists, + Set modsWithActorVariations, Mod merger) + throws PatchingException { + super(texts, merger); + this.texts = texts; + re_unroll = Pattern.compile("^[^_]+_CLChar_.+", + Pattern.CASE_INSENSITIVE); + re_variation = Pattern.compile("^[^_]+_LChar_(Variations|VoiceSpawns).+", + Pattern.CASE_INSENSITIVE); + this.modsWithCompactLists = modsWithCompactLists; + this.modsWithActorVariations = modsWithActorVariations; + } + + /**Reqtify LeveledCharacters. + * Loop over the LeveledCharacters and reqtify those that have merge + * conflicts from Requiem-patches. Also the Requiem-defined compact + * spawnlists are unrolled. (These use counts = N entries instead of N + * entries to denote the spawn probabilities in an easier editable way.) + * Actorvariations are not processed directly, but the function returns a + * list of all LeveledCharacters that were identified as ActorVariations. + * + * @param merger the mod contained the merged imports + * @param patch the global to export to + * @param merge true if LeveledCharacter edits from patches shall be merged + * @return an ArrayList of all records that need to be processed as + * ActorVariations + * @throws PatchingException if an error occurs while merging changes from + * Requiem patches + */ + public ArrayList reqtifyLeveledChars(Mod merger, Mod patch, boolean + merge) throws PatchingException, UnexpectedException { + GRUP leveledChars = merger.getLeveledCreatures(); + ArrayList actorvariations = new ArrayList<>(); + int maxRecords = leveledChars.numRecords(); + startCategory(maxRecords, "Leveled Chars", + texts.format("patch.gui_titles.leveled_chars")); + for (LVLN lChar: leveledChars) { + ThreadContext.put(contextRecord, formatRecord(lChar)); + try { + boolean changed = false; + if (is_merge_candidate(merge, lChar)) { + changed = true; + lChar = (LVLN) merge_lists(lChar, + shouldBeUnrolled(lChar)); + } + if (isActorVariation(lChar)) { + actorvariations.add(lChar); + continue; + } + if (shouldBeUnrolled(lChar)) { + changed = true; + unrollLeveledlist(lChar); + } + if (changed) { + patch.addRecord(lChar); + } + updateProgressBar(); + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), lChar, ex); + } + + } + endCategory(); + return actorvariations; + } + + /**Test if the given Leveled Character is in compact notation. + * The test is true, if:
    + *
  1. the EditorID is matched by re_unroll
  2. + *
  3. the record originates from a mod registered for compact lists
+ * + * @param leveledchar the Leveled Character to test + * @return true if the list is compact and must be unrolled + */ + private boolean shouldBeUnrolled(LVLN leveledchar) { + Matcher match = re_unroll.matcher(leveledchar.getEDID()); + return match.find() && modsWithCompactLists.contains(leveledchar.getFormMaster()); + } + + /**Test if the given Leveled Character is an ActorVariation. + * The test is true, if:
    + *
  1. the EditorID is matched by re_variation
  2. + *
  3. the record originates from a mod registered for ActorVariations
+ * + * @param leveledchar the Leveled Character to test + * @return true if the list is an ActorVariation and must be cross-combined + */ + private boolean isActorVariation(LVLN leveledchar) { + Matcher match = re_variation.matcher(leveledchar.getEDID()); + return match.find() && modsWithActorVariations.contains(leveledchar.getFormMaster()); + } +} diff --git a/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledItems.java b/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledItems.java new file mode 100644 index 00000000..6a2079ea --- /dev/null +++ b/Java/Reqtificator/src/main/java/Reqtificator/components/LeveledItems.java @@ -0,0 +1,452 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ + +package Reqtificator.components; + +import Reqtificator.exceptions.PatchingException; +import Reqtificator.exceptions.UnexpectedException; +import Reqtificator.logging_and_gui.TextManager; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.ThreadContext; +import skyproc.*; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/**Processing class for Leveled Items. + * This class processes the Leveled Items. The performed operations are the + * unrolling of compact leveled lists, merging changes from Requiem patches and + * the tempering of items in LeveledItems that are flagged as Tempered Lists. + * @author Ogerboss + */ +public class LeveledItems extends LeveledLists { + + private final static Logger logger = LogManager.getLogger(); + final private TextManager texts; + + /** + * Internal Cache for item quality distributions. This is cache for the item + * quality distributions definec by a specific (tier, size, distribution) + * tuple extracted from EditorIDs. + */ + private final TreeMap> templates; + /** + * The allowed origin mods for unrolling Leveled Items. + */ + private final Set modsWithCompactLists; + /** + * The allowed origin mods for tempered Leveled Items. + */ + private final Set modsWithTemperedItems; + + private final Pattern patternTemperedItem; + + private final Pattern patternCompactList; + + /** + * Maps segments sizes specified in EditorID to internal data. This map + * transforms the segment size (H, N, D) extracted from the EditorID into a + * SizeFactor enum element. + */ + private final Map trafo_sizefactor; + /** + * Maps quality distributions specified in EditorID to internal data. This + * map transforms the segment size (fall, const, rise) extracted from the + * EditorID into a QualityDistribution enum element. + */ + private final Map trafo_distribution; + + /** + * Create a new LeveledItems processing instance. + * While processing LeveledItems, it will merge changes made by Requiem + * patches and it will unroll suitable LeveledItems given in compact + * notation into a Skyrim-understandable version. Furthermore, it will also + * fill Tempered Lists with various tempered versions of the base item. + * + * @param modsWithCompactLists list of origin mods with compact LeveledItems + * @param modsWithTemperedItems list of origin mods with Tempered Lists + * @param merger the merged import mod to fetch the exclusive lists + * @throws PatchingException if a patch requested exclusive edit rights on + * a form it does not edit + */ + public LeveledItems(TextManager texts, Set modsWithCompactLists, + Set modsWithTemperedItems, Mod merger) + throws PatchingException { + super(texts, merger); + this.texts = texts; + this.patternCompactList = Pattern.compile("^[^_]+_CLI_.+", + Pattern.CASE_INSENSITIVE); + this.patternTemperedItem = Pattern.compile("^[^_]+.+_Quality([0-9]+)_([A-z])_([A-z]+)$", + Pattern.CASE_INSENSITIVE); + this.templates = new TreeMap<>(); + this.modsWithCompactLists = modsWithCompactLists; + this.modsWithTemperedItems = modsWithTemperedItems; + + trafo_sizefactor = new TreeMap<>(); + trafo_sizefactor.put("h", SizeFactor.half); + trafo_sizefactor.put("n", SizeFactor.normal); + trafo_sizefactor.put("d", SizeFactor.twice); + trafo_distribution = new TreeMap<>(); + trafo_distribution.put("fall", QualityDistribution.fall); + trafo_distribution.put("const", QualityDistribution.constant); + trafo_distribution.put("rise", QualityDistribution.rise); + } + + /**Reqtify LeveledItems. + * Loop over the LeveledItems and reqtify those that have merge conflicts + * from Requiem-patches. Also the Requiem-defined compact spawnlists are + * unrolled. (These use counts = N entries instead of N entries to denote + * the spawn probabilities in an easier editable way.) In addition Tempered + * items are processed. (These contain 1 item and will be filled with + * tempered variations of this item.) + * + * @param merger the mod contained the merged imports + * @param patch the global to export to + * @param merge true if LeveledItem edits from patches shall be merged + * @throws PatchingException if an error occurs while merging changes from + * Requiem patches + */ + public void reqtifyLeveledItems(Mod merger, Mod patch, boolean merge) + throws PatchingException, UnexpectedException { + GRUP leveledItems = merger.getLeveledItems(); + int maxRecords = leveledItems.numRecords(); + startCategory(maxRecords, "Leveled Items", + texts.format("patch.gui_titles.leveled_items")); + for (LVLI lItem: leveledItems) { + ThreadContext.put(contextRecord, formatRecord(lItem)); + try { + boolean changed = false; + Quality qualitykey = getTemperingData(lItem); + if (qualitykey != null) { + } else if (is_merge_candidate(merge, lItem)) { + changed = true; + lItem = (LVLI) merge_lists(lItem, should_be_unrolled(lItem)); + } + if (should_be_unrolled(lItem)) { + changed = true; + unrollLeveledlist(lItem); + } else if (qualitykey != null) { + reqtifyItemQuality(lItem, qualitykey); + changed = true; + } + if (changed) { + patch.addRecord(lItem); + } + updateProgressBar(); + } catch (PatchingException ex) { + throw ex; + } catch (Exception ex) { + throw new UnexpectedException(ex.getMessage(), + ex.getLocalizedMessage(), lItem, ex); + } + + } + endCategory(); + } + + /**Checks if a LeveledItem is a Tempered List. + * The test is true, if:
    + *
  1. the EditorID is matched by patternTemperedItem
  2. + *
  3. the record originates from a mod registered for compact lists
+ * + * @param lvlitem the Leveled Item to test + * @return the Quality extracted from the EditorID or null if + * the test was negative + * @throws PatchingException if the parameters specified in the EditorID are + * invalid or the LeveledItem contains more than one entry + */ + private Quality getTemperingData(LVLI lvlitem) throws PatchingException { + Matcher match = patternTemperedItem.matcher(lvlitem.getEDID()); + if (match.find() && modsWithTemperedItems.contains(lvlitem.getFormMaster())) { + if (lvlitem.numEntries() != 1) { + String reason = texts.format( + "patch.leveled_items.temper_data_error_moreitems"); + String gui = texts.format( + "patch.leveled_items.tempering_data_error", + lvlitem.getEDID(), formatRecordPretty(lvlitem), + lvlitem.getModImportedFrom().print(), reason, + texts.format("urls.tempered_lists", true)); + throw new PatchingException("invalid tempering data", gui); + } + return new Quality(Integer.valueOf(match.group(1)), + match.group(2), match.group(3), lvlitem); + } + return null; + } + + /**Test if the given Leveled Item is in compact notation. + * The test is true, if:
    + *
  1. the EditorID is matched by patternCompactList
  2. + *
  3. the record originates from a mod registered for compact lists
+ * + * @param leveleditem the Leveled Item to test + * @return true if the list is compact and must be unrolled + */ + private boolean should_be_unrolled(LVLI leveleditem) { + Matcher search = patternCompactList.matcher(leveleditem.getEDID()); + return search.find() && modsWithCompactLists.contains(leveleditem.getFormMaster()); + } + + /**Temper the given Leveled Item. + * Replace the single item in the specified leveled item by several variants + * of varying quality. The occuring tempering values are determined by the + * given quality instance. + * + * @param litem the Leveled Item to temper + * @param key the quality scheme to use + */ + private void reqtifyItemQuality(LVLI litem, Quality key) { + ArrayList template = get_tempering_levels(key); + FormID baseitem = litem.getEntry(0).getForm(); + litem.clearEntries(); + for (Float health: template) { + LeveledEntry entry = new LeveledEntry(baseitem, 1, 1); + entry.setItemCondition(health); + litem.addEntry(entry); + } + logger.debug("created tempered item list"); + } + + /**Translate a Quality instance into a list of tempering health values. + * This function takes the tier, size and distribution information from the + * given Quality object and constructs a list of the tempering health values + * that should appear in the list. Depending on the chosen distribution, the + * same value can appear multiple times in the list. + * + * @param key the quality instance with the information + * @return an ArrayList with all tempering health values to use + */ + private ArrayList get_tempering_levels(Quality key) { + if (!templates.containsKey(key)) { + int tier = key.tier; + int size = key.factor.getValue(); + ArrayList tempering = new ArrayList<>(); + int offset = (tier - 1) * 3 * size; + for (int seg = 0; seg < 3; seg++) { + int factor = key.distribution.getValue(seg); + for (int health = 0; health < size; health++) { + for (int mult = 0; mult < factor; mult++) { + float val = offset + health + seg * size; + tempering.add(1.0f + val/10.0f); + } + } + } + templates.put(key, tempering); + } + return templates.get(key); + } + + /**Enum for the known segment sizes. + * This Enum indicates how large the tempering quality segments of a list + * should be. Larger sections are usually used for bigger items. + */ + private enum SizeFactor { + + /**Segment size with one level per segment. + * This size is used for small weapons like daggers. + */ + half(1), + /**Segment size with two levels per segments. + * This size is used for normal one-handed weapons and all armor. Skyrim + * applies special rules to armors, the chest pieces automatically get + * twice the armor rating from tempering. + */ + normal(2), + /**Segment size with four levels per segments. + * This size is used for two-handed weapons, including bows and + * crossbows. + */ + twice(4); + + /**The segment size of this SizeFactor instance. + * See the documentation of Quality for usage details. + */ + private final int value; + + /**Create a new SizeFactor instance. + * The new SizeFactor will have the specified segment size. + * + * @param value segment size to use + */ + SizeFactor(int value) { + this.value = value; + } + + /**Query the segment size of this SizeFactor. + * + * @return the number of distinct tempering levels in each segment. + */ + public int getValue() { + return value; + } + } + + /**Enum for the known quality distributions. + * This Enum indicates how likely values from the different quality segments + * of a list should spawn. The distribution can be either uniform or biased + * towards either end of the quality range spanned by the Tempered List. + */ + private enum QualityDistribution { + + /**A QualityDistribution with a bias towards lower quality gear. + * This Distribution will make spawns of items with qualities from the + * lowest segment more likely. + */ + fall(3, 2, 1), + + /**A QualityDistribution with no bias. + * With this Distribution, tempering levels from all three segments are + * equally likely to be spawned. + */ + constant(1, 1, 1), + + /**A QualityDistribution with a bias towards higher quality gear. + * This Distribution has a bias towards spawns of items with tempering + * levels from the third segment of the list. + */ + rise(1, 2, 3); + + /**The spawn ratios for this list. + * The three elements of the array are the multipliers for the uniform + * spawn ratios found in the tempering range constructed from the tier + * and size information. The first element is for the lowest quality + * segment, the second for the middle one and the last for the highest + * quality segment. + */ + private final int[] values; + + /**Construct a QualityDistribution with the given spawn ratios. + * + * @param seg1 spawn rate multiplier for the low quality segment + * @param seg2 spawn rate multiplier for the medium quality segment + * @param seg3 spawn rate multiplier for the high quality segment + */ + QualityDistribution(int seg1, int seg2, int seg3) { + this.values = new int[3]; + this.values[0] = seg1; + this.values[1] = seg2; + this.values[2] = seg3; + } + + /**Query the spawn rate multiplier for the given segment. + * + * @param index the segment index, must be 0, 1 or 2 + * @return the spawn rate multiplier for the queried segment + */ + public int getValue(int index) { + return values[index]; + } + } + + /**A representation of the tempering quality in a Tempered List. + * The tempering quality consists of three components: the tier, size and + * distribution scheme.