Skip to content

Instantly share code, notes, and snippets.

@factubsio
Created October 4, 2022 21:18
Show Gist options
  • Save factubsio/5fcd53f9759ca2ee76941d170b522280 to your computer and use it in GitHub Desktop.
Save factubsio/5fcd53f9759ca2ee76941d170b522280 to your computer and use it in GitHub Desktop.
using BubbleRaces.Config;
using BubbleRaces.Extensions;
using BubbleRaces.Utilities;
using HarmonyLib;
using Kingmaker;
using Kingmaker.Blueprints;
using Kingmaker.Blueprints.Area;
using Kingmaker.Blueprints.CharGen;
using Kingmaker.Blueprints.Classes;
using Kingmaker.Blueprints.Classes.Selection;
using Kingmaker.Blueprints.Items.Armors;
using Kingmaker.Blueprints.Items.Weapons;
using Kingmaker.Blueprints.Root;
using Kingmaker.BundlesLoading;
using Kingmaker.Designers.Mechanics.Buffs;
using Kingmaker.Designers.Mechanics.Facts;
using Kingmaker.Enums;
using Kingmaker.PubSubSystem;
using Kingmaker.ResourceLinks;
using Kingmaker.RuleSystem.Rules;
using Kingmaker.UnitLogic;
using Kingmaker.UnitLogic.FactLogic;
using Kingmaker.View;
using Kingmaker.View.Equipment;
using Kingmaker.Visual.CharacterSystem;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
namespace BubbleRaces {
public static class BubbleRacesRoot {
public static void Install() {
Uninstall();
try {
Grippli.Install();
} catch (Exception ex) {
Main.Error(ex, "races");
}
}
internal static void Uninstall() {
Grippli.Uninstall();
foreach (var asset in ModSettings.AssetsInBundles.Keys) {
ResourcesLibrary.s_LoadedResources.Remove(asset);
}
if (BundlesLoadService.Instance.m_Bundles.TryGetValue("bubbleraces_builtin", out var bundle)) {
if (bundle.Bundle != null)
bundle.Bundle.Unload(false);
BundlesLoadService.Instance.m_Bundles.Remove("bubbleraces_builtin");
}
}
}
public static class EntityLinks {
public static void Uninstall() {
foreach (var id in Installed)
ResourcesLibrary.s_LoadedResources.Remove(id);
foreach (var u in Undo)
u.Invoke();
}
public static List<string> Installed = new();
private static List<Action> Undo = new();
}
public class GrippliDefensiveTrainingComponent : UnitFactComponentDelegate, IRulebookHandler<RuleAttackRoll>, ITargetRulebookHandler<RuleAttackRoll>, ISubscriber, ITargetRulebookSubscriber {
public static BlueprintFeatureReference MagicalBeast = BP.Ref<BlueprintFeatureReference>("625827490ea69d84d8e599a33929fdc6");
public static BlueprintFeatureReference Animal = BP.Ref<BlueprintFeatureReference>("a95311b3dc996964cbaa30ff9965aaf6");
public void OnEventAboutToTrigger(RuleAttackRoll evt) {
var isAnimal = Animal.Get();
var isMagicalBeast = MagicalBeast.Get();
if (evt.Initiator.State.Size >= Kingmaker.Enums.Size.Large && (evt.Initiator.HasFact(isAnimal) || evt.Initiator.HasFact(isMagicalBeast))) {
evt.AddTemporaryModifier(evt.Target.Stats.AC.AddModifier(2, Runtime, Kingmaker.Enums.ModifierDescriptor.Dodge));
}
}
public void OnEventDidTrigger(RuleAttackRoll evt) { }
}
public class GrippliCamouflageComponent : UnitFactComponentDelegate, ISubscriber, IGlobalSubscriber, ITeleportHandler {
public bool InForestOrMarsh => AreaService.Instance.CurrentAreaSetting == Kingmaker.Enums.AreaSetting.Forest;
public override void OnTurnOff() {
Update(true);
}
public override void OnTurnOn() {
Update();
}
private void Update(bool forceOff = false) {
if (InForestOrMarsh && !forceOff) {
Owner.Stats.SkillStealth.AddModifierUnique(4, Runtime, Kingmaker.Enums.ModifierDescriptor.Racial);
} else {
Owner.Stats.SkillStealth.RemoveModifiersFrom(Runtime);
}
}
public void HandlePartyTeleport(AreaEnterPoint enterPoint) {
Update();
}
}
class Grippli {
private static BlueprintRace grippliRace;
private static string grippliPortraitGuid;
public static ref BlueprintRaceReference[] Races => ref BlueprintRoot.Instance.Progression.m_CharacterRaces;
public static void Install() {
Main.Log($"Using race content version: {BubbleForce.RaceContentVersion}");
var camoIcon = BP.Feature("ff1b5aa8dcc7d7d4d9aa85e1cb3f9e88").m_Icon;
var camouflageFeature = Helpers.CreateBlueprint<BlueprintFeature>("grippli-camouflage", camo => {
camo.Ranks = 1;
camo.SetNameDescription("Camouflage", "Gripplis receive a +4 racial bonus on Stealth checks in marshes and forested areas.");
camo.AddComponent<GrippliCamouflageComponent>();
camo.Groups = Helpers.Arr(FeatureGroup.Racial);
camo.m_Icon = camoIcon;
});
var defTrainingIcon = BP.Feature("f268a00e42618144e86c9db76af7f3e9").m_Icon;
var defTrainingFeature = Helpers.CreateBlueprint<BlueprintFeature>("grippli-def-training", defTraining => {
defTraining.Ranks = 1;
defTraining.SetNameDescription("Defensive Training",
"Gripplis often live in close proximity to very large animals and dangerous creatures they must learn to avoid in order to survive. " +
"They gain a +2 dodge bonus to AC against Large or larger animals and magical beasts. ");
defTraining.Groups = Helpers.Arr(FeatureGroup.Racial);
defTraining.m_Icon = defTrainingIcon;
defTraining.AddComponent<GrippliDefensiveTrainingComponent>();
});
var baseFeature = BP.Ref<BlueprintFeatureReference>("8b2da1d0f9504872809e63b39c24c502");
var princley = Helpers.CreateBlueprint<BlueprintFeature>("grippli-prince", prince => {
prince.SetNameDescription("Princely", "The grippli gains proficiency with rapiers and a +2 racial bonus on Diplomacy and Intimidate checks. This racial trait replaces Defensive Training.");
prince.AddComponent<RemoveFeatureOnApply>(rem => {
rem.m_Feature = defTrainingFeature.ToReference<BlueprintUnitFactReference>();
});
prince.m_Icon = new SpriteLink { AssetId = "0032672ed815cbd4f873fc600c173a49" }.Load(false);
prince.AddComponent<AddStatBonus>(persuasion => {
persuasion.Value = 2;
persuasion.Descriptor = Kingmaker.Enums.ModifierDescriptor.Racial;
persuasion.Stat = Kingmaker.EntitySystem.Stats.StatType.SkillPersuasion;
});
prince.AddComponent<AddProficiencies>(rapier => {
rapier.WeaponProficiencies = Helpers.Arr(WeaponCategory.Rapier);
rapier.ArmorProficiencies = Array.Empty<ArmorProficiencyGroup>();
});
});
var heritageIcon = BP.Feature("584d8b50817b49b2bb7aab3d6add8d3a").m_Icon;
var grippliHeritage = Helpers.CreateBlueprint<BlueprintFeatureSelection>("grippli-heritage", heritage => {
heritage.SetNameDescription("Racial Heritage", "Various places and living conditions create sub-races differing from their peers. They gain unique racial {g|Encyclopedia:Trait}traits{/g} in exchange for losing some of the usual ones.");
heritage.m_Features = Helpers.Arr(baseFeature, princley.Ref());
heritage.m_AllFeatures = Helpers.Arr(baseFeature, princley.Ref());
heritage.Groups = Helpers.Arr(FeatureGroup.Racial);
heritage.m_Icon = heritageIcon;
});
var gnome = BP.Race("ef35a22c9a27da345a4528f0d5889157");
Main.LogNotNull("gnome", gnome);
var gnomeFemaleHeadBottom = new EquipmentEntityLink { AssetId = "0b5ca4949682a5e49ad3bbaac1002bae" }.Load(false);
Main.LogNotNull("femhead", gnomeFemaleHeadBottom);
//{
// Main.Log("RENDERER: " + gnomeFemaleHeadBottom.BodyParts[1].SkinnedRenderer.name);
// var weights = gnomeFemaleHeadBottom.BodyParts[1].SkinnedRenderer.sharedMesh.boneWeights;
// List<string> lines = new();
// var bones = gnomeFemaleHeadBottom.BodyParts[1].SkinnedRenderer.bones;
// for (int i = 0; i < bones.Length; i++) {
// lines.Add($"bone[{i}] " + bones[i].name);
// }
// for (int i = 0; i < weights.Length; i++) {
// BoneWeight weight = weights[i];
// lines.Add($"vertex[{i}].weights");
// if (weight.weight0 > 0) {
// lines.Add($" {weight.weight0:F10}");
// lines.Add($" {weight.boneIndex0}");
// lines.Add($" {bones[weight.boneIndex0]}");
// }
// if (weight.weight1 > 0) {
// lines.Add($" {weight.weight1:F10}");
// lines.Add($" {weight.boneIndex1}");
// lines.Add($" {bones[weight.boneIndex1]}");
// }
// if (weight.weight2 > 0) {
// lines.Add($" {weight.weight2:F10}");
// lines.Add($" {weight.boneIndex2}");
// lines.Add($" {bones[weight.boneIndex2]}");
// }
// if (weight.weight3 > 0) {
// lines.Add($" {weight.weight3:F10}");
// lines.Add($" {weight.boneIndex3}");
// lines.Add($" {bones[weight.boneIndex3]}");
// }
// }
// File.WriteAllLines(@"D:\weights.txt", lines);
//}
Main.Log("Creating BlueprintRace");
grippliRace = Helpers.CreateCopy<BlueprintRace>(gnome, race => {
race.name = "bubble-race-grippli";
race.AssetGuid = ModSettings.Blueprints.GetGUID(race.name);
race.m_DisplayName = "Grippli".Loc();
race.m_Description = @"Gripplis stand just over 2 feet tall and have mottled green-and-brown skin.
Most gripplis are primitive hunter gatherers, living on large insects and fish found near their treetop homes, and are unconcerned about events outside their swamps.
The rare grippli who leaves the safety of the swamp tends to be a ranger or alchemist seeking to trade for metals and gems.
Gripplis gain +2 to Dexterity and Wisdom, but -2 to Strength.".TrimLines().Loc("grippli-desc");
race.m_Features = Helpers.Arr(camouflageFeature.BaseRef(), defTrainingFeature.BaseRef(), grippliHeritage.BaseRef());
race.Components = Array.Empty<BlueprintComponent>();
BlueprintRaceVisualPreset[] presets = new BlueprintRaceVisualPreset[race.m_Presets.Length];
EquipmentEntityLink originalBodyM = null;
EquipmentEntityLink originalBodyF = null;
var skin = Helpers.CreateCopy(race.m_Presets[0].Get().Skin, skin => {
skin.name = skin.name.Replace("Gnome", "Grippli");
skin.AssetGuid = ModSettings.Blueprints.GetGUID(skin.name);
BP.AddBlueprint(skin);
originalBodyM = skin.m_MaleArray[0];
originalBodyF = skin.m_FemaleArray[0];
skin.m_MaleArray = Helpers.Arr(new EquipmentEntityLink { AssetId = "44d4e305c9b84264aa7edc88f107fe6b" });
skin.m_FemaleArray = Helpers.Arr(new EquipmentEntityLink { AssetId = "eb94c1363f07c4149a1d718eeed79ae7" });
});
Action<UnityEngine.Object> MakeBodyPatch(EquipmentEntityLink original) {
return obj => {
var entity = obj as EquipmentEntity;
var from = original.Load(false);
entity.BodyParts = new(from.BodyParts);
foreach (var bp in entity.BodyParts) {
Main.Log("skin body part.type: " + bp.Type);
Main.Log("skin body part.renderer: " + bp.RendererPrefab?.name ?? "<missing>");
bp.Textures = new(bp.Textures);
if (bp.Textures.Count > 0 && bp.Textures[0].NormalActive)
bp.Textures[0].NormalTexture = new Texture2DLink { AssetId = "8e662dafd4534f54c936d3ff4e80ebeb" }.Load(false);
}
};
}
AssetPatcher.LoadActions["44d4e305c9b84264aa7edc88f107fe6b"] = MakeBodyPatch(originalBodyM);
AssetPatcher.LoadActions["eb94c1363f07c4149a1d718eeed79ae7"] = MakeBodyPatch(originalBodyF);
for (int i = 0; i < presets.Length; i++) {
presets[i] = Helpers.CreateCopy(race.m_Presets[i].Get(), p => {
p.name = p.name.Replace("Gnome", "Grippli");
p.AssetGuid = ModSettings.Blueprints.GetGUID(p.name);
BP.AddBlueprint(p);
p.m_Skin = skin.ToReference<KingmakerEquipmentEntityReference>();
});
}
race.m_Presets = presets.Select(p => p.ToReference<BlueprintRaceVisualPresetReference>()).ToArray();
race.AddComponent<AddStatBonus>(stat => {
stat.Descriptor = Kingmaker.Enums.ModifierDescriptor.Racial;
stat.Value = 2;
stat.Stat = Kingmaker.EntitySystem.Stats.StatType.Dexterity;
});
race.AddComponent<AddStatBonus>(stat => {
stat.Descriptor = Kingmaker.Enums.ModifierDescriptor.Racial;
stat.Value = 2;
stat.Stat = Kingmaker.EntitySystem.Stats.StatType.Wisdom;
});
race.AddComponent<AddStatBonusIfHasFact>(stat => {
stat.Descriptor = Kingmaker.Enums.ModifierDescriptor.Racial;
stat.Value = -2;
stat.Stat = Kingmaker.EntitySystem.Stats.StatType.Strength;
stat.m_CheckedFacts = Helpers.Arr(BP.Ref<BlueprintUnitFactReference>("325f078c584318849bfe3da9ea245b9d"));
stat.InvertCondition = true;
});
race.SelectableRaceStat = false;
race.MaleOptions.Beards = Array.Empty<EquipmentEntityLink>();
Main.Log("Original head mask: " + race.MaleOptions.m_Heads[0].Load(false).BodyParts[0].Textures[0].MaskTexture.name);
var head = new EquipmentEntityLink { AssetId = "408110b2bca08ba4ca3e14bb33b4873f" };
race.MaleOptions.m_Heads = Helpers.Arr(head);
race.MaleOptions.m_HeadsCache = null;
race.MaleOptions.Horns = new EquipmentEntityLink[] { };
race.FemaleOptions.m_Heads = Helpers.Arr(new EquipmentEntityLink { AssetId = "be16724ac338a6741afc5703b25c37fd" });
race.FemaleOptions.m_HeadsCache = null;
race.MaleOptions.m_Hair = Array.Empty<EquipmentEntityLink>();
race.MaleOptions.m_HairCache = null;
race.MaleOptions.m_Eyebrows = Array.Empty<EquipmentEntityLink>();
race.MaleOptions.m_EyebrowsCache = null;
race.FemaleOptions.m_Hair = Array.Empty<EquipmentEntityLink>();
race.FemaleOptions.m_HairCache = null;
race.FemaleOptions.m_Eyebrows = Array.Empty<EquipmentEntityLink>();
race.FemaleOptions.m_EyebrowsCache = null;
});
var portrait = Helpers.CreateBlueprint<BlueprintPortrait>("grippli-portrait-1", p => {
p.Data = new() {
PortraitCategory = PortraitCategory.Wrath,
m_FullLengthImage = new SpriteLink { AssetId = "03d6896925e516e438fef02c45c92288" },
m_HalfLengthImage = new SpriteLink { AssetId = "9c158e689e838be4da73ce9042651190" },
m_PortraitImage = new SpriteLink { AssetId = "bc873cdf88bc40d4d9d278935ca3df57" },
};
p.AddComponent<PortraitDollSettings>(set => {
set.m_Race = grippliRace.ToReference<BlueprintRaceReference>();
set.Gender = Gender.Male;
});
grippliPortraitGuid = p.AssetGuid.ToString();
});
Helpers.AppendInPlace(ref BlueprintRoot.Instance.CharGen.m_Portraits, portrait.ToReference<BlueprintPortraitReference>());
BP.AddBlueprint(grippliRace);
Helpers.AppendInPlace(ref Races, grippliRace.ToReference<BlueprintRaceReference>());
}
public static void Uninstall() {
if (grippliRace != null) {
BlueprintRoot.Instance.Progression.m_CharacterRaces = BlueprintRoot.Instance.Progression.m_CharacterRaces.Where(r => r.deserializedGuid != grippliRace.AssetGuid.ToString()).ToArray();
grippliRace = null;
}
if (grippliPortraitGuid != null) {
BlueprintRoot.Instance.CharGen.m_Portraits = BlueprintRoot.Instance.CharGen.m_Portraits.Where(p => p.deserializedGuid != grippliPortraitGuid).ToArray();
grippliPortraitGuid = null;
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment