Skip to content

Commit

Permalink
Merge pull request #35 from xADDBx/PatchTool
Browse files Browse the repository at this point in the history
Patch Tool for Blueprints
  • Loading branch information
xADDBx authored Nov 18, 2024
2 parents 5e4bfac + 24d6442 commit ac032f0
Show file tree
Hide file tree
Showing 23 changed files with 1,445 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class BlueprintIdCache {
typeof(BlueprintBrain), typeof(BlueprintFeature), typeof(BlueprintUnitFact),
typeof(BlueprintPlanet), typeof(BlueprintColony), typeof(BlueprintStarSystemMap),
typeof(BlueprintColonyTrait), typeof(BlueprintResource), typeof(BlueprintColonyEventResult),
typeof(BlueprintUnitAsksList)
typeof(BlueprintUnitAsksList), typeof(BlueprintAbilityFXSettings)
};

private static BlueprintIdCache _instance;
Expand Down
3 changes: 2 additions & 1 deletion ToyBox/Classes/MainUI/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
using UniRx;
using UnityEngine;
using UnityModManagerNet;
using static ModKit.UI;
using ToyBox.PatchTool;
using LocalizationManager = ModKit.LocalizationManager;

namespace ToyBox {
Expand Down Expand Up @@ -58,6 +58,7 @@ internal static class Main {
new NamedAction("Dialog & NPCs", DialogAndNPCs.OnGUI),
new NamedAction("Saves", GameSavesBrowser.OnGUI),
new NamedAction("Achievements", AchievementsUnlocker.OnGUI),
new NamedAction("Patch Tool", PatchToolUIManager.OnGUI),
new NamedAction("Settings", SettingsUI.OnGUI)
};
private static int partyTabID = -1;
Expand Down
19 changes: 19 additions & 0 deletions ToyBox/Classes/MainUI/PatchTool/Infrastructure/Patch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using Kingmaker.Blueprints;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ToyBox.PatchTool;
public class Patch {
public string PatchId = Guid.NewGuid().ToString();
public string BlueprintGuid;
public List<string> PreviousPatches;
public List<PatchOperation> Operations;
public Patch(string blueprintGuid, List<PatchOperation> operations, List<string> previousPatches = null) {
BlueprintGuid = blueprintGuid;
Operations = operations;
PreviousPatches = previousPatches;
}
}
153 changes: 153 additions & 0 deletions ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchOperation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
using HarmonyLib;
using Kingmaker.Blueprints;
using Kingmaker.Blueprints.JsonSystem.Converters;
using Newtonsoft.Json.Linq;
using RogueTrader.SharedTypes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ToyBox.PatchTool;
public class PatchOperation {
public enum PatchOperationType {
ModifyPrimitive,
ModifyUnityReference,
ModifyBlueprintReference,
ModifyComplex,
ModifyCollection
}
public enum CollectionPatchOperationType {
AddAtIndex,
RemoveAtIndex,
ModifyAtIndex
}
public string FieldName;
public Type NewValueType;
public object NewValue;
public int CollectionIndex;
public PatchOperation NestedOperation;
public Type PatchedObjectType;
public PatchOperationType OperationType;
public CollectionPatchOperationType CollectionOperationType;
public PatchOperation() { }
public PatchOperation(PatchOperationType operationType, string fieldName, Type newValueType, object newValue, Type patchedObjectType, PatchOperation nestedOperation = null) {
OperationType = operationType;
FieldName = fieldName;
NewValue = newValue;
PatchedObjectType = patchedObjectType;
NestedOperation = nestedOperation;
NewValueType = newValueType;
}
public PatchOperation(PatchOperationType operationType, string fieldName, Type newValueType, object newValue, Type patchedObjectType, CollectionPatchOperationType collectionOperationType, int collectionIndex, PatchOperation nestedOperation = null) {
OperationType = operationType;
FieldName = fieldName;
NewValue = newValue;
PatchedObjectType = patchedObjectType;
CollectionOperationType = collectionOperationType;
CollectionIndex = collectionIndex;
NestedOperation = nestedOperation;
NewValueType = newValueType;
}
public FieldInfo GetFieldInfo(Type type) {
return AccessTools.Field(type, FieldName);
}
public object Apply(object instance) {
if (!(OperationType == PatchOperationType.ModifyCollection) && !PatchedObjectType.IsAssignableFrom(instance.GetType())) throw new ArgumentException($"Type to patch {PatchedObjectType} is not assignable from instance type {instance.GetType()}");
bool IsPatchingCollectionDirectly = PatchToolUtils.IsListOrArray(instance?.GetType());

var field = IsPatchingCollectionDirectly ? null : GetFieldInfo(PatchedObjectType);

switch (OperationType) {
case PatchOperationType.ModifyCollection: {
object collection;
if (IsPatchingCollectionDirectly) {
collection = instance;
} else {
collection = field.GetValue(instance);
}
switch (CollectionOperationType) {
case CollectionPatchOperationType.AddAtIndex: {
if (collection.GetType() is Type type && type.IsArray) {
Array array = collection as Array;
if (CollectionIndex == -1) CollectionIndex = array.Length;
var elementType = type.GetElementType();
Array newArray = Array.CreateInstance(elementType, array.Length + 1);
Array.Copy(array, 0, newArray, 0, CollectionIndex);
newArray.SetValue(Activator.CreateInstance(NewValueType), CollectionIndex);
Array.Copy(array, CollectionIndex, newArray, CollectionIndex + 1, array.Length - CollectionIndex);
collection = newArray;
} else if (collection is IList list) {
if (CollectionIndex == -1) CollectionIndex = list.Count;
list.Insert(CollectionIndex, Activator.CreateInstance(NewValueType));
collection = list;
}
}
break;
case CollectionPatchOperationType.RemoveAtIndex: {
if (collection.GetType() is Type type && type.IsArray) {
Array array = collection as Array;
var elementType = type.GetElementType();
var tmpList = Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)) as IList;
foreach (var item in array)
tmpList.Add(item);
tmpList.RemoveAt(CollectionIndex);
Array resizedArray = Array.CreateInstance(elementType, tmpList.Count);
tmpList.CopyTo(resizedArray, 0);
collection = resizedArray;
} else if (collection is IList list) {
list.RemoveAt(CollectionIndex);
collection = list;
}
}
break;
case CollectionPatchOperationType.ModifyAtIndex: {
if (collection.GetType() is Type type && type.IsArray) {
Array array = collection as Array;
var orig = array.GetValue(CollectionIndex);
var modified = NestedOperation.Apply(orig);
array.SetValue(modified, CollectionIndex);
collection = array;
} else if (collection is IList list) {
var orig = list[CollectionIndex];
var modified = NestedOperation.Apply(orig);
list[CollectionIndex] = modified;
collection = list;
}
}
break;
default: throw new NotImplementedException($"Unknown CollectionOperation: {CollectionOperationType}");
}
field.SetValue(instance, collection);
}
break;
case PatchOperationType.ModifyUnityReference: {
throw new NotImplementedException("Modifying Unity Objects is not supported.");
}
#pragma warning disable CS0162 // Unreachable code detected
break;
#pragma warning restore CS0162 // Unreachable code detected
case PatchOperationType.ModifyComplex: {
var @object = field.GetValue(instance);
NestedOperation.Apply(@object);
field.SetValue(instance, @object);
}
break;
case PatchOperationType.ModifyPrimitive: {
field.SetValue(instance, Convert.ChangeType(NewValue, NewValueType));
}
break;
case PatchOperationType.ModifyBlueprintReference: {
var bpRef = Activator.CreateInstance(NewValueType) as BlueprintReferenceBase;
bpRef.guid = NewValue as string;
field.SetValue(instance, Convert.ChangeType(bpRef, NewValueType));
}
break;
default: throw new NotImplementedException($"Unknown PatchOperation: {OperationType}");
}
return instance;
}
}
61 changes: 61 additions & 0 deletions ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using HarmonyLib;
using Kingmaker.Blueprints;
using ModKit;
using ModKit.Utility.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ToyBox.PatchTool;
public class PatchState {
public SimpleBlueprint Blueprint;
public List<PatchOperation> Operations = new();
private Patch UnderlyingPatch;
public bool IsDirty = false;
public PatchState(SimpleBlueprint blueprint) {
SetupFromBlueprint(blueprint);
}
public PatchState(Patch patch) {
UnderlyingPatch = patch;
var bp = ResourcesLibrary.TryGetBlueprint(patch.BlueprintGuid);
if (!Patcher.AppliedPatches.ContainsKey(patch.BlueprintGuid)) {
patch.ApplyPatch();
}
Operations = patch.Operations;
SetupFromBlueprint(bp);
}
public void SetupFromBlueprint(SimpleBlueprint blueprint) {
Blueprint = blueprint;
if (Patcher.KnownPatches.TryGetValue(blueprint.AssetGuid, out UnderlyingPatch)) {
Operations = UnderlyingPatch.Operations;
}
}
public void CreateAndRegisterPatch() {
if ((Operations?.Count ?? 0) == 0) return;
CreatePatch().RegisterPatch();
}
public Patch CreatePatch() {
try {
IsDirty = true;
if (UnderlyingPatch != null) {
UnderlyingPatch.Operations = Operations;
return UnderlyingPatch;
} else {
return new(Blueprint.AssetGuid, Operations);
}
} catch (Exception ex) {
Mod.Log($"Error trying to create patch for blueprint {Blueprint.AssetGuid}:\n{ex.ToString()}");
}
return null;
}
public void AddOp(PatchOperation op) {
var foD = Operations.FirstOrDefault(i => i.OperationType == PatchOperation.PatchOperationType.ModifyPrimitive && i.PatchedObjectType == op.PatchedObjectType && i.FieldName == op.FieldName);
if (foD != default) {
Operations.Remove(foD);
}
Operations.Add(op);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Kingmaker.Blueprints;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ToyBox.PatchTool;
public class PatchToolJsonConverter : JsonConverter {
public override bool CanConvert(Type objectType) {
return objectType == typeof(PatchOperation);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
var jsonObject = Newtonsoft.Json.Linq.JObject.Load(reader);
var operation = jsonObject.ToObject<PatchOperation>();

var typeString = (string)jsonObject["NewValueType"];
if (!string.IsNullOrEmpty(typeString)) {
var targetType = Type.GetType(typeString);
if (targetType != null && !((string)jsonObject["NewValue"]).IsNullOrEmpty()) {
if (typeof(BlueprintReferenceBase).IsAssignableFrom(targetType)) {
operation.NewValue = jsonObject["NewValue"].ToObject(typeof(string));
} else {
operation.NewValue = jsonObject["NewValue"].ToObject(targetType);
}
}
}

return operation;
}
public override bool CanWrite {
get { return false; }
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
}
}
30 changes: 30 additions & 0 deletions ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchToolPatches.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using HarmonyLib;
using Kingmaker.Blueprints.JsonSystem;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ToyBox.PatchTool;

[HarmonyPatch]
public static class PatchToolPatches {
private static bool Initialized = false;
[HarmonyPriority(Priority.LowerThanNormal)]
[HarmonyPatch(typeof(BlueprintsCache), nameof(BlueprintsCache.Init)), HarmonyPostfix]
public static void Init_Postfix() {
try {
if (Initialized) {
ModKit.Mod.Log("Already initialized blueprints cache.");
return;
}
Initialized = true;

ModKit.Mod.Log("Patching blueprints.");
Patcher.PatchAll();
} catch (Exception e) {
ModKit.Mod.Log(string.Concat("Failed to initialize.", e));
}
}
}
Loading

0 comments on commit ac032f0

Please sign in to comment.