private void SerializeStatusInstances(InstanceType type, System.IO.BinaryWriter writer, Action <System.IO.BinaryWriter, StatusInstance <TObject>, StatusTracker <TObject> > statusInstanceCallback) { MultiValueDictionary <TBaseStatus, StatusInstance <TObject> > dict = statusInstances[type]; writer.Write(dict.GetAllKeys().Count()); foreach (IGrouping <TBaseStatus, StatusInstance <TObject> > pair in dict) { writer.Write(pair.Key); List <StatusInstance <TObject> > values = pair.ToList(); writer.Write(values.Count); foreach (StatusInstance <TObject> instance in values) { instance.SerializeInternal(writer); statusInstanceCallback?.Invoke(writer, instance, this); } } }
// 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, Relationship>(); var positiveRelationships = new MultiValueDictionary <StatusPair, Relationship>(); var mutualSuppressions = new EasyHashSet <StatusPair>(); 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(r.SourceStatus, r.TargetStatus), r); } else { positiveRelationships.Add(new StatusPair(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 Instance' 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(r.SourceStatus, r.TargetStatus); if (!mutualSuppressions[pair]) // If it hasn't already been handled... { mutualSuppressions[pair] = true; mutualSuppressions[new StatusPair(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.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.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); }