public static void RunPatch(IPatcherState <ISkyrimMod, ISkyrimModGetter> state) { var outputPath = $@"{state.Settings.DataFolderPath}\SKSE\Plugins\Experience\"; var questConfig = RequiemExperience.Properties.Resources.quests.Split('\n', StringSplitOptions.TrimEntries) .Where(x => !x.StartsWith('#') && x.Length != 0) .Select(x => x.Split(new char[] { ';' })[0].Trim()) .Select(x => x.Split(new char[] { '=', ':' }, StringSplitOptions.RemoveEmptyEntries)); var questOverride = questConfig .Where(x => x.Length >= 2) .ToDictionary( x => x[0].Trim(), x => Enum.TryParse <Quest.TypeEnum>(x[1].Trim(), true, out var res) ? res : Quest.TypeEnum.Misc ); var questCond = questConfig .Where(x => x.Length >= 3) .ToDictionary( x => x[0].Trim(), x => x[2].Trim() ); StringBuilder?quests = null; if (questConfig.Where(x => x.Length >= 1 && x[0].Equals("debug", StringComparison.InvariantCultureIgnoreCase)).Any()) { quests = new StringBuilder(); } quests?.Append("EditorID;Type;Name;Stages;Stages Text\r\n"); Console.WriteLine($"Processing Quests Patch:\r\n + Overrides count is {questOverride.Count}\r\n + Conditions count is {questCond.Count}\r\n + Debug = {quests != null}"); FormList?radiantExcl = null; if (questCond.Count > 0) { radiantExcl = state.PatchMod.FormLists.AddNew("vf_RadiantExclusion"); radiantExcl.FormVersion = 44; } bool anyQuests = false; foreach (var quest in state.LoadOrder.PriorityOrder.WinningOverrides <IQuestGetter>()) { if (quest.EditorID == null) { continue; } string?key = null; Quest? patchQ = null; if (questOverride.TryGetValue(quest.EditorID, out var type)) { key = quest.EditorID; patchQ = state.PatchMod.Quests.GetOrAddAsOverride(quest); patchQ.Type = type; anyQuests = true; } else { var lookup = questOverride .Where(d => Regex.IsMatch(quest.EditorID, "^" + d.Key + "$", RegexOptions.IgnoreCase)) .ToDictionary(d => d.Key, d => d.Value); if (lookup.Values.Count == 1) { key = lookup.Keys.ElementAt(0); patchQ = state.PatchMod.Quests.GetOrAddAsOverride(quest); patchQ.Type = lookup.Values.ElementAt(0); anyQuests = true; } } if (key != null && patchQ != null && radiantExcl != null && questCond.TryGetValue(key, out var condition)) { foreach (var alias in patchQ.Aliases) { if (alias.Name != null && alias.Name.Equals(condition, StringComparison.InvariantCultureIgnoreCase)) { ConditionFloat cond = new ConditionFloat(); cond.CompareOperator = CompareOperator.NotEqualTo; cond.ComparisonValue = 1.0f; FunctionConditionData func = new FunctionConditionData(); func.Function = (ushort)ConditionData.Function.GetInCurrentLocFormList; func.ParameterOneRecord = radiantExcl; cond.Data = func; alias.Conditions.Insert(1, cond); } } } if (quests != null) { quests?.Append(quest.EditorID + ";" + quest.Type + ";\"" + (quest.Name?.ToString() ?? "null").Replace('"', '-') + "\";" + quest.Objectives.Count + ";\""); foreach (var obj in quest.Objectives) { quests?.Append((obj.DisplayText?.ToString() ?? "null").Replace('"', '-') + ", "); } quests?.Append("\"\r\n"); } } var racesConfig = RequiemExperience.Properties.Resources.npcs.Split('\n', StringSplitOptions.TrimEntries) .Where(x => !x.StartsWith('#') && x.Length != 0) .Select(x => x.Split(new char[] { ';' })[0].Trim()) .Select(x => x.Split(new char[] { '=', ':' }, StringSplitOptions.RemoveEmptyEntries)); StringBuilder?npcs = null; if (racesConfig.Where(x => x.Length >= 1 && x[0].Equals("debug", StringComparison.InvariantCultureIgnoreCase)).Any()) { npcs = new StringBuilder(); npcs?.Append("NPC;Race;Level;Comments\r\n"); } var averageMode = racesConfig.Where(x => x.Length >= 2 && x[0].Equals("mode", StringComparison.InvariantCultureIgnoreCase)) .Select(x => x[1].Trim().ToLowerInvariant()).DefaultIfEmpty("median").First(); var raceOutFile = racesConfig.Where(x => x.Length >= 2 && x[0].Equals("file", StringComparison.InvariantCultureIgnoreCase)) .Select(x => x[1].Trim().ToLowerInvariant()).DefaultIfEmpty("DefaultRaces").First(); var racesGroups = racesConfig .Where(x => x.Length >= 3 && x[0].Equals("Groups", StringComparison.InvariantCultureIgnoreCase)) .ToDictionary( x => x[1].Trim(), x => x[2].Trim().Split(',', StringSplitOptions.RemoveEmptyEntries) ); var ignoreableNPCs = racesConfig .Where(x => x.Length >= 2 && x[0].Equals("Ignore", StringComparison.InvariantCultureIgnoreCase)) .Select(x => x[1].Trim()).ToList(); var uniqueNPCs = racesConfig .Where(x => x.Length >= 2 && x[0].Equals("Unique", StringComparison.InvariantCultureIgnoreCase)) .Select(x => x[1].Trim()).ToList(); var overrideRaces = racesConfig .Where(x => x.Length >= 2 && x[0].Equals("Override", StringComparison.InvariantCultureIgnoreCase)) .ToDictionary( x => x[1].Trim(), x => int.TryParse(x[2].Trim(), out var res) ? res : 0 ); Console.WriteLine($"Processing NPC Races:\r\n + Averaging mode is {averageMode}\r\n + Groups count is {racesGroups.Count}\r\n + Ignorable NPCs count is {ignoreableNPCs.Count}\r\n + Unique NPCs count is {uniqueNPCs.Count}\r\n + Race XP overrides count is {overrideRaces.Count}\r\n + Debug = {npcs != null}"); var races = new StringBuilder(); var racesLevels = new Dictionary <string, ICollection <double> >(); foreach (var npc in state.LoadOrder.PriorityOrder.WinningOverrides <INpcGetter>()) { int level = -1; if (npc.Configuration.Level is IPcLevelMult) { level = npc.Configuration.CalcMinLevel; if (npc.Configuration.CalcMaxLevel != 0) { level = (level + npc.Configuration.CalcMaxLevel) / 2; } } else if (npc.Configuration.Level is INpcLevelGetter npcLevel) { level = npcLevel.Level; } string? key = null; string? val = null; string? EditorID = null; IRaceGetter?race = null; var ignore = npc.EditorID == null || ignoreableNPCs.Where(x => Regex.IsMatch(npc.EditorID, "^" + x + "$", RegexOptions.IgnoreCase)).Any(); var unique = npc.EditorID == null || uniqueNPCs.Where(x => Regex.IsMatch(npc.EditorID, "^" + x + "$", RegexOptions.IgnoreCase)).Any(); //npc.AttackRace.TryResolve(state.LinkCache, out arace); if (npc.Race.TryResolve(state.LinkCache, out race) && !ignore) { EditorID = race.EditorID; if (unique) { var otherFormLists = state.LoadOrder.PriorityOrder.WinningOverrides <IFormListGetter>() .Where(x => x.ContainedFormLinks.Any(y => race.FormKey == y.FormKey)) .Select(x => state.PatchMod.FormLists.GetOrAddAsOverride(x)); EditorID = EditorID + "__" + npc.EditorID; var newRace = state.PatchMod.Races.AddNew(EditorID); newRace.DeepCopyIn(race, new Race.TranslationMask(defaultOn: true) { EditorID = false }); newRace.MorphRace = race.MorphRace.FormKeyNullable ?? race.FormKey; newRace.AttackRace = race.AttackRace.FormKeyNullable ?? race.FormKey; newRace.ArmorRace = race.AttackRace.FormKeyNullable ?? race.FormKey; var patchN = state.PatchMod.Npcs.GetOrAddAsOverride(npc); patchN.Race = newRace; foreach (var otherFormList in otherFormLists) { var formLinks = otherFormList.ContainedFormLinks; if (formLinks.Any(x => x.FormKey == race.FormKey)) { otherFormList.Items.Add(newRace); } } } else { raceGroupLookup(racesGroups, EditorID, out key, out val); } if (key != null && racesGroups.TryGetValue(key, out var group)) { if (!racesLevels.TryGetValue(key + val, out var levels)) { racesLevels.Add(key + val, new List <double>() { level }); } else { levels.Add(level); } } else if (EditorID != null && racesLevels.TryGetValue(EditorID, out var levels)) { levels.Add(level); } else if (EditorID != null) { racesLevels.Add(EditorID, new List <double>() { level }); } } npcs?.Append(npc.EditorID + "," + EditorID + "," + level + "," + key + val + (ignore ? " ignored" : "") + (unique ? " unique" : "") + "\r\n"); } foreach (var race in state.LoadOrder.PriorityOrder.WinningOverrides <IRaceGetter>()) { raceGroupLookup(racesGroups, race.EditorID, out var key, out var val); if (overrideRaces.TryGetValue(race.EditorID ?? "null", out int overrid)) { races.Append(race.EditorID); races.Append(","); races.Append(overrid); races.Append('\n'); } else if (key != null && racesLevels.TryGetValue(key + val, out var glevels) && glevels.Any()) { races.Append(race.EditorID); races.Append(","); races.Append(average(glevels, averageMode)); races.Append('\n'); } else if (racesLevels.TryGetValue(race.EditorID ?? "null", out var levels) && levels.Any()) { races.Append(race.EditorID); races.Append(","); races.Append(average(levels, averageMode)); races.Append('\n'); } else if (race.Starting.TryGetValue(BasicStat.Health, out var startingHealth)) { // fallback for races which don't have NPCs defined races.Append(race.EditorID); races.Append(","); races.Append(Math.Floor(Math.Sqrt(startingHealth) + 0.5)); races.Append('\n'); } } Console.WriteLine($@"Creating folder: {outputPath}"); Directory.CreateDirectory(outputPath); Console.WriteLine($@"Writing races patch: {outputPath}Races\{raceOutFile}.csv"); File.WriteAllText($@"{outputPath}Races\{raceOutFile}.csv", races.ToString()); if (quests != null) { Console.WriteLine($@"Writing debug file: {outputPath}quests.csv"); File.WriteAllText($@"{outputPath}quests.csv", quests?.ToString()); } if (npcs != null) { Console.WriteLine($@"Writing debug file: {outputPath}npcs.csv"); File.WriteAllText($@"{outputPath}npcs.csv", npcs?.ToString()); } if (!anyQuests) { state.PatchMod.Clear(); } }
public static void RunPatch(IPatcherState <ISkyrimMod, ISkyrimModGetter> state) { var functionsOfInterest = new HashSet <ConditionData.Function>() { ConditionData.Function.GetIsRace, ConditionData.Function.GetPCIsRace }; bool isConditionOnPlayerRace(IFunctionConditionDataGetter x) { return(functionsOfInterest.Contains((ConditionData.Function)x.Function) && vanillaRaceToActorProxyKeywords.ContainsKey(x.ParameterOneRecord.FormKey)); } bool isConditionOnPlayerRaceProxyKeyword(IFunctionConditionDataGetter x) { return(x.Function == (int)ConditionData.Function.HasKeyword && actorProxyKeywords.Contains(x.ParameterOneRecord.FormKey)); } bool isVictim(IDialogResponsesGetter x) { bool ok = false; foreach (var data in x.Conditions .OfType <IConditionFloatGetter>() .Select(x => x.Data) .OfType <IFunctionConditionDataGetter>()) { if (!ok && isConditionOnPlayerRace(data)) { ok = true; } if (isConditionOnPlayerRaceProxyKeyword(data)) { return(false); } } return(ok); } int responseCounter = 0; var dialogueSet = new HashSet <FormKey>(); foreach (var item in state.LoadOrder.PriorityOrder.DialogResponses().WinningContextOverrides(state.LinkCache)) { if (!isVictim(item.Record)) { continue; } var response = item.GetOrAddAsOverride(state.PatchMod); //Console.WriteLine(response.FormKey); responseCounter++; if (item.Parent?.Record is IDialogTopicGetter getter) { dialogueSet.Add(getter.FormKey); } for (var i = response.Conditions.Count - 1; i >= 0; i--) { if (response.Conditions[i] is not ConditionFloat condition) { continue; } if (condition.Data is not FunctionConditionData data) { continue; } if (!isConditionOnPlayerRace(data)) { continue; } var newCondition = new ConditionFloat(); newCondition.DeepCopyIn(condition, new Condition.TranslationMask(defaultOn: true) { Unknown1 = false }); switch (condition.CompareOperator) { case CompareOperator.EqualTo: if (condition.ComparisonValue == 0) { newCondition.Flags = condition.Flags | Condition.Flag.OR; } break; case CompareOperator.NotEqualTo: if (condition.ComparisonValue == 1) { newCondition.Flags = condition.Flags | Condition.Flag.OR; } break; case CompareOperator.GreaterThan: case CompareOperator.LessThan: case CompareOperator.GreaterThanOrEqualTo: case CompareOperator.LessThanOrEqualTo: Console.WriteLine($"TODO not sure how to handle condition in {item.Record.FormKey}"); continue; } var newData = new FunctionConditionData { Function = (ushort)ConditionData.Function.HasKeyword, ParameterOneRecord = vanillaRaceToActorProxyKeywords[data.ParameterOneRecord.FormKey] }; newData.DeepCopyIn(data, new FunctionConditionData.TranslationMask(defaultOn: true) { Function = false, ParameterOneRecord = false }); if ((ConditionData.Function)data.Function is ConditionData.Function.GetPCIsRace) { newData.Unknown3 = (int)Condition.RunOnType.Reference; newData.Unknown4 = 0x00000014; // PlayerRef [PLYR:000014] } newCondition.Data = newData; response.Conditions.Insert(i, newCondition); } } int dialogueCounter = dialogueSet.Count; Console.WriteLine($"Modified {responseCounter} responses to {dialogueCounter} dialogue topics."); }