diff --git a/Source/ConformalDecals/ConformalDecals.csproj b/Source/ConformalDecals/ConformalDecals.csproj
index 5901447..3b85b28 100644
--- a/Source/ConformalDecals/ConformalDecals.csproj
+++ b/Source/ConformalDecals/ConformalDecals.csproj
@@ -10,7 +10,8 @@
v4.6
512
true
- 7
+ 7.3
+ ConformalDecals
true
@@ -43,6 +44,12 @@
+
+
+
+
+
+
diff --git a/Source/ConformalDecals/Logging.cs b/Source/ConformalDecals/Logging.cs
new file mode 100644
index 0000000..ccebab3
--- /dev/null
+++ b/Source/ConformalDecals/Logging.cs
@@ -0,0 +1,23 @@
+using System;
+using UnityEngine;
+
+namespace ConformalDecals {
+ public static class Logging {
+ public static void Log(this PartModule module, string message) => Debug.Log(FormatMessage(module, message));
+
+ public static void LogWarning(this PartModule module, string message) =>
+ Debug.LogWarning(FormatMessage(module, message));
+
+ public static void LogError(this PartModule module, string message) =>
+ Debug.LogError(FormatMessage(module, message));
+
+ public static void LogException(this PartModule module, string message, Exception exception) =>
+ Debug.LogException(new Exception(FormatMessage(module, message), exception));
+
+
+ private static string FormatMessage(PartModule module, string message) =>
+ $"[{GetPartName(module.part)} {module.GetType()}] {message}";
+
+ private static string GetPartName(Part part) => part.partInfo?.name ?? part.name;
+ }
+}
\ No newline at end of file
diff --git a/Source/ConformalDecals/MaterialModifiers/ColorPropertyMaterialModifier.cs b/Source/ConformalDecals/MaterialModifiers/ColorPropertyMaterialModifier.cs
new file mode 100644
index 0000000..6cda4e2
--- /dev/null
+++ b/Source/ConformalDecals/MaterialModifiers/ColorPropertyMaterialModifier.cs
@@ -0,0 +1,16 @@
+using System;
+using UnityEngine;
+
+namespace ConformalDecals.MaterialModifiers {
+ public class ColorPropertyMaterialModifier : MaterialModifier {
+ private readonly Color _color;
+
+ public ColorPropertyMaterialModifier(ConfigNode node) : base(node) {
+ _color = ParsePropertyColor(node, "color", false);
+ }
+
+ public override void Modify(Material material) {
+ material.SetColor(_propertyID, _color);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ConformalDecals/MaterialModifiers/FloatPropertyMaterialModifier.cs b/Source/ConformalDecals/MaterialModifiers/FloatPropertyMaterialModifier.cs
new file mode 100644
index 0000000..bf9ea66
--- /dev/null
+++ b/Source/ConformalDecals/MaterialModifiers/FloatPropertyMaterialModifier.cs
@@ -0,0 +1,16 @@
+using System;
+using UnityEngine;
+
+namespace ConformalDecals.MaterialModifiers {
+ public class FloatPropertyMaterialModifier : MaterialModifier {
+ private readonly float _value;
+
+ public FloatPropertyMaterialModifier(ConfigNode node) : base(node) {
+ _value = ParsePropertyFloat(node, "value", false);
+ }
+
+ public override void Modify(Material material) {
+ material.SetFloat(_propertyID, _value);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ConformalDecals/MaterialModifiers/MaterialModifier.cs b/Source/ConformalDecals/MaterialModifiers/MaterialModifier.cs
new file mode 100644
index 0000000..6cdc711
--- /dev/null
+++ b/Source/ConformalDecals/MaterialModifiers/MaterialModifier.cs
@@ -0,0 +1,78 @@
+using System;
+using UnityEngine;
+
+namespace ConformalDecals.MaterialModifiers {
+ public abstract class MaterialModifier {
+ public string Name { get; }
+
+ protected readonly int _propertyID;
+
+
+ protected MaterialModifier(ConfigNode node) {
+ Name = node.GetValue("name");
+
+ if (Name == null)
+ throw new FormatException("name not found, cannot create material modifier");
+
+ if (Name == string.Empty)
+ throw new FormatException("name is empty, cannot create material modifier");
+
+ _propertyID = Shader.PropertyToID(Name);
+ }
+
+ public abstract void Modify(Material material);
+
+ private delegate bool TryParseDelegate(string valueString, out T value);
+
+ protected bool ParsePropertyBool(ConfigNode node, string valueName, bool isOptional = false, bool defaultValue = false) {
+ return ParseProperty(node, valueName, bool.TryParse, isOptional, defaultValue);
+ }
+
+ protected float ParsePropertyFloat(ConfigNode node, string valueName, bool isOptional = false, float defaultValue = 0.0f) {
+ return ParseProperty(node, valueName, float.TryParse, isOptional, defaultValue);
+ }
+
+ protected int ParsePropertyInt(ConfigNode node, string valueName, bool isOptional = false, int defaultValue = 0) {
+ return ParseProperty(node, valueName, int.TryParse, isOptional, defaultValue);
+ }
+
+ protected Color ParsePropertyColor(ConfigNode node, string valueName, bool isOptional = false, Color defaultValue = default(Color)) {
+ return ParseProperty(node, valueName, ParseExtensions.TryParseColor, isOptional, defaultValue);
+ }
+
+ protected Rect ParsePropertyRect(ConfigNode node, string valueName, bool isOptional = false, Rect defaultValue = default(Rect)) {
+ return ParseProperty(node, valueName, ParseExtensions.TryParseRect, isOptional, defaultValue);
+ }
+
+ protected Vector2 ParsePropertyVector2(ConfigNode node, string valueName, bool isOptional = false, Vector2 defaultValue = default(Vector2)) {
+ return ParseProperty(node, valueName, ParseExtensions.TryParseVector2, isOptional, defaultValue);
+ }
+
+ private T ParseProperty(ConfigNode node, string valueName, TryParseDelegate tryParse, bool isOptional = false, T defaultValue = default(T)) {
+ string valueString = node.GetValue(valueName);
+
+ if (isOptional) {
+ if (string.IsNullOrEmpty(valueString)) return defaultValue;
+ }
+ else {
+ if (valueString == null)
+ throw new FormatException($"Missing {typeof(T)} value {valueName} in property '{Name}'");
+
+ if (valueString == string.Empty)
+ throw new FormatException($"Empty {typeof(T)} value {valueName} in property '{Name}'");
+ }
+
+ if (tryParse(valueString, out var value)) {
+ return value;
+ }
+
+ if (isOptional) {
+ return defaultValue;
+ }
+
+ else {
+ throw new FormatException($"Improperly formatted {typeof(T)} value {valueName} in property '{Name}'");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ConformalDecals/MaterialModifiers/TexturePropertyMaterialModifier.cs b/Source/ConformalDecals/MaterialModifiers/TexturePropertyMaterialModifier.cs
new file mode 100644
index 0000000..4fff832
--- /dev/null
+++ b/Source/ConformalDecals/MaterialModifiers/TexturePropertyMaterialModifier.cs
@@ -0,0 +1,55 @@
+using System;
+using UnityEngine;
+
+namespace ConformalDecals.MaterialModifiers {
+ public class TexturePropertyMaterialModifier : MaterialModifier {
+ private readonly string _textureURL;
+ private readonly Texture2D _texture;
+
+ private Vector2 _textureOffset;
+ private Vector2 _textureScale;
+
+ public bool IsNormal { get; }
+ public bool IsMain { get; }
+ public bool AutoScale { get; }
+
+ public Rect TileRect { get; }
+
+ public TexturePropertyMaterialModifier(ConfigNode node) : base(node) {
+ _textureURL = node.GetValue("textureURL");
+
+ var textureInfo = GameDatabase.Instance.GetTextureInfo(_textureURL);
+
+ if (textureInfo == null)
+ throw new Exception($"Cannot find texture: '{_textureURL}'");
+
+ _texture = IsNormal ? textureInfo.normalMap : textureInfo.texture;
+
+ if (_texture == null)
+ throw new Exception($"Cannot get texture from texture info '{_textureURL}' isNormalMap = {IsNormal}");
+
+ IsNormal = ParsePropertyBool(node, "isNormalMap", true, false);
+ IsMain = ParsePropertyBool(node, "isMain", true, false);
+ AutoScale = ParsePropertyBool(node, "autoScale", true, false);
+ TileRect = ParsePropertyRect(node, "tileRect", true, new Rect(0, 0, _texture.width, _texture.height));
+
+ _textureScale.x = TileRect.width / _texture.width;
+ _textureScale.y = TileRect.height / _texture.height;
+
+ _textureOffset.x = TileRect.x / _texture.width;
+ _textureOffset.y = TileRect.y / _texture.height;
+ }
+
+ public override void Modify(Material material) {
+ material.SetTexture(_propertyID, _texture);
+ material.SetTextureOffset(_propertyID, _textureOffset);
+ material.SetTextureScale(_propertyID, _textureScale);
+ }
+
+ public void UpdateScale(Material material, Vector2 scale) {
+ if (AutoScale) {
+ material.SetTextureScale(_propertyID, new Vector2(_textureScale.x * scale.x, _textureScale.y * scale.y));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Source/ConformalDecals/ModuleConformalDecal.cs b/Source/ConformalDecals/ModuleConformalDecal.cs
new file mode 100644
index 0000000..1904e83
--- /dev/null
+++ b/Source/ConformalDecals/ModuleConformalDecal.cs
@@ -0,0 +1,5 @@
+namespace ConformalDecals {
+ public class ModuleConformalDecal : PartModule {
+ [KSPField] public string decalPreviewTransform = "";
+ }
+}
\ No newline at end of file