diff --git a/.gitignore b/.gitignore
index 9791284a..93d42729 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,7 +58,7 @@ _ReSharper*
*.ncrunch*
.*crunch*.local.xml
-# Installshield output folder
+# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
@@ -106,8 +106,8 @@ _UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
-
-ModuleManager.csproj
ModuleManager.csproj.user
-ModuleManager.*.dll
\ No newline at end of file
+ModuleManager.*.dll
+
+.vs*
diff --git a/CopyLocalFalse.txt b/CopyLocalFalse.txt
new file mode 100644
index 00000000..d5a69533
--- /dev/null
+++ b/CopyLocalFalse.txt
@@ -0,0 +1,2 @@
+Copy Local has been set to false for all dependencies.
+Remark by Jan Van der Haegen: This file has been added because Nuget would installs packages to the solution instead of the project if there are only PowerShell scripts... (So yes: it's safe to remove this file!)
\ No newline at end of file
diff --git a/CustomConfigsManager.cs b/CustomConfigsManager.cs
deleted file mode 100644
index 2532c43e..00000000
--- a/CustomConfigsManager.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System;
-using System.IO;
-using UnityEngine;
-
-namespace ModuleManager
-{
- [KSPAddon(KSPAddon.Startup.SpaceCentre, false)]
- public class CustomConfigsManager : MonoBehaviour
- {
- internal void Start()
- {
- if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != MMPatchLoader.techTreeFile && File.Exists(MMPatchLoader.techTreePath))
- {
- log("Setting moddeed tech tree as the active one");
- HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = MMPatchLoader.techTreeFile;
- }
-
- if (PhysicsGlobals.PhysicsDatabaseFilename != MMPatchLoader.physicsFile && File.Exists(MMPatchLoader.physicsPath))
- {
- log("Setting moddeed physics as the active one");
-
- PhysicsGlobals.PhysicsDatabaseFilename = MMPatchLoader.physicsFile;
-
- if (!PhysicsGlobals.Instance.LoadDatabase())
- log("Something went wrong while setting the active physics config.");
- }
- }
-
- public static void log(String s)
- {
- print("[CustomConfigsManager] " + s);
- }
-
- }
-}
diff --git a/ModuleManager.csproj b/ModuleManager.csproj
deleted file mode 100644
index e4b787a9..00000000
--- a/ModuleManager.csproj
+++ /dev/null
@@ -1,64 +0,0 @@
-
-
-
- Debug
- AnyCPU
- 10.0.0
- 2.0
- {02C8E3AF-69F9-4102-AB60-DD6DE60662D3}
- Library
- ModuleManager
- ModuleManager
- v3.5
-
-
- True
- full
- False
- ..
- DEBUG;
- prompt
- 4
- False
- 5
-
-
- none
- True
- bin\Release\
- prompt
- 4
- False
-
-
-
-
-
-
-
-
-
- False
- False
-
-
-
-
- False
- False
-
-
-
-
-
-
-
-
-
-
-
- echo copying to "%25KSPDIR%25\GameData\"
-copy "$(TargetPath)" "C:\Games\ksp-win_dev\GameData\"
-del "C:\Games\ksp-win_dev\GameData\ModuleManager.ConfigCache"
-
-
\ No newline at end of file
diff --git a/ModuleManager.sln b/ModuleManager.sln
index ce332b6d..ac8200bc 100644
--- a/ModuleManager.sln
+++ b/ModuleManager.sln
@@ -1,7 +1,15 @@
-Microsoft Visual Studio Solution File, Format Version 11.00
-# Visual Studio 2010
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleManager", "ModuleManager.csproj", "{02C8E3AF-69F9-4102-AB60-DD6DE60662D3}"
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 15
+VisualStudioVersion = 15.0.26730.12
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleManager", "ModuleManager\ModuleManager.csproj", "{02C8E3AF-69F9-4102-AB60-DD6DE60662D3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModuleManagerTests", "ModuleManagerTests\ModuleManagerTests.csproj", "{BC2A08C8-64EF-4823-A40B-8889C1CCFD75}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtils", "TestUtils\TestUtils.csproj", "{20EAAFE6-510D-4374-8D2F-6B52D0178E85}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestUtilsTests", "TestUtilsTests\TestUtilsTests.csproj", "{E695C11F-4217-4014-9B51-7232A654C205}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -13,6 +21,24 @@ Global
{02C8E3AF-69F9-4102-AB60-DD6DE60662D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{02C8E3AF-69F9-4102-AB60-DD6DE60662D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{02C8E3AF-69F9-4102-AB60-DD6DE60662D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BC2A08C8-64EF-4823-A40B-8889C1CCFD75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BC2A08C8-64EF-4823-A40B-8889C1CCFD75}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BC2A08C8-64EF-4823-A40B-8889C1CCFD75}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BC2A08C8-64EF-4823-A40B-8889C1CCFD75}.Release|Any CPU.Build.0 = Release|Any CPU
+ {20EAAFE6-510D-4374-8D2F-6B52D0178E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {20EAAFE6-510D-4374-8D2F-6B52D0178E85}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {20EAAFE6-510D-4374-8D2F-6B52D0178E85}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {20EAAFE6-510D-4374-8D2F-6B52D0178E85}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E695C11F-4217-4014-9B51-7232A654C205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E695C11F-4217-4014-9B51-7232A654C205}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E695C11F-4217-4014-9B51-7232A654C205}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E695C11F-4217-4014-9B51-7232A654C205}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {4DCC10BA-6036-4DCF-A78B-086DF4487922}
EndGlobalSection
GlobalSection(MonoDevelopProperties) = preSolution
StartupItem = ModuleManager.csproj
diff --git a/ModuleManager/Cats/CatAnimator.cs b/ModuleManager/Cats/CatAnimator.cs
new file mode 100644
index 00000000..7806d4aa
--- /dev/null
+++ b/ModuleManager/Cats/CatAnimator.cs
@@ -0,0 +1,40 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+
+namespace ModuleManager.Cats
+{
+ class CatAnimator : MonoBehaviour
+ {
+
+ public Sprite[] frames;
+ public float secFrame = 0.07f;
+
+ private SpriteRenderer spriteRenderer;
+ private int spriteIdx;
+
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ void Start()
+ {
+ spriteRenderer = GetComponent();
+ spriteRenderer.sortingOrder = 3;
+ StartCoroutine(Animate());
+ }
+
+
+ IEnumerator Animate()
+ {
+ if (frames.Length == 0)
+ yield return null;
+
+ WaitForSeconds yield = new WaitForSeconds(secFrame);
+
+ while (true)
+ {
+ spriteIdx = (spriteIdx + 1) % frames.Length;
+ spriteRenderer.sprite = frames[spriteIdx];
+ yield return yield;
+ }
+ }
+ }
+}
diff --git a/ModuleManager/Cats/CatManager.cs b/ModuleManager/Cats/CatManager.cs
new file mode 100644
index 00000000..05f671d6
--- /dev/null
+++ b/ModuleManager/Cats/CatManager.cs
@@ -0,0 +1,115 @@
+using System;
+using UnityEngine;
+
+namespace ModuleManager.Cats
+{
+ public static class CatManager
+ {
+ private static Sprite[] catFrames;
+ private static Texture2D rainbow;
+ private static int scale = 1;
+
+ public static void LaunchCat()
+ {
+ InitCats();
+
+ GameObject cat = LaunchCat(scale);
+ cat.AddComponent();
+ }
+
+ public static void LaunchCats()
+ {
+ InitCats();
+
+ GameObject catSun = LaunchCat(scale);
+ CatOrbiter catSunOrbiter = catSun.AddComponent();
+ catSunOrbiter.Init(null, 0);
+
+ int cats = UnityEngine.Random.Range(6, 10);
+ for (int i = 0; i < cats; i++)
+ {
+ GameObject cat = LaunchCat(scale);
+ CatOrbiter catOrbiter = cat.AddComponent();
+ catOrbiter.Init(catSunOrbiter, Screen.height * 0.5f);
+
+ int moons = UnityEngine.Random.Range(0, 4);
+
+ for (int j = 0; j < moons; j++)
+ {
+ GameObject catMoon = LaunchCat(scale);
+ CatOrbiter catMoonOrbiter = catMoon.AddComponent();
+ catMoonOrbiter.Init(catOrbiter, Screen.height * 0.06f);
+ }
+ }
+ }
+
+ private static void InitCats()
+ {
+ Texture2D[] tex = new Texture2D[12];
+ for (int i = 0; i < tex.Length; i++)
+ {
+ tex[i] = new Texture2D(70, 42, TextureFormat.ARGB32, false);
+ }
+ tex[0].LoadImage(Properties.Resources.cat1);
+ tex[1].LoadImage(Properties.Resources.cat2);
+ tex[2].LoadImage(Properties.Resources.cat3);
+ tex[3].LoadImage(Properties.Resources.cat4);
+ tex[4].LoadImage(Properties.Resources.cat5);
+ tex[5].LoadImage(Properties.Resources.cat6);
+ tex[6].LoadImage(Properties.Resources.cat7);
+ tex[7].LoadImage(Properties.Resources.cat8);
+ tex[8].LoadImage(Properties.Resources.cat9);
+ tex[9].LoadImage(Properties.Resources.cat10);
+ tex[10].LoadImage(Properties.Resources.cat11);
+ tex[11].LoadImage(Properties.Resources.cat12);
+
+ rainbow = new Texture2D(39, 36, TextureFormat.ARGB32, false);
+ rainbow.LoadImage(Properties.Resources.rainbow);
+ rainbow.Apply();
+
+ catFrames = new Sprite[12];
+
+ for (int i = 0; i < tex.Length; i++)
+ {
+ tex[i].Apply();
+ catFrames[i] = Sprite.Create(tex[i], new Rect(0, 0, tex[i].width, tex[i].height), new Vector2(.5f, .5f));
+ catFrames[i].name = "cat" + i;
+ }
+
+ scale = 1;
+ if (Screen.height >= 1080)
+ scale *= 2;
+ if (Screen.height > 1440)
+ scale *= 3;
+
+ Physics2D.gravity = Vector2.zero;
+ }
+
+ private static GameObject LaunchCat(int scale)
+ {
+ GameObject cat = new GameObject("NyanCat");
+ SpriteRenderer sr = cat.AddComponent();
+ TrailRenderer trail = cat.AddComponent();
+ CatAnimator catAnimator = cat.AddComponent();
+
+ sr.sprite = catFrames[0];
+
+ trail.material = new Material(Shader.Find("Legacy Shaders/Particles/Alpha Blended"));
+
+ Debug.Log("material = " + trail.material);
+ trail.material.mainTexture = rainbow;
+ trail.time = 1.5f;
+ trail.startWidth = 0.6f * scale * rainbow.height;
+
+ trail.endColor = Color.white.A(0.1f);
+ trail.colorGradient = new Gradient {alphaKeys = new GradientAlphaKey[3] { new GradientAlphaKey(1, 0), new GradientAlphaKey(1, 0.75f), new GradientAlphaKey(0.2f, 1) }};
+ trail.Clear();
+ cat.layer = LayerMask.NameToLayer("UI");
+
+ catAnimator.frames = catFrames;
+
+ cat.transform.localScale = 70 * scale * Vector3.one;
+ return cat;
+ }
+ }
+}
diff --git a/ModuleManager/Cats/CatMover.cs b/ModuleManager/Cats/CatMover.cs
new file mode 100644
index 00000000..d97a3f7c
--- /dev/null
+++ b/ModuleManager/Cats/CatMover.cs
@@ -0,0 +1,81 @@
+using System.Diagnostics.CodeAnalysis;
+using UnityEngine;
+
+namespace ModuleManager.Cats
+{
+ public class CatMover : MonoBehaviour
+ {
+ public Vector3 spos;
+
+ public float vel = 5;
+ private float offsetY;
+
+ public TrailRenderer trail;
+ private SpriteRenderer spriteRenderer;
+
+ private int totalLenth = 100;
+ private float activePos = 0;
+
+ public float scale = 2;
+
+ private const float time = 5;
+ private const float trailTime = time / 4;
+
+ private bool clearTrail = false;
+
+ // Use this for initialization
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ void Start()
+ {
+ trail = GetComponent();
+ trail.sortingOrder = 2;
+
+ spriteRenderer = GetComponent();
+
+ offsetY = Mathf.FloorToInt(0.2f * Screen.height);
+
+ spos.z = -1;
+
+ totalLenth = (int) (Screen.width / time * trail.time) + 150;
+ trail.time = trailTime;
+ trail.widthCurve = new AnimationCurve(new Keyframe(0, trail.startWidth ), new Keyframe(0.7f, trail.startWidth), new Keyframe(1, trail.startWidth * 0.9f));
+ clearTrail = true;
+ }
+
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ void Update()
+ {
+ if (trail.time <= 0f)
+ {
+ trail.time = trailTime;
+ }
+
+ activePos += ((Screen.width / time) * Time.deltaTime);
+
+ if (activePos > (Screen.width + totalLenth))
+ {
+ activePos = -spriteRenderer.sprite.rect.width;
+ clearTrail = true;
+ }
+
+ float f = 2f * Mathf.PI * (activePos) / (Screen.width * 0.5f);
+
+ float heightOffset = Mathf.Sin(f) * (spriteRenderer.sprite.rect.height * scale);
+
+ spos.x = activePos;
+ spos.y = offsetY + heightOffset;
+
+ transform.position = KSP.UI.UIMainCamera.Camera.ScreenToWorldPoint(spos);
+ transform.rotation = Quaternion.Euler(0, 0, Mathf.Cos(f) * 0.25f * Mathf.PI * Mathf.Rad2Deg);
+
+ if (clearTrail)
+ {
+ trail.Clear();
+ clearTrail = false;
+ }
+
+ }
+
+
+ }
+}
diff --git a/ModuleManager/Cats/CatOrbiter.cs b/ModuleManager/Cats/CatOrbiter.cs
new file mode 100644
index 00000000..34b1f6e8
--- /dev/null
+++ b/ModuleManager/Cats/CatOrbiter.cs
@@ -0,0 +1,139 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using KSP.UI;
+using UnityEngine;
+using Random = UnityEngine.Random;
+
+namespace ModuleManager.Cats
+{
+ class CatOrbiter : MonoBehaviour
+ {
+ private static readonly List orbiters = new List();
+
+ private static CatOrbiter sun;
+
+ private double _mass;
+ public Rigidbody2D rb;
+
+ private Vector2d pos;
+ private Vector2d vel;
+ private Vector2d force;
+ private float scale = 1;
+
+ private const double G = 6.67408E-11;
+
+ public double Mass
+ {
+ get { return _mass; }
+ set
+ {
+ _mass = value;
+ if (rb!=null)
+ rb.mass = (float)_mass;
+ }
+ }
+
+ public void Init(CatOrbiter parent, float soi)
+ {
+
+ TimingManager.FixedUpdateAdd(TimingManager.TimingStage.Earlyish, DoForces);
+
+ orbiters.Add(this);
+ rb = gameObject.AddComponent();
+ rb.isKinematic = true;
+
+ if (orbiters.Count == 1)
+ {
+ sun = this;
+ Vector3 spos = new Vector3(Screen.width * 0.5f, Screen.height * 0.5f, -1);
+ transform.position = KSP.UI.UIMainCamera.Camera.ScreenToWorldPoint(spos);
+ Mass = 2E17;
+ pos.x = transform.position.x;
+ pos.y = transform.position.y;
+ }
+ else
+ {
+ Vector2 relativePos = Random.insideUnitCircle;
+ if (relativePos.magnitude < 0.2)
+ relativePos = relativePos.normalized * 0.3f;
+ Vector3 spos = UIMainCamera.Camera.WorldToScreenPoint(parent.transform.position) + (Vector3)(relativePos * soi);
+ spos.z = -1;
+ transform.position = UIMainCamera.Camera.ScreenToWorldPoint(spos);
+
+ pos.x = transform.position.x;
+ pos.y = transform.position.y;
+
+ //int scaleRange = 10;
+ //
+ //float factor = (1 + (scaleRange - 1) * Random.value);
+ //
+ //scale = parent.scale * factor / scaleRange;
+ scale = parent.scale * 0.6f;
+
+ transform.localScale *= scale;
+ TrailRenderer trail = gameObject.GetComponent();
+ trail.colorGradient = new Gradient() {alphaKeys = new GradientAlphaKey[3] { new GradientAlphaKey(1, 0), new GradientAlphaKey(1, 0.7f), new GradientAlphaKey(0, 1) }};
+ trail.startWidth *= scale;
+ //trail.endWidth *= scale;
+ trail.widthCurve = new AnimationCurve(new Keyframe(0, trail.startWidth ), new Keyframe(0.7f, trail.startWidth), new Keyframe(1, trail.startWidth * 0.9f));
+
+ //Mass = factor * 2E16;
+
+ Mass = parent.Mass * 0.025;
+
+ Vector2d dist = parent.pos - pos;
+ double circularVel = Math.Sqrt(G * (Mass + parent.Mass) / dist.magnitude);
+ if (parent == sun)
+ circularVel *= Random.Range(0.9f, 1.1f);
+ Debug.Log("CatOrbiter " + circularVel.ToString("F3") + " " + Mass.ToString("F2") + " " + orbiters[0].Mass.ToString("F2") + " " +
+ dist.magnitude.ToString("F2"));
+
+ Vector3d normal = (Random.value >= 0.3) ? Vector3d.back : Vector3d.forward;
+
+ Vector3d vel3d = Vector3d.Cross(dist, normal).normalized * circularVel;
+ vel.x = parent.vel.x + vel3d.x;
+ vel.y = parent.vel.y + vel3d.y;
+ }
+
+ rb.MovePosition(new Vector2((float)pos.x, (float)pos.y));
+ }
+
+ private void DoForces()
+ {
+ force = Vector2d.zero;
+ foreach (CatOrbiter cat in orbiters)
+ {
+ if (cat == this)
+ continue;
+
+ // F = G * (m1 * m2) / r^2
+ Vector2d dir = cat.pos - pos;
+ double f = G * (cat.Mass * Mass) / (dir.sqrMagnitude + 10); // +10 to avoid div/0
+ force += (float)f * dir.normalized;
+ }
+ }
+
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ void OnDestroy()
+ {
+ orbiters.Remove(this);
+ TimingManager.FixedUpdateRemove(TimingManager.TimingStage.Earlyish, DoForces);
+ }
+
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ void FixedUpdate()
+ {
+ //if (this == sun)
+ // return;
+
+ vel += Time.fixedDeltaTime * force / Mass;
+ pos += Time.fixedDeltaTime * vel;
+
+ rb.MovePosition(new Vector2((float)pos.x, (float)pos.y));
+
+ double angle = Math.Atan2(vel.y, vel.x) * Mathf.Rad2Deg;
+ rb.MoveRotation((float)angle);
+ }
+ }
+}
diff --git a/ModuleManager/Collections/ArrayEnumerator.cs b/ModuleManager/Collections/ArrayEnumerator.cs
new file mode 100644
index 00000000..775a35dd
--- /dev/null
+++ b/ModuleManager/Collections/ArrayEnumerator.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace ModuleManager.Collections
+{
+ public struct ArrayEnumerator : IEnumerator
+ {
+ private readonly T[] array;
+ private readonly int startIndex;
+ private readonly int length;
+
+ private int index;
+
+ public ArrayEnumerator(params T[] array) : this(array, 0) { }
+
+ public ArrayEnumerator(T[] array, int startIndex) : this(array, startIndex, (array?.Length ?? -1) - startIndex) { }
+
+ public ArrayEnumerator(T[] array, int startIndex, int length)
+ {
+ this.array = array ?? throw new ArgumentNullException(nameof(array));
+
+ if (startIndex < 0)
+ throw new ArgumentException($"must be non-negative (got {startIndex})", nameof(startIndex));
+ if (startIndex > array.Length)
+ throw new ArgumentException(
+ $"must be less than or equal to array length (array length {array.Length}, startIndex {startIndex})",
+ nameof(startIndex)
+ );
+ if (length < 0)
+ throw new ArgumentException($"must be non-negative (got {length})", nameof(length));
+ if (startIndex + length > array.Length)
+ throw new ArgumentException(
+ $"must fit within the string (array length {array.Length}, startIndex {startIndex}, length {length})",
+ nameof(length)
+ );
+
+ this.startIndex = startIndex;
+ this.length = length;
+ index = startIndex - 1;
+ }
+
+ public T Current => array[index];
+ object IEnumerator.Current => Current;
+
+ public void Dispose() { }
+
+ public bool MoveNext()
+ {
+ index++;
+ return index < startIndex + length;
+ }
+
+ public void Reset()
+ {
+ index = startIndex - 1;
+ }
+ }
+}
diff --git a/ModuleManager/Collections/ImmutableStack.cs b/ModuleManager/Collections/ImmutableStack.cs
new file mode 100644
index 00000000..d710c423
--- /dev/null
+++ b/ModuleManager/Collections/ImmutableStack.cs
@@ -0,0 +1,82 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace ModuleManager.Collections
+{
+ public class ImmutableStack : IEnumerable
+ {
+ public struct Enumerator : IEnumerator
+ {
+ private readonly ImmutableStack head;
+ private ImmutableStack currentStack;
+
+ public Enumerator(ImmutableStack stack)
+ {
+ head = stack;
+ currentStack = null;
+ }
+
+ public T Current => currentStack.value;
+ object IEnumerator.Current => Current;
+
+ public void Dispose() { }
+
+ public bool MoveNext()
+ {
+ if (currentStack == null)
+ {
+ currentStack = head;
+ return true;
+ }
+ else if (!currentStack.IsRoot)
+ {
+ currentStack = currentStack.parent;
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ public void Reset() => currentStack = null;
+ }
+
+ public readonly T value;
+ public readonly ImmutableStack parent;
+
+ public ImmutableStack(T value)
+ {
+ this.value = value;
+ }
+
+ private ImmutableStack(T value, ImmutableStack parent)
+ {
+ this.value = value;
+ this.parent = parent;
+ }
+
+ public bool IsRoot => parent == null;
+ public ImmutableStack Root => IsRoot? this : parent.Root;
+
+ public int Depth => IsRoot ? 1 : parent.Depth + 1;
+
+ public ImmutableStack Push(T newValue)
+ {
+ return new ImmutableStack(newValue, this);
+ }
+
+ public ImmutableStack Pop()
+ {
+ if (IsRoot) throw new InvalidOperationException("Cannot pop from the root of a stack");
+ return parent;
+ }
+
+ public ImmutableStack ReplaceValue(T newValue) => new ImmutableStack(newValue, parent);
+
+ public Enumerator GetEnumerator() => new Enumerator(this);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/ModuleManager/Collections/KeyValueCache.cs b/ModuleManager/Collections/KeyValueCache.cs
new file mode 100644
index 00000000..0aaee61b
--- /dev/null
+++ b/ModuleManager/Collections/KeyValueCache.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+
+namespace ModuleManager.Collections
+{
+ public class KeyValueCache
+ {
+ private readonly Dictionary dict = new Dictionary();
+ private readonly object lockObject = new object();
+
+ public TValue Fetch(TKey key, Func createValue)
+ {
+ if (createValue == null) throw new ArgumentNullException(nameof(createValue));
+ lock(lockObject)
+ {
+ if (dict.TryGetValue(key, out TValue value))
+ {
+ return value;
+ }
+ else
+ {
+ TValue newValue = createValue();
+ dict.Add(key, newValue);
+ return newValue;
+ }
+ }
+ }
+ }
+}
diff --git a/ModuleManager/Collections/MessageQueue.cs b/ModuleManager/Collections/MessageQueue.cs
new file mode 100644
index 00000000..b6038a77
--- /dev/null
+++ b/ModuleManager/Collections/MessageQueue.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace ModuleManager.Collections
+{
+ public interface IMessageQueue : IEnumerable
+ {
+ void Add(T value);
+ IMessageQueue TakeAll();
+ }
+
+ public class MessageQueue : IMessageQueue, IEnumerable
+ {
+ public sealed class Enumerator : IEnumerator
+ {
+ private readonly MessageQueue queue;
+ private Node current;
+
+ public Enumerator(MessageQueue queue)
+ {
+ this.queue = queue;
+ }
+
+ public T Current => current.value;
+ object IEnumerator.Current => Current;
+
+ public void Dispose() { }
+
+ public bool MoveNext()
+ {
+ if (current == null)
+ current = queue.head;
+ else
+ current = current.next;
+
+ return current != null;
+ }
+
+ public void Reset()
+ {
+ current = null;
+ }
+ }
+
+ private class Node
+ {
+ public Node next;
+ public readonly T value;
+
+ public Node(T value)
+ {
+ this.value = value;
+ }
+ }
+
+ private readonly object lockObject = new object();
+ private Node head;
+ private Node tail;
+
+ public void Add(T value)
+ {
+ Node node = new Node(value);
+ lock (lockObject)
+ {
+ if (head == null)
+ {
+ head = node;
+ tail = node;
+ }
+ else
+ {
+ tail.next = node;
+ tail = node;
+ }
+ }
+ }
+
+ public IMessageQueue TakeAll()
+ {
+ MessageQueue queue = new MessageQueue();
+ lock(lockObject)
+ {
+ queue.head = head;
+ queue.tail = tail;
+ head = null;
+ tail = null;
+ }
+ return queue;
+ }
+
+ public Enumerator GetEnumerator() => new Enumerator(this);
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ }
+}
diff --git a/ModuleManager/Command.cs b/ModuleManager/Command.cs
new file mode 100644
index 00000000..5e7dde49
--- /dev/null
+++ b/ModuleManager/Command.cs
@@ -0,0 +1,25 @@
+using System;
+
+namespace ModuleManager
+{
+ public enum Command
+ {
+ Insert,
+
+ Delete,
+
+ Edit,
+
+ Replace,
+
+ Copy,
+
+ Rename,
+
+ Paste,
+
+ Special,
+
+ Create
+ }
+}
diff --git a/ModuleManager/CommandParser.cs b/ModuleManager/CommandParser.cs
new file mode 100644
index 00000000..89029b77
--- /dev/null
+++ b/ModuleManager/CommandParser.cs
@@ -0,0 +1,59 @@
+using System;
+
+namespace ModuleManager
+{
+ public static class CommandParser
+ {
+ public static Command Parse(string name, out string valueName)
+ {
+ if (name.Length == 0)
+ {
+ valueName = string.Empty;
+ return Command.Insert;
+ }
+ Command ret;
+ switch (name[0])
+ {
+ case '@':
+ ret = Command.Edit;
+ break;
+
+ case '%':
+ ret = Command.Replace;
+ break;
+
+ case '-':
+ case '!':
+ ret = Command.Delete;
+ break;
+
+ case '+':
+ case '$':
+ ret = Command.Copy;
+ break;
+
+ case '|':
+ ret = Command.Rename;
+ break;
+
+ case '#':
+ ret = Command.Paste;
+ break;
+
+ case '*':
+ ret = Command.Special;
+ break;
+
+ case '&':
+ ret = Command.Create;
+ break;
+
+ default:
+ valueName = name;
+ return Command.Insert;
+ }
+ valueName = name.Substring(1);
+ return ret;
+ }
+ }
+}
diff --git a/ModuleManager/CustomConfigsManager.cs b/ModuleManager/CustomConfigsManager.cs
new file mode 100644
index 00000000..d7444363
--- /dev/null
+++ b/ModuleManager/CustomConfigsManager.cs
@@ -0,0 +1,27 @@
+using System;
+using System.IO;
+using UnityEngine;
+
+using static ModuleManager.FilePathRepository;
+
+namespace ModuleManager
+{
+ [KSPAddon(KSPAddon.Startup.SpaceCentre, false)]
+ public class CustomConfigsManager : MonoBehaviour
+ {
+ internal void Start()
+ {
+ if (HighLogic.CurrentGame.Parameters.Career.TechTreeUrl != techTreeFile && File.Exists(techTreePath))
+ {
+ Log("Setting modded tech tree as the active one");
+ HighLogic.CurrentGame.Parameters.Career.TechTreeUrl = techTreeFile;
+ }
+ }
+
+ public static void Log(String s)
+ {
+ print("[CustomConfigsManager] " + s);
+ }
+
+ }
+}
diff --git a/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs
new file mode 100644
index 00000000..471b1fa8
--- /dev/null
+++ b/ModuleManager/ExceptionIntercept/InterceptLogHandler.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using UnityEngine;
+using Object = UnityEngine.Object;
+
+namespace ModuleManager.UnityLogHandle
+{
+ class InterceptLogHandler : ILogHandler
+ {
+ private readonly ILogHandler baseLogHandler;
+ private readonly List brokenAssemblies = new List();
+ private readonly int gamePathLength;
+
+ public static string Warnings { get; private set; } = "";
+
+ public InterceptLogHandler(ILogHandler baseLogHandler)
+ {
+ this.baseLogHandler = baseLogHandler ?? throw new ArgumentNullException(nameof(baseLogHandler));
+ gamePathLength = Path.GetFullPath(KSPUtil.ApplicationRootPath).Length;
+ }
+
+ public void LogFormat(LogType logType, Object context, string format, params object[] args)
+ {
+ baseLogHandler.LogFormat(logType, context, format, args);
+ }
+
+ public void LogException(Exception exception, Object context)
+ {
+ baseLogHandler.LogException(exception, context);
+
+ if (exception is ReflectionTypeLoadException ex)
+ {
+ string message = "Intercepted a ReflectionTypeLoadException. List of broken DLLs:\n";
+ try
+ {
+ var assemblies = ex.Types.Where(x => x != null).Select(x => x.Assembly).Distinct();
+ foreach (Assembly assembly in assemblies)
+ {
+ if (Warnings == "")
+ {
+ Warnings = "Mod(s) DLL that are not compatible with this version of KSP\n";
+ }
+ string modInfo = assembly.GetName().Name + " " + assembly.GetName().Version + " " +
+ assembly.Location.Remove(0, gamePathLength) + "\n";
+ if (!brokenAssemblies.Contains(assembly))
+ {
+ brokenAssemblies.Add(assembly);
+ Warnings += modInfo;
+ }
+ message += modInfo;
+ }
+ }
+ catch (Exception e)
+ {
+ message += "Exception " + e.GetType().Name + " while handling the exception...";
+ }
+ ModuleManager.Log(message);
+ }
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/ByteArrayExtensions.cs b/ModuleManager/Extensions/ByteArrayExtensions.cs
new file mode 100644
index 00000000..520393df
--- /dev/null
+++ b/ModuleManager/Extensions/ByteArrayExtensions.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace ModuleManager.Extensions
+{
+ public static class ByteArrayExtensions
+ {
+ public static string ToHex(this byte[] data)
+ {
+ if (data == null) throw new ArgumentNullException(nameof(data));
+ char[] result = new char[data.Length * 2];
+
+ for (int i = 0; i < data.Length; i++)
+ {
+ result[i * 2] = GetHexValue(data[i] / 16);
+ result[i * 2 + 1] = GetHexValue(data[i] % 16);
+ }
+
+ return new string(result);
+ }
+
+ private static char GetHexValue(int i)
+ {
+ if (i < 10)
+ return (char)(i + '0');
+ else
+ return (char)(i - 10 + 'a');
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/ConfigNodeExtensions.cs b/ModuleManager/Extensions/ConfigNodeExtensions.cs
new file mode 100644
index 00000000..9f552566
--- /dev/null
+++ b/ModuleManager/Extensions/ConfigNodeExtensions.cs
@@ -0,0 +1,107 @@
+using System;
+using System.Text;
+
+namespace ModuleManager.Extensions
+{
+ public static class ConfigNodeExtensions
+ {
+ public static void ShallowCopyFrom(this ConfigNode toNode, ConfigNode fromeNode)
+ {
+ toNode.ClearData();
+ foreach (ConfigNode.Value value in fromeNode.values)
+ toNode.values.Add(value);
+ foreach (ConfigNode node in fromeNode.nodes)
+ toNode.nodes.Add(node);
+ }
+
+ // KSP implementation of ConfigNode.CreateCopy breaks with badly formed nodes (nodes with a blank name)
+ public static ConfigNode DeepCopy(this ConfigNode from)
+ {
+ ConfigNode to = new ConfigNode(from.name);
+ foreach (ConfigNode.Value value in from.values)
+ to.AddValueSafe(value.name, value.value);
+ foreach (ConfigNode node in from.nodes)
+ {
+ ConfigNode newNode = DeepCopy(node);
+ to.nodes.Add(newNode);
+ }
+ return to;
+ }
+
+ public static void PrettyPrint(this ConfigNode node, ref StringBuilder sb, string indent)
+ {
+ if (sb == null) throw new ArgumentNullException(nameof(sb));
+ if (indent == null) indent = string.Empty;
+ if (node == null)
+ {
+ sb.Append(indent + "\n");
+ return;
+ }
+ sb.AppendFormat("{0}{1}\n{2}{{\n", indent, node.name ?? "", indent);
+ string newindent = indent + " ";
+ if (node.values == null)
+ {
+ sb.AppendFormat("{0}\n", newindent);
+ }
+ else
+ {
+ foreach (ConfigNode.Value value in node.values)
+ {
+ if (value == null)
+ sb.AppendFormat("{0}\n", newindent);
+ else
+ sb.AppendFormat("{0}{1} = {2}\n", newindent, value.name ?? "", value.value ?? "");
+ }
+ }
+
+ if (node.nodes == null)
+ {
+ sb.AppendFormat("{0}\n", newindent);
+ }
+ else
+ {
+ foreach (ConfigNode subnode in node.nodes)
+ {
+ subnode.PrettyPrint(ref sb, newindent);
+ }
+ }
+
+ sb.AppendFormat("{0}}}\n", indent);
+ }
+
+ public static void AddValueSafe(this ConfigNode node, string name, string value)
+ {
+ node.values.Add(new ConfigNode.Value(name, value));
+ }
+
+ public static void EscapeValuesRecursive(this ConfigNode theNode)
+ {
+ foreach (ConfigNode subNode in theNode.nodes)
+ {
+ subNode.EscapeValuesRecursive();
+ }
+
+ foreach (ConfigNode.Value value in theNode.values)
+ {
+ value.value = value.value.Replace("\n", "\\n");
+ value.value = value.value.Replace("\r", "\\r");
+ value.value = value.value.Replace("\t", "\\t");
+ }
+ }
+
+ public static void UnescapeValuesRecursive(this ConfigNode theNode)
+ {
+ foreach (ConfigNode subNode in theNode.nodes)
+ {
+ subNode.UnescapeValuesRecursive();
+ }
+
+ foreach (ConfigNode.Value value in theNode.values)
+ {
+ value.value = value.value.Replace("\\n", "\n");
+ value.value = value.value.Replace("\\r", "\r");
+ value.value = value.value.Replace("\\t", "\t");
+ }
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/IBasicLoggerExtensions.cs b/ModuleManager/Extensions/IBasicLoggerExtensions.cs
new file mode 100644
index 00000000..9fd29be3
--- /dev/null
+++ b/ModuleManager/Extensions/IBasicLoggerExtensions.cs
@@ -0,0 +1,26 @@
+using System;
+using UnityEngine;
+using ModuleManager.Logging;
+
+namespace ModuleManager.Extensions
+{
+ public static class IBasicLoggerExtensions
+ {
+ public static void Info(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Log, message));
+ public static void Warning(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Warning, message));
+ public static void Error(this IBasicLogger logger, string message) => logger.Log(new LogMessage(LogType.Error, message));
+
+ public static void Exception(this IBasicLogger logger, Exception exception)
+ {
+ if (exception == null) throw new ArgumentNullException(nameof(exception));
+ logger.Log(new LogMessage(LogType.Exception, exception.ToString()));
+ }
+
+ public static void Exception(this IBasicLogger logger, string message, Exception exception)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ if (exception == null) throw new ArgumentNullException(nameof(exception));
+ logger.Log(new LogMessage(LogType.Exception, message + ": " + exception.ToString()));
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/NodeStackExtensions.cs b/ModuleManager/Extensions/NodeStackExtensions.cs
new file mode 100644
index 00000000..c6357dca
--- /dev/null
+++ b/ModuleManager/Extensions/NodeStackExtensions.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Linq;
+using System.Text;
+using NodeStack = ModuleManager.Collections.ImmutableStack;
+
+namespace ModuleManager.Extensions
+{
+ public static class NodeStackExtensions
+ {
+ public static string GetPath(this NodeStack stack)
+ {
+ int length = stack.Sum(node => node.name.Length) + stack.Depth - 1;
+ StringBuilder sb = new StringBuilder(length);
+
+ foreach (ConfigNode node in stack)
+ {
+ string nodeName = node.name;
+ sb.Insert(0, node.name);
+ if (sb.Length < sb.Capacity) sb.Insert(0, '/');
+ }
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/StringExtensions.cs b/ModuleManager/Extensions/StringExtensions.cs
new file mode 100644
index 00000000..634fab7a
--- /dev/null
+++ b/ModuleManager/Extensions/StringExtensions.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Text.RegularExpressions;
+
+namespace ModuleManager.Extensions
+{
+ public static class StringExtensions
+ {
+ public static bool IsBracketBalanced(this string s)
+ {
+ int level = 0;
+ foreach (char c in s)
+ {
+ if (c == '[') level++;
+ else if (c == ']') level--;
+
+ if (level < 0) return false;
+ }
+ return level == 0;
+ }
+
+ private static readonly Regex whitespaceRegex = new Regex(@"\s+");
+
+ public static string RemoveWS(this string withWhite)
+ {
+ return whitespaceRegex.Replace(withWhite, "");
+ }
+
+ public static bool Contains(this string str, string value, out int index)
+ {
+ if (str == null) throw new ArgumentNullException(nameof(str));
+ if (value == null) throw new ArgumentNullException(nameof(value));
+
+ index = str.IndexOf(value, StringComparison.CurrentCultureIgnoreCase);
+ return index != -1;
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/UrlConfigExtensions.cs b/ModuleManager/Extensions/UrlConfigExtensions.cs
new file mode 100644
index 00000000..d5ce9fbe
--- /dev/null
+++ b/ModuleManager/Extensions/UrlConfigExtensions.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Text;
+
+namespace ModuleManager.Extensions
+{
+ public static class UrlConfigExtensions
+ {
+ public static string SafeUrl(this UrlDir.UrlConfig url)
+ {
+ if (url == null) return "";
+
+ string nodeName;
+
+ if (!string.IsNullOrEmpty(url.type?.Trim()))
+ {
+ nodeName = url.type;
+ }
+ else if (url.type == null)
+ {
+ nodeName = "";
+ }
+ else
+ {
+ nodeName = "";
+ }
+
+ string parentUrl = null;
+
+ if (url.parent != null)
+ {
+ try
+ {
+ parentUrl = url.parent.url;
+ }
+ catch
+ {
+ parentUrl = "";
+ }
+ }
+
+ if (parentUrl == null)
+ return nodeName;
+ else
+ return parentUrl + "/" + nodeName;
+ }
+
+ public static string PrettyPrint(this UrlDir.UrlConfig config)
+ {
+ if (config == null) return "";
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.Append(config.SafeUrl());
+ sb.Append('\n');
+ config.config.PrettyPrint(ref sb, " ");
+
+ return sb.ToString();
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/UrlDirExtensions.cs b/ModuleManager/Extensions/UrlDirExtensions.cs
new file mode 100644
index 00000000..3a57bf6a
--- /dev/null
+++ b/ModuleManager/Extensions/UrlDirExtensions.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Linq;
+
+namespace ModuleManager.Extensions
+{
+ public static class UrlDirExtensions
+ {
+ public static UrlDir.UrlFile Find(this UrlDir urlDir, string url)
+ {
+ if (urlDir == null) throw new ArgumentNullException(nameof(urlDir));
+ if (url == null) throw new ArgumentNullException(nameof(url));
+ string[] splits = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
+
+ UrlDir currentDir = urlDir;
+
+ for (int i = 0; i < splits.Length - 1; i++)
+ {
+ currentDir = currentDir.children.FirstOrDefault(subDir => subDir.name == splits[i]);
+ if (currentDir == null) return null;
+ }
+
+ string fileName = splits[splits.Length - 1];
+ string fileExtension = null;
+
+ int idx = fileName.LastIndexOf('.');
+
+ if (idx > -1)
+ {
+ fileExtension = fileName.Substring(idx + 1);
+ fileName = fileName.Substring(0, idx);
+ }
+
+ foreach (UrlDir.UrlFile file in currentDir.files)
+ {
+ if (file.name != fileName) continue;
+ if (fileExtension != null && fileExtension != file.fileExtension) continue;
+ return file;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/ModuleManager/Extensions/UrlFileExtensions.cs b/ModuleManager/Extensions/UrlFileExtensions.cs
new file mode 100644
index 00000000..3d203b24
--- /dev/null
+++ b/ModuleManager/Extensions/UrlFileExtensions.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace ModuleManager.Extensions
+{
+ public static class UrlFileExtensions
+ {
+ public static string GetUrlWithExtension(this UrlDir.UrlFile urlFile)
+ {
+ return $"{urlFile.url}.{urlFile.fileExtension}";
+ }
+ public static string GetNameWithExtension(this UrlDir.UrlFile urlFile)
+ {
+ return $"{urlFile.name}.{urlFile.fileExtension}";
+ }
+ }
+}
diff --git a/ModuleManager/FatalErrorHandler.cs b/ModuleManager/FatalErrorHandler.cs
new file mode 100644
index 00000000..06bf686e
--- /dev/null
+++ b/ModuleManager/FatalErrorHandler.cs
@@ -0,0 +1,38 @@
+using System;
+using UnityEngine;
+
+namespace ModuleManager
+{
+ public static class FatalErrorHandler
+ {
+ public static void HandleFatalError(string message)
+ {
+ try
+ {
+ PopupDialog.SpawnPopupDialog(new Vector2(0.5f, 0.5f),
+ new Vector2(0.5f, 0.5f),
+ new MultiOptionDialog(
+ "ModuleManagerFatalError",
+ $"ModuleManager has encountered a fatal error and KSP needs to close.\n\n{message}\n\nPlease see KSP's log for addtional details",
+ "ModuleManager - Fatal Error",
+ HighLogic.UISkin,
+ new Rect(0.5f, 0.5f, 500f, 60f),
+ new DialogGUIFlexibleSpace(),
+ new DialogGUIHorizontalLayout(
+ new DialogGUIFlexibleSpace(),
+ new DialogGUIButton("Quit", Application.Quit, 140.0f, 30.0f, true),
+ new DialogGUIFlexibleSpace()
+ )
+ ),
+ true,
+ HighLogic.UISkin);
+ }
+ catch(Exception ex)
+ {
+ Debug.LogError("Exception while trying to create the fatal exception dialog");
+ Debug.LogException(ex);
+ Application.Quit();
+ }
+ }
+ }
+}
diff --git a/ModuleManager/FilePathRepository.cs b/ModuleManager/FilePathRepository.cs
new file mode 100644
index 00000000..0d1448d7
--- /dev/null
+++ b/ModuleManager/FilePathRepository.cs
@@ -0,0 +1,26 @@
+using System;
+using System.IO;
+
+namespace ModuleManager
+{
+ internal static class FilePathRepository
+ {
+ internal static readonly string normalizedRootPath = Path.GetFullPath(KSPUtil.ApplicationRootPath);
+ internal static readonly string cachePath = Path.Combine(normalizedRootPath, "GameData", "ModuleManager.ConfigCache");
+
+ internal static readonly string techTreeFile = Path.Combine("GameData", "ModuleManager.TechTree");
+ internal static readonly string techTreePath = Path.Combine(normalizedRootPath, techTreeFile);
+
+ internal static readonly string physicsFile = Path.Combine("GameData", "ModuleManager.Physics");
+ internal static readonly string physicsPath = Path.Combine(normalizedRootPath, physicsFile);
+ internal static readonly string defaultPhysicsPath = Path.Combine(normalizedRootPath, "Physics.cfg");
+
+ internal static readonly string partDatabasePath = Path.Combine(normalizedRootPath, "PartDatabase.cfg");
+
+ internal static readonly string shaPath = Path.Combine(normalizedRootPath, "GameData", "ModuleManager.ConfigSHA");
+
+ internal static readonly string logsDirPath = Path.Combine(normalizedRootPath, "Logs", "ModuleManager");
+ internal static readonly string logPath = Path.Combine(logsDirPath, "ModuleManager.log");
+ internal static readonly string patchLogPath = Path.Combine(logsDirPath, "MMPatch.log");
+ }
+}
diff --git a/ModuleManager/Fix16.cs b/ModuleManager/Fix16.cs
new file mode 100644
index 00000000..b567eb56
--- /dev/null
+++ b/ModuleManager/Fix16.cs
@@ -0,0 +1,83 @@
+using System.Collections;
+using System.Diagnostics.CodeAnalysis;
+
+namespace ModuleManager
+{
+ class Fix16 : LoadingSystem
+ {
+ [SuppressMessage("CodeQuality", "IDE0051", Justification = "Called by Unity")]
+ private void Awake()
+ {
+ if (Instance != null)
+ {
+ DestroyImmediate(this);
+ return;
+ }
+
+ Instance = this;
+ DontDestroyOnLoad(gameObject);
+ }
+
+ private static Fix16 Instance { get; set; }
+
+ private bool ready;
+
+ private int count;
+
+ private int current;
+
+ private const int yieldStep = 20;
+
+ public override bool IsReady()
+ {
+ return ready;
+ }
+
+ public override string ProgressTitle()
+ {
+ return "Fix 1.6.0 " + current + "/" + count;
+ }
+
+ public override float ProgressFraction()
+ {
+ return (float) current / count;
+ }
+
+ public override void StartLoad()
+ {
+ ready = false;
+
+ count = PartLoader.LoadedPartsList.Count;
+
+ StartCoroutine(DoFix());
+ }
+
+ private IEnumerator DoFix()
+ {
+ int yieldCounter = 0;
+ for (current = 0; current < count; current++)
+ {
+ AvailablePart avp = PartLoader.LoadedPartsList[current];
+ if (avp.partPrefab.dragModel == Part.DragModel.CUBE && !avp.partPrefab.DragCubes.Procedural &&
+ !avp.partPrefab.DragCubes.None && avp.partPrefab.DragCubes.Cubes.Count == 0)
+ {
+ DragCubeSystem.Instance.LoadDragCubes(avp.partPrefab);
+ }
+
+ if (yieldCounter++ >= yieldStep)
+ {
+ yieldCounter = 0;
+ yield return null;
+ }
+ }
+
+ ready = true;
+ yield return null;
+ }
+
+ public override float LoadWeight()
+ {
+ return 0.1f;
+ }
+ }
+}
diff --git a/ModuleManager/Logging/IBasicLogger.cs b/ModuleManager/Logging/IBasicLogger.cs
new file mode 100644
index 00000000..25cd9367
--- /dev/null
+++ b/ModuleManager/Logging/IBasicLogger.cs
@@ -0,0 +1,10 @@
+using System;
+
+namespace ModuleManager.Logging
+{
+ // Stripped down version of UnityEngine.ILogger
+ public interface IBasicLogger
+ {
+ void Log(ILogMessage message);
+ }
+}
diff --git a/ModuleManager/Logging/ILogMessage.cs b/ModuleManager/Logging/ILogMessage.cs
new file mode 100644
index 00000000..bf85f488
--- /dev/null
+++ b/ModuleManager/Logging/ILogMessage.cs
@@ -0,0 +1,13 @@
+using System;
+using UnityEngine;
+
+namespace ModuleManager.Logging
+{
+ public interface ILogMessage
+ {
+ LogType LogType { get; }
+ DateTime Timestamp { get; }
+ string Message { get; }
+ string ToLogString();
+ }
+}
diff --git a/ModuleManager/Logging/LogMessage.cs b/ModuleManager/Logging/LogMessage.cs
new file mode 100644
index 00000000..c6554aaf
--- /dev/null
+++ b/ModuleManager/Logging/LogMessage.cs
@@ -0,0 +1,53 @@
+using System;
+using UnityEngine;
+
+namespace ModuleManager.Logging
+{
+ public class LogMessage : ILogMessage
+ {
+ private const string DATETIME_FORMAT_STRING = "HH:mm:ss.fff";
+
+ public LogType LogType { get; }
+ public DateTime Timestamp { get; }
+ public string Message { get; }
+
+ public LogMessage(LogType logType, string message)
+ {
+ LogType = logType;
+ Timestamp = DateTime.Now;
+ Message = message ?? throw new ArgumentNullException(nameof(message));
+ }
+
+ public LogMessage(ILogMessage logMessage, string newMessage)
+ {
+ if (logMessage == null) throw new ArgumentNullException(nameof(logMessage));
+ LogType = logMessage.LogType;
+ Timestamp = logMessage.Timestamp;
+ Message = newMessage ?? throw new ArgumentNullException(nameof(newMessage));
+ }
+
+ public string ToLogString()
+ {
+ string prefix;
+ if (LogType == LogType.Log)
+ prefix = "LOG";
+ else if (LogType == LogType.Warning)
+ prefix = "WRN";
+ else if (LogType == LogType.Error)
+ prefix = "ERR";
+ else if (LogType == LogType.Assert)
+ prefix = "AST";
+ else if (LogType == LogType.Exception)
+ prefix = "EXC";
+ else
+ prefix = "???";
+
+ return $"[{prefix} {Timestamp.ToString(DATETIME_FORMAT_STRING)}] {Message}";
+ }
+
+ public override string ToString()
+ {
+ return $"[{GetType().FullName} LogType={LogType} Message={Message}]";
+ }
+ }
+}
diff --git a/ModuleManager/Logging/LogSplitter.cs b/ModuleManager/Logging/LogSplitter.cs
new file mode 100644
index 00000000..b1d2c89f
--- /dev/null
+++ b/ModuleManager/Logging/LogSplitter.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace ModuleManager.Logging
+{
+ public class LogSplitter : IBasicLogger
+ {
+ private readonly IBasicLogger logger1;
+ private readonly IBasicLogger logger2;
+
+ public LogSplitter(IBasicLogger logger1, IBasicLogger logger2)
+ {
+ this.logger1 = logger1 ?? throw new ArgumentNullException(nameof(logger1));
+ this.logger2 = logger2 ?? throw new ArgumentNullException(nameof(logger2));
+ }
+
+ public void Log(ILogMessage message)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ logger1.Log(message);
+ logger2.Log(message);
+ }
+ }
+}
diff --git a/ModuleManager/Logging/PrefixLogger.cs b/ModuleManager/Logging/PrefixLogger.cs
new file mode 100644
index 00000000..410473fa
--- /dev/null
+++ b/ModuleManager/Logging/PrefixLogger.cs
@@ -0,0 +1,23 @@
+using System;
+
+namespace ModuleManager.Logging
+{
+ public class PrefixLogger : IBasicLogger
+ {
+ private readonly string prefix;
+ private readonly IBasicLogger logger;
+
+ public PrefixLogger(string prefix, IBasicLogger logger)
+ {
+ if (string.IsNullOrEmpty(prefix)) throw new ArgumentNullException(nameof(prefix));
+ this.prefix = $"[{prefix}] ";
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void Log(ILogMessage message)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ logger.Log(new LogMessage(message, prefix + message.Message));
+ }
+ }
+}
diff --git a/ModuleManager/Logging/QueueLogRunner.cs b/ModuleManager/Logging/QueueLogRunner.cs
new file mode 100644
index 00000000..f78cf611
--- /dev/null
+++ b/ModuleManager/Logging/QueueLogRunner.cs
@@ -0,0 +1,68 @@
+using System;
+using System.IO;
+
+using ModuleManager.Collections;
+
+namespace ModuleManager.Logging
+{
+ public class QueueLogRunner
+ {
+ private enum State
+ {
+ Initialized,
+ Running,
+ StopRequested,
+ Stopped,
+ }
+
+ private State state = State.Initialized;
+ private readonly IMessageQueue logQueue;
+ private readonly long timeToWaitForLogsMs;
+
+ public QueueLogRunner(IMessageQueue logQueue, long timeToWaitForLogsMs = 50)
+ {
+ this.logQueue = logQueue ?? throw new ArgumentNullException(nameof(logQueue));
+ if (timeToWaitForLogsMs < 0) throw new ArgumentException("must be non-negative", nameof(timeToWaitForLogsMs));
+ this.timeToWaitForLogsMs = timeToWaitForLogsMs;
+ }
+
+ public void RequestStop()
+ {
+ if (state == State.StopRequested || state == State.Stopped) return;
+ if (state != State.Running) throw new InvalidOperationException($"Cannot request stop from {state} state");
+ state = State.StopRequested;
+ }
+
+ public void Run(IBasicLogger logger)
+ {
+ if (state != State.Initialized) throw new InvalidOperationException($"Cannot run from {state} state");
+ if (logger == null) throw new ArgumentNullException(nameof(logger));
+ state = State.Running;
+
+ System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
+
+ while (state == State.Running)
+ {
+ stopwatch.Start();
+
+ foreach (ILogMessage message in logQueue.TakeAll())
+ {
+ logger.Log(message);
+ }
+
+ long timeRemaining = timeToWaitForLogsMs - stopwatch.ElapsedMilliseconds;
+ if (timeRemaining > 0)
+ System.Threading.Thread.Sleep((int)timeRemaining);
+
+ stopwatch.Reset();
+ }
+
+ foreach (ILogMessage message in logQueue.TakeAll())
+ {
+ logger.Log(message);
+ }
+
+ state = State.Stopped;
+ }
+ }
+}
diff --git a/ModuleManager/Logging/QueueLogger.cs b/ModuleManager/Logging/QueueLogger.cs
new file mode 100644
index 00000000..b7cd288a
--- /dev/null
+++ b/ModuleManager/Logging/QueueLogger.cs
@@ -0,0 +1,21 @@
+using System;
+using ModuleManager.Collections;
+
+namespace ModuleManager.Logging
+{
+ public class QueueLogger : IBasicLogger
+ {
+ private readonly IMessageQueue queue;
+
+ public QueueLogger(IMessageQueue queue)
+ {
+ this.queue = queue ?? throw new ArgumentNullException(nameof(queue));
+ }
+
+ public void Log(ILogMessage message)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ queue.Add(message);
+ }
+ }
+}
diff --git a/ModuleManager/Logging/StreamLogger.cs b/ModuleManager/Logging/StreamLogger.cs
new file mode 100644
index 00000000..d020f64c
--- /dev/null
+++ b/ModuleManager/Logging/StreamLogger.cs
@@ -0,0 +1,32 @@
+using System;
+using System.IO;
+
+namespace ModuleManager.Logging
+{
+ public sealed class StreamLogger : IBasicLogger, IDisposable
+ {
+ private readonly StreamWriter streamWriter;
+ private bool disposed = false;
+
+ public StreamLogger(Stream stream)
+ {
+ streamWriter = new StreamWriter(stream);
+ }
+
+ public void Log(ILogMessage message)
+ {
+ if (disposed) throw new InvalidOperationException("Object has already been disposed");
+ if (message == null) throw new ArgumentNullException(nameof(message));
+
+ streamWriter.WriteLine(message.ToLogString());
+ }
+
+ public void Dispose()
+ {
+ // Flushes and closes the StreamWriter and the underlying stream
+ streamWriter.Close();
+
+ disposed = true;
+ }
+ }
+}
diff --git a/ModuleManager/Logging/UnityLogger.cs b/ModuleManager/Logging/UnityLogger.cs
new file mode 100644
index 00000000..d51063c4
--- /dev/null
+++ b/ModuleManager/Logging/UnityLogger.cs
@@ -0,0 +1,21 @@
+using System;
+using UnityEngine;
+
+namespace ModuleManager.Logging
+{
+ public class UnityLogger : IBasicLogger
+ {
+ private readonly ILogger logger;
+
+ public UnityLogger(ILogger logger)
+ {
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public void Log(ILogMessage message)
+ {
+ if (message == null) throw new ArgumentNullException(nameof(message));
+ logger.Log(message.LogType, message.Message);
+ }
+ }
+}
diff --git a/ModuleManager/MMPatchLoader.cs b/ModuleManager/MMPatchLoader.cs
new file mode 100644
index 00000000..5278758b
--- /dev/null
+++ b/ModuleManager/MMPatchLoader.cs
@@ -0,0 +1,1777 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+
+using ModuleManager.Collections;
+using ModuleManager.Logging;
+using ModuleManager.Extensions;
+using ModuleManager.Threading;
+using ModuleManager.Tags;
+using ModuleManager.Patches;
+using ModuleManager.Progress;
+using NodeStack = ModuleManager.Collections.ImmutableStack;
+
+using static ModuleManager.FilePathRepository;
+
+namespace ModuleManager
+{
+ public class MMPatchLoader
+ {
+ private const string PHYSICS_NODE_NAME = "PHYSICSGLOBALS";
+ private const string TECH_TREE_NODE_NAME = "TechTree";
+
+ public string status = "";
+
+ public string errors = "";
+
+ public static bool keepPartDB = false;
+
+ private static readonly KeyValueCache regexCache = new KeyValueCache();
+
+ private string configSha;
+ private readonly Dictionary filesSha = new Dictionary();
+
+ private const int STATUS_UPDATE_INVERVAL_MS = 33;
+
+ private readonly IEnumerable modsAddedByAssemblies;
+ private readonly IBasicLogger logger;
+
+ public static void AddPostPatchCallback(ModuleManagerPostPatchCallback callback)
+ {
+ PostPatchLoader.AddPostPatchCallback(callback);
+ }
+
+ public MMPatchLoader(IEnumerable modsAddedByAssemblies, IBasicLogger logger)
+ {
+ this.modsAddedByAssemblies = modsAddedByAssemblies ?? throw new ArgumentNullException(nameof(modsAddedByAssemblies));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public IEnumerable Run()
+ {
+ Stopwatch patchSw = new Stopwatch();
+ patchSw.Start();
+
+ status = "Checking Cache";
+ logger.Info(status);
+
+ bool useCache = false;
+ try
+ {
+ useCache = IsCacheUpToDate();
+ }
+ catch (Exception ex)
+ {
+ logger.Exception("Exception in IsCacheUpToDate", ex);
+ }
+
+#if DEBUG
+ //useCache = false;
+#endif
+
+ IEnumerable databaseConfigs = null;
+
+ if (!useCache)
+ {
+ if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath);
+ MessageQueue patchLogQueue = new MessageQueue();
+ QueueLogRunner logRunner = new QueueLogRunner(patchLogQueue);
+ ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate
+ {
+ using StreamLogger streamLogger = new StreamLogger(new FileStream(patchLogPath, FileMode.Create));
+ streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
+ logRunner.Run(streamLogger);
+ streamLogger.Info("Done!");
+ });
+ IBasicLogger patchLogger = new LogSplitter(logger, new QueueLogger(patchLogQueue));
+
+ IPatchProgress progress = new PatchProgress(patchLogger);
+ status = "Pre patch init";
+ patchLogger.Info(status);
+ IEnumerable mods = ModListGenerator.GenerateModList(modsAddedByAssemblies, progress, patchLogger);
+
+ // If we don't use the cache then it is best to clean the PartDatabase.cfg
+ if (!keepPartDB && File.Exists(partDatabasePath))
+ File.Delete(partDatabasePath);
+
+ LoadPhysicsConfig();
+
+ #region Sorting Patches
+
+ status = "Extracting patches";
+ patchLogger.Info(status);
+
+ UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData && dir.name == "");
+ INeedsChecker needsChecker = new NeedsChecker(mods, gameData, progress, patchLogger);
+ ITagListParser tagListParser = new TagListParser(progress);
+ IProtoPatchBuilder protoPatchBuilder = new ProtoPatchBuilder(progress);
+ IPatchCompiler patchCompiler = new PatchCompiler();
+ PatchExtractor extractor = new PatchExtractor(progress, patchLogger, needsChecker, tagListParser, protoPatchBuilder, patchCompiler);
+
+ // Have to convert to an array because we will be removing patches
+ IEnumerable extractedPatches =
+ GameDatabase.Instance.root.AllConfigs.Select(urlConfig => extractor.ExtractPatch(urlConfig)).Where(patch => patch != null);
+ PatchList patchList = new PatchList(mods, extractedPatches, progress);
+
+ #endregion
+
+ #region Applying patches
+
+ status = "Applying patches";
+ patchLogger.Info(status);
+
+ IPass currentPass = null;
+
+ progress.OnPassStarted.Add(delegate (IPass pass)
+ {
+ currentPass = pass;
+ StatusUpdate(progress, currentPass.Name);
+ });
+
+ System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
+ stopwatch.Start();
+
+ progress.OnPatchApplied.Add(delegate
+ {
+ long timeRemaining = STATUS_UPDATE_INVERVAL_MS - stopwatch.ElapsedMilliseconds;
+ if (timeRemaining < 0)
+ {
+ StatusUpdate(progress, currentPass.Name);
+ stopwatch.Reset();
+ stopwatch.Start();
+ }
+ });
+
+ PatchApplier applier = new PatchApplier(progress, patchLogger);
+ databaseConfigs = applier.ApplyPatches(patchList);
+
+ stopwatch.Stop();
+ StatusUpdate(progress);
+
+ patchLogger.Info("Done patching");
+
+ #endregion Applying patches
+
+ #region Saving Cache
+
+ foreach (KeyValuePair item in progress.Counter.warningFiles)
+ {
+ patchLogger.Warning(item.Value + " warning" + (item.Value > 1 ? "s" : "") + " related to GameData/" + item.Key);
+ }
+
+ if (progress.Counter.errors > 0 || progress.Counter.exceptions > 0)
+ {
+ foreach (KeyValuePair item in progress.Counter.errorFiles)
+ {
+ errors += item.Value + " error" + (item.Value > 1 ? "s" : "") + " related to GameData/" + item.Key
+ + "\n";
+ }
+
+ patchLogger.Warning("Errors in patch prevents the creation of the cache");
+ try
+ {
+ if (File.Exists(cachePath))
+ File.Delete(cachePath);
+ if (File.Exists(shaPath))
+ File.Delete(shaPath);
+ }
+ catch (Exception e)
+ {
+ patchLogger.Exception("Exception while deleting stale cache ", e);
+ }
+ }
+ else
+ {
+ status = "Saving Cache";
+ patchLogger.Info(status);
+ CreateCache(databaseConfigs, progress.Counter.patchedNodes);
+ }
+
+ StatusUpdate(progress);
+
+ #endregion Saving Cache
+
+ SaveModdedTechTree(databaseConfigs);
+ SaveModdedPhysics(databaseConfigs);
+
+ logRunner.RequestStop();
+
+ while (loggingThreadStatus.IsRunning)
+ {
+ System.Threading.Thread.Sleep(100);
+ }
+
+ if (loggingThreadStatus.IsExitedWithError)
+ {
+ logger.Error("The patching thread threw an exception");
+ throw loggingThreadStatus.Exception;
+ }
+ }
+ else
+ {
+ status = "Loading from Cache";
+ logger.Info(status);
+ databaseConfigs = LoadCache();
+
+ if (File.Exists(patchLogPath))
+ {
+ logger.Info("Dumping patch log");
+ logger.Info("\n#### BEGIN PATCH LOG ####\n\n\n" + File.ReadAllText(patchLogPath) + "\n\n\n#### END PATCH LOG ####");
+ }
+ else
+ {
+ logger.Error("Patch log does not exist: " + patchLogPath);
+ }
+ }
+
+ if (KSP.Localization.Localizer.Instance != null)
+ KSP.Localization.Localizer.SwitchToLanguage(KSP.Localization.Localizer.CurrentLanguage);
+
+ logger.Info(status + "\n" + errors);
+
+ patchSw.Stop();
+ logger.Info("Ran in " + ((float)patchSw.ElapsedMilliseconds / 1000).ToString("F3") + "s");
+
+ return databaseConfigs;
+ }
+
+ private void LoadPhysicsConfig()
+ {
+ logger.Info("Loading Physics.cfg");
+ UrlDir gameDataDir = GameDatabase.Instance.root.AllDirectories.First(d => d.path.EndsWith("GameData") && d.name == "" && d.url == "");
+ // need to use a file with a cfg extension to get the right fileType or you can't AddConfig on it
+ UrlDir.UrlFile physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath));
+ // Since it loaded the default config badly (sub node only) we clear it first
+ physicsUrlFile.configs.Clear();
+ // And reload it properly
+ ConfigNode physicsContent = ConfigNode.Load(defaultPhysicsPath);
+ physicsContent.name = PHYSICS_NODE_NAME;
+ physicsUrlFile.AddConfig(physicsContent);
+ gameDataDir.files.Add(physicsUrlFile);
+ }
+
+ private void SaveModdedPhysics(IEnumerable databaseConfigs)
+ {
+ IEnumerable configs = databaseConfigs.Where(config => config.NodeType == PHYSICS_NODE_NAME);
+ int count = configs.Count();
+
+ if (count == 0)
+ {
+ logger.Info($"No {PHYSICS_NODE_NAME} node found. No custom Physics config will be saved");
+ return;
+ }
+
+ if (count > 1)
+ {
+ logger.Info($"{count} {PHYSICS_NODE_NAME} nodes found. A patch may be wrong. Using the first one");
+ }
+
+ configs.First().Node.Save(physicsPath);
+ }
+
+ private bool IsCacheUpToDate()
+ {
+ Stopwatch sw = new Stopwatch();
+ sw.Start();
+
+ using System.Security.Cryptography.SHA256 sha = System.Security.Cryptography.SHA256.Create();
+ using System.Security.Cryptography.SHA256 filesha = System.Security.Cryptography.SHA256.Create();
+ UrlDir.UrlFile[] files = GameDatabase.Instance.root.AllConfigFiles.ToArray();
+
+ filesSha.Clear();
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ string url = files[i].GetUrlWithExtension();
+ // Hash the file path so the checksum change if files are moved
+ byte[] pathBytes = Encoding.UTF8.GetBytes(url);
+ sha.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);
+
+ // hash the file content
+ byte[] contentBytes = File.ReadAllBytes(files[i].fullPath);
+ sha.TransformBlock(contentBytes, 0, contentBytes.Length, contentBytes, 0);
+
+ filesha.ComputeHash(contentBytes);
+ if (!filesSha.ContainsKey(url))
+ {
+ filesSha.Add(url, BitConverter.ToString(filesha.Hash));
+ }
+ else
+ {
+ logger.Warning("Duplicate fileSha key. This should not append. The key is " + url);
+ }
+ }
+
+ // Hash the mods dll path so the checksum change if dlls are moved or removed (impact NEEDS)
+ foreach (AssemblyLoader.LoadedAssembly dll in AssemblyLoader.loadedAssemblies)
+ {
+ string path = dll.url + "/" + dll.name;
+ byte[] pathBytes = Encoding.UTF8.GetBytes(path);
+ sha.TransformBlock(pathBytes, 0, pathBytes.Length, pathBytes, 0);
+ }
+
+ foreach (ModListGenerator.ModAddedByAssembly mod in modsAddedByAssemblies)
+ {
+ byte[] modBytes = Encoding.UTF8.GetBytes(mod.modName);
+ sha.TransformBlock(modBytes, 0, modBytes.Length, modBytes, 0);
+ }
+
+ byte[] godsFinalMessageToHisCreation = Encoding.UTF8.GetBytes("We apologize for the inconvenience.");
+ sha.TransformFinalBlock(godsFinalMessageToHisCreation, 0, godsFinalMessageToHisCreation.Length);
+
+ configSha = BitConverter.ToString(sha.Hash);
+ sha.Clear();
+ filesha.Clear();
+
+ sw.Stop();
+
+ logger.Info("SHA generated in " + ((float)sw.ElapsedMilliseconds / 1000).ToString("F3") + "s");
+ logger.Info(" SHA = " + configSha);
+
+ bool useCache = false;
+ if (File.Exists(shaPath))
+ {
+ ConfigNode shaConfigNode = ConfigNode.Load(shaPath);
+ if (shaConfigNode != null && shaConfigNode.HasValue("SHA") && shaConfigNode.HasValue("version") && shaConfigNode.HasValue("KSPVersion"))
+ {
+ string storedSHA = shaConfigNode.GetValue("SHA");
+ string version = shaConfigNode.GetValue("version");
+ string kspVersion = shaConfigNode.GetValue("KSPVersion");
+ ConfigNode filesShaNode = shaConfigNode.GetNode("FilesSHA");
+ useCache = CheckFilesChange(files, filesShaNode);
+ useCache = useCache && storedSHA.Equals(configSha);
+ useCache = useCache && version.Equals(Assembly.GetExecutingAssembly().GetName().Version.ToString());
+ useCache = useCache && kspVersion.Equals(Versioning.version_major + "." + Versioning.version_minor + "." + Versioning.Revision + "." + Versioning.BuildID);
+ useCache = useCache && File.Exists(cachePath);
+ useCache = useCache && File.Exists(physicsPath);
+ useCache = useCache && File.Exists(techTreePath);
+ logger.Info("Cache SHA = " + storedSHA);
+ logger.Info("useCache = " + useCache);
+ }
+ }
+ return useCache;
+ }
+
+ private bool CheckFilesChange(UrlDir.UrlFile[] files, ConfigNode shaConfigNode)
+ {
+ bool noChange = true;
+ StringBuilder changes = new StringBuilder();
+
+ changes.Append("Changes :\n");
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ string url = files[i].GetUrlWithExtension();
+ ConfigNode fileNode = GetFileNode(shaConfigNode, url);
+ string fileSha = fileNode?.GetValue("SHA");
+
+ if (fileNode == null)
+ continue;
+
+ if (fileSha == null || filesSha[url] != fileSha)
+ {
+ changes.Append("Changed : " + fileNode.GetValue("filename") + ".cfg\n");
+ noChange = false;
+ }
+ }
+ for (int i = 0; i < files.Length; i++)
+ {
+ string url = files[i].GetUrlWithExtension();
+ ConfigNode fileNode = GetFileNode(shaConfigNode, url);
+
+ if (fileNode == null)
+ {
+ changes.Append("Added : " + url + "\n");
+ noChange = false;
+ }
+ shaConfigNode.RemoveNode(fileNode);
+ }
+ foreach (ConfigNode fileNode in shaConfigNode.GetNodes())
+ {
+ changes.Append("Deleted : " + fileNode.GetValue("filename") + "\n");
+ noChange = false;
+ }
+ if (!noChange)
+ logger.Info(changes.ToString());
+ return noChange;
+ }
+
+ private ConfigNode GetFileNode(ConfigNode shaConfigNode, string filename)
+ {
+ for (int i = 0; i < shaConfigNode.nodes.Count; i++)
+ {
+ ConfigNode file = shaConfigNode.nodes[i];
+ if (file.name == "FILE" && file.GetValue("filename") == filename)
+ return file;
+ }
+ return null;
+ }
+
+
+ private void CreateCache(IEnumerable databaseConfigs, int patchedNodeCount)
+ {
+ ConfigNode shaConfigNode = new ConfigNode();
+ shaConfigNode.AddValue("SHA", configSha);
+ shaConfigNode.AddValue("version", Assembly.GetExecutingAssembly().GetName().Version.ToString());
+ shaConfigNode.AddValue("KSPVersion", Versioning.version_major + "." + Versioning.version_minor + "." + Versioning.Revision + "." + Versioning.BuildID);
+ ConfigNode filesSHANode = shaConfigNode.AddNode("FilesSHA");
+
+ ConfigNode cache = new ConfigNode();
+
+ cache.AddValue("patchedNodeCount", patchedNodeCount.ToString());
+
+ foreach (IProtoUrlConfig urlConfig in databaseConfigs)
+ {
+ ConfigNode node = cache.AddNode("UrlConfig");
+ node.AddValue("parentUrl", urlConfig.UrlFile.GetUrlWithExtension());
+
+ ConfigNode urlNode = urlConfig.Node.DeepCopy();
+ urlNode.EscapeValuesRecursive();
+
+ node.AddNode(urlNode);
+ }
+
+ foreach (var file in GameDatabase.Instance.root.AllConfigFiles)
+ {
+ string url = file.GetUrlWithExtension();
+ // "/Physics" is the node we created manually to loads the PHYSIC config
+ if (file.url != "/Physics" && filesSha.ContainsKey(url))
+ {
+ ConfigNode shaNode = filesSHANode.AddNode("FILE");
+ shaNode.AddValue("filename", url);
+ shaNode.AddValue("SHA", filesSha[url]);
+ filesSha.Remove(url);
+ }
+ }
+
+ logger.Info("Saving cache");
+
+ try
+ {
+ shaConfigNode.Save(shaPath);
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Exception while saving the sha", e);
+ }
+ try
+ {
+ cache.Save(cachePath);
+ return;
+ }
+ catch (NullReferenceException e)
+ {
+ logger.Exception("NullReferenceException while saving the cache", e);
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Exception while saving the cache", e);
+ }
+
+ try
+ {
+ logger.Error("An error occured while creating the cache. Deleting the cache files to avoid keeping a bad cache");
+ if (File.Exists(cachePath))
+ File.Delete(cachePath);
+ if (File.Exists(shaPath))
+ File.Delete(shaPath);
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Exception while deleting the cache", e);
+ }
+ }
+
+ private void SaveModdedTechTree(IEnumerable databaseConfigs)
+ {
+ IEnumerable configs = databaseConfigs.Where(config => config.NodeType == TECH_TREE_NODE_NAME);
+ int count = configs.Count();
+
+ if (count == 0)
+ {
+ logger.Info($"No {TECH_TREE_NODE_NAME} node found. No custom {TECH_TREE_NODE_NAME} will be saved");
+ return;
+ }
+
+ if (count > 1)
+ {
+ logger.Info($"{count} {TECH_TREE_NODE_NAME} nodes found. A patch may be wrong. Using the first one");
+ }
+
+ ConfigNode techNode = new ConfigNode(TECH_TREE_NODE_NAME);
+ techNode.AddNode(configs.First().Node);
+ techNode.Save(techTreePath);
+ }
+
+ private IEnumerable LoadCache()
+ {
+ ConfigNode cache = ConfigNode.Load(cachePath);
+
+ if (cache.HasValue("patchedNodeCount") && int.TryParse(cache.GetValue("patchedNodeCount"), out int patchedNodeCount))
+ status = "ModuleManager: " + patchedNodeCount + " patch" + (patchedNodeCount != 1 ? "es" : "") + " loaded from cache";
+
+ // Create the fake file where we load the physic config cache
+ UrlDir gameDataDir = GameDatabase.Instance.root.AllDirectories.First(d => d.path.EndsWith("GameData") && d.name == "" && d.url == "");
+ // need to use a file with a cfg extension to get the right fileType or you can't AddConfig on it
+ UrlDir.UrlFile physicsUrlFile = new UrlDir.UrlFile(gameDataDir, new FileInfo(defaultPhysicsPath));
+ gameDataDir.files.Add(physicsUrlFile);
+
+ List databaseConfigs = new List(cache.nodes.Count);
+
+ foreach (ConfigNode node in cache.nodes)
+ {
+ string parentUrl = node.GetValue("parentUrl");
+
+ UrlDir.UrlFile parent = gameDataDir.Find(parentUrl);
+ if (parent != null)
+ {
+ node.nodes[0].UnescapeValuesRecursive();
+ databaseConfigs.Add(new ProtoUrlConfig(parent, node.nodes[0]));
+ }
+ else
+ {
+ logger.Warning("Parent null for " + parentUrl);
+ }
+ }
+ logger.Info("Cache Loaded");
+
+ return databaseConfigs;
+ }
+
+ private void StatusUpdate(IPatchProgress progress, string activity = null)
+ {
+ status = "ModuleManager: " + progress.Counter.patchedNodes + " patch" + (progress.Counter.patchedNodes != 1 ? "es" : "") + " applied";
+ if (progress.ProgressFraction < 1f - float.Epsilon)
+ status += " (" + progress.ProgressFraction.ToString("P0") + ")";
+
+ if (activity != null)
+ status += "\n" + activity;
+
+ if (progress.Counter.warnings > 0)
+ status += ", found " + progress.Counter.warnings + " warning" + (progress.Counter.warnings != 1 ? "s" : "") + "";
+
+ if (progress.Counter.errors > 0)
+ status += ", found " + progress.Counter.errors + " error" + (progress.Counter.errors != 1 ? "s" : "") + "";
+
+ if (progress.Counter.exceptions > 0)
+ status += ", encountered " + progress.Counter.exceptions + " exception" + (progress.Counter.exceptions != 1 ? "s" : "") + "";
+ }
+
+ #region Applying Patches
+
+ // Name is group 1, index is group 2, vector related filed is group 3, vector separator is group 4, operator is group 5
+ private static readonly Regex parseValue = new Regex(@"([\w\&\-\.\?\*\#+/^!\(\) ]+(?:,[^*\d][\w\&\-\.\?\*\(\) ]*)*)(?:,(-?[0-9\*]+))?(?:\[((?:[0-9\*]+)+)(?:,(.))?\])?");
+
+ // ModifyNode applies the ConfigNode mod as a 'patch' to ConfigNode original, then returns the patched ConfigNode.
+ // it uses FindConfigNodeIn(src, nodeType, nodeName, nodeTag) to recurse.
+ public static ConfigNode ModifyNode(NodeStack original, ConfigNode mod, PatchContext context)
+ {
+ ConfigNode newNode = original.value.DeepCopy();
+ NodeStack nodeStack = original.ReplaceValue(newNode);
+
+ #region Values
+
+ #if LOGSPAM
+ string vals = "[ModuleManager] modding values";
+ #endif
+ foreach (ConfigNode.Value modVal in mod.values)
+ {
+ #if LOGSPAM
+ vals += "\n " + modVal.name + "= " + modVal.value;
+ #endif
+
+ Command cmd = CommandParser.Parse(modVal.name, out string valName);
+
+ Operator op;
+ if (valName.Length > 2 && valName[valName.Length - 2] == ',')
+ op = Operator.Assign;
+ else
+ op = OperatorParser.Parse(valName, out valName);
+
+ if (cmd == Command.Special)
+ {
+ ConfigNode.Value val = RecurseVariableSearch(valName, nodeStack.Push(mod), context);
+
+ if (val == null)
+ {
+ context.progress.Error(context.patchUrl, "Error - Cannot find value assigning command: " + valName);
+ continue;
+ }
+
+ if (op != Operator.Assign)
+ {
+ if (double.TryParse(modVal.value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s)
+ && double.TryParse(val.value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double os))
+ {
+ switch (op)
+ {
+ case Operator.Multiply:
+ val.value = (os * s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Divide:
+ val.value = (os / s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Add:
+ val.value = (os + s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Subtract:
+ val.value = (os - s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Exponentiate:
+ val.value = Math.Pow(os, s).ToString(CultureInfo.InvariantCulture);
+ break;
+ }
+ }
+ }
+ else
+ {
+ val.value = modVal.value;
+ }
+ continue;
+ }
+
+ Match match = parseValue.Match(valName);
+ if (!match.Success)
+ {
+ context.progress.Error(context.patchUrl, "Error - Cannot parse value modifying command: " + valName);
+ continue;
+ }
+
+ // Get the bits and pieces from the regexp
+ valName = match.Groups[1].Value;
+
+ // Get a position for editing a vector
+ int position = 0;
+ bool isPosStar = false;
+ if (match.Groups[3].Success)
+ {
+ if (match.Groups[3].Value == "*")
+ isPosStar = true;
+ else if (!int.TryParse(match.Groups[3].Value, out position))
+ {
+ context.progress.Error(context.patchUrl, "Error - Unable to parse number as number. Very odd.");
+ continue;
+ }
+ }
+ char seperator = ',';
+ if (match.Groups[4].Success)
+ {
+ seperator = match.Groups[4].Value[0];
+ }
+
+ // In this case insert the value at position index (with the same node names)
+ int index = 0;
+ bool isStar = false;
+ if (match.Groups[2].Success)
+ {
+ if (match.Groups[2].Value == "*")
+ isStar = true;
+ // can have "node,n *" (for *= ect)
+ else if (!int.TryParse(match.Groups[2].Value, out index))
+ {
+ context.progress.Error(context.patchUrl, "Error - Unable to parse number as number. Very odd.");
+ continue;
+ }
+ }
+
+ int valCount = 0;
+ for (int i=0; i " + value;
+ #endif
+
+ if (cmd != Command.Copy)
+ origVal.value = value;
+ else
+ newNode.AddValueSafe(valName, value);
+ }
+ }
+ else
+ {
+ context.progress.Error(context.patchUrl, "Error - Cannot parse variable search when editing key " + valName + " = " + modVal.value);
+ }
+
+ if (isStar) index++;
+ else break;
+ }
+ break;
+
+ case Command.Delete:
+ if (match.Groups[5].Success)
+ {
+ context.progress.Error(context.patchUrl, "Error - Cannot use operators with delete (- or !) value: " + mod.name);
+ }
+ else if (match.Groups[2].Success)
+ {
+ while (index < valCount)
+ {
+ // If there is an index, use it.
+ ConfigNode.Value v = FindValueIn(newNode, valName, index);
+ if (v != null)
+ newNode.values.Remove(v);
+ if (isStar) index++;
+ else break;
+ }
+ }
+ else if (valName.Contains('*') || valName.Contains('?'))
+ {
+ // Delete all matching wildcard
+ ConfigNode.Value last = null;
+ while (true)
+ {
+ ConfigNode.Value v = FindValueIn(newNode, valName, index++);
+ if (v == last)
+ break;
+ last = v;
+ newNode.values.Remove(v);
+ }
+ }
+ else
+ {
+ // Default is to delete ALL values that match. (backwards compatibility)
+ newNode.RemoveValues(valName);
+ }
+ break;
+
+ case Command.Rename:
+ if (nodeStack.IsRoot)
+ {
+ context.progress.Error(context.patchUrl, "Error - Renaming nodes does not work on top nodes");
+ break;
+ }
+ newNode.name = modVal.value;
+ break;
+
+ case Command.Create:
+ if (match.Groups[2].Success || match.Groups[5].Success || valName.Contains('*')
+ || valName.Contains('?'))
+ {
+ if (match.Groups[2].Success)
+ context.progress.Error(context.patchUrl, "Error - Cannot use index with create (&) value: " + mod.name);
+ if (match.Groups[5].Success)
+ context.progress.Error(context.patchUrl, "Error - Cannot use operators with create (&) value: " + mod.name);
+ if (valName.Contains('*') || valName.Contains('?'))
+ context.progress.Error(context.patchUrl, "Error - Cannot use wildcards (* or ?) with create (&) value: " + mod.name);
+ }
+ else
+ {
+ varValue = ProcessVariableSearch(modVal.value, nodeStack, context);
+ if (varValue != null)
+ {
+ if (!newNode.HasValue(valName))
+ newNode.AddValueSafe(valName, varValue);
+ }
+ else
+ {
+ context.progress.Error(context.patchUrl, "Error - Cannot parse variable search when replacing (&) key " + valName + " = " +
+ modVal.value);
+ }
+ }
+ break;
+ }
+ }
+ #if LOGSPAM
+ log(vals);
+ #endif
+
+ #endregion Values
+
+ #region Nodes
+
+ foreach (ConfigNode subMod in mod.nodes)
+ {
+ subMod.name = subMod.name.RemoveWS();
+
+ if (!subMod.name.IsBracketBalanced())
+ {
+ context.progress.Error(context.patchUrl,
+ "Error - Skipping a patch subnode with unbalanced square brackets or a space (replace them with a '?') in "
+ + mod.name + " : \n" + subMod.name + "\n");
+ continue;
+ }
+
+ string subName = subMod.name;
+ Command command = CommandParser.Parse(subName, out string tmp);
+
+ if (command == Command.Insert)
+ {
+ ConfigNode newSubMod = new ConfigNode(subMod.name);
+ newSubMod = ModifyNode(nodeStack.Push(newSubMod), subMod, context);
+ subName = newSubMod.name;
+ if (subName.Contains(",") && int.TryParse(subName.Split(',')[1], out int index))
+ {
+ // In this case insert the node at position index (with the same node names)
+ newSubMod.name = subName.Split(',')[0];
+ InsertNode(newNode, newSubMod, index);
+ }
+ else
+ {
+ newNode.AddNode(newSubMod);
+ }
+ }
+ else if (command == Command.Paste)
+ {
+ //int start = subName.IndexOf('[');
+ //int end = subName.LastIndexOf(']');
+ //if (start == -1 || end == -1 || end - start < 1)
+ //{
+ // log("Pasting a node require a [path] to the node to paste" + mod.name + " : \n" + subMod.name + "\n");
+ // errorCount++;
+ // continue;
+ //}
+
+ //string newName = subName.Substring(0, start);
+ //string path = subName.Substring(start + 1, end - start - 1);
+
+ ConfigNode toPaste = RecurseNodeSearch(subName.Substring(1), nodeStack, context);
+
+ if (toPaste == null)
+ {
+ context.progress.Error(context.patchUrl, "Error - Can not find the node to paste in " + mod.name + " : " + subMod.name + "\n");
+ continue;
+ }
+
+ ConfigNode newSubMod = new ConfigNode(toPaste.name);
+ newSubMod = ModifyNode(nodeStack.Push(newSubMod), toPaste, context);
+ if (subName.LastIndexOf(',') > 0 && int.TryParse(subName.Substring(subName.LastIndexOf(',') + 1), out int index))
+ {
+ // In this case insert the node at position index
+ InsertNode(newNode, newSubMod, index);
+ }
+ else
+ newNode.AddNode(newSubMod);
+ }
+ else
+ {
+ string constraints = "";
+ string tag = "";
+ string nodeType, nodeName;
+ int index = 0;
+ #if LOGSPAM
+ string msg = "";
+ #endif
+ List subNodes = new List();
+
+ // three ways to specify:
+ // NODE,n will match the nth node (NODE is the same as NODE,0)
+ // NODE,* will match ALL nodes
+ // NODE:HAS[condition] will match ALL nodes with condition
+ if (subName.Contains(":HAS[", out int hasStart))
+ {
+ constraints = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5);
+ subName = subName.Substring(0, hasStart);
+ }
+
+ if (subName.Contains(","))
+ {
+ tag = subName.Split(',')[1];
+ subName = subName.Split(',')[0];
+ int.TryParse(tag, out index);
+ }
+
+ if (subName.Contains("["))
+ {
+ // format @NODETYPE[Name] {...}
+ // or @NODETYPE[Name, index] {...}
+ nodeType = subName.Substring(1).Split('[')[0];
+ nodeName = subName.Split('[')[1].Replace("]", "");
+ }
+ else
+ {
+ // format @NODETYPE {...} or ! instead of @
+ nodeType = subName.Substring(1);
+ nodeName = null;
+ }
+
+ if (tag == "*" || constraints.Length > 0)
+ {
+ // get ALL nodes
+ if (command != Command.Replace)
+ {
+ ConfigNode n, last = null;
+ while (true)
+ {
+ n = FindConfigNodeIn(newNode, nodeType, nodeName, index++);
+ if (n == last || n == null)
+ break;
+ if (CheckConstraints(n, constraints))
+ subNodes.Add(n);
+ last = n;
+ }
+ }
+#if LOGSPAM
+ else
+ msg += " cannot wildcard a % node: " + subMod.name + "\n";
+#endif
+ }
+ else
+ {
+ // just get one node
+ ConfigNode n = FindConfigNodeIn(newNode, nodeType, nodeName, index);
+ if (n != null)
+ subNodes.Add(n);
+ }
+
+ if (command == Command.Replace)
+ {
+ // if the original exists modify it
+ if (subNodes.Count > 0)
+ {
+ #if LOGSPAM
+ msg += " Applying subnode " + subMod.name + "\n";
+ #endif
+ ConfigNode newSubNode = ModifyNode(nodeStack.Push(subNodes[0]), subMod, context);
+ subNodes[0].ShallowCopyFrom(newSubNode);
+ subNodes[0].name = newSubNode.name;
+ }
+ else
+ {
+ // if not add the mod node without the % in its name
+ #if LOGSPAM
+ msg += " Adding subnode " + subMod.name + "\n";
+ #endif
+
+ ConfigNode copy = new ConfigNode(nodeType);
+
+ if (nodeName != null)
+ copy.AddValueSafe("name", nodeName);
+
+ ConfigNode newSubNode = ModifyNode(nodeStack.Push(copy), subMod, context);
+ newNode.nodes.Add(newSubNode);
+ }
+ }
+ else if (command == Command.Create)
+ {
+ if (subNodes.Count == 0)
+ {
+ #if LOGSPAM
+ msg += " Adding subnode " + subMod.name + "\n";
+ #endif
+
+ ConfigNode copy = new ConfigNode(nodeType);
+
+ if (nodeName != null)
+ copy.AddValueSafe("name", nodeName);
+
+ ConfigNode newSubNode = ModifyNode(nodeStack.Push(copy), subMod, context);
+ newNode.nodes.Add(newSubNode);
+ }
+ }
+ else
+ {
+ // find each original subnode to modify, modify it and add the modified.
+ #if LOGSPAM
+ if (subNodes.Count == 0) // no nodes to modify!
+ msg += " Could not find node(s) to modify: " + subMod.name + "\n";
+ #endif
+
+ foreach (ConfigNode subNode in subNodes)
+ {
+ #if LOGSPAM
+ msg += " Applying subnode " + subMod.name + "\n";
+ #endif
+ ConfigNode newSubNode;
+ switch (command)
+ {
+ case Command.Edit:
+
+ // Edit in place
+ newSubNode = ModifyNode(nodeStack.Push(subNode), subMod, context);
+ subNode.ShallowCopyFrom(newSubNode);
+ subNode.name = newSubNode.name;
+ break;
+
+ case Command.Delete:
+
+ // Delete the node
+ newNode.nodes.Remove(subNode);
+ break;
+
+ case Command.Copy:
+
+ // Copy the node
+ newSubNode = ModifyNode(nodeStack.Push(subNode), subMod, context);
+ newNode.nodes.Add(newSubNode);
+ break;
+ }
+ }
+ }
+ #if LOGSPAM
+ print(msg);
+ #endif
+ }
+ }
+
+ #endregion Nodes
+
+ return newNode;
+ }
+
+
+ // Search for a ConfigNode by a path alike string
+ private static ConfigNode RecurseNodeSearch(string path, NodeStack nodeStack, PatchContext context)
+ {
+ //log("Path : \"" + path + "\"");
+
+ if (path[0] == '/')
+ {
+ return RecurseNodeSearch(path.Substring(1), nodeStack.Root, context);
+ }
+
+ int nextSep = path.IndexOf('/');
+
+ bool root = (path[0] == '@');
+ int shift = root ? 1 : 0;
+ string subName = (nextSep != -1) ? path.Substring(shift, nextSep - shift) : path.Substring(shift);
+ string nodeType, nodeName;
+ string constraint = "";
+
+ int index = 0;
+ if (subName.Contains(":HAS[", out int hasStart))
+ {
+ constraint = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5);
+ subName = subName.Substring(0, hasStart);
+ }
+ else if (subName.Contains(","))
+ {
+ string tag = subName.Split(',')[1];
+ subName = subName.Split(',')[0];
+ int.TryParse(tag, out index);
+ }
+
+ if (subName.Contains("["))
+ {
+ // NODETYPE[Name]
+ nodeType = subName.Split('[')[0];
+ nodeName = subName.Split('[')[1].Replace("]", "");
+ }
+ else
+ {
+ // NODETYPE
+ nodeType = subName;
+ nodeName = null;
+ }
+
+ // ../XXXXX
+ if (path.StartsWith("../"))
+ {
+ if (nodeStack.IsRoot)
+ return null;
+
+ return RecurseNodeSearch(path.Substring(3), nodeStack.Pop(), context);
+ }
+
+ //log("nextSep : \"" + nextSep + " \" root : \"" + root + " \" nodeType : \"" + nodeType + "\" nodeName : \"" + nodeName + "\"");
+
+ // @XXXXX
+ if (root)
+ {
+ bool foundNodeType = false;
+ foreach (IProtoUrlConfig urlConfig in context.databaseConfigs)
+ {
+ ConfigNode node = urlConfig.Node;
+
+ if (node.name != nodeType) continue;
+
+ foundNodeType = true;
+
+ if (nodeName == null || (node.GetValue("name") is string testNodeName && WildcardMatch(testNodeName, nodeName)))
+ {
+ nodeStack = new NodeStack(node);
+ break;
+ }
+ }
+
+ if (!foundNodeType) context.logger.Warning("Can't find nodeType:" + nodeType);
+ if (nodeStack == null) return null;
+ }
+ else
+ {
+ if (constraint.Length > 0)
+ {
+ // get the first one matching
+ ConfigNode last = null;
+ while (true)
+ {
+ ConfigNode n = FindConfigNodeIn(nodeStack.value, nodeType, nodeName, index++);
+ if (n == last || n == null)
+ {
+ nodeStack = null;
+ break;
+ }
+ if (CheckConstraints(n, constraint))
+ {
+ nodeStack = nodeStack.Push(n);
+ break;
+ }
+ last = n;
+ }
+ }
+ else
+ {
+ // just get one node
+ nodeStack = nodeStack.Push(FindConfigNodeIn(nodeStack.value, nodeType, nodeName, index));
+ }
+ }
+
+ // XXXXXX/
+ if (nextSep > 0 && nodeStack != null)
+ {
+ path = path.Substring(nextSep + 1);
+ //log("NewPath : \"" + path + "\"");
+ return RecurseNodeSearch(path, nodeStack, context);
+ }
+
+ return nodeStack.value;
+ }
+
+ // KeyName is group 1, index is group 2, value index is group 3, value separator is group 4
+ private static readonly Regex parseVarKey = new Regex(@"([\w\&\-\.]+)(?:,((?:[0-9]+)+))?(?:\[((?:[0-9]+)+)(?:,(.))?\])?");
+
+ // Search for a value by a path alike string
+ private static ConfigNode.Value RecurseVariableSearch(string path, NodeStack nodeStack, PatchContext context)
+ {
+ //log("path:" + path);
+ if (path[0] == '/')
+ return RecurseVariableSearch(path.Substring(1), nodeStack.Root, context);
+ int nextSep = path.IndexOf('/');
+
+ // make sure we don't stop on a ",/" which would be a value separator
+ // it's a hack that should be replaced with a proper regex for the whole node search
+ while (nextSep > 0 && path[nextSep - 1] == ',')
+ nextSep = path.IndexOf('/', nextSep + 1);
+
+ if (path[0] == '@')
+ {
+ if (nextSep < 2)
+ return null;
+
+ string subName = path.Substring(1, nextSep - 1);
+ string nodeType, nodeName;
+
+ if (subName.Contains("["))
+ {
+ // @NODETYPE[Name]/
+ nodeType = subName.Split('[')[0];
+ nodeName = subName.Split('[')[1].Replace("]", "");
+ }
+ else
+ {
+ // @NODETYPE/
+ nodeType = subName;
+ nodeName = null;
+ }
+
+ bool foundNodeType = false;
+ foreach (IProtoUrlConfig urlConfig in context.databaseConfigs)
+ {
+ ConfigNode node = urlConfig.Node;
+
+ if (node.name != nodeType) continue;
+
+ foundNodeType = true;
+
+ if (nodeName == null || (node.GetValue("name") is string testNodeName && WildcardMatch(testNodeName, nodeName)))
+ {
+ return RecurseVariableSearch(path.Substring(nextSep + 1), new NodeStack(node), context);
+ }
+ }
+
+ if (!foundNodeType) context.logger.Warning("Can't find nodeType:" + nodeType);
+
+ return null;
+ }
+ if (path.StartsWith("../"))
+ {
+ if (nodeStack.IsRoot)
+ return null;
+
+ return RecurseVariableSearch(path.Substring(3), nodeStack.Pop(), context);
+ }
+
+ // Node search
+ if (nextSep > 0 && path[nextSep - 1] != ',')
+ {
+ // Big case of code duplication here ...
+ // TODO : replace with a regex
+
+ string subName = path.Substring(0, nextSep);
+ string constraint = "";
+ string nodeType, nodeName;
+ int index = 0;
+ if (subName.Contains(":HAS[", out int hasStart))
+ {
+ constraint = subName.Substring(hasStart + 5, subName.LastIndexOf(']') - hasStart - 5);
+ subName = subName.Substring(0, hasStart);
+ }
+ else if (subName.Contains(','))
+ {
+ string tag = subName.Split(',')[1];
+ subName = subName.Split(',')[0];
+ int.TryParse(tag, out index);
+ }
+
+ if (subName.Contains("["))
+ {
+ // format NODETYPE[Name] {...}
+ // or NODETYPE[Name, index] {...}
+ nodeType = subName.Split('[')[0];
+ nodeName = subName.Split('[')[1].Replace("]", "");
+ }
+ else
+ {
+ // format NODETYPE {...}
+ nodeType = subName;
+ nodeName = null;
+ }
+
+ if (constraint.Length > 0)
+ {
+ // get the first one matching
+ ConfigNode last = null;
+ while (true)
+ {
+ ConfigNode n = FindConfigNodeIn(nodeStack.value, nodeType, nodeName, index++);
+ if (n == last || n == null)
+ break;
+ if (CheckConstraints(n, constraint))
+ return RecurseVariableSearch(path.Substring(nextSep + 1), nodeStack.Push(n), context);
+ last = n;
+ }
+ return null;
+ }
+ else
+ {
+ // just get one node
+ ConfigNode n = FindConfigNodeIn(nodeStack.value, nodeType, nodeName, index);
+ if (n != null)
+ return RecurseVariableSearch(path.Substring(nextSep + 1), nodeStack.Push(n), context);
+ return null;
+ }
+ }
+
+ // Value search
+
+ Match match = parseVarKey.Match(path);
+ if (!match.Success)
+ {
+ context.logger.Warning("Cannot parse variable search command: " + path);
+ return null;
+ }
+
+ string valName = match.Groups[1].Value;
+
+ int idx = 0;
+ if (match.Groups[2].Success)
+ int.TryParse(match.Groups[2].Value, out idx);
+
+ ConfigNode.Value cVal = FindValueIn(nodeStack.value, valName, idx);
+ if (cVal == null)
+ {
+ context.logger.Warning("Cannot find key " + valName + " in " + nodeStack.value.name);
+ return null;
+ }
+
+ if (match.Groups[3].Success)
+ {
+ ConfigNode.Value newVal = new ConfigNode.Value(cVal.name, cVal.value);
+ int.TryParse(match.Groups[3].Value, out int splitIdx);
+
+ char sep = ',';
+ if (match.Groups[4].Success)
+ sep = match.Groups[4].Value[0];
+ string[] split = newVal.value.Split(sep);
+ if (splitIdx < split.Length)
+ newVal.value = split[splitIdx];
+ else
+ newVal.value = "";
+ return newVal;
+ }
+ return cVal;
+ }
+
+ private static string ProcessVariableSearch(string value, NodeStack nodeStack, PatchContext context)
+ {
+ // value = #xxxx$yyyyy$zzzzz$aaaa$bbbb
+ // There is 2 or more '$'
+ if (value.Length > 0 && value[0] == '#' && value.IndexOf('$') != -1 && value.IndexOf('$') != value.LastIndexOf('$'))
+ {
+ //log("variable search input : =\"" + value + "\"");
+ string[] split = value.Split('$');
+
+ if (split.Length % 2 != 1)
+ return null;
+
+ StringBuilder builder = new StringBuilder();
+ builder.Append(split[0].Substring(1));
+
+ for (int i = 1; i < split.Length - 1; i += 2)
+ {
+ ConfigNode.Value result = RecurseVariableSearch(split[i], nodeStack, context);
+ if (result == null || result.value == null)
+ return null;
+ builder.Append(result.value);
+ builder.Append(split[i + 1]);
+ }
+ value = builder.ToString();
+ //log("variable search output : =\"" + value + "\"");
+ }
+ return value;
+ }
+
+ private static string FindAndReplaceValue(
+ ConfigNode mod,
+ ref string valName,
+ string value,
+ ConfigNode newNode,
+ Operator op,
+ int index,
+ out ConfigNode.Value origVal,
+ PatchContext context,
+ bool hasPosIndex = false,
+ int posIndex = 0,
+ bool hasPosStar = false,
+ char seperator = ',')
+ {
+ origVal = FindValueIn(newNode, valName, index);
+ if (origVal == null)
+ return null;
+ string oValue = origVal.value;
+
+ string[] strArray = new string[] { oValue };
+ if (hasPosIndex)
+ {
+ strArray = oValue.Split(new char[] { seperator }, StringSplitOptions.RemoveEmptyEntries);
+ if (posIndex >= strArray.Length)
+ {
+ context.progress.Error(context.patchUrl, "Invalid Vector Index!");
+ return null;
+ }
+ }
+ string backupValue = value;
+ while (posIndex < strArray.Length)
+ {
+ value = backupValue;
+ oValue = strArray[posIndex];
+ if (op != Operator.Assign)
+ {
+ if (op == Operator.RegexReplace)
+ {
+ try
+ {
+ string[] split = value.Split(value[0]);
+
+ Regex replace = regexCache.Fetch(split[1], delegate
+ {
+ return new Regex(split[1]);
+ });
+
+ value = replace.Replace(oValue, split[2]);
+ }
+ catch (Exception ex)
+ {
+ context.progress.Exception(context.patchUrl, "Error - Failed to do a regexp replacement: " + mod.name + " : original value=\"" + oValue +
+ "\" regexp=\"" + value +
+ "\" \nNote - to use regexp, the first char is used to subdivide the string (much like sed)", ex);
+ return null;
+ }
+ }
+ else if (double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double s)
+ && double.TryParse(oValue, NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double os))
+ {
+ switch (op)
+ {
+ case Operator.Multiply:
+ value = (os * s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Divide:
+ value = (os / s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Add:
+ value = (os + s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Subtract:
+ value = (os - s).ToString(CultureInfo.InvariantCulture);
+ break;
+
+ case Operator.Exponentiate:
+ value = Math.Pow(os, s).ToString(CultureInfo.InvariantCulture);
+ break;
+ }
+ }
+ else
+ {
+ context.progress.Error(context.patchUrl, "Error - Failed to do a maths replacement: " + mod.name + " : original value=\"" + oValue +
+ "\" operator=" + op + " mod value=\"" + value + "\"");
+ return null;
+ }
+ }
+ strArray[posIndex] = value;
+ if (hasPosStar) posIndex++;
+ else break;
+ }
+ value = String.Join(new string(seperator, 1), strArray);
+ return value;
+ }
+
+ #endregion Applying Patches
+
+ #region Condition checking
+
+ // Split condiction while not getting lost in embeded brackets
+ public static List SplitConstraints(string condition)
+ {
+ condition = condition.RemoveWS() + ",";
+ List conditions = new List();
+ int start = 0;
+ int level = 0;
+ for (int end = 0; end < condition.Length; end++)
+ {
+ if ((condition[end] == ',' || condition[end] == '&') && level == 0)
+ {
+ conditions.Add(condition.Substring(start, end - start));
+ start = end + 1;
+ }
+ else if (condition[end] == '[')
+ level++;
+ else if (condition[end] == ']')
+ level--;
+ }
+ return conditions;
+ }
+
+ static readonly char[] contraintSeparators = { '[', ']' };
+
+ public static bool CheckConstraints(ConfigNode node, string constraints)
+ {
+ constraints = constraints.RemoveWS();
+
+ if (constraints.Length == 0)
+ return true;
+
+ List constraintList = SplitConstraints(constraints);
+
+ if (constraintList.Count == 1)
+ {
+ constraints = constraintList[0];
+
+ string remainingConstraints = "";
+ if (constraints.Contains(":HAS[", out int hasStart))
+ {
+ hasStart += 5;
+ remainingConstraints = constraints.Substring(hasStart, constraintList[0].LastIndexOf(']') - hasStart);
+ constraints = constraints.Substring(0, hasStart - 5);
+ }
+
+ string[] splits = constraints.Split(contraintSeparators, 3);
+ string type = splits[0].Substring(1);
+ string name = splits.Length > 1 ? splits[1] : null;
+
+ switch (constraints[0])
+ {
+ case '@':
+ case '!':
+
+ // @MODULE[ModuleAlternator] or !MODULE[ModuleAlternator]
+ bool not = (constraints[0] == '!');
+
+ bool any = false;
+ int index = 0;
+ ConfigNode last = null;
+ while (true)
+ {
+ ConfigNode subNode = FindConfigNodeIn(node, type, name, index++);
+ if (subNode == last || subNode == null)
+ break;
+ any = any || CheckConstraints(subNode, remainingConstraints);
+ last = subNode;
+ }
+ if (last != null)
+ {
+ //print("CheckConstraints: " + constraints + " " + (not ^ any));
+ return not ^ any;
+ }
+ //print("CheckConstraints: " + constraints + " " + (not ^ false));
+ return not ^ false;
+
+ case '#':
+
+ // #module[Winglet]
+ if (node.HasValue(type) && WildcardMatchValues(node, type, name))
+ {
+ bool ret2 = CheckConstraints(node, remainingConstraints);
+ //print("CheckConstraints: " + constraints + " " + ret2);
+ return ret2;
+ }
+ //print("CheckConstraints: " + constraints + " false");
+ return false;
+
+ case '~':
+
+ // ~breakingForce[] breakingForce is not present
+ // or: ~breakingForce[100] will be true if it's present but not 100, too.
+ if (name == "" && node.HasValue(type))
+ {
+ //print("CheckConstraints: " + constraints + " false");
+ return false;
+ }
+ if (name != "" && WildcardMatchValues(node, type, name))
+ {
+ //print("CheckConstraints: " + constraints + " false");
+ return false;
+ }
+ bool ret = CheckConstraints(node, remainingConstraints);
+ //print("CheckConstraints: " + constraints + " " + ret);
+ return ret;
+
+ default:
+ //print("CheckConstraints: " + constraints + " false");
+ return false;
+ }
+ }
+
+ bool ret3 = true;
+ foreach (string constraint in constraintList)
+ {
+ ret3 = ret3 && CheckConstraints(node, constraint);
+ }
+ //print("CheckConstraints: " + constraints + " " + ret3);
+ return ret3;
+ }
+
+ public static bool WildcardMatchValues(ConfigNode node, string type, string value)
+ {
+ double val = 0;
+ bool compare = value != null && value.Length > 1 && (value[0] == '<' || value[0] == '>');
+ compare = compare && double.TryParse(value.Substring(1), NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out val);
+
+ string[] values = node.GetValues(type);
+ for (int i = 0; i < values.Length; i++)
+ {
+ if (!compare && WildcardMatch(values[i], value))
+ return true;
+
+ if (compare && double.TryParse(values[i], NumberStyles.Float, CultureInfo.InvariantCulture.NumberFormat, out double val2)
+ && ((value[0] == '<' && val2 < val) || (value[0] == '>' && val2 > val)))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static bool WildcardMatch(string s, string wildcard)
+ {
+ if (wildcard == null)
+ return true;
+ string pattern = "^" + Regex.Escape(wildcard).Replace(@"\*", ".*").Replace(@"\?", ".") + "$";
+
+ Regex regex = regexCache.Fetch(pattern, delegate
+ {
+ return new Regex(pattern);
+ });
+ return regex.IsMatch(s);
+ }
+
+ #endregion Condition checking
+
+ #region Config Node Utilities
+
+ private static void InsertNode(ConfigNode newNode, ConfigNode subMod, int index)
+ {
+ string modName = subMod.name;
+
+ ConfigNode[] oldValues = newNode.GetNodes(modName);
+ if (index < oldValues.Length)
+ {
+ newNode.RemoveNodes(modName);
+ int i = 0;
+ for (; i < index; ++i)
+ newNode.AddNode(oldValues[i]);
+ newNode.AddNode(subMod);
+ for (; i < oldValues.Length; ++i)
+ newNode.AddNode(oldValues[i]);
+ }
+ else
+ newNode.AddNode(subMod);
+ }
+
+ private static void InsertValue(ConfigNode newNode, int index, string name, string value)
+ {
+ string[] oldValues = newNode.GetValues(name);
+ if (index < oldValues.Length)
+ {
+ newNode.RemoveValues(name);
+ int i = 0;
+ for (; i < index; ++i)
+ newNode.AddValueSafe(name, oldValues[i]);
+ newNode.AddValueSafe(name, value);
+ for (; i < oldValues.Length; ++i)
+ newNode.AddValueSafe(name, oldValues[i]);
+ return;
+ }
+ newNode.AddValueSafe(name, value);
+ }
+
+ //FindConfigNodeIn finds and returns a ConfigNode in src of type nodeType.
+ //If nodeName is not null, it will only find a node of type nodeType with the value name=nodeName.
+ //If nodeTag is not null, it will only find a node of type nodeType with the value name=nodeName and tag=nodeTag.
+ public static ConfigNode FindConfigNodeIn(
+ ConfigNode src,
+ string nodeType,
+ string nodeName = null,
+ int index = 0)
+ {
+ List nodes = new List();
+ int c = src.nodes.Count;
+ for(int i = 0; i < c; ++i)
+ {
+ if (WildcardMatch(src.nodes[i].name, nodeType))
+ nodes.Add(src.nodes[i]);
+ }
+ int nodeCount = nodes.Count;
+ if (nodeCount == 0)
+ return null;
+ if (nodeName == null)
+ {
+ if (index >= 0)
+ return nodes[Math.Min(index, nodeCount - 1)];
+ return nodes[Math.Max(0, nodeCount + index)];
+ }
+ ConfigNode last = null;
+ if (index >= 0)
+ {
+ for (int i = 0; i < nodeCount; ++i)
+ {
+ if (nodes[i].HasValue("name") && WildcardMatch(nodes[i].GetValue("name"), nodeName))
+ {
+ last = nodes[i];
+ if (--index < 0)
+ return last;
+ }
+ }
+ return last;
+ }
+ for (int i = nodeCount - 1; i >= 0; --i)
+ {
+ if (nodes[i].HasValue("name") && WildcardMatch(nodes[i].GetValue("name"), nodeName))
+ {
+ last = nodes[i];
+ if (++index >= 0)
+ return last;
+ }
+ }
+ return last;
+ }
+
+ private static ConfigNode.Value FindValueIn(ConfigNode newNode, string valName, int index)
+ {
+ ConfigNode.Value v = null;
+ for (int i = 0; i < newNode.values.Count; ++i)
+ {
+ if (WildcardMatch(newNode.values[i].name, valName))
+ {
+ v = newNode.values[i];
+ if (--index < 0)
+ return v;
+ }
+ }
+ return v;
+ }
+
+ #endregion Config Node Utilities
+ }
+}
diff --git a/ModuleManager/MMPatchRunner.cs b/ModuleManager/MMPatchRunner.cs
new file mode 100644
index 00000000..6c3d63a0
--- /dev/null
+++ b/ModuleManager/MMPatchRunner.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using ModuleManager.Collections;
+using ModuleManager.Extensions;
+using ModuleManager.Logging;
+using ModuleManager.Threading;
+
+using static ModuleManager.FilePathRepository;
+
+namespace ModuleManager
+{
+ public class MMPatchRunner
+ {
+ private readonly IBasicLogger kspLogger;
+
+ public string Status { get; private set; } = "";
+ public string Errors { get; private set; } = "";
+
+ public MMPatchRunner(IBasicLogger kspLogger)
+ {
+ this.kspLogger = kspLogger ?? throw new ArgumentNullException(nameof(kspLogger));
+ }
+
+ public IEnumerator Run()
+ {
+ PostPatchLoader.Instance.databaseConfigs = null;
+
+ if (!Directory.Exists(logsDirPath)) Directory.CreateDirectory(logsDirPath);
+
+ kspLogger.Info("Patching started on a new thread, all output will be directed to " + logPath);
+
+ MessageQueue mmLogQueue = new MessageQueue();
+ QueueLogRunner logRunner = new QueueLogRunner(mmLogQueue);
+ ITaskStatus loggingThreadStatus = BackgroundTask.Start(delegate
+ {
+ using StreamLogger streamLogger = new StreamLogger(new FileStream(logPath, FileMode.Create));
+ streamLogger.Info("Log started at " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
+ logRunner.Run(streamLogger);
+ streamLogger.Info("Done!");
+ });
+
+ // Wait for game database to be initialized for the 2nd time and wait for any plugins to initialize
+ yield return null;
+ yield return null;
+
+ IBasicLogger mmLogger = new QueueLogger(mmLogQueue);
+
+ IEnumerable modsAddedByAssemblies = ModListGenerator.GetAdditionalModsFromStaticMethods(mmLogger);
+
+ IEnumerable databaseConfigs = null;
+
+ MMPatchLoader patchLoader = new MMPatchLoader(modsAddedByAssemblies, mmLogger);
+
+ ITaskStatus patchingThreadStatus = BackgroundTask.Start(delegate
+ {
+ databaseConfigs = patchLoader.Run();
+ });
+
+ while(true)
+ {
+ yield return null;
+
+ if (!patchingThreadStatus.IsRunning)
+ logRunner.RequestStop();
+
+ Status = patchLoader.status;
+ Errors = patchLoader.errors;
+
+ if (!patchingThreadStatus.IsRunning && !loggingThreadStatus.IsRunning) break;
+ }
+
+ if (patchingThreadStatus.IsExitedWithError)
+ {
+ kspLogger.Exception("The patching thread threw an exception", patchingThreadStatus.Exception);
+ FatalErrorHandler.HandleFatalError("The patching thread threw an exception");
+ yield break;
+ }
+
+ if (loggingThreadStatus.IsExitedWithError)
+ {
+ kspLogger.Exception("The logging thread threw an exception", loggingThreadStatus.Exception);
+ FatalErrorHandler.HandleFatalError("The logging thread threw an exception");
+ yield break;
+ }
+
+ if (databaseConfigs == null)
+ {
+ kspLogger.Error("The patcher returned a null collection of configs");
+ FatalErrorHandler.HandleFatalError("The patcher returned a null collection of configs");
+ yield break;
+ }
+
+ PostPatchLoader.Instance.databaseConfigs = databaseConfigs;
+ }
+ }
+}
diff --git a/ModuleManager/ModListGenerator.cs b/ModuleManager/ModListGenerator.cs
new file mode 100644
index 00000000..5d017232
--- /dev/null
+++ b/ModuleManager/ModListGenerator.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Diagnostics;
+using System.Reflection;
+using UnityEngine;
+using ModuleManager.Extensions;
+using ModuleManager.Logging;
+using ModuleManager.Utils;
+using ModuleManager.Progress;
+
+namespace ModuleManager
+{
+ public static class ModListGenerator
+ {
+ public static IEnumerable GenerateModList(IEnumerable modsAddedByAssemblies, IPatchProgress progress, IBasicLogger logger)
+ {
+ #region List of mods
+
+ //string envInfo = "ModuleManager env info\n";
+ //envInfo += " " + Environment.OSVersion.Platform + " " + ModuleManager.intPtr.ToInt64().ToString("X16") + "\n";
+ //envInfo += " " + Convert.ToString(ModuleManager.intPtr.ToInt64(), 2) + " " + Convert.ToString(ModuleManager.intPtr.ToInt64() >> 63, 2) + "\n";
+ //string gamePath = Environment.GetCommandLineArgs()[0];
+ //envInfo += " Args: " + gamePath.Split(Path.DirectorySeparatorChar).Last() + " " + string.Join(" ", Environment.GetCommandLineArgs().Skip(1).ToArray()) + "\n";
+ //envInfo += " Executable SHA256 " + FileSHA(gamePath);
+ //
+ //log(envInfo);
+
+ List mods = new List();
+
+ StringBuilder modListInfo = new StringBuilder();
+
+ modListInfo.Append("compiling list of loaded mods...\nMod DLLs found:\n");
+
+ string format = " {0,-40}{1,-25}{2,-25}{3,-25}{4}\n";
+
+ modListInfo.AppendFormat(
+ format,
+ "Name",
+ "Assembly Version",
+ "Assembly File Version",
+ "KSPAssembly Version",
+ "SHA256"
+ );
+
+ modListInfo.Append('\n');
+
+ foreach (AssemblyLoader.LoadedAssembly mod in AssemblyLoader.loadedAssemblies)
+ {
+
+ if (string.IsNullOrEmpty(mod.assembly.Location)) //Diazo Edit for xEvilReeperx AssemblyReloader mod
+ continue;
+
+ FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(mod.assembly.Location);
+
+ AssemblyName assemblyName = mod.assembly.GetName();
+
+ string kspAssemblyVersion;
+ if (mod.versionMajor == 0 && mod.versionMinor == 0)
+ kspAssemblyVersion = "";
+ else
+ kspAssemblyVersion = mod.versionMajor + "." + mod.versionMinor;
+
+ string fileSha = "";
+ try
+ {
+ fileSha = FileUtils.FileSHA(mod.assembly.Location);
+ }
+ catch (Exception e)
+ {
+ progress.Exception("Exception while generating SHA for assembly " + assemblyName.Name, e);
+ }
+
+ modListInfo.AppendFormat(
+ format,
+ assemblyName.Name,
+ assemblyName.Version,
+ fileVersionInfo.FileVersion,
+ kspAssemblyVersion,
+ fileSha
+ );
+
+ // modlist += String.Format(" {0,-50} SHA256 {1}\n", modInfo, FileSHA(mod.assembly.Location));
+
+ if (!mods.Contains(assemblyName.Name, StringComparer.OrdinalIgnoreCase))
+ mods.Add(assemblyName.Name);
+ }
+
+ modListInfo.Append("Non-DLL mods added (:FOR[xxx]):\n");
+ foreach (UrlDir.UrlConfig cfgmod in GameDatabase.Instance.root.AllConfigs)
+ {
+ if (CommandParser.Parse(cfgmod.type, out string name) != Command.Insert)
+ {
+ if (name.Contains(":FOR["))
+ {
+ name = name.RemoveWS();
+
+ // check for FOR[] blocks that don't match loaded DLLs and add them to the pass list
+ try
+ {
+ string dependency = name.Substring(name.IndexOf(":FOR[") + 5);
+ dependency = dependency.Substring(0, dependency.IndexOf(']'));
+ if (!mods.Contains(dependency, StringComparer.OrdinalIgnoreCase))
+ {
+ // found one, now add it to the list.
+ mods.Add(dependency);
+ modListInfo.AppendFormat(" {0}\n", dependency);
+ }
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ progress.Error(cfgmod, "Skipping :FOR init for line " + name +
+ ". The line most likely contains a space that should be removed");
+ }
+ }
+ }
+ }
+ modListInfo.Append("Mods by directory (sub directories of GameData):\n");
+ UrlDir gameData = GameDatabase.Instance.root.children.First(dir => dir.type == UrlDir.DirectoryType.GameData);
+ foreach (UrlDir subDir in gameData.children)
+ {
+ string cleanName = subDir.name.RemoveWS();
+ if (!mods.Contains(cleanName, StringComparer.OrdinalIgnoreCase))
+ {
+ mods.Add(cleanName);
+ modListInfo.AppendFormat(" {0}\n", cleanName);
+ }
+ }
+
+ modListInfo.Append("Mods added by assemblies:\n");
+ foreach (ModAddedByAssembly mod in modsAddedByAssemblies)
+ {
+ if (!mods.Contains(mod.modName, StringComparer.OrdinalIgnoreCase))
+ {
+ mods.Add(mod.modName);
+ modListInfo.AppendFormat(" {0}\n", mod);
+ }
+ }
+
+ logger.Info(modListInfo.ToString());
+
+ mods.Sort();
+
+ #endregion List of mods
+
+ return mods;
+ }
+
+ public class ModAddedByAssembly
+ {
+ public readonly string modName;
+ public readonly string assemblyName;
+
+ public ModAddedByAssembly(string modName, string assemblyName)
+ {
+ this.modName = modName ?? throw new ArgumentNullException(nameof(modName));
+ this.assemblyName = assemblyName ?? throw new ArgumentNullException(nameof(assemblyName));
+ }
+
+ public override string ToString()
+ {
+ return $"{modName} (added by {assemblyName})";
+ }
+ }
+
+ public static IEnumerable GetAdditionalModsFromStaticMethods(IBasicLogger logger)
+ {
+ List result = new List();
+ foreach (Assembly ass in AppDomain.CurrentDomain.GetAssemblies())
+ {
+ try
+ {
+ foreach (Type type in ass.GetTypes())
+ {
+ MethodInfo method = type.GetMethod("ModuleManagerAddToModList", BindingFlags.Public | BindingFlags.Static);
+
+ if (method != null && method.GetParameters().Length == 0 && typeof(IEnumerable).IsAssignableFrom(method.ReturnType))
+ {
+ string methodName = $"{ass.GetName().Name}.{type.Name}.{method.Name}()";
+ try
+ {
+ logger.Info("Calling " + methodName);
+ IEnumerable modsToAdd = (IEnumerable)method.Invoke(null, null);
+
+ if (modsToAdd == null)
+ {
+ logger.Error("ModuleManagerAddToModList returned null: " + methodName);
+ continue;
+ }
+
+ foreach (string mod in modsToAdd)
+ {
+ result.Add(new ModAddedByAssembly(mod, ass.GetName().Name));
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Exception while calling " + methodName, e);
+ }
+ }
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Add to mod list threw an exception in loading " + ass.FullName, e);
+ }
+ }
+
+ foreach (MonoBehaviour obj in UnityEngine.Object.FindObjectsOfType())
+ {
+ MethodInfo method = obj.GetType().GetMethod("ModuleManagerAddToModList", BindingFlags.Public | BindingFlags.Instance);
+
+ if (method != null && method.GetParameters().Length == 0 && typeof(IEnumerable).IsAssignableFrom(method.ReturnType))
+ {
+ string methodName = $"{obj.GetType().Name}.{method.Name}()";
+ try
+ {
+ logger.Info("Calling " + methodName);
+ IEnumerable modsToAdd = (IEnumerable)method.Invoke(obj, null);
+
+ if (modsToAdd == null)
+ {
+ logger.Error("ModuleManagerAddToModList returned null: " + methodName);
+ continue;
+ }
+
+ foreach (string mod in modsToAdd)
+ {
+ result.Add(new ModAddedByAssembly(mod, obj.GetType().Assembly.GetName().Name));
+ }
+ }
+ catch (Exception e)
+ {
+ logger.Exception("Exception while calling " + methodName, e);
+ }
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/ModuleManager/ModuleManager.cs b/ModuleManager/ModuleManager.cs
new file mode 100644
index 00000000..dd5e04e5
--- /dev/null
+++ b/ModuleManager/ModuleManager.cs
@@ -0,0 +1,593 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using TMPro;
+using UnityEngine;
+using Debug = UnityEngine.Debug;
+using ModuleManager.Cats;
+using ModuleManager.Extensions;
+using ModuleManager.Logging;
+using ModuleManager.UnityLogHandle;
+
+namespace ModuleManager
+{
+ [KSPAddon(KSPAddon.Startup.Instantly, true)]
+ public class ModuleManager : MonoBehaviour
+ {
+ #region state
+
+ private bool inRnDCenter;
+
+ public bool showUI = false;
+ private float textPos = 0;
+
+ //private Texture2D tex;
+ //private Texture2D tex2;
+
+ private bool nyan = false;
+ private bool nCats = false;
+ public static bool dumpPostPatch = false;
+ public static bool DontCopyLogs { get; private set; } = false;
+
+ private PopupDialog menu;
+
+ private MMPatchRunner patchRunner;
+
+ #endregion state
+
+ private static bool loadedInScene;
+
+ internal void OnRnDCenterSpawn()
+ {
+ inRnDCenter = true;
+ }
+
+ internal void OnRnDCenterDeSpawn()
+ {
+ inRnDCenter = false;
+ }
+
+ public static void Log(String s)
+ {
+ print("[ModuleManager] " + s);
+ }
+
+ private readonly Stopwatch totalTime = new Stopwatch();
+
+ internal void Awake()
+ {
+ if (LoadingScreen.Instance == null)
+ {
+ Destroy(gameObject);
+ return;
+ }
+
+ // Ensure that only one copy of the service is run per scene change.
+ if (loadedInScene || !ElectionAndCheck())
+ {
+ Assembly currentAssembly = Assembly.GetExecutingAssembly();
+ Log("Multiple copies of current version. Using the first copy. Version: " +
+ currentAssembly.GetName().Version);
+ Destroy(gameObject);
+ return;
+ }
+
+ totalTime.Start();
+
+ Debug.unityLogger.logHandler = new InterceptLogHandler(Debug.unityLogger.logHandler);
+
+ // Allow loading the background in the loading screen
+ Application.runInBackground = true;
+ QualitySettings.vSyncCount = 0;
+ Application.targetFrameRate = -1;
+
+ // More cool loading screen. Less 4 stoke logo.
+ for (int i = 0; i < LoadingScreen.Instance.Screens.Count; i++)
+ {
+ var state = LoadingScreen.Instance.Screens[i];
+ state.fadeInTime = i < 3 ? 0.1f : 1;
+ state.displayTime = i < 3 ? 1 : 3;
+ state.fadeOutTime = i < 3 ? 0.1f : 1;
+ }
+
+ TextMeshProUGUI[] texts = LoadingScreen.Instance.gameObject.GetComponentsInChildren();
+ foreach (var text in texts)
+ {
+ textPos = Mathf.Min(textPos, text.rectTransform.localPosition.y);
+ }
+ DontDestroyOnLoad(gameObject);
+
+ // Subscribe to the RnD center spawn/deSpawn events
+ GameEvents.onGUIRnDComplexSpawn.Add(OnRnDCenterSpawn);
+ GameEvents.onGUIRnDComplexDespawn.Add(OnRnDCenterDeSpawn);
+
+
+ LoadingScreen screen = FindObjectOfType();
+ if (screen == null)
+ {
+ Log("Can't find LoadingScreen type. Aborting ModuleManager execution");
+ return;
+ }
+ List list = LoadingScreen.Instance.loaders;
+
+ if (list != null)
+ {
+ // So you can insert a LoadingSystem object in this list at any point.
+ // GameDatabase is first in the list, and PartLoader is second
+ // We could insert ModuleManager after GameDatabase to get it to run there
+ // and SaveGameFixer after PartLoader.
+
+ int gameDatabaseIndex = list.FindIndex(s => s is GameDatabase);
+
+ GameObject aGameObject = new GameObject("ModuleManager");
+ DontDestroyOnLoad(aGameObject);
+
+ Log(string.Format("Adding post patch to the loading screen {0}", list.Count));
+ list.Insert(gameDatabaseIndex + 1, aGameObject.AddComponent());
+
+ patchRunner = new MMPatchRunner(new PrefixLogger("ModuleManager", new UnityLogger(Debug.unityLogger)));
+ StartCoroutine(patchRunner.Run());
+
+ // Workaround for 1.6.0 Editor bug after a PartDatabase rebuild.
+ if (Versioning.version_major == 1 && Versioning.version_minor == 6 && Versioning.Revision == 0)
+ {
+ Fix16 fix16 = aGameObject.AddComponent();
+ list.Add(fix16);
+ }
+ }
+
+ bool foolsDay = (DateTime.Now.Month == 4 && DateTime.Now.Day == 1);
+ bool catDay = (DateTime.Now.Month == 2 && DateTime.Now.Day == 22);
+ nyan = foolsDay
+ || Environment.GetCommandLineArgs().Contains("-nyan-nyan");
+
+ nCats = catDay
+ || Environment.GetCommandLineArgs().Contains("-ncats");
+
+ dumpPostPatch = Environment.GetCommandLineArgs().Contains("-mm-dump");
+
+ DontCopyLogs = Environment.GetCommandLineArgs().Contains("-mm-dont-copy-logs");
+
+ loadedInScene = true;
+ }
+
+ private TextMeshProUGUI status;
+ private TextMeshProUGUI errors;
+ private TextMeshProUGUI warning;
+
+ [SuppressMessage("Code Quality", "IDE0051", Justification = "Called by Unity")]
+ private void Start()
+ {
+ if (nCats)
+ CatManager.LaunchCats();
+ else if (nyan)
+ CatManager.LaunchCat();
+
+ Canvas canvas = LoadingScreen.Instance.GetComponentInChildren