forked from cabarius/ToyBox
-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from xADDBx/PatchTool
Patch Tool for Blueprints
- Loading branch information
Showing
23 changed files
with
1,445 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
153
ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchOperation.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
61
ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchState.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchToolJsonConverter.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
30
ToyBox/Classes/MainUI/PatchTool/Infrastructure/PatchToolPatches.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
} | ||
} | ||
} |
Oops, something went wrong.