private void CheckActualValueChanged(TBaseStatus status) { int newValue; if (currentRaw[InstanceType.Suppress][status] > 0) { newValue = 0; } else { newValue = currentRaw[InstanceType.Feed][status]; } int oldValue = currentActualValues[status]; if (newValue != oldValue) { currentActualValues[status] = newValue; bool increased = newValue > oldValue; if (!GenerateNoMessages) { GetHandler(status, increased, false)?.Invoke(obj, status, oldValue, newValue); } if (!GenerateNoEffects) { GetHandler(status, increased, true)?.Invoke(obj, status, oldValue, newValue); } UpdateFeed(status, InstanceType.Feed, newValue); if (increased) { foreach (TBaseStatus cancelledStatus in rules.statusesCancelledBy[status]) { var pair = new StatusPair(status, cancelledStatus); var condition = rules.cancellationConditions[pair]; // if a condition exists, it must return true for the if (condition == null || condition(newValue)) { Cancel(cancelledStatus); // status to be cancelled. } } } UpdateFeed(status, InstanceType.Suppress, newValue); // Cancellations happen before suppression to prevent some infinite loops UpdateFeed(status, InstanceType.Prevent, newValue); } }
private void UpdateFeed(TBaseStatus status, InstanceType type, int newValue) { foreach (TBaseStatus fedStatus in rules.statusesFedBy[type][status]) { int newFedValue = newValue; var pair = new StatusPair(status, fedStatus); Converter conv; if (rules.converters[type].TryGetValue(pair, out conv)) { newFedValue = conv(newFedValue); } int oldFedValue; Dictionary <TBaseStatus, int> fedValues; if (internalFeeds[type].TryGetValue(fedStatus, out fedValues)) { fedValues.TryGetValue(status, out oldFedValue); } else { oldFedValue = 0; } if (newFedValue != oldFedValue) { if (fedValues == null) { fedValues = new Dictionary <TBaseStatus, int>(); fedValues.Add(status, newFedValue); internalFeeds[type].Add(fedStatus, fedValues); } else { fedValues[status] = newFedValue; } CheckRawChanged(fedStatus, type); } } }
// 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); }
private IEnumerable <Relationship> GetConnectionsFrom(TStatus status) { foreach (TStatus targetStatus in rules.statusesExtendedBy[status]) { yield return(new Relationship { Path = new List <DirectRelation> { new DirectRelation { Status = status, Relation = RelationType.Self }, new DirectRelation { Status = targetStatus, Relation = RelationType.Extends } } }); } foreach (TStatus targetStatus in rules.statusesCancelledBy[status]) { var pair = new StatusPair(status, targetStatus); bool conditional = rules.cancellationConditions[pair] != null; yield return(new Relationship { Path = new List <DirectRelation> { new DirectRelation { Status = status, Relation = RelationType.Self }, new DirectRelation { Status = targetStatus, Relation = RelationType.Cancels, IsConditional = conditional } } }); } foreach (InstanceType instanceType in new InstanceType[] { InstanceType.Feed, InstanceType.Suppress, InstanceType.Prevent }) { RelationType relation; switch (instanceType) { case InstanceType.Feed: relation = RelationType.Feeds; break; case InstanceType.Suppress: relation = RelationType.Suppresses; break; case InstanceType.Prevent: relation = RelationType.Prevents; break; default: throw new NotImplementedException(); } foreach (TStatus targetStatus in rules.statusesFedBy[instanceType][status]) { var pair = new StatusPair(status, targetStatus); bool conditional = rules.converters[instanceType].ContainsKey(pair); yield return(new Relationship { Path = new List <DirectRelation> { new DirectRelation { Status = status, Relation = RelationType.Self }, new DirectRelation { Status = targetStatus, Relation = relation, IsConditional = conditional } } }); } } }
public bool Equals(StatusPair <TStatus> other) => status1.Equals(other.status1) && status2.Equals(other.status2);
// These are disabled because there's currently no valid reason to use them. //public void Suppresses(int fedValue, params TBaseStatus[] suppressedStatuses) => FeedsInternal(SourceType.Suppression, null, fedValue, null, suppressedStatuses); //public void Suppresses<TStatus>(int fedValue, params TStatus[] suppressedStatuses) where TStatus : struct => FeedsInternal(SourceType.Suppression, null, fedValue, null, Convert(suppressedStatuses)); //public void Suppresses(int fedValue, Func<int, bool> condition, params TBaseStatus[] suppressedStatuses) => FeedsInternal(SourceType.Suppression, null, fedValue, condition, suppressedStatuses); //public void Suppresses<TStatus>(int fedValue, Func<int, bool> condition, params TStatus[] suppressedStatuses) where TStatus : struct => FeedsInternal(SourceType.Suppression, null, fedValue, condition, Convert(suppressedStatuses)); //public void Suppresses(Converter converter, params TBaseStatus[] suppressedStatuses) => FeedsInternal(SourceType.Suppression, converter, null, null, suppressedStatuses); //public void Suppresses<TStatus>(Converter converter, params TStatus[] suppressedStatuses) where TStatus : struct => FeedsInternal(SourceType.Suppression, converter, null, null, Convert(suppressedStatuses)); //public void Prevents(int fedValue, params TBaseStatus[] preventedStatuses) => FeedsInternal(SourceType.Prevention, null, fedValue, null, preventedStatuses); //public void Prevents<TStatus>(int fedValue, params TStatus[] preventedStatuses) where TStatus : struct => FeedsInternal(SourceType.Prevention, null, fedValue, null, Convert(preventedStatuses)); //public void Prevents(int fedValue, Func<int, bool> condition, params TBaseStatus[] preventedStatuses) => FeedsInternal(SourceType.Prevention, null, fedValue, condition, preventedStatuses); //public void Prevents<TStatus>(int fedValue, Func<int, bool> condition, params TStatus[] preventedStatuses) where TStatus : struct => FeedsInternal(SourceType.Prevention, null, fedValue, condition, Convert(preventedStatuses)); //public void Prevents(Converter converter, params TBaseStatus[] preventedStatuses) => FeedsInternal(SourceType.Prevention, converter, null, null, preventedStatuses); //public void Prevents<TStatus>(Converter converter, params TStatus[] preventedStatuses) where TStatus : struct => FeedsInternal(SourceType.Prevention, converter, null, null, Convert(preventedStatuses)); protected void FeedsInternal(SourceType type, Converter converter, int?fedValue, Func <int, bool> condition, params TBaseStatus[] fedStatuses) { if (fedStatuses.Length == 0) { throw new ArgumentException(StatusExpected); } if (condition != null) { if (fedValue != null) { int fed = fedValue.Value; converter = i => { if (condition(i)) { return(fed); //todo: cache this? } else { return(0); } }; } else { converter = i => { if (condition(i)) { return(i); //todo: cache? } else { return(0); } }; } } else { if (fedValue != null) { int fed = fedValue.Value; converter = i => { if (i > 0) { return(fed); //todo: cache? } else { return(0); } }; } } if (converter != null) { rules.ValidateConverter(converter); } foreach (TBaseStatus fedStatus in fedStatuses) { rules.statusesFedBy[type].AddUnique(status, fedStatus); if (converter != null) { var pair = new StatusPair <TBaseStatus>(status, fedStatus); rules.converters[type][pair] = converter; } } }