Ejemplo n.º 1
0
 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();
 }