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]; } } /// Type that will be scanned /// Attribute with settings for this instance of /// List of members that can be used as value providers for 's formula internal static List 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() != null; return isProvider || (member.IsMemberReadPublic() && member is not MethodInfo); }).ToList(); } /// Member to check if it can provide any subtypes besides itself /// TRUE whenever given member is provider for 's formula /// TRUE if this member has parameter, but not specified with type, only by user's string /// All subtypes that this member can provide. If type can't be specified then it is dynamic. internal static List GetSubTypesInProvider(MemberInfo member, out bool isProvider, out bool isDynamic) { isProvider = true; isDynamic = false; var providerAttribute = member.GetCustomAttribute(); 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(); } 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($"{member.Name} has invalid definition!
" + $"[{nameof(DynamicValueProviderAttribute)}] Supports only methods with ONE parameter that is string or type!"); return new List(); } // BAD - return type is required! if (methodInfo.ReturnType == typeof(void)) { Debug.LogError($"{member.Name} has invalid definition!
" + $"[{nameof(DynamicValueProviderAttribute)}] Supports only methods with defined return type!"); return new List(); } // GOOD by string - polymorphism not supported - parameter is dynamic, which means text can have anything if (isStringParameter) { isDynamic = true; return new List { methodInfo.ReturnType }; } // GOOD by type - polymorphism supported return UnityEditor.TypeCache.GetTypesDerivedFrom(methodInfo.ReturnType) .Where(type => !type.IsAbstract && !type.IsGenericType) .ToList(); } /// Type of value that this marker leads to /// When member at path could not be found 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(); } /// /// Returns chain of that will be used to get value for this marker. /// /// Array of used by this marker. /// When given path from marker doesn't exist in source. 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(); // 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 string or Type 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(); } /// /// /// /// /// [Only when MethodInfo] List of parameters that will be used for specific member. /// Can be string or Type. /// /// public MemberInfo[] GetMembers(out object[] membersParameters) { var members = GetMembers(); membersParameters = _membersParameters.ToArray(); return members; } } }