Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Common/Utilities/IInvariantSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal interface IInvariantSet : IReadOnlySet<string>

/// <summary>Get an invariant set with the given values added to this set's values.</summary>
/// <param name="other">The values to add.</param>
IInvariantSet GetWith(ICollection<string> other);
IInvariantSet GetWith(IEnumerable<string> other);

/// <summary>Get an invariant set with the given value removed from this set's values.</summary>
/// <param name="other">The value to remove.</param>
Expand Down
5 changes: 3 additions & 2 deletions Common/Utilities/InvariantSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;

namespace Pathoschild.Stardew.Common.Utilities;

Expand Down Expand Up @@ -139,9 +140,9 @@ public IInvariantSet GetWith(string other)
}

/// <inheritdoc cref="IInvariantSet" />
public IInvariantSet GetWith(ICollection<string> other)
public IInvariantSet GetWith(IEnumerable<string> other)
{
if (other.Count == 0)
if (!other.Any())
return this;

if (this.Count == 0)
Expand Down
6 changes: 6 additions & 0 deletions ContentPatcher/Framework/AggregateContextual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ public IInvariantSet GetTokensUsed()
);
}

/// <summary>Get the token names used by this entity as mutable invariant set.</summary>
public MutableInvariantSet GetMutableTokensUsed()
{
return [.. this.ValuesImpl.SelectMany(p => p.GetTokensUsed())];
}

/// <inheritdoc />
public IContextualState GetDiagnosticState()
{
Expand Down
23 changes: 23 additions & 0 deletions ContentPatcher/Framework/ConfigModels/ContentConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Pathoschild.Stardew.Common;
using Pathoschild.Stardew.Common.Utilities;
using StardewModdingAPI;
Expand Down Expand Up @@ -49,4 +52,24 @@ public ContentConfig(ISemanticVersion? format, DynamicTokenConfig?[]? dynamicTok
this.Changes = changes?.WhereNotNull().ToArray() ?? [];
this.ConfigSchema = configSchema ?? new InvariantDictionary<ConfigSchemaFieldConfig?>();
}


/// <summary>Get the content fields which aren't allowed for a secondary file which were set.</summary>
/// <param name="content">The content to validate.</param>
public IEnumerable<string> GetInvalidFieldsForSubFile(params string[] allowedFields)
{
foreach (PropertyInfo property in typeof(ContentConfig).GetProperties())
{
if (allowedFields.Contains(property.Name))
continue;

object? value = property.GetValue(this);
bool hasValue = value is IEnumerable list
? list.Cast<object>().Any()
: value != null;

if (hasValue)
yield return property.Name;
}
}
}
7 changes: 6 additions & 1 deletion ContentPatcher/Framework/ConfigModels/DynamicTokenConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Pathoschild.Stardew.Common.Utilities;
using StardewModdingAPI.Utilities;

namespace ContentPatcher.Framework.ConfigModels;

Expand All @@ -14,6 +15,9 @@ internal class DynamicTokenConfig
/// <summary>The value to set.</summary>
public string? Value { get; }

/// <summary>If set, include tokens from a dynamic token include file.</summary>
public string? IncludeFromFile { get; }

/// <summary>The criteria to apply. See the README for valid values.</summary>
public InvariantDictionary<string?> When { get; }

Expand All @@ -25,10 +29,11 @@ internal class DynamicTokenConfig
/// <param name="name">The name of the token to set.</param>
/// <param name="value">The value to set.</param>
/// <param name="when">The criteria to apply. See the README for valid values.</param>
public DynamicTokenConfig(string name, string value, InvariantDictionary<string?>? when)
public DynamicTokenConfig(string name, string value, string? includeFromFile, InvariantDictionary<string?>? when)
{
this.Name = name;
this.Value = value;
this.IncludeFromFile = PathUtilities.NormalizePath(includeFromFile);
this.When = when ?? new();
}
}
58 changes: 43 additions & 15 deletions ContentPatcher/Framework/ModTokenContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ internal class ModTokenContext : IContext
/// <summary>The dynamic tokens stored in the <see cref="DynamicContext"/>.</summary>
private readonly InvariantDictionary<ManagedManualToken> DynamicTokens = new();

/// <summary>The possible values for the <see cref="DynamicTokens"/>.</summary>
/// <remarks>These must be stored in registration order, since each token value may affect the value of subsequent tokens.</remarks>
private readonly List<DynamicTokenValue> DynamicTokenValues = [];
/// <summary>
/// The contextual entities for the <see cref="DynamicTokens"/> that can receive context updates in <see cref="UpdateContext"/>.
/// They are either associated with a particular <see cref="DynamicTokens"/>, or serves only as parent to subsequent values.
/// </summary>
/// <remarks>These must be stored in registration order, since each contextual value may affect the value of subsequent tokens.</remarks>
private readonly List<DynamicTokenContextual> DynamicTokenContextualList = [];

/// <summary>The alias token names defined for the content pack.</summary>
private readonly InvariantDictionary<string> AliasTokenNames = new();
Expand Down Expand Up @@ -104,7 +107,8 @@ public void RemoveLocalToken(string name)
/// <param name="name">The token name.</param>
/// <param name="rawValue">The token value to set.</param>
/// <param name="conditions">The conditions that must match to set this value.</param>
public void AddDynamicToken(string name, IManagedTokenString rawValue, Condition[] conditions)
/// <param name="parent">The parent contextual entity of this dynamic token value.</param>
public void AddDynamicToken(string name, IManagedTokenString rawValue, Condition[] conditions, DynamicTokenContextual? parent)
{
// validate
if (this.ParentContext.Contains(name, enforceContext: false))
Expand All @@ -121,7 +125,7 @@ public void AddDynamicToken(string name, IManagedTokenString rawValue, Condition
}

// create token value handler
var tokenValue = new DynamicTokenValue(managed, rawValue, conditions);
var tokenValue = new DynamicTokenValue(managed, rawValue, conditions, parent);
IInvariantSet tokensUsed = tokenValue.GetTokensUsed().GetWithout(name);

// save value info
Expand All @@ -132,8 +136,29 @@ public void AddDynamicToken(string name, IManagedTokenString rawValue, Condition
managed.ValueProvider.SetValue(tokenValue.Value);
managed.ValueProvider.SetReady(true);
}
this.DynamicTokenValues.Add(tokenValue);

this.AddDynamicTokenContextual(tokenValue, tokensUsed);
}

/// <summary>Add a dynamic token value to the context.</summary>
/// <param name="name">The include name.</param>
/// <param name="conditions">The conditions to be updated in this context.</param>
/// <param name="parent">The parent contextual entity of this dynamic token value.</param>
/// <returns></returns>
public DynamicTokenContextual AddDynamicTokenInclude(string name, Condition[] conditions, DynamicTokenContextual? parent)
{
var tokenGroup = new DynamicTokenContextual(name, conditions, parent);
this.AddDynamicTokenContextual(tokenGroup, tokenGroup.GetTokensUsed().GetWithout(name));
return tokenGroup;
}

/// <summary>Add a dynamic token contextual entity.</summary>
/// <param name="tokenCtx">Token contextual entity to add</param>
/// <param name="tokensUsed">Tokens this contextual entity depends on</param>
private void AddDynamicTokenContextual(DynamicTokenContextual tokenCtx, IInvariantSet tokensUsed)
{
this.DynamicTokenContextualList.Add(tokenCtx);
string name = tokenCtx.Name;
// track token dependencies
if (tokensUsed.Any())
{
Expand All @@ -152,7 +177,6 @@ public void AddDynamicToken(string name, IManagedTokenString rawValue, Condition
{
if (!this.DynamicTokenDependents.TryGetValue(dependency, out MutableInvariantSet? dependents))
this.DynamicTokenDependents[dependency] = dependents = [];

dependents.Add(name);
}

Expand Down Expand Up @@ -260,18 +284,22 @@ public void UpdateContext(IInvariantSet globalChangedTokens)
}
}

foreach (DynamicTokenValue tokenValue in this.DynamicTokenValues)
foreach (DynamicTokenContextual tokenCtx in this.DynamicTokenContextualList)
{
if (!resetDynamicTokens && !updateDynamicTokens!.Contains(tokenValue.Name))
if (!resetDynamicTokens && !updateDynamicTokens!.Contains(tokenCtx.Name))
{
continue;

tokenValue.UpdateContext(this);
if (tokenValue.IsReady && tokenValue.Conditions.All(p => p.IsMatch))
}
tokenCtx.UpdateContext(this);
if (tokenCtx is DynamicTokenValue tokenValue)
{
ManualValueProvider valueProvider = tokenValue.ParentToken.ValueProvider;
if (tokenValue.IsReady && tokenValue.AllConditionsMatch())
{
ManualValueProvider valueProvider = tokenValue.ParentToken.ValueProvider;

valueProvider.SetValue(tokenValue.Value);
valueProvider.SetReady(true);
valueProvider.SetValue(tokenValue.Value);
valueProvider.SetReady(true);
}
}
}
}
Expand Down
25 changes: 19 additions & 6 deletions ContentPatcher/Framework/PatchLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,40 +144,53 @@ public void UnloadPatchesLoadedBy(RawContentPack pack)
/// <param name="tokenParser">Handles low-level parsing and validation for tokens.</param>
/// <param name="path">The path to the value from the root content file.</param>
/// <param name="conditions">The normalized conditions.</param>
/// <param name="immutableRequiredModIds">The immutable mod IDs always required by these conditions (if they're <see cref="ConditionType.HasMod"/> and immutable).</param>
/// <param name="requiredModIds">Mutable set of mod IDs always required by these conditions (if they're <see cref="ConditionType.HasMod"/> and immutable).</param>
/// <param name="error">An error message indicating why normalization failed.</param>
public bool TryParseConditions(IDictionary<string, string?>? raw, TokenParser tokenParser, LogPathBuilder path, out Condition[] conditions, out IInvariantSet immutableRequiredModIds, [NotNullWhen(false)] out string? error)
public bool TryParseConditions(IDictionary<string, string?>? raw, TokenParser tokenParser, LogPathBuilder path, out Condition[] conditions, ref MutableInvariantSet? requiredModIds, [NotNullWhen(false)] out string? error)
{
// no conditions
if (raw == null || !raw.Any())
{
immutableRequiredModIds = InvariantSets.Empty;
conditions = [];
error = null;
return true;
}

// parse conditions
MutableInvariantSet requiredModIds = [];
requiredModIds ??= [];
InvariantDictionary<Condition> parsed = new();
foreach ((string key, string? value) in raw.OrderBy(p => this.GetConditionParseOrder(p.Key, p.Value)))
{
if (!this.TryParseCondition(key, value, tokenParser, path.With(key), out Condition? condition, ref requiredModIds, out error))
{
immutableRequiredModIds = InvariantSets.Empty;
requiredModIds = null;
conditions = [];
return false;
}

parsed[key] = condition;
}

immutableRequiredModIds = requiredModIds.Lock();
conditions = parsed.Values.ToArray();
error = null;
return true;
}

/// <summary>Normalize and parse the given condition values.</summary>
/// <param name="raw">The raw condition values to normalize.</param>
/// <param name="tokenParser">Handles low-level parsing and validation for tokens.</param>
/// <param name="path">The path to the value from the root content file.</param>
/// <param name="conditions">The normalized conditions.</param>
/// <param name="immutableRequiredModIDs">The immutable mod IDs always required by these conditions (if they're <see cref="ConditionType.HasMod"/> and immutable).</param>
/// <param name="error">An error message indicating why normalization failed.</param>
public bool TryParseConditions(IDictionary<string, string?>? raw, TokenParser tokenParser, LogPathBuilder path, out Condition[] conditions, out IInvariantSet immutableRequiredModIDs, [NotNullWhen(false)] out string? error)
{
MutableInvariantSet? requiredModIDs = null;
bool result = this.TryParseConditions(raw, tokenParser, path, out conditions, ref requiredModIDs, out error);
immutableRequiredModIDs = requiredModIDs?.Lock() ?? InvariantSet.Empty;
return result;
}

/// <summary>Normalize and parse the given update rate.</summary>
/// <param name="raw">The raw update rate to normalize.</param>
/// <param name="updateRate">The normalized update rate.</param>
Expand Down
25 changes: 2 additions & 23 deletions ContentPatcher/Framework/Patches/IncludePatch.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ContentPatcher.Framework.Conditions;
using ContentPatcher.Framework.ConfigModels;
using ContentPatcher.Framework.Tokens;
Expand Down Expand Up @@ -144,10 +142,10 @@ public void AttemptLoad()
}

// validate fields
string[] invalidFields = this.GetInvalidFields(content).ToArray();
var invalidFields = content.GetInvalidFieldsForSubFile(nameof(ContentConfig.Changes));
if (invalidFields.Any())
{
this.WarnForPatch($"file contains fields which aren't allowed for a secondary file ({string.Join(", ", invalidFields.OrderByHuman())}).");
this.WarnForPatch($"file contains fields which aren't allowed for a content include file ({string.Join(", ", invalidFields.OrderByHuman())}).");
return;
}

Expand Down Expand Up @@ -206,25 +204,6 @@ private bool IsSameFilePath(string? left, string? right)
return left.EqualsIgnoreCase(right);
}

/// <summary>Get the content fields which aren't allowed for a secondary file which were set.</summary>
/// <param name="content">The content to validate.</param>
private IEnumerable<string> GetInvalidFields(ContentConfig content)
{
foreach (PropertyInfo property in typeof(ContentConfig).GetProperties())
{
if (property.Name == nameof(ContentConfig.Changes))
continue;

object? value = property.GetValue(content);
bool hasValue = value is IEnumerable list
? list.Cast<object>().Any()
: value != null;

if (hasValue)
yield return property.Name;
}
}

/// <summary>Get the root log path for included patches.</summary>
/// <param name="filePath">The file path being loaded.</param>
private LogPathBuilder GetIncludedLogPath(string filePath)
Expand Down
Loading