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
<string> { "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<string> Header {get;}
public SaveData(string path, XDocument saveState, IEnumerable<string> 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
<string>();
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<XElement> factory, Func<XElement, bool> predicate, int count)
{
while (container.Elements().Count(predicate) < count)
{
container.AddFirst(factory());
}
}
}