public void GetApplicablePermutationsForTheseConditions() { // arrange ConditionFactory factory = new ConditionFactory(); HashSet <ConditionKey> keys = new HashSet <ConditionKey> { ConditionKey.DayOfWeek, ConditionKey.Season }; ConditionDictionary conditions = factory.BuildEmpty(); // act IEnumerable <IDictionary <ConditionKey, string> > permutations = factory.GetApplicablePermutationsForTheseConditions(keys, conditions); // assert IEnumerable <string> actual = permutations.Select(permutation => "(" + this.SortAndCommaDelimit(permutation.Select(p => $"{p.Key}:{p.Value}")) + ")"); List <string> expected = new List <string>(); foreach (DayOfWeek dayOfWeek in this.DaysOfWeek) { foreach (string season in this.Seasons) { expected.Add($"({ConditionKey.DayOfWeek}:{dayOfWeek}, {ConditionKey.Season}:{season})"); } } this.SortAndCommaDelimit(actual).Should().Be(this.SortAndCommaDelimit(expected)); }
/// <summary> /// 取得要引用的條件 /// </summary> /// <param name="AParamName">條件參數名稱</param> /// <param name="AParamValue">條件參數值</param> /// <returns>條件語法內容</returns> public virtual string getCondition(string AParamName, string AParamValue, bool bReplaceKeyword) { if (AParamName != null && ConditionDictionary.ContainsKey(AParamName)) { if ((AParamName == "MingTai") || (AParamName == "Aegon") || (AParamName == "PcaLife") || (AParamName == "EmployeeSerNoTree") || (AParamName == "EmployeeTree") || (AParamName == "DBGSernoNotIn") || (AParamName == "DBGSernoIn") || (AParamName == "RTATree") || (AParamName == "CAPTree") || (AParamName == "RstStatusIn") || (AParamName == "MutileTree") || (AParamName == "JobIDIn") || (AParamName == "CampaignSerNoIn") || (AParamName == "EmployeeBossTree") || (AParamName == "CAPTree") || (AParamName == "RTACategory2In") || (AParamName == "RosterAliasSerNoIn") || (AParamName == "StatusIn")) { #if (Encrypt) foreach (string field in _encryptFields) { if (ConditionDictionary[AParamName].ToString().IndexOf(field) > -1) { return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : Encrypt(AParamValue))); } } return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : AParamValue)); #else return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : AParamValue)); #endif } else { #if (Encrypt) foreach (string field in _encryptFields) { if (ConditionDictionary[AParamName].ToString().IndexOf(field) > -1) { return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : Encrypt(AParamValue))); } } return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : AParamValue)); #else return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue == null ? "" : ((bReplaceKeyword) ? AParamValue.Replace("'", "''") : AParamValue))); #endif } } else { return(""); } }
public void CanConditionsOverlap_WithImpliedConditions() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary left = factory.BuildEmpty(); ConditionDictionary right = factory.BuildEmpty(); // act bool canOverlap = factory.CanConditionsOverlap(left, right); // assert canOverlap.Should().BeTrue(); }
public string GetPossibleStrings_WithMultipleTokens(string raw, params ConditionKey[] conditionKeys) { // arrange ConditionFactory factory = new ConditionFactory(); TokenString tokenStr = new TokenString(raw, new HashSet <ConditionKey>(conditionKeys), TokenStringBuilder.TokenPattern); ConditionDictionary conditions = factory.BuildEmpty(); // act IEnumerable <string> actual = factory.GetPossibleStrings(tokenStr, conditions); // assert return(this.SortAndCommaDelimit(actual)); }
/**** ** Condition parsing ****/ /// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> public bool TryParseConditions(InvariantDictionary <string> raw, out ConditionDictionary conditions, out string error) { // no conditions if (raw == null || !raw.Any()) { conditions = this.ConditionFactory.BuildEmpty(); error = null; return(true); } // parse conditions conditions = this.ConditionFactory.BuildEmpty(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!Enum.TryParse(pair.Key, true, out ConditionKey key)) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", this.ConditionFactory.GetValidConditions())}"; conditions = null; return(false); } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{key} can't be empty"; conditions = null; return(false); } // restrict to allowed values InvariantHashSet validValues = new InvariantHashSet(this.ConditionFactory.GetValidValues(key)); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {key} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } // create condition conditions[key] = new Condition(key, values); } // return parsed conditions error = null; return(true); }
/// <summary> /// 取得要引用的條件 /// </summary> /// <param name="AParamName">條件參數名稱</param> /// <param name="AParamValue">條件參數值</param> /// <returns>條件語法內容</returns> public virtual string getCondition(string AParamName, string AParamValue) { if (ConditionDictionary.ContainsKey(AParamName)) { if (((string)ConditionDictionary[AParamName]).IndexOf("'?'") > 0) { ((string)ConditionDictionary[AParamName]).Replace("'?'", "N'?'"); } return(((string)ConditionDictionary[AParamName]).Replace("?", AParamValue)); } else { return(""); } }
public void GetPossibleStrings_WithOneToken(ConditionKey condition) { // arrange ConditionFactory factory = new ConditionFactory(); TokenString tokenStr = new TokenString("{{" + condition + "}}", new HashSet <ConditionKey> { condition }, TokenStringBuilder.TokenPattern); ConditionDictionary conditions = factory.BuildEmpty(); // act IEnumerable <string> actual = factory.GetPossibleStrings(tokenStr, conditions); // assert this.SortAndCommaDelimit(actual).Should().Be(this.CommaDelimitedValues[condition]); }
/// <summary>Get whether two sets of conditions can potentially both match in some contexts.</summary> /// <param name="left">The first set of conditions to compare.</param> /// <param name="right">The second set of conditions to compare.</param> public bool CanConditionsOverlap(ConditionDictionary left, ConditionDictionary right) { IDictionary <ConditionKey, InvariantHashSet> leftValues = this.GetPossibleTokenisableValues(left); IDictionary <ConditionKey, InvariantHashSet> rightValues = this.GetPossibleTokenisableValues(right); foreach (ConditionKey key in this.GetTokenisableConditions()) { if (!leftValues[key].Intersect(rightValues[key], StringComparer.InvariantCultureIgnoreCase).Any()) { return(false); } } return(true); }
/// <summary>Get whether two sets of conditions can potentially both match in some contexts.</summary> /// <param name="left">The first set of conditions to compare.</param> /// <param name="right">The second set of conditions to compare.</param> public bool CanConditionsOverlap(ConditionDictionary left, ConditionDictionary right) { IDictionary <ConditionKey, InvariantHashSet> leftValues = this.GetPossibleValues(left); IDictionary <ConditionKey, InvariantHashSet> rightValues = this.GetPossibleValues(right); foreach (ConditionKey key in this.GetValidConditions()) { if (!leftValues[key].Intersect(rightValues[key]).Any()) { return(false); } } return(true); }
public void GetPossibleValues_WithOnlyImpliedConditions() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary conditions = factory.BuildEmpty(); // act IDictionary <ConditionKey, InvariantHashSet> possibleValues = factory.GetPossibleTokenisableValues(conditions); // assert foreach (ConditionKey key in this.TokenisableConditions) { this.SortAndCommaDelimit(possibleValues[key]).Should().Be(this.CommaDelimitedValues[key], $"should match for {key}"); } }
public void CanConditionsOverlap_WhenSeasonsOverlap() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary left = factory.BuildEmpty(); ConditionDictionary right = factory.BuildEmpty(); left.Add(ConditionKey.Season, new[] { "Spring", "SUMMer" }); // should match case-insensitively right.Add(ConditionKey.Season, new[] { "Summer", "Fall" }); // act bool canOverlap = factory.CanConditionsOverlap(left, right); // assert canOverlap.Should().BeTrue(); }
public void CanConditionsOverlap_WhenDaysAndDaysOfWeekDistinct() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary left = factory.BuildEmpty(); ConditionDictionary right = factory.BuildEmpty(); left.Add(ConditionKey.Day, new[] { "1", "2" }); right.Add(ConditionKey.DayOfWeek, new[] { DayOfWeek.Wednesday.ToString(), DayOfWeek.Thursday.ToString() }); // act bool canOverlap = factory.CanConditionsOverlap(left, right); // assert canOverlap.Should().BeFalse(); }
/// <summary>Get all possible values of a tokenable string.</summary> /// <param name="tokenable">The tokenable string.</param> /// <param name="conditions">The conditions for which to filter permutations.</param> public IEnumerable <string> GetPossibleStrings(TokenString tokenable, ConditionDictionary conditions) { // no tokens: return original string if (!tokenable.ConditionTokens.Any()) { yield return(tokenable.Raw); yield break; } // yield token permutations foreach (IDictionary <ConditionKey, string> permutation in this.GetApplicablePermutationsForTheseConditions(tokenable.ConditionTokens, conditions)) { yield return(tokenable.GetStringWithTokens(permutation)); } }
public void CanConditionsOverlap_WhenWeathersDistinct() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary left = factory.BuildEmpty(); ConditionDictionary right = factory.BuildEmpty(); left.Add(ConditionKey.Weather, new[] { Weather.Rain.ToString(), Weather.Snow.ToString() }); right.Add(ConditionKey.Weather, new[] { Weather.Storm.ToString(), Weather.Sun.ToString() }); // act bool canOverlap = factory.CanConditionsOverlap(left, right); // assert canOverlap.Should().BeFalse(); }
public void CanConditionsOverlap_WhenSeasonsDistinct() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary left = factory.BuildEmpty(); ConditionDictionary right = factory.BuildEmpty(); left.Add(ConditionKey.Season, new[] { "Spring", "Summer" }); right.Add(ConditionKey.Season, new[] { "Fall", "Winter" }); // act bool canOverlap = factory.CanConditionsOverlap(left, right); // assert canOverlap.Should().BeFalse(); }
/// <summary>Get all possible values of the given conditions, taking into account the interactions between them (e.g. day one isn't possible without mondays).</summary> /// <param name="conditions">The conditions to check.</param> public IDictionary <ConditionKey, InvariantHashSet> GetPossibleValues(ConditionDictionary conditions) { // get applicable values IDictionary <ConditionKey, InvariantHashSet> values = new Dictionary <ConditionKey, InvariantHashSet>(); foreach (ConditionKey key in this.GetValidConditions()) { values[key] = new InvariantHashSet(conditions.GetImpliedValues(key)); } // filter days per days-of-week if (values[ConditionKey.DayOfWeek].Count < this.GetValidValues(ConditionKey.DayOfWeek).Count()) { // get covered days HashSet <string> possibleDays = new HashSet <string>(); foreach (string str in values[ConditionKey.DayOfWeek]) { DayOfWeek dayOfWeek = (DayOfWeek)Enum.Parse(typeof(DayOfWeek), str); foreach (int day in this.GetDaysFor(dayOfWeek)) { possibleDays.Add(day.ToString()); } } // remove non-covered days values[ConditionKey.Day].RemoveWhere(p => !possibleDays.Contains(p)); } // filter days-of-week per days if (values[ConditionKey.Day].Count < this.GetValidValues(ConditionKey.Day).Count()) { // get covered days of week HashSet <string> possibleDaysOfWeek = new HashSet <string>(); foreach (string str in values[ConditionKey.Day]) { int day = int.Parse(str); possibleDaysOfWeek.Add(this.GetDayOfWeekFor(day).ToString()); } // remove non-covered days of week values[ConditionKey.DayOfWeek].RemoveWhere(p => !possibleDaysOfWeek.Contains(p)); } return(values); }
public void GetPossibleValues_WithImpliedConditionsAndRestrictedDaysOfWeek() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary conditions = factory.BuildEmpty(); conditions.Add(ConditionKey.DayOfWeek, new[] { DayOfWeek.Tuesday.ToString(), DayOfWeek.Wednesday.ToString() }); // act IDictionary <ConditionKey, InvariantHashSet> possibleValues = factory.GetPossibleTokenisableValues(conditions); // assert this.SortAndCommaDelimit(possibleValues[ConditionKey.Day]).Should().Be("10, 16, 17, 2, 23, 24, 3, 9", $"should match for {ConditionKey.Day}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.DayOfWeek]).Should().Be($"{DayOfWeek.Tuesday}, {DayOfWeek.Wednesday}", $"should match for {ConditionKey.DayOfWeek}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Language]).Should().Be(this.CommaDelimitedValues[ConditionKey.Language], $"should match for {ConditionKey.Language}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Season]).Should().Be(this.CommaDelimitedValues[ConditionKey.Season], $"should match for {ConditionKey.Season}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Weather]).Should().Be(this.CommaDelimitedValues[ConditionKey.Weather], $"should match for {ConditionKey.Weather}"); }
public void GetPossibleValues_WithSubsetForEachCondition() { // arrange ConditionFactory factory = new ConditionFactory(); ConditionDictionary conditions = factory.BuildEmpty(); conditions.Add(ConditionKey.Day, new[] { "1" /*Monday*/, "2" /*Tuesday*/, "17" /*Wednesday*/, "26" /*Friday*/, "28" /*Sunday*/ }); conditions.Add(ConditionKey.DayOfWeek, new[] { DayOfWeek.Monday.ToString(), DayOfWeek.Thursday.ToString(), DayOfWeek.Saturday.ToString(), DayOfWeek.Sunday.ToString() }); conditions.Add(ConditionKey.Language, new[] { LocalizedContentManager.LanguageCode.en.ToString(), LocalizedContentManager.LanguageCode.pt.ToString() }); conditions.Add(ConditionKey.Season, new[] { "Spring", "Fall" }); conditions.Add(ConditionKey.Weather, new InvariantHashSet { Weather.Rain.ToString(), Weather.Sun.ToString() }); // act IDictionary <ConditionKey, InvariantHashSet> possibleValues = factory.GetPossibleTokenisableValues(conditions); // assert this.SortAndCommaDelimit(possibleValues[ConditionKey.Day]).Should().Be("1, 28", $"should match for {ConditionKey.Day}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.DayOfWeek]).Should().Be($"{DayOfWeek.Monday}, {DayOfWeek.Sunday}", $"should match for {ConditionKey.DayOfWeek}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Language]).Should().Be($"{LocalizedContentManager.LanguageCode.en}, {LocalizedContentManager.LanguageCode.pt}", $"should match for {ConditionKey.Language}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Season]).Should().Be("Fall, Spring", $"should match for {ConditionKey.Season}"); this.SortAndCommaDelimit(possibleValues[ConditionKey.Weather]).Should().Be($"{Weather.Rain}, {Weather.Sun}", $"should match for {ConditionKey.Weather}"); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="logName">A unique name for this patch shown in log messages.</param> /// <param name="assetLoader">Handles loading assets from content packs.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="assetName">The normalised asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromLocalAsset">The asset key to load from the content pack instead.</param> /// <param name="fromArea">The sprite area from which to read an image.</param> /// <param name="toArea">The sprite area to overwrite.</param> /// <param name="patchMode">Indicates how the image should be patched.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="normaliseAssetName">Normalise an asset name.</param> public EditImagePatch(string logName, AssetLoader assetLoader, IContentPack contentPack, TokenString assetName, ConditionDictionary conditions, TokenString fromLocalAsset, Rectangle fromArea, Rectangle toArea, PatchMode patchMode, IMonitor monitor, Func <string, string> normaliseAssetName) : base(logName, PatchType.EditImage, assetLoader, contentPack, assetName, conditions, normaliseAssetName) { this.FromLocalAsset = fromLocalAsset; this.FromArea = fromArea != Rectangle.Empty ? fromArea : null as Rectangle?; this.ToArea = toArea != Rectangle.Empty ? toArea : null as Rectangle?; this.PatchMode = patchMode; this.Monitor = monitor; }
/// <summary>Get the filtered permutations of conditions for the given keys.</summary> /// <param name="keys">The condition keys to include.</param> /// <param name="conditions">The conditions for which to filter permutations.</param> public IEnumerable <IDictionary <ConditionKey, string> > GetApplicablePermutationsForTheseConditions(ISet <ConditionKey> keys, ConditionDictionary conditions) { // get possible values for given conditions IDictionary <ConditionKey, InvariantHashSet> possibleValues = this.GetPossibleTokenisableValues(conditions); // restrict permutations to relevant keys foreach (ConditionKey key in possibleValues.Keys.ToArray()) { if (!keys.Contains(key)) { possibleValues.Remove(key); } } // get permutations var rawValues = new InvariantDictionary <InvariantHashSet>(possibleValues.ToDictionary(p => p.Key.ToString(), p => p.Value)); foreach (InvariantDictionary <string> permutation in this.GetPermutations(rawValues)) { yield return(permutation.ToDictionary(p => ConditionKey.Parse(p.Key), p => p.Value)); } }
/**** ** Condition parsing ****/ /// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> public bool TryParseConditions(InvariantDictionary <string> raw, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, out ConditionDictionary conditions, out string error) { // no conditions if (raw == null || !raw.Any()) { conditions = this.ConditionFactory.BuildEmpty(); error = null; return(true); } // parse conditions conditions = this.ConditionFactory.BuildEmpty(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!Enum.TryParse(pair.Key, true, out ConditionKey key)) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", this.ConditionFactory.GetValidConditions())}"; conditions = null; return(false); } // check compatibility if (formatVersion.IsOlderThan("1.4")) { if (key == ConditionKey.DayEvent || key == ConditionKey.HasFlag || key == ConditionKey.HasSeenEvent || key == ConditionKey.Spouse) { error = $"{key} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)"; conditions = null; return(false); } } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{key} can't be empty"; conditions = null; return(false); } // restrict to allowed values string[] rawValidValues = this.ConditionFactory.GetValidValues(key)?.ToArray(); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {key} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // create condition conditions[key] = new Condition(key, values); } // return parsed conditions error = null; return(true); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="logName">A unique name for this patch shown in log messages.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="assetName">The normalised asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="localAsset">The asset key to load from the content pack instead.</param> /// <param name="normaliseAssetName">Normalise an asset name.</param> public LoadPatch(string logName, ManagedContentPack contentPack, TokenString assetName, ConditionDictionary conditions, TokenString localAsset, Func <string, string> normaliseAssetName) : base(logName, PatchType.Load, contentPack, assetName, conditions, normaliseAssetName) { this.LocalAsset = localAsset; }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="logName">A unique name for this patch shown in log messages.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="assetName">The normalised asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="records">The data records to edit.</param> /// <param name="fields">The data fields to edit.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="normaliseAssetName">Normalise an asset name.</param> public EditDataPatch(string logName, ManagedContentPack contentPack, TokenString assetName, ConditionDictionary conditions, IDictionary <string, string> records, IDictionary <string, IDictionary <int, string> > fields, IMonitor monitor, Func <string, string> normaliseAssetName) : base(logName, PatchType.EditData, contentPack, assetName, conditions, normaliseAssetName) { this.Records = records; this.Fields = fields; this.Monitor = monitor; }
/// <summary>Prepare a local asset file for a patch to use.</summary> /// <param name="pack">The content pack being loaded.</param> /// <param name="path">The asset path in the content patch.</param> /// <param name="config">The config values to apply.</param> /// <param name="conditions">The conditions to apply.</param> /// <param name="error">The error reason if preparing the asset fails.</param> /// <param name="tokenedPath">The parsed value.</param> /// <param name="shouldPreload">Whether to preload assets if needed.</param> /// <returns>Returns whether the local asset was successfully prepared.</returns> private bool TryPrepareLocalAsset(ManagedContentPack pack, string path, InvariantDictionary <ConfigField> config, ConditionDictionary conditions, out string error, out TokenString tokenedPath, bool shouldPreload) { // normalise raw value path = this.NormaliseLocalAssetPath(pack, path); if (path == null) { error = $"must set the {nameof(PatchConfig.FromFile)} field for this action type."; tokenedPath = null; return(false); } // tokenise if (!this.TryParseTokenString(path, config, out string tokenError, out TokenStringBuilder builder)) { error = $"the {nameof(PatchConfig.FromFile)} is invalid: {tokenError}"; tokenedPath = null; return(false); } tokenedPath = builder.Build(); // preload & validate possible file paths InvariantHashSet missingFiles = new InvariantHashSet(); foreach (string localKey in this.ConditionFactory.GetPossibleStrings(tokenedPath, conditions)) { if (!pack.FileExists(localKey)) { missingFiles.Add(localKey); } else if (shouldPreload) { pack.PreloadIfNeeded(localKey); } } if (missingFiles.Any()) { error = tokenedPath.ConditionTokens.Any() || missingFiles.Count > 1 ? $"{nameof(PatchConfig.FromFile)} '{path}' matches files which don't exist ({string.Join(", ", missingFiles.OrderBy(p => p))})." : $"{nameof(PatchConfig.FromFile)} '{path}' matches a file which doesn't exist."; tokenedPath = null; return(false); } // looks OK error = null; return(true); }
/// <summary>Prepare a local asset file for a patch to use.</summary> /// <param name="pack">The content pack being loaded.</param> /// <param name="path">The asset path in the content patch.</param> /// <param name="config">The config values to apply.</param> /// <param name="conditions">The conditions to apply.</param> /// <param name="error">The error reason if preparing the asset fails.</param> /// <param name="tokenedPath">The parsed value.</param> /// <param name="checkOnly">Prepare the asset info and check if it's valid, but don't actually preload the asset.</param> /// <returns>Returns whether the local asset was successfully prepared.</returns> private bool TryPrepareLocalAsset(IContentPack pack, string path, InvariantDictionary <ConfigField> config, ConditionDictionary conditions, out string error, out TokenString tokenedPath, bool checkOnly) { // normalise raw value path = this.NormaliseLocalAssetPath(pack, path); if (path == null) { error = $"must set the {nameof(PatchConfig.FromFile)} field for this action type."; tokenedPath = null; return(false); } // tokenise if (!this.TryParseTokenString(path, config, out string tokenError, out TokenStringBuilder builder)) { error = $"the {nameof(PatchConfig.FromFile)} is invalid: {tokenError}"; tokenedPath = null; return(false); } tokenedPath = builder.Build(); // validate all possible files exist // + preload PNG assets to avoid load-in-draw-loop error InvariantHashSet missingFiles = new InvariantHashSet(); foreach (string localKey in this.ConditionFactory.GetPossibleStrings(tokenedPath, conditions)) { // check-only mode if (checkOnly) { if (!this.AssetLoader.FileExists(pack, localKey)) { missingFiles.Add(localKey); } continue; } // else preload try { if (this.AssetLoader.PreloadIfNeeded(pack, localKey)) { this.VerboseLog($" preloaded {localKey}."); } } catch (FileNotFoundException) { missingFiles.Add(localKey); } } if (missingFiles.Any()) { error = tokenedPath.ConditionTokens.Any() || missingFiles.Count > 1 ? $"{nameof(PatchConfig.FromFile)} '{path}' matches files which don't exist ({string.Join(", ", missingFiles.OrderBy(p => p))})." : $"{nameof(PatchConfig.FromFile)} '{path}' matches a file which doesn't exist."; tokenedPath = null; return(false); } // looks OK error = null; return(true); }
/********* ** Protected methods *********/ /// <summary>Construct an instance.</summary> /// <param name="logName">A unique name for this patch shown in log messages.</param> /// <param name="type">The patch type.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="assetName">The normalised asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="normaliseAssetName">Normalise an asset name.</param> protected Patch(string logName, PatchType type, ManagedContentPack contentPack, TokenString assetName, ConditionDictionary conditions, Func <string, string> normaliseAssetName) { this.LogName = logName; this.Type = type; this.ContentPack = contentPack; this.TokenableAssetName = assetName; this.Conditions = conditions; this.NormaliseAssetName = normaliseAssetName; }
/// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="tokenContext">The tokens available for this content pack.</param> /// <param name="migrator">The migrator which validates and migrates content pack data.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> private bool TryParseConditions(InvariantDictionary <string> raw, IContext tokenContext, IMigration migrator, out ConditionDictionary conditions, out string error) { conditions = new ConditionDictionary(); // no conditions if (raw == null || !raw.Any()) { error = null; return(true); } // parse conditions Lexer lexer = new Lexer(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key ILexToken[] lexTokens = lexer.ParseBits(pair.Key, impliedBraces: true).ToArray(); if (lexTokens.Length != 1 || !(lexTokens[0] is LexTokenToken lexToken) || lexToken.PipedTokens.Any()) { error = $"'{pair.Key}' isn't a valid token name"; conditions = null; return(false); } TokenName name = new TokenName(lexToken.Name, lexToken.InputArg?.Text); // apply migrations if (!migrator.TryMigrate(ref name, out error)) { conditions = null; return(false); } // get token IToken token = tokenContext.GetToken(name, enforceContext: false); if (token == null) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", tokenContext.GetTokens(enforceContext: false).Select(p => p.Name).OrderBy(p => p))}"; conditions = null; return(false); } // validate subkeys if (!token.CanHaveSubkeys) { if (name.HasSubkey()) { error = $"{name.Key} conditions don't allow subkeys (:)"; conditions = null; return(false); } } else if (token.RequiresSubkeys) { if (!name.HasSubkey()) { error = $"{name.Key} conditions must specify a token subkey (see readme for usage)"; conditions = null; return(false); } } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{name} can't be empty"; conditions = null; return(false); } // validate token keys & values if (!token.TryValidate(name, values, out string customError)) { error = $"invalid {name} condition: {customError}"; conditions = null; return(false); } // create condition conditions[name] = new Condition(name, values); } // return parsed conditions error = null; return(true); }
/// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="tokenContext">The tokens available for this content pack.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="minumumTokenVersions">The minimum format versions for newer condition types.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> private bool TryParseConditions(InvariantDictionary <string> raw, IContext tokenContext, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, out ConditionDictionary conditions, out string error) { conditions = new ConditionDictionary(); // no conditions if (raw == null || !raw.Any()) { error = null; return(true); } // parse conditions foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!TokenName.TryParse(pair.Key, out TokenName name)) { error = $"'{pair.Key}' isn't a valid token name"; conditions = null; return(false); } // get token IToken token = tokenContext.GetToken(name, enforceContext: false); if (token == null) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", tokenContext.GetTokens(enforceContext: false).Select(p => p.Name).OrderBy(p => p))}"; conditions = null; return(false); } // validate subkeys if (!token.CanHaveSubkeys) { if (name.HasSubkey()) { error = $"{name.Key} conditions don't allow subkeys (:)"; conditions = null; return(false); } } else if (token.RequiresSubkeys) { if (!name.HasSubkey()) { error = $"{name.Key} conditions must specify a token subkey (see readme for usage)"; conditions = null; return(false); } } // check compatibility if (minumumTokenVersions.TryGetValue(name.Key, out ISemanticVersion minVersion) && minVersion.IsNewerThan(formatVersion)) { error = $"{name} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)"; conditions = null; return(false); } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{name} can't be empty"; conditions = null; return(false); } // restrict to allowed values InvariantHashSet rawValidValues = token.GetAllowedValues(name); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {name} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // perform custom validation if (!token.TryCustomValidation(values, out string customError)) { error = $"invalid {name} values: {customError}"; conditions = null; return(false); } // create condition conditions[name] = new Condition(name, values); } // return parsed conditions error = null; return(true); }
/**** ** Condition parsing ****/ /// <summary>Normalise and parse the given condition values.</summary> /// <param name="raw">The raw condition values to normalise.</param> /// <param name="formatVersion">The format version specified by the content pack.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="conditions">The normalised conditions.</param> /// <param name="error">An error message indicating why normalisation failed.</param> public bool TryParseConditions(InvariantDictionary <string> raw, ISemanticVersion formatVersion, ISemanticVersion latestFormatVersion, out ConditionDictionary conditions, out string error) { // no conditions if (raw == null || !raw.Any()) { conditions = this.ConditionFactory.BuildEmpty(); error = null; return(true); } // parse conditions conditions = this.ConditionFactory.BuildEmpty(); foreach (KeyValuePair <string, string> pair in raw) { // parse condition key if (!ConditionKey.TryParse(pair.Key, out ConditionKey key)) { error = $"'{pair.Key}' isn't a valid condition; must be one of {string.Join(", ", Enum.GetValues(typeof(ConditionType)))}"; conditions = null; return(false); } // validate types which require an ID if (this.TypesRequireID.Contains(key.Type) && string.IsNullOrWhiteSpace(key.ForID)) { error = $"{key.Type} conditions must specify a separate ID (see readme for usage)"; conditions = null; return(false); } // check compatibility foreach (var versionPair in this.MinimumVersions) { if (formatVersion.IsOlderThan(versionPair.Key) && versionPair.Value.Contains(key.Type)) { error = $"{key} isn't available with format version {formatVersion} (change the {nameof(ContentConfig.Format)} field to {latestFormatVersion} to use newer features)"; conditions = null; return(false); } } // parse values InvariantHashSet values = this.ParseCommaDelimitedField(pair.Value); if (!values.Any()) { error = $"{key} can't be empty"; conditions = null; return(false); } // restrict to allowed values string[] rawValidValues = this.ConditionFactory.GetValidValues(key)?.ToArray(); if (rawValidValues?.Any() == true) { InvariantHashSet validValues = new InvariantHashSet(rawValidValues); { string[] invalidValues = values.Except(validValues, StringComparer.InvariantCultureIgnoreCase).ToArray(); if (invalidValues.Any()) { error = $"invalid {key} values ({string.Join(", ", invalidValues)}); expected one of {string.Join(", ", validValues)}"; conditions = null; return(false); } } } // create condition conditions[key] = new Condition(key, values); } // return parsed conditions error = null; return(true); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="logName">A unique name for this patch shown in log messages.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="assetName">The normalised asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="records">The data records to edit.</param> /// <param name="fields">The data fields to edit.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="normaliseAssetName">Normalise an asset name.</param> public EditDataPatch(string logName, ManagedContentPack contentPack, TokenString assetName, ConditionDictionary conditions, IEnumerable <EditDataPatchRecord> records, IEnumerable <EditDataPatchField> fields, IMonitor monitor, Func <string, string> normaliseAssetName) : base(logName, PatchType.EditData, contentPack, assetName, conditions, normaliseAssetName) { this.Records = records.ToArray(); this.Fields = fields.ToArray(); this.Monitor = monitor; this.MutableTokenStrings = this.GetTokenStrings(this.Records, this.Fields).Where(str => str.Tokens.Any()).ToArray(); }