public BaseStatusSystem() { requiredConversionChecks = new List <Action>(); extraEnumTypes = new List <Type>(); DoNothing = (obj, status, ov, nv) => { }; Total = ints => { int total = 0; foreach (int i in ints) { total += i; } return(total); }; Bool = ints => { int total = 0; foreach (int i in ints) { total += i; } if (total > 0) { return(1); } else { return(0); } }; MaximumOrZero = ints => { int max = 0; foreach (int i in ints) { if (i > max) { max = i; } } return(max); }; defaultAggs = new Dictionary <SourceType, Func <IEnumerable <int>, int> >(); defaultAggs[SourceType.Feed] = Total; defaultAggs[SourceType.Suppress] = Bool; defaultAggs[SourceType.Prevent] = Bool; valueAggs = new DefaultValueDictionary <TBaseStatus, Aggregator>(); SingleSource = new DefaultHashSet <TBaseStatus>(); statusesCancelledBy = new MultiValueDictionary <TBaseStatus, TBaseStatus>(); statusesExtendedBy = new MultiValueDictionary <TBaseStatus, TBaseStatus>(); statusesThatExtend = new MultiValueDictionary <TBaseStatus, TBaseStatus>(); statusesFedBy = new Dictionary <SourceType, MultiValueDictionary <TBaseStatus, TBaseStatus> >(); converters = new Dictionary <SourceType, Dictionary <StatusPair <TBaseStatus>, Func <int, int> > >(); cancellationConditions = new DefaultValueDictionary <StatusPair <TBaseStatus>, Func <int, bool> >(); foreach (SourceType type in Enum.GetValues(typeof(SourceType))) { statusesFedBy[type] = new MultiValueDictionary <TBaseStatus, TBaseStatus>(); converters[type] = new Dictionary <StatusPair <TBaseStatus>, Func <int, int> >(); } onChangedHandlers = new DefaultValueDictionary <TBaseStatus, DefaultValueDictionary <StatusChange <TBaseStatus>, OnChangedHandler <TObject, TBaseStatus> > >(); extraPreventionConditions = new MultiValueDictionary <TBaseStatus, Func <TObject, TBaseStatus, bool> >(); }
private void CheckRpsErrors(List <string> result, bool includeWarnings) { foreach (Relationship r in relationships.GetAllValues()) { // This part finds rock-paper-scissors relationships and decides whether they're potentially dangerous or not. if (r.SourceStatus.Equals(r.TargetStatus) && r.IsNegative && r.ChainBroken && r.Path.Count == 4) { RelationType relation = r.Path[1].Relation; // The first is 'self'. Skip that one. if (r.Path.Skip(2).All(x => x.Relation == relation)) // If the rest are the same, we have a basic rock-paper-scissors relationship. { var trio = r.Path.Take(3).Select(x => x.Status).ToList(); if (visitedRps[trio]) { continue; // If already visited, skip it. } RecordRpsVisited(trio); bool notDangerous = false; DefaultHashSet <RelationType> negatives = new DefaultHashSet <RelationType> { RelationType.Cancels, RelationType.Prevents, RelationType.Suppresses }; for (int i = 0; i < 3; ++i) // For each pair (A -> B)... { TStatus source = r.Path[i].Status; TStatus target = r.Path[i + 1].Status; DefaultHashSet <RelationType> removed = new DefaultHashSet <RelationType>(); foreach (RelationType presentRelation in negatives) { if (!relationships[source] .Where(x => x.TargetStatus.Equals(target) && !x.ChainBroken && x.Relation == presentRelation) .Any()) // Note which relation types are present for all statuses in this cycle. { removed[presentRelation] = true; if (presentRelation == RelationType.Suppresses) { notDangerous = true; // No suppression means no danger. } } } negatives.ExceptWith(removed); // Remove the no-longer-true relationship flags. bool cancelled = false; if (relationships[source] .Where(x => x.TargetStatus.Equals(target) && !x.ChainBroken && x.Relation == RelationType.Cancels && !x.IsConditional) .Any()) // If B is cancelled (unconditionally) by A, that's half of what we need to know. { cancelled = true; } HashSet <TStatus> targetRelatives = GetExtendedFamily(target); // Target, any that extend it, and so on. var fedRelationships = relationships.GetAllValues(); fedRelationships = fedRelationships.Where(x => targetRelatives.Contains(x.TargetStatus)); fedRelationships = fedRelationships.Where(x => x.Relation == RelationType.Feeds && x.Path.Count == 2); bool fed = fedRelationships.Any(); // If B can be fed by another value, that's the other half of what we need to know. if (cancelled && !fed) // If its status can actually be cancelled (without still being fed) { notDangerous = true; // then this one can't be dangerous. break; } } string verbs; if (negatives.Count == 3) { verbs = "foils"; } else { verbs = string.Join(" and ", negatives.Select(x => x.ToString().ToLower())); } if (notDangerous) { if (includeWarnings) { string error = $"OKAY: Rock-paper-scissors relationship: Each of these statuses {verbs} the next:"; error += GetPathString(r.Path, false); result.Add(error); } } else { if (r.IsConditional) { if (includeWarnings) { string error = $"CRITICAL WARNING: Infinite suppression loop likely for this rock-paper-scissors relationship: Each of these statuses {verbs} the next:"; error += GetPathString(r.Path, false); result.Add(error); } } else { string error = $"CRITICAL ERROR: Infinite suppression loop guaranteed for this rock-paper-scissors relationship: Each of these statuses {verbs} the next:"; error += GetPathString(r.Path, false); result.Add(error); } } } } } }
// There is room for improvement here: errors/warnings could be proper objects, not just strings. internal List <string> GetErrors(bool includeWarnings) { List <string> result = new List <string>(); CheckRpsErrors(result, includeWarnings); var negativeRelationships = new MultiValueDictionary <StatusPair <TStatus>, Relationship>(); var positiveRelationships = new MultiValueDictionary <StatusPair <TStatus>, Relationship>(); var mutualSuppressions = new DefaultHashSet <StatusPair <TStatus> >(); foreach (Relationship r in relationships.GetAllValues()) { if (r.Path.Count == 1) { continue; // Skip any 'self' relationships. } if (!r.ChainBroken) // Tally negative and positive (direct) relationships. These are compared later. { if (r.IsNegative) { negativeRelationships.Add(new StatusPair <TStatus>(r.SourceStatus, r.TargetStatus), r); } else { positiveRelationships.Add(new StatusPair <TStatus>(r.SourceStatus, r.TargetStatus), r); } } if (r.SourceStatus.Equals(r.TargetStatus) && !r.ChainBroken && !r.IsNegative) { if (r.IsConditional) { if (includeWarnings) { string error = $"CRITICAL WARNING: Status \"{GetBestName(r.SourceStatus)}\" might feed itself infinitely:"; error += GetPathString(r.Path, true); result.Add(error); } } else { string error = $"CRITICAL ERROR: Status \"{GetBestName(r.SourceStatus)}\" feeds itself infinitely:"; error += GetPathString(r.Path, false); result.Add(error); } } if (r.SourceStatus.Equals(r.TargetStatus) && !r.ChainBroken) { switch (r.Relation) { case RelationType.Suppresses: { string error = $"CRITICAL ERROR: Status \"{GetBestName(r.SourceStatus)}\" suppresses itself. This will always cause an infinite loop:"; error += GetPathString(r.Path, false); result.Add(error); } break; case RelationType.Cancels: if (includeWarnings) { string error = $"WARNING: Status \"{GetBestName(r.SourceStatus)}\" cancels itself:"; error += GetPathString(r.Path, false); error += GetErrorLine("(Take a look at the 'Single Source' setting to see if that's what you actually want.)"); result.Add(error); } break; case RelationType.Prevents: if (includeWarnings) { string error = $"WARNING: Status \"{GetBestName(r.SourceStatus)}\" prevents itself:"; error += GetPathString(r.Path, false); result.Add(error); } break; } } if (r.SourceStatus.Equals(r.TargetStatus) && r.Path.Count >= 4 && r.IsNegative && r.ChainBroken && !r.Path.Where(x => x.Relation == RelationType.Cancels || x.Relation == RelationType.Prevents).Any()) { List <TStatus> negativeList = r.Path.Skip(1) .Where(x => x.Relation != RelationType.Feeds && x.Relation != RelationType.Extends) .Select(x => x.Status).ToList(); // Remove the extra links... if (!visitedRps[negativeList]) // If this has already been handled as RPS, skip it. { RecordRpsVisited(negativeList); if (r.IsConditional) { if (includeWarnings) { string error = $"CRITICAL WARNING: Conditional suppression cycle. This might cause an infinite loop:"; error += GetPathString(r.Path, false); result.Add(error); } } else { string error = $"CRITICAL ERROR: Suppression cycle. This will always cause an infinite loop:"; error += GetPathString(r.Path, false); result.Add(error); } } } if (includeWarnings) { if (!r.SourceStatus.Equals(r.TargetStatus) && !r.ChainBroken && r.Relation == RelationType.Suppresses) // Whenever a status suppresses another... { var otherWay = relationships[r.TargetStatus].ToList() .Find(x => x.TargetStatus.Equals(r.SourceStatus) && !x.ChainBroken && x.Relation == RelationType.Suppresses); if (otherWay != null) // ...find out whether they both suppress one another. { var pair = new StatusPair <TStatus>(r.SourceStatus, r.TargetStatus); if (!mutualSuppressions[pair]) // If it hasn't already been handled... { mutualSuppressions[pair] = true; mutualSuppressions[new StatusPair <TStatus>(r.TargetStatus, r.SourceStatus)] = true; string error = $"WARNING: Mutual suppression. (This warning exists to make certain that you don't expect these 2 statuses to \"cancel each other out\". Instead, whichever status is created first will win. See the docs for more info.):"; error += GetPathString(r.Path, false); error += GetPathString(otherWay.Path, false); result.Add(error); } } } } } if (includeWarnings) { // Check for feed + extend. foreach (var pair in positiveRelationships) { var list = pair.Value.ToList(); var feed = list.Find(x => x.Relation == RelationType.Feeds); var extend = list.Find(x => x.Relation == RelationType.Extends); if (feed != null && extend != null) { string error = $"WARNING: Possible conflict: Status \"{GetBestName(pair.Key.status1)}\" extends AND feeds status \"{GetBestName(pair.Key.status2)}\"."; error += GetPathString(feed.Path, false, "feed"); error += GetPathString(extend.Path, false, "extend"); error += GetErrorLine("(Note for feed+extend: the 'feeds' value change is applied before the 'extends' value change.)"); result.Add(error); } } // Check for positive + negative. foreach (var statusPair in negativeRelationships.GetAllKeys().Intersect(positiveRelationships.GetAllKeys())) { string error = $"WARNING: Possible conflict: Status \"{GetBestName(statusPair.status1)}\"'s relationship to status \"{GetBestName(statusPair.status2)}\" has both negative & positive elements:"; var allPairRelationships = negativeRelationships[statusPair].Concat(positiveRelationships[statusPair]).ToList(); foreach (Relationship r in allPairRelationships) { error += GetPathString(r.Path, false, r.Relation.ToString().ToLower()); } if (ContainsBoth(RelationType.Feeds, RelationType.Suppresses, allPairRelationships)) { error += GetErrorLine("(Note for feed+suppress: the 'feeds' value change is applied before the suppression.)"); } if (ContainsBoth(RelationType.Extends, RelationType.Suppresses, allPairRelationships)) { error += GetErrorLine("(Note for extend+suppress: the suppression is applied before the 'extends' value is propagated. Therefore, this is very unlikely to be useful unless a condition is present.)"); } if (ContainsBoth(RelationType.Extends, RelationType.Cancels, allPairRelationships)) { error += GetErrorLine("(Note for extend+cancel: the cancellation is applied before the 'extends' value is propagated. This is very unlikely to be useful.)"); } result.Add(error); } // Check for inconsistent aggregators. foreach (var pair in rules.valueAggs) // For every status that has an aggregator defined... { foreach (var otherStatus in rules.statusesThatExtend[pair.Key]) { if (rules.valueAggs[pair.Key] != rules.valueAggs[otherStatus]) { string error = $"WARNING: Possibly inconsistent aggregators between extended statuses. Status \"{GetBestName(otherStatus)}\" doesn't seem to use the same aggregator as its parent status \"{GetBestName(pair.Key)}\"."; result.Add(error); } } } // Check for overlapping values in the enums. var allEnumTypes = rules.extraEnumTypes; if (typeof(TStatus).IsEnum) { allEnumTypes.Add(typeof(TStatus)); } if (allEnumTypes.Count > 1) { MultiValueDictionary <int, string> enumNames = new MultiValueDictionary <int, string>(); MultiValueDictionary <int, Type> recordedIntsPerType = new MultiValueDictionary <int, Type>(); foreach (var enumType in allEnumTypes) { foreach (string enumName in Enum.GetNames(enumType)) { object value = Enum.Parse(enumType, enumName); enumNames.Add((int)value, $"{enumType.Name}.{enumName}"); recordedIntsPerType.AddUnique((int)value, enumType); // Note that this enum has a name for this value. } } foreach (var pair in recordedIntsPerType) { if (pair.Value.Count() > 1) // If more than one enum has a name for this value... { var names = enumNames[pair.Key]; string error = $"WARNING: Multiple enums with the same value ({pair.Key}): "; error += GetErrorLine(string.Join(", ", names)); result.Add(error); } } } } return(result); }
internal RuleChecker(BaseStatusSystem <TObject, TStatus> rules) { visitedRps = new DefaultHashSet <List <TStatus> >(new IEnumValueEquality <TStatus>()); // Compare lists with value equality! this.rules = rules; CheckRules(); }