This commit is contained in:
2026-04-25 23:37:10 +02:00
commit 19d6bd934a
476 changed files with 9198 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c535423420394136821e97a0798f170b
timeCreated: 1759333996
@@ -0,0 +1,272 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using RPGCoreCommon.DynamicValues;
using RPGCoreCommon.Helpers;
using RPGCoreCommon.Helpers.CustomTypes;
using RPGCoreCommon.Helpers.Exceptions;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Object = UnityEngine.Object;
namespace RPGCoreCommon.DynamicValues.Editor
{
[Serializable]
[CustomPropertyDrawer(typeof(DynamicValue))]
public class DynamicValueDrawer : PropertyDrawer
{
[SerializeField] private VisualTreeAsset _visualTreeAsset;
private SerializedProperty _property;
private SerializedProperty _propertySourceDict;
private SerializedProperty _propertyTypeDict;
private SerializedProperty _propertyFormula;
private DynamicValueAttribute _attribute;
// Elements
private VisualElement _background;
private Foldout _foldoutLabel;
private TextField _formulaEdit;
private PropertyField _objectDictField;
private PropertyField _typeDictField;
private ScrollView _markerList;
private Label _formulaPreview;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
_property = property;
_propertySourceDict = property.FindPropertyRelative(nameof(DynamicValue._serializedSources));
_propertyTypeDict = property.FindPropertyRelative(nameof(DynamicValue._serializedTypes));
_propertyFormula = property.FindPropertyRelative(nameof(DynamicValue._serializedFormula));
_attribute = fieldInfo.GetCustomAttribute<DynamicValueAttribute>() ?? new DynamicValueAttribute();
var rootVisualElement = _visualTreeAsset.CloneTree();
// FIND ELEMENTS
_background = rootVisualElement.Q("dv-background");
_foldoutLabel = rootVisualElement.Q<Foldout>("dv-foldout-label");
_foldoutLabel.text = property.displayName;
_foldoutLabel.Q<Toggle>().RegisterValueChangedCallback(ev => TogglePreview(!ev.newValue));
_formulaEdit = rootVisualElement.Q<TextField>("dv-formula-edit");
_formulaEdit.BindProperty(_propertyFormula);
_objectDictField = rootVisualElement.Q<PropertyField>("dv-sources-objects");
_objectDictField.label = "";
_objectDictField.BindProperty(_propertySourceDict);
_objectDictField.TrackPropertyValue(_propertySourceDict, _ => OnSourceChanged());
_typeDictField = rootVisualElement.Q<PropertyField>("dv-sources-types");
_typeDictField.label = "";
_typeDictField.BindProperty(_propertyTypeDict);
_typeDictField.TrackPropertyValue(_propertyTypeDict, _ => OnSourceChanged());
_markerList = rootVisualElement.Q("dv-markers").Q<ScrollView>();
_formulaPreview = rootVisualElement.Q<Label>("dv-formula-preview");
// BACKGROUND HIGHLIGHT
rootVisualElement.RegisterCallback<MouseEnterEvent>(_ => _background.visible = true);
rootVisualElement.RegisterCallback<MouseLeaveEvent>(_ => _background.visible = false);
OnSourceChanged();
// DEFAULT STATE
TogglePreview(!string.IsNullOrEmpty(_propertyFormula.stringValue));
return rootVisualElement;
}
private void TogglePreview(bool show)
{
_formulaPreview.style.display = show ? DisplayStyle.Flex : DisplayStyle.None;
_background.style.display = show ? DisplayStyle.None : DisplayStyle.Flex;
_foldoutLabel.SetValueWithoutNotify(!show);
_formulaPreview.text = _propertyFormula.stringValue;
if (show) ValidateMarkers();
}
private void ValidateMarkers()
{
var sourceList = GetSourceTypes();
var invalidMarkers = new List<string>();
var value = (DynamicValue)_property.boxedValue;
// This is fix - when using SerializedReference via boxedValue it will always return null.
// To prevent this we have to get manually SerializedReference values via SerializedProperty.
// Also, we don't want to update serialized data!
value._serializedTypes.Keys.ToList().ForEach((i, key) => value._serializedTypes[key] =
_propertyTypeDict.FindPropertyRelative("_values").GetArrayElementAtIndex(i).boxedValue as SerializableType);
foreach (var marker in value.GetMarkers())
{
var color = "#23C552";
try
{
var sourceName = sourceList.Keys.FirstOrDefault(sourceName =>
sourceName.Equals(marker.sourceMarker, StringComparison.OrdinalIgnoreCase));
// Check if used marker uses source that is provided via serialization (via object or type)
if (string.IsNullOrEmpty(sourceName))
throw new DynamicValueInvalidMarkerException("SOURCE TYPE DO NOT EXIST");
// This will parse marker and throw exception if something wrong will occur
marker.GetMembers();
}
catch (Exception e)
{
if (e is not DynamicValueInvalidMarkerException and not MemberNotFoundException) throw;
Debug.LogException(e);
color = "#F84F31";
invalidMarkers.Add(marker.wholeMarker);
}
_formulaPreview.text = _formulaPreview.text.Replace(marker.wholeMarker, $"<color={color}>{marker.wholeMarker}</color>");
}
if (invalidMarkers.Any())
{
var errorBox = new HelpBox($"Found invalid markers: <b>{string.Join("</b>, <b>", invalidMarkers)}</b>.", HelpBoxMessageType.Error);
errorBox.FadeOut(30000, 250, true);
errorBox.RemoveOnClick();
_foldoutLabel.InsertBefore(errorBox);
}
}
private void OnSourceChanged()
{
// CREATE SOURCE LIST
switch (_attribute.type)
{
case DynamicValueType.BySerializedSources:
_objectDictField.style.display = DisplayStyle.Flex;
_typeDictField.style.display = DisplayStyle.None;
break;
case DynamicValueType.ByDynamicTypes:
case DynamicValueType.ByStaticTypes:
_typeDictField.SetEnabled(false);
_objectDictField.style.display = DisplayStyle.None;
_typeDictField.style.display = DisplayStyle.Flex;
break;
default:
throw new ArgumentOutOfRangeException();
}
var allSourceTypes = GetSourceTypes();
// REMOVE MARKER LIST - remove changed and deleted ones
_markerList.Children().ToList()
.Where(markerElement => allSourceTypes.GetValueOrDefault(markerElement.name) != (Type)markerElement.userData)
.ForEach(_markerList.Remove);
// CREATE MARKER LIST - add missing ones
allSourceTypes
.Where(pair => _markerList.Children().All(marker => marker.name != pair.Key))
.Select(pair => CreateMarkerListElement(pair.Key, pair.Value))
.ForEach(_markerList.Add);
if (!_foldoutLabel.value) ValidateMarkers();
}
private VisualElement CreateMarkerListElement(string name, Type type, string path = "")
{
var foldout = new Foldout();
foldout.text = $"<b>{name}</b> ({type.Name})";
foldout.value = false;
// Path not available - first level marker - this string represent source
if (string.IsNullOrEmpty(path))
{
foldout.name = name;
foldout.userData = type;
path = name;
}
var copyButton = new Button();
copyButton.text = "Copy";
copyButton.style.width = 50f;
copyButton.style.unityTextAlign = TextAnchor.MiddleCenter;
copyButton.style.paddingRight = 0;
copyButton.style.paddingLeft = 0;
copyButton.style.marginTop = 0;
copyButton.style.marginBottom = 0;
copyButton.clicked += () => EditorGUIUtility.systemCopyBuffer = $"{{{path}}}";
copyButton.clicked += () => copyButton.text = "Copied!";
copyButton.clicked += () => copyButton.schedule.Execute(() => copyButton.text = "Copy").ExecuteLater(3000);
var foldoutText = foldout.Q(className: "unity-foldout__text");
foldoutText.style.flexShrink = 1;
var foldoutInput = foldout.Q(className: "unity-foldout__input");
foldoutInput.style.maxWidth = Length.Percent(100f);
foldoutInput.Add(copyButton);
var membersToDisplay = MarkerParser.GetMembersForType(type, _attribute);
// BACKGROUND HIGHLIGHT
foldoutInput.RegisterCallback<MouseEnterEvent>(_ => foldoutInput.style.backgroundColor = new Color(255,255,255,0.1f));
foldoutInput.RegisterCallback<MouseLeaveEvent>(_ => foldoutInput.style.backgroundColor = StyleKeyword.Null);
// NO CHILDREN - hide extend/collapse arrow
if (!membersToDisplay.Any()) foldout.Q("unity-checkmark").style.visibility = Visibility.Hidden;
// ONLY ONCE - when foldout extended then fill its content
foldout.RegisterCallbackOnce<ChangeEvent<bool>>(_ =>
{
foreach (var member in membersToDisplay)
{
var parameters = MarkerParser.GetSubTypesInProvider(member, out var isProvider, out var isDynamic);
if (isProvider)
{
// This is provider - it can have multiple parameters in addition to this marker
parameters.ForEach(subType =>
{
var subName = $"{member.Name}[" + (isDynamic ? "---" : subType.Name)+ "]";
var element = CreateMarkerListElement(subName, subType, $"{path}.{subName}");
foldout.Add(element);
});
}
else
{
// This is just simple marker
foldout.Add(CreateMarkerListElement($"{member.Name}", member.GetMemberValueType(), $"{path}.{member.Name}"));
}
}
});
// LITTLE FIX - dragger sometimes don't change its height
foldout.RegisterValueChangedCallback(_ => _markerList.Q("unity-dragger").style.height = Length.Auto());
return foldout;
}
private Dictionary<string, Type> GetSourceTypes()
{
switch (_attribute.type)
{
case DynamicValueType.BySerializedSources:
var objKeyProps = _propertySourceDict.FindPropertyRelative("_keys");
var objValueProps = _propertySourceDict.FindPropertyRelative("_values");
return Enumerable.Range(0, objKeyProps.arraySize)
.ToDictionary(
i => objKeyProps.GetArrayElementAtIndex(i).boxedValue as string,
i => objValueProps.GetArrayElementAtIndex(i).boxedValue.GetType()
)
.Where(pair => pair.Value != null)
.ToDictionary(pair => pair.Key, pair => pair.Value);
case DynamicValueType.ByDynamicTypes or DynamicValueType.ByStaticTypes:
var typeKeyProps = _propertyTypeDict.FindPropertyRelative("_keys");
var typeValueProps = _propertyTypeDict.FindPropertyRelative("_values");
return Enumerable.Range(0, typeKeyProps.arraySize)
.ToDictionary(
i => typeKeyProps.GetArrayElementAtIndex(i).boxedValue as string,
i => (typeValueProps.GetArrayElementAtIndex(i).boxedValue as SerializableType)?.type
)
.Where(pair => pair.Value != null)
.ToDictionary(pair => pair.Key, pair => pair.Value);
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 7310d287a63540638415ae227def94b9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences:
- _visualTreeAsset: {fileID: 9197481963319205126, guid: e692746f57132b74aadeafd745f0f16e, type: 3}
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,84 @@
#dv-background {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.04);
margin-left: -12px;
margin-right: -12px;
visibility: hidden;
}
#dv-foldout-label {
}
#dv-foldout-label Foldout > Toggle {
margin-left: 0;
margin-top: 1px;
margin-bottom: 1px;
max-width: 100%;
}
#dv-formula-preview {
padding-top: 1px;
padding-right: 4px;
padding-bottom: 1px;
padding-left: 4px;
border-top-width: 1px;
border-right-width: 1px;
border-bottom-width: 1px;
border-left-width: 1px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
border-left-color: var(--unity-colors-helpbox-border);
border-right-color: var(--unity-colors-helpbox-border);
border-top-color: var(--unity-colors-helpbox-border);
border-bottom-color: var(--unity-colors-helpbox-border);
background-color: var(--unity-colors-helpbox-background);
margin-left: 15px;
min-height: 18px;
}
#dv-formula-edit {
margin-left: 0;
}
#dv-formula-edit > TextInput {
min-height: 45px;
}
#dv-sources {
margin-top: 5px;
padding-right: 3px;
padding-left: 3px;
margin-bottom: 5px;
}
#dv-markers {
margin-top: 5px;
padding-left: 3px;
padding-right: 3px;
margin-bottom: 5px;
}
.border {
border-top-left-radius: 2px;
border-bottom-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom-right-radius: 2px;
border-right-width: 1px;
border-top-width: 1px;
border-bottom-width: 1px;
border-left-width: 1px;
border-left-color: rgb(0, 0, 0);
border-right-color: rgb(0, 0, 0);
border-top-color: rgb(0, 0, 0);
border-bottom-color: rgb(0, 0, 0);
}
.unity-base-field {
margin-right: 0;
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 683dc949e5b79274f8fbc2bc650fd84b
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12385, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
@@ -0,0 +1,17 @@
<ui:UXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" noNamespaceSchemaLocation="../../../../../UIElementsSchema/UIElements.xsd" editor-extension-mode="True">
<Style src="project://database/Assets/TheVVaS/RPGCoreCommon/DynamicValues/Editor/DynamicValueDrawer.uss?fileID=7433441132597879392&amp;guid=683dc949e5b79274f8fbc2bc650fd84b&amp;type=3#DynamicValueDrawer" />
<ui:VisualElement name="dv-root">
<ui:VisualElement name="dv-background" />
<ui:Foldout text="{property_name}" name="dv-foldout-label">
<ui:TextField placeholder-text="Formula here" multiline="true" name="dv-formula-edit" />
<ui:Foldout text="Sources:" name="dv-sources" class="border">
<uie:PropertyField name="dv-sources-objects" />
<uie:PropertyField name="dv-sources-types" />
</ui:Foldout>
<ui:Foldout text="Markers:" name="dv-markers" class="border">
<ui:ScrollView name="dv-markers-list" horizontal-scroller-visibility="Hidden" vertical-scroller-visibility="AlwaysVisible" />
</ui:Foldout>
</ui:Foldout>
<ui:Label text="{preview_formula}" name="dv-formula-preview" />
</ui:VisualElement>
</ui:UXML>
@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: e692746f57132b74aadeafd745f0f16e
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 13804, guid: 0000000000000000e000000000000000, type: 0}
@@ -0,0 +1,20 @@
{
"name": "RPGCoreCommon.DynamicValues.Editor",
"rootNamespace": "RPGCoreCommon.DynamicValues.Editor",
"references": [
"RPGCoreCommon.DynamicValues",
"RPGCoreCommon.Helpers.Editor",
"RPGCoreCommon.Helpers"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 63d26f6c5e724ba085544f845889ee92
timeCreated: 1759334877
+3
View File
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e9decfc31d224dcd8d04e812ca9b1f20
timeCreated: 1759333999
@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using RPGCoreCommon.Helpers;
using RPGCoreCommon.Helpers.CustomTypes;
using UnityEngine;
using Object = UnityEngine.Object;
namespace RPGCoreCommon.DynamicValues
{
[Serializable]
public class DynamicValue
{
// User defined formula or text with markers
[SerializeField] internal string _serializedFormula;
[SerializeField] internal SerializableDictionary<string, SerializableType> _serializedTypes = new();
[SerializeField] internal SerializableDictionary<string, Object> _serializedSources = new();
private bool _isParsed;
private string _formulaFormat;
private MarkerParser[] _markers;
private void Parse()
{
if (Application.isPlaying && _isParsed) return;
_isParsed = true;
var matches = new Regex("{[a-zA-z0-9-\\.]*}").Matches(_serializedFormula);
_markers = new MarkerParser[matches.Count];
_formulaFormat = _serializedFormula;
for (var i = matches.Count-1; i >= 0; i--)
{
_markers[i] = new MarkerParser(matches[i].Value);
_markers[i].sourceType = GetSourceType(_markers[i].sourceMarker);
_formulaFormat = _formulaFormat.Remove(matches[i].Index, matches[i].Length).Insert(matches[i].Index, $"{{{i}}}");
}
}
private Type GetSourceType(string sourceMarker)
{
if (_serializedTypes.TryGetValue(sourceMarker, out var type)) return type;
if (_serializedSources.TryGetValue(sourceMarker, out var obj)) return obj.GetType();
return null;
}
private object GetSource(string sourceMarker, DynamicValueSourceContext sourceContext = null)
{
if (_serializedSources.TryGetValue(sourceMarker, out var s1)) return s1;
if (sourceContext != null && sourceContext.TryGetSource(sourceMarker, out var s2)) return s2;
return null;
}
public void RemoveDynamicTypes()
{
_serializedTypes.Clear();
}
public void SetDynamicTypes(Dictionary<string, Type> types)
{
RemoveDynamicTypes();
types.DictForEach(SetDynamicType);
}
public void SetDynamicType(string name, Type type)
{
if (Application.isPlaying)
{
Debug.LogError($"method '{nameof(SetDynamicType)}' is only available in editor. Dynamic types should not be changed runtime!");
return;
}
_serializedTypes[name] = new SerializableType(type);
}
public MarkerParser[] GetMarkers()
{
Parse();
return _markers.ToArray();
}
public string GetString(DynamicValueSourceContext sourceContext = null)
{
Parse();
var values = _markers
.Select(marker => marker.GetValue(GetSource(marker.sourceMarker, sourceContext)))
.ToArray<object>();
return string.Format(_formulaFormat, values);
}
public int GetValue(DynamicValueSourceContext sourceContext = null)
{
Parse();
ExpressionEvaluator.Evaluate(GetString(sourceContext), out int value);
return value;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d3e7897496b64571880ab2ffe679fa43
timeCreated: 1759334381
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Object = UnityEngine.Object;
namespace RPGCoreCommon.DynamicValues
{
[AttributeUsage(AttributeTargets.Field)]
public class DynamicValueAttribute : PropertyAttribute
{
internal DynamicValueType type { private set; get; }
internal List<Type> staticTypes { private set; get; }
internal List<Type> ignoredDeclaringTypes { private set; get; } = new()
{
typeof(Object),
typeof(Behaviour),
typeof(MonoBehaviour),
typeof(Component)
};
/// <summary>
/// <see cref="DynamicValue"/> will be managed via code (adding/removing in inspector is)
/// </summary>
public DynamicValueAttribute(DynamicValueType type = DynamicValueType.BySerializedSources, params Type[] staticTypes)
{
this.type = type;
if (type is DynamicValueType.ByStaticTypes) this.staticTypes = staticTypes.ToList();
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0eb1b79e28da4cff8d0554fd6b270f26
timeCreated: 1759335563
@@ -0,0 +1,13 @@
using System;
using UnityEngine;
namespace RPGCoreCommon.DynamicValues
{
public class DynamicValueExistingTypeException : UnityException
{
public DynamicValueExistingTypeException(string text) : base(text)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 40c9343cc5534008aecf4a1734171915
timeCreated: 1759685699
@@ -0,0 +1,11 @@
using UnityEngine;
namespace RPGCoreCommon.DynamicValues
{
public class DynamicValueInvalidMarkerException : UnityException
{
public DynamicValueInvalidMarkerException(string text) : base(text)
{
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 902c960012c84504a20e452871dadc15
timeCreated: 1760275028
@@ -0,0 +1,32 @@
using System;
using UnityEngine;
namespace RPGCoreCommon.DynamicValues
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Method)]
public class DynamicValueProviderAttribute : PropertyAttribute
{
/// <summary>
/// <b>[Fields/Property]</b><br/>
/// Marks non-public field or property to provide value for <see cref="DynamicValue"/>'s formula.
/// Public fields and properties are always used as value providers.
/// Collections are ALWAYS ignored - if you want to get values from collection try to use custom method. More info below.
/// <br/><br/>
/// <b>[Method]</b><br/>
/// Method can be marked as custom value provider.
/// Those methods can have any name, but require specific return value and one parameter.
/// Only two definitions of methods are valid, ex.:
/// <ul>
/// <li><b>T CustomProviderMethod(Type)</b> - this usage supports polymorphism,
/// type in parameter will be supplied with type of any class that implements &lt;T&gt;.
/// Thanks to this type of value we want can be specified in marker.</li>
/// <li><b>T CustomProviderMethod(string)</b> - this usage DOESN'T support polymorphism,
/// string in parameter is exactly value specified in marker.</li>
/// </ul>
/// </summary>
public DynamicValueProviderAttribute()
{
// TODO: tutaj jeszcze jako argument trzeba przekazać nazwę drugiej metody, która ma listę możliwych stringów w przypadku użycia na CustomProviderMethod(string), wtedy będzie można to fajnie walidować już na poziomie tego narzędzia
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b49ba0d446154c368a559d69b8edf97b
timeCreated: 1763033238
@@ -0,0 +1,30 @@
using System.Collections.Generic;
namespace RPGCoreCommon.DynamicValues
{
public class DynamicValueSourceContext
{
private readonly Dictionary<string, object> _sources = new();
public void SetSource(string name, object source)
{
_sources.Remove(name.ToLower());
_sources.Add(name.ToLower(), source);
}
public void RemoveResource(string name)
{
_sources.Remove(name.ToLower());
}
public void RemoveSources()
{
_sources.Clear();
}
public bool TryGetSource(string name, out object source)
{
return _sources.TryGetValue(name.ToLower(), out source);
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c7ffbb14acb7403a982719f0771d95f4
timeCreated: 1763657503
@@ -0,0 +1,23 @@
namespace RPGCoreCommon.DynamicValues
{
public enum DynamicValueType
{
/// <summary>
/// [Default option] Sources defined by user in editor.
/// </summary>
BySerializedSources,
/// <summary>
/// Types of sources defined runtime.
/// Usually when other field can define what types will be used.
/// Sources needs to be added later in runtime.
/// </summary>
ByDynamicTypes,
/// <summary>
/// Types of sources defined via attribute.
/// Sources needs to be added later in runtime.
/// </summary>
ByStaticTypes
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dc2e7b666a654564ae758ccb7c7b2571
timeCreated: 1762786038
@@ -0,0 +1,8 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("RPGCoreCommon.DynamicValues.Editor")]
namespace RPGCoreCommon.DynamicValues
{
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 77ee6ce56165430e8dbf117a46d1182f
timeCreated: 1759337937
@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using RPGCoreCommon.Helpers;
using RPGCoreCommon.Helpers.Exceptions;
using UnityEngine;
namespace RPGCoreCommon.DynamicValues
{
public class MarkerParser
{
private static readonly BindingFlags BindingFlags = BindingFlags.Instance | BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
public string wholeMarker { get; private set; }
public string sourceMarker { get; private set; }
internal Type sourceType;
private MemberInfo[] _members;
private object[] _membersParameters;
private string[] _markers;
private string[] _parameters;
internal MarkerParser(string wholeMarker)
{
this.wholeMarker = wholeMarker;
var tempMarker = wholeMarker.Trim('{', '}').Split('.');
// Source marker should be always present (even if not it will be as empty string)
sourceMarker = tempMarker[0];
if (tempMarker.Length <= 1) return;
_markers = new string[tempMarker.Length - 1];
_parameters = new string[tempMarker.Length - 1];
for (var i = 1; i < tempMarker.Length; i++)
{
var valueMarker = tempMarker[i];
var tempValueMarker = valueMarker.TrimEnd(']').Split('[');
_markers[i - 1] = tempValueMarker[0];
if (tempValueMarker.Length > 1) _parameters[i - 1] = tempValueMarker[1];
}
}
/// <param name="type">Type that will be scanned</param>
/// <param name="attribute">Attribute with settings for this instance of <see cref="DynamicValue"/></param>
/// <returns>List of members that can be used as value providers for <see cref="DynamicValue"/>'s formula</returns>
internal static List<MemberInfo> GetMembersForType(Type type, DynamicValueAttribute attribute)
{
return type.GetAllTypes()
.SelectMany(t => t.GetMembers(BindingFlags))
.Where(member =>
{
if (attribute.ignoredDeclaringTypes.Contains(member.DeclaringType)) return false;
if (member is not MethodInfo and not PropertyInfo and not FieldInfo) return false;
var isProvider = member.GetCustomAttribute<DynamicValueProviderAttribute>() != null;
return isProvider || (member.IsMemberReadPublic() && member is not MethodInfo);
}).ToList();
}
/// <param name="member">Member to check if it can provide any subtypes besides itself</param>
/// <param name="isProvider">TRUE whenever given member is provider for <see cref="DynamicValue"/>'s formula</param>
/// <param name="isDynamic">TRUE if this member has parameter, but not specified with type, only by user's string</param>
/// <returns>All subtypes that this member can provide. If type can't be specified then it is dynamic.</returns>
internal static List<Type> GetSubTypesInProvider(MemberInfo member, out bool isProvider, out bool isDynamic)
{
isProvider = true;
isDynamic = false;
var providerAttribute = member.GetCustomAttribute<DynamicValueProviderAttribute>();
var methodInfo = member as MethodInfo;
// Only Methods support parameters, but only when attribute is also given
if (methodInfo == null || providerAttribute == null)
{
isProvider = false;
return new List<Type>();
}
var parameter = methodInfo.GetParameters().FirstOrDefault();
var isStringParameter = parameter?.ParameterType == typeof(string);
var isTypeParameter = parameter?.ParameterType == typeof(Type);
// BAD - invalid parameter definition in method!
if ((!isStringParameter && !isTypeParameter) || methodInfo.GetParameters().Length != 1)
{
Debug.LogError($"<b>{member.Name}</b> has invalid definition!<br>" +
$"[{nameof(DynamicValueProviderAttribute)}] Supports only methods with ONE parameter that is <b>string</b> or <b>type</b>!");
return new List<Type>();
}
// BAD - return type is required!
if (methodInfo.ReturnType == typeof(void))
{
Debug.LogError($"<b>{member.Name}</b> has invalid definition!<br>" +
$"[{nameof(DynamicValueProviderAttribute)}] Supports only methods with defined return type!");
return new List<Type>();
}
// GOOD by string - polymorphism not supported - parameter is dynamic, which means text can have anything
if (isStringParameter)
{
isDynamic = true;
return new List<Type> { methodInfo.ReturnType };
}
// GOOD by type - polymorphism supported
return UnityEditor.TypeCache.GetTypesDerivedFrom(methodInfo.ReturnType)
.Where(type => !type.IsAbstract && !type.IsGenericType)
.ToList();
}
/// <returns>Type of value that this marker leads to</returns>
/// <exception cref="MemberNotFoundException">When member at path could not be found</exception>
public Type GetValueType()
{
return GetMembers().Last().GetMemberValueType();
}
public string GetValue(object source)
{
// This will also refill _members and _membersParameters
GetMembers();
var tempObject = source;
for (var i = 0; i < _members.Length; i++)
{
if (_members[i] is FieldInfo or PropertyInfo)
tempObject = _members[i].GetMemberValue(tempObject);
else if (_members[i] is MethodInfo methodInfo)
tempObject = methodInfo.Invoke(tempObject, new[] { _membersParameters[i] });
}
return tempObject.ToString();
}
/// <summary>
/// Returns chain of <see cref="MemberInfo"/> that will be used to get value for this marker.
/// </summary>
/// <returns>Array of <see cref="MemberInfo"/> used by this marker.</returns>
/// <exception cref="MemberNotFoundException">When given path from marker doesn't exist in source.</exception>
public MemberInfo[] GetMembers()
{
if (Application.isPlaying && _members != null) return _members.ToArray();
_members = new MemberInfo[_markers.Length];
_membersParameters = new object[_markers.Length];
var tempType = sourceType;
for (var i = 0; i < _markers.Length; i++)
{
var member = tempType.GetAllTypes()
.SelectMany(t => t.GetMember(_markers[i], BindingFlags)).FirstOrDefault();
// BAD - whole member doesn't exist
if (member == null)
throw new MemberNotFoundException($"Member [{_markers[i]}] is not found!", tempType);
var attribute = member.GetCustomAttribute<DynamicValueProviderAttribute>();
// BAD - non-public member without attribute
if (!member.IsMemberReadPublic() && attribute is null)
throw new MemberNotFoundException($"Member [{_markers[i]}] found, but its non-public without {nameof(DynamicValueProviderAttribute)}!", tempType);
// BAD - member is collection, collections aren't supported
if (typeof(Enumerable).IsAssignableFrom(member.GetMemberValueType()))
throw new MemberNotFoundException($"Member [{_markers[i]}] found, but it is collection. Collections aren't supported!", tempType);
tempType = member.GetMemberValueType();
if (member is not MethodInfo methodInfo)
{
// BAD - simple non-collection found, but marker has parameter
if (_parameters[i] != null)
throw new MemberNotFoundException($"Member [{_markers[i]}] found, but marker has parameter. Only methods can have parameters!", tempType);
// GOOD - simple non-collection field or property
_members[i] = member;
continue;
}
// BAD - is method, but without required attribute
if (attribute is null)
throw new MemberNotFoundException($"Member [{_markers[i]}] found, but without required {nameof(DynamicValueProviderAttribute)}!", tempType);
var validReturnType = methodInfo.ReturnType != typeof(void);
var parameter = methodInfo.GetParameters().FirstOrDefault();
var isStringParameter = parameter?.ParameterType == typeof(string);
var isTypeParameter = parameter?.ParameterType == typeof(Type);
// BAD - provider method has invalid definition
if (!validReturnType || (!isStringParameter && !isTypeParameter))
throw new MemberNotFoundException($"Member [{_markers[i]}] found, but definition is invalid! Must contain only one parameter <b>string</b> or <b>Type</b> and also must return something.", tempType);
_members[i] = methodInfo;
// GOOD - parameter is string, which means it is dynamic, only body of method can use it.
// Polymorphism is NOT supported here, return value is always method's return type
if (isStringParameter)
{
_membersParameters[i] = _parameters[i];
tempType = methodInfo.ReturnType;
continue;
}
// GOOD - parameter is Type, which we can match by its name.
// Polymorphism is supported here, dynamic value know type referenced by marker.
tempType = UnityEditor.TypeCache.GetTypesDerivedFrom(methodInfo.ReturnType)
.FirstOrDefault(t => t.Name.Equals(_parameters[i], StringComparison.OrdinalIgnoreCase));
_membersParameters[i] = tempType;
}
return _members.ToArray();
}
/// <summary>
/// <inheritdoc cref="GetMembers()"/>
/// </summary>
/// <param name="membersParameters">
/// <b>[Only when MethodInfo]</b> List of parameters that will be used for specific member.
/// Can be <b>string</b> or <b>Type</b>.
/// </param>
/// <inheritdoc cref="GetMembers()"/>
public MemberInfo[] GetMembers(out object[] membersParameters)
{
var members = GetMembers();
membersParameters = _membersParameters.ToArray();
return members;
}
}
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 99a9d00853004ff788e659a257298db0
timeCreated: 1763109073
@@ -0,0 +1,16 @@
{
"name": "RPGCoreCommon.DynamicValues",
"rootNamespace": "RPGCoreCommon.DynamicValues",
"references": [
"RPGCoreCommon.Helpers"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ade6d5973db3446ab20f0bb7e4a872f8
timeCreated: 1759334847