void Main() { //Be carefull when editing quicksaves, the backup can be out-of-date if you never delete it. var saveName = "Quicksave 1"; //The names of the characters that will be edited. //Unique characters like Marshal Kwon don't have a display name var pcNames = new HashSet { "William", "Li-Tsing" }; var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), @"My GamesWasteland3Save Games", saveName, saveName + ".bak"); if(!File.Exists(path)) { File.Copy(Path.ChangeExtension(path, ".bak"), path); } var saveData = Load(path); var xml = saveData.SaveState; //Dumps the xml part of the save to a new output panel with syntax highlighting. //PanelManager.DisplaySyntaxColoredText(xml.ToString(), SyntaxLanguageStyle.XML); return; var pcs = xml.Root.Descendants("pc") .Where(pc => pc.Element("displayName") != null) .Where(pc => pcNames.Contains((string)pc.Element("displayName"))) .Dump() ; var attributes = new (string attribute, int value)[] { ("coordination", 10), ("luck", 10), ("awareness", 10), ("strength", 10), ("speed", 10), ("intelligence", 10), ("charisma", 10), //("xp", 5999), //Current hitpoints //("hitpoints", 491), ("money", 99999), ("availableAttributePoints", 0), ("availableSkillPoints", 100), ("perkPoints", 0), }; var newPerks = new (string perkname, int count)[] { //-1 is skip, 0 will remove 1+ will add it that many times. //Custom Ranger Backgrounds ("BCK_Bookworm", -1), //5% Experience ("BCK_DesertCat", 5), //1 Perception ("BCK_DiscipleOfTheMetal", 1), //15% Fire Damage ("BCK_Explodomaniac", 1), //15% Explosive Damage ("BCK_GoatKiller", 1), //5% Critical Chance ("BCK_GreaseMonkey", 1), //10% Damage to Robots & Vehicles ("BCK_LethalWeapon", 1), //10% Melee Damage ("BCK_Mannerite", -1), //1 Kick Ass ("BCK_Moneybags", -1), //1 Barter ("BCK_MopeyPoet", 10), //5% Evasion ("BCK_Paladin", 10), //10% Crit Resistance ("BCK_RaiderHater", 1), //10% Damage to Humans ("BCK_SexMachine", 10), //0.2 Combat Speed ("BCK_Stoner", 10), //10% Status Effect Resistance ("BCK_TheBoss", -1), //1 Hard Ass ("BCK_ViciousAvenger", 10), //2 Penetration //Premade Ranger Backgrounds ("BCK_Scout", 1), //10% Sneak Attack Damage ("BCK_Farmer", 1), //1 HP/Level ("BCK_Technician", -1), //??? ("BCK_Nomad", -1), //??? ("BCK_Bouncer", 1), //10% Melee Damage ("BCK_Thief", 10), //1 Second Detection Time ("BCK_Hacker", 1), //10% Damage to Robots & Synths ("BCK_Evangelical", 1), //3m Leadership Range ("BCK_Drifter", 10), //4 Armor ("BCK_Miner", 10), //15% Explosive Resistance ("BCK_Mercenary", 1), //10% Crit Resistance ("BCK_Gearhead", 1), //10% Damage to Vehicles ("BCK_ConArtist", 1), //5% Initiative ("BCK_Academic", -1), //10% Experience //Unique Ranger Backgrounds ("BCK_Yuri", 1), //5% Ranged Damage ("BCK_Spence", 1), //3% Evasion ("BCK_Bronco", 1), //5% Melee Damage ("BCK_Kickboy", 1), //5% Initiative ("BCK_William", 1), //0.2 Combat Speed ("BCK_LiTsing", 1), //10% Sneak Attack Damage ("BCK_Dusty", 1), //5% Crit Resistance ("BCK_Marie", -1), //5% Experience ("BCK_Chris", 1), //1 Perception ("BCK_Kris", 10), //10% Energy Resistance, 5% Energy Damage //Companion Backgrounds ("BCK_MarshalKwon", 1), //30% Initiative ("BCK_LuciaWesson", 5), //5% Strike Rate ("BCK_JodieBell", 10), //1 Quick Slot ("BCK_Fishlips", 1), //0.4 Critical Damage ("BCK_IroncladCordite", 1), //4 Armor ("BCK_Scotchmo", 1), //10% Status Effect Resistance //Quirks ("QRK_None", -1), // ("QRK_DeathWish", 1), //+3 AP, +3 AP (Max); Cannot Wear Any Kind of Armor ("QRK_DoomsdayPrepper", -1), //+35% Status Effect Resistance; Cannot Read Skill Books ("QRK_Prospector", 1), //Occasionally Find Gold Nuggets When Digging for Buried Items; -1 Quick Slot ("QRK_SerialKiller", 1), //-1 AP; +3 AP Per Kill (Once Per Turn) ("QRK_WasteRoamer", 1), //100% Resistance to Bleeding, Poisoned, Shocked, Burning, Frozen; -15% Experience //Special Perks ("PRK_CyborgTech", 1), //Equip Cyborg Tech //("PRK_MarshalTraining", 1), //2m Leadership Range //Generic Perks ("PRK_Generic_DeepPockets", 1), //1 Quick Slot ("PRK_Generic_Hardened", 1), //2 Armor ("PRK_Generic_Healthy", 10), //35 HP ("PRK_Generic_QuickReflexes", 5), //5% Evasion ("PRK_Generic_Weathered", 1), //10% Crit Resistance //("PRK_DuckAndCover", 5), //20% Fire & Explosive Resistance }; foreach (var pc in pcs) { foreach (var attr in attributes) { pc.SetElementValue(attr.attribute, attr.value); } var perks = pc.Element("perks"); foreach (var newPerk in newPerks) { perks.EnsurePerkCount(newPerk.perkname, newPerk.count); } perks.Add(new XElement("perk", new XElement("perkname", "BCK_JorenPizepi"))); } DuplicateMods(saveData, 5); Save(Path.ChangeExtension(saveData.Path, ".xml"), saveData); } public static void DuplicateMods(SaveData saveData, int duplicateCount) { var items = saveData.SaveState.Root.Element("hostInventory"); foreach (var item in items.Descendants("item").Where(itm => itm.Element("templateName").Value.Contains("Mod_", StringComparison.Ordinal)).ToList()) { var templateName = item.Element("templateName").Value; while (items.Descendants("item").Where(itm => itm.Element("templateName").Value.Equals(templateName, StringComparison.Ordinal)).Count() < duplicateCount) { var copy = new XElement(item); copy.SetElementValue("uid", Guid.NewGuid()); items.Add(copy); } } } public sealed class SaveData { public string Path {get;} public XDocument SaveState {get;} public IReadOnlyList Header {get;} public SaveData(string path, XDocument saveState, IEnumerable header) { Path = path; SaveState = saveState; Header = header.ToList().AsReadOnly(); } } public static SaveData Load(string path) { var contents = File.ReadAllBytes(path); var index = 0; var header = new List(); for (int lfFound = 0; lfFound < 11; lfFound++) { var next = Array.FindIndex(contents, index, b => b == (byte)'n') + 1; header.Add(Encoding.UTF8.GetString(contents, index, next - index)); index = next; } var compressed = new byte[contents.Length - index]; Array.Copy(contents, index, compressed, 0, contents.Length - index); var result = CLZF2.Decompress(compressed); var xml = XDocument.Load(new MemoryStream(result)); return new SaveData(path, xml, header); } public static void Save(string path, SaveData saveData) { var tempStream = new MemoryStream(); var xmlSettings = new XmlWriterSettings { OmitXmlDeclaration = true, Indent = false, }; using (var writer = XmlWriter.Create(tempStream, xmlSettings)) { saveData.SaveState.Save(writer); } var changedData = tempStream.ToArray(); var compressedChangedData = CLZF2.Compress(changedData); using (var newSave = File.Create(path)) { for (int i = 0; i < saveData.Header.Count; i++) { var line = saveData.Header[i]; if (i == 4 || i == 5) { line = Regex.Replace(line, @"(d+)", i == 4 ? changedData.Length.ToString() : compressedChangedData.Length.ToString()); } var lineBytes = Encoding.UTF8.GetBytes(line); newSave.Write(lineBytes, 0, lineBytes.Length); } newSave.Write(compressedChangedData, 0, compressedChangedData.Length); } } public static class LinqToXmlExtensions { public static void EnsurePerkCount(this XContainer perksContainer, string perkname, int count) { if(count < 0) return; var chosenPerkEntries = perksContainer.Elements("perk") .Where(p => string.Equals(p.Element("perkname").Value, perkname, StringComparison.Ordinal)); chosenPerkEntries.Skip(count).Remove(); var perksToAdd = count - chosenPerkEntries.Count(); for (int i = 0; i < perksToAdd; i++) { perksContainer.AddFirst(new XElement("perk", new XElement("perkname", perkname))); } } public static void AddFirstUntilCount(this XContainer container, Func factory, Func predicate, int count) { while (container.Elements().Count(predicate) < count) { container.AddFirst(factory()); } } }