/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } // before 1.6, the 'sun' weather included 'wind' foreach (PatchConfig patch in content.Changes) { if (patch.When != null && patch.When.TryGetValue(ConditionType.Weather.ToString(), out string value) && value.Contains("Sun")) { patch.When[ConditionType.Weather.ToString()] = $"{value}, Wind"; } } return(true); }
/// <inheritdoc /> public override bool TryMigrate(ContentConfig content, [NotNullWhen(false)] out string?error) { if (!base.TryMigrate(content, out error)) { return(false); } // before 1.6, the 'sun' weather included 'wind' foreach (PatchConfig patch in content.Changes.WhereNotNull()) { if (patch.When.TryGetValue(nameof(ConditionType.Weather), out string?value) && value?.Contains("Sun") == true) { patch.When[nameof(ConditionType.Weather)] = $"{value}, Wind"; } } return(true); }
/// <inheritdoc /> public override bool TryMigrate(ContentConfig content, [NotNullWhen(false)] out string?error) { if (!base.TryMigrate(content, out error)) { return(false); } foreach (PatchConfig?patch in content.Changes) { // 1.16 adds Include if (this.HasAction(patch, PatchType.Include)) { error = this.GetNounPhraseError($"using action {nameof(PatchType.Include)}"); return(false); } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } foreach (PatchConfig patch in content.Changes) { // 1.13 adds map tile patches if (patch.MapTiles.Any()) { error = this.GetNounPhraseError($"using {nameof(PatchConfig.MapTiles)}"); return(false); } } return(true); }
/// <inheritdoc /> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } foreach (PatchConfig patch in content.Changes) { // 1.18 adds 'TextOperations' field if (patch.TextOperations.Any()) { error = this.GetNounPhraseError($"using {nameof(patch.TextOperations)}"); return(false); } } return(true); }
/// <inheritdoc /> public override bool TryMigrate(ContentConfig content, [NotNullWhen(false)] out string?error) { if (!base.TryMigrate(content, out error)) { return(false); } // 1.26 adds config sections foreach (ConfigSchemaFieldConfig?config in content.ConfigSchema.Values) { if (!string.IsNullOrWhiteSpace(config?.Section)) { error = this.GetNounPhraseError($"using {nameof(config.Section)} with a {nameof(content.ConfigSchema)} entry"); return(false); } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } foreach (PatchConfig patch in content.Changes) { // 1.16 adds Include if (Enum.TryParse(patch.Action, true, out PatchType action) && action == PatchType.Include) { error = this.GetNounPhraseError($"using action {nameof(PatchType.Include)}"); return(false); } } return(true); }
private IEnumerable <RawContentPack> GetContentPacks(IMigration[] migrations) { this.Monitor.VerboseLog("Preloading content packs..."); foreach (IContentPack contentPack in this.Helper.ContentPacks.GetOwned()) { RawContentPack rawContentPack; try { // validate content.json has required fields ContentConfig content = contentPack.ReadJsonFile <ContentConfig>(this.PatchFileName); if (content == null) { this.Monitor.Log($"Ignored content pack '{contentPack.Manifest.Name}' because it has no {this.PatchFileName} file.", LogLevel.Error); continue; } if (content.Format == null || content.Changes == null) { this.Monitor.Log($"Ignored content pack '{contentPack.Manifest.Name}' because it doesn't specify the required {nameof(ContentConfig.Format)} or {nameof(ContentConfig.Changes)} fields.", LogLevel.Error); continue; } // apply migrations IMigration migrator = new AggregateMigration(content.Format, this.SupportedFormatVersions, migrations); if (!migrator.TryMigrate(content, out string error)) { this.Monitor.Log($"Loading content pack '{contentPack.Manifest.Name}' failed: {error}.", LogLevel.Error); continue; } // init rawContentPack = new RawContentPack(new ManagedContentPack(contentPack), content, migrator); } catch (Exception ex) { this.Monitor.Log($"Error preloading content pack '{contentPack.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } yield return(rawContentPack); } }
public void ContentConfigToInformationTest1() { ContentConfig config = new ContentConfig { DashboardTitle = "Title", ContentProviders = new List <ContentProviderConfig> { new ContentProviderConfig { Id = "id1" } } }; Information info = ContentConfigToInformation.Map(config); Assert.NotNull(info); Assert.AreEqual(1, info.ContentProviders.Count); Assert.AreEqual(config.ContentProviders[0].Id, info.ContentProviders[0].Id); }
/// <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>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } if (content.Changes?.Any() == true) { foreach (PatchConfig patch in content.Changes) { // 1.17 adds 'Update' field if (patch.Update != null) { error = this.GetNounPhraseError($"specifying the patch update rate ('{nameof(patch.Update)}' field)"); return(false); } // pre-1.17 patches which used {{IsOutdoors}}/{{LocationName}} would update on location change // (Technically that applies to all references, but parsing tokens at this // point is difficult and a review of existing content packs shows that they // only used these as condition keys.) if (patch.When?.Any() == true) { bool hasLocationToken = patch.When.Keys.Any(key => !string.IsNullOrWhiteSpace(key) && (key.ContainsIgnoreCase("IsOutdoors") || key.ContainsIgnoreCase("LocationName")) && // quick check with false positives Regex.IsMatch(key, @"\b(?:IsOutdoors|LocationName)\b") // slower but reliable check ); if (hasLocationToken) { patch.Update = UpdateRate.OnLocationChange.ToString(); } } } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } // 1.8 adds MoveEntries if (content.Changes?.Any() == true) { foreach (PatchConfig patch in content.Changes) { if (patch.MoveEntries?.Any() == true) { error = this.GetNounPhraseError($"using {nameof(PatchConfig.MoveEntries)}"); return(false); } } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } if (content.Changes?.Any() == true) { foreach (PatchConfig patch in content.Changes) { // 1.10 allows 'FromFile' with 'EditData' patches if (patch.FromFile != null && Enum.TryParse(patch.Action, true, out PatchType action) && action == PatchType.EditData) { error = this.GetNounPhraseError($"using {nameof(PatchConfig.FromFile)} with action {nameof(PatchType.EditData)}"); return(false); } } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public bool TryMigrate(ContentConfig content, out string error) { // validate format version if (!this.ValidVersions.Contains(content.Format.ToString())) { error = $"unsupported format {content.Format} (supported version: {string.Join(", ", this.ValidVersions)})."; return(false); } // apply migrations foreach (IMigration migration in this.Migrations) { if (!migration.TryMigrate(content, out error)) { return(false); } } // no issues found error = null; return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } // 1.7 adds tokens in dynamic token values if (content.DynamicTokens != null) { if (content.DynamicTokens.Any(p => p.Value?.Contains("{{") == true)) { error = this.GetNounPhraseError("using tokens in dynamic token values"); return(false); } } // 1.7 adds tokens in field keys and condition values if (content.Changes?.Any() == true) { foreach (PatchConfig patch in content.Changes) { if (patch.Fields != null && patch.Fields.Keys.Any(key => key.Contains("{{"))) { error = this.GetNounPhraseError("using tokens in field keys"); return(false); } if (patch.When != null && patch.When.Any(condition => condition.Value?.Contains("{{") == true && !condition.Value.ContainsIgnoreCase("HasFile"))) { error = this.GetNounPhraseError("using tokens in condition values"); return(false); } } } return(true); }
/// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public override bool TryMigrate(ContentConfig content, out string error) { if (!base.TryMigrate(content, out error)) { return(false); } // 1.3 adds config.json if (content.ConfigSchema?.Any() == true) { error = this.GetNounPhraseError($"using the {nameof(ContentConfig.ConfigSchema)} field"); return(false); } // check patch format if (content.Changes?.Any() == true) { foreach (PatchConfig patch in content.Changes) { // 1.3 adds tokens in FromFile if (patch.FromFile != null && patch.FromFile.Contains("{{")) { error = this.GetNounPhraseError($"using the {{{{token}}}} feature in {nameof(PatchConfig.FromFile)} fields"); return(false); } // 1.3 adds When if (patch.When?.Any() == true) { error = this.GetNounPhraseError($"using the condition feature ({nameof(ContentConfig.Changes)}.{nameof(PatchConfig.When)} field)"); return(false); } } } return(true); }
private IEnumerable <ManagedContentPack> GetContentPacks() { this.VerboseLog("Preloading content packs..."); foreach (IContentPack contentPack in this.Helper.GetContentPacks()) { try { // validate content.json has required fields ContentConfig content = contentPack.ReadJsonFile <ContentConfig>(this.PatchFileName); if (content == null) { this.Monitor.Log($"Ignored content pack '{contentPack.Manifest.Name}' because it has no {this.PatchFileName} file.", LogLevel.Error); continue; } if (content.Format == null || content.Changes == null) { this.Monitor.Log($"Ignored content pack '{contentPack.Manifest.Name}' because it doesn't specify the required {nameof(ContentConfig.Format)} or {nameof(ContentConfig.Changes)} fields.", LogLevel.Error); continue; } // validate format if (!this.ValidateFormatVersion(contentPack, content, usesConfig: content.ConfigSchema?.Any() == true)) { continue; } } catch (Exception ex) { this.Monitor.Log($"Error preloading content pack '{contentPack.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } yield return(new ManagedContentPack(contentPack)); } }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="content">The content pack being validated.</param> public Migration_1_15_Rewrites(ContentConfig content) : base(new SemanticVersion(1, 15, 0)) { this.LocalTokenNames = new Lazy <ISet <string> >(() => this.GetLocalTokenNames(content)); }
private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks) { // load content packs ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}")); foreach (RawContentPack current in contentPacks) { this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'..."); try { ContentConfig content = current.Content; // load tokens ModTokenContext tokenContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack); { // load config.json InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema); configFileHandler.Save(current.ManagedPack, config, this.Helper); if (config.Any()) { this.Monitor.VerboseLog($" found config.json with {config.Count} fields..."); } // load config tokens foreach (KeyValuePair <string, ConfigField> pair in config) { ConfigField field = pair.Value; tokenContext.Add(new ImmutableToken(pair.Key, field.Value, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple)); } // load dynamic tokens foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0]) { void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn); // validate token key if (!TokenName.TryParse(entry.Name, out TokenName name)) { LogSkip("the name could not be parsed as a token key."); continue; } if (name.HasSubkey()) { LogSkip("the token name cannot contain a subkey (:)."); continue; } if (name.TryGetConditionType(out ConditionType conflictingType)) { LogSkip($"conflicts with global token '{conflictingType}'."); continue; } if (config.ContainsKey(name.Key)) { LogSkip($"conflicts with player config token '{conflictingType}'."); continue; } // parse values InvariantHashSet values = entry.Value != null?this.ParseCommaDelimitedField(entry.Value) : new InvariantHashSet(); // parse conditions ConditionDictionary conditions; { if (!this.TryParseConditions(entry.When, tokenContext, current.Migrator, out conditions, out string error)) { this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {error}.", LogLevel.Warn); continue; } } // add token tokenContext.Add(new DynamicTokenValue(name, values, conditions)); } } // load patches content.Changes = this.SplitPatches(content.Changes).ToArray(); this.NamePatches(current.ManagedPack, content.Changes); foreach (PatchConfig patch in content.Changes) { this.Monitor.VerboseLog($" loading {patch.LogName}..."); this.LoadPatch(current.ManagedPack, patch, tokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn)); } } catch (Exception ex) { this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } } }
/********* ** Public methods *********/ /// <inheritdoc /> public virtual bool TryMigrate(ContentConfig content, [NotNullWhen(false)] out string?error) { error = null; return(true); }
/********* ** Public methods *********/ /// <summary>Migrate a content pack.</summary> /// <param name="content">The content pack data to migrate.</param> /// <param name="error">An error message which indicates why migration failed.</param> /// <returns>Returns whether the content pack was successfully migrated.</returns> public virtual bool TryMigrate(ContentConfig content, out string error) { error = null; return(true); }
private void LoadContentPacks(IEnumerable <RawContentPack> contentPacks) { // load content packs ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}", LogLevel.Warn)); foreach (RawContentPack current in contentPacks) { this.Monitor.VerboseLog($"Loading content pack '{current.Manifest.Name}'..."); try { ContentConfig content = current.Content; // load tokens ModTokenContext modContext = this.TokenManager.TrackLocalTokens(current.ManagedPack.Pack); { // load config.json InvariantDictionary <ConfigField> config = configFileHandler.Read(current.ManagedPack, content.ConfigSchema, current.Content.Format); configFileHandler.Save(current.ManagedPack, config, this.Helper); if (config.Any()) { this.Monitor.VerboseLog($" found config.json with {config.Count} fields..."); } // load config tokens foreach (KeyValuePair <string, ConfigField> pair in config) { ConfigField field = pair.Value; modContext.Add(new ImmutableToken(pair.Key, field.Value, scope: current.Manifest.UniqueID, allowedValues: field.AllowValues, canHaveMultipleValues: field.AllowMultiple)); } // load dynamic tokens foreach (DynamicTokenConfig entry in content.DynamicTokens ?? new DynamicTokenConfig[0]) { void LogSkip(string reason) => this.Monitor.Log($"Ignored {current.Manifest.Name} > dynamic token '{entry.Name}': {reason}", LogLevel.Warn); // validate token key if (string.IsNullOrWhiteSpace(entry.Name)) { LogSkip("the token name can't be empty."); continue; } if (entry.Name.Contains(InternalConstants.InputArgSeparator)) { LogSkip($"the token name can't have an input argument ({InternalConstants.InputArgSeparator} character)."); continue; } if (Enum.TryParse <ConditionType>(entry.Name, true, out _)) { LogSkip("the token name is already used by a global token."); continue; } if (config.ContainsKey(entry.Name)) { LogSkip("the token name is already used by a config token."); continue; } // parse values ITokenString values; if (!string.IsNullOrWhiteSpace(entry.Value)) { if (!this.TryParseStringTokens(entry.Value, modContext, current.Migrator, out string valueError, out values)) { LogSkip($"the token value is invalid: {valueError}"); continue; } } else { values = new LiteralString(""); } // parse conditions IList <Condition> conditions; { if (!this.TryParseConditions(entry.When, modContext, current.Migrator, out conditions, out string conditionError)) { this.Monitor.Log($"Ignored {current.Manifest.Name} > '{entry.Name}' token: its {nameof(DynamicTokenConfig.When)} field is invalid: {conditionError}.", LogLevel.Warn); continue; } } // add token modContext.Add(new DynamicTokenValue(entry.Name, values, conditions)); } } // load patches IContext patchTokenContext = new SinglePatchContext(current.Manifest.UniqueID, parentContext: modContext); // make patch tokens available to patches content.Changes = this.SplitPatches(content.Changes).ToArray(); this.NamePatches(current.ManagedPack, content.Changes); foreach (PatchConfig patch in content.Changes) { this.Monitor.VerboseLog($" loading {patch.LogName}..."); this.LoadPatch(current.ManagedPack, patch, patchTokenContext, current.Migrator, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn)); } } catch (Exception ex) { this.Monitor.Log($"Error loading content pack '{current.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); continue; } } }
private void LoadContentPacks() { ConfigFileHandler configFileHandler = new ConfigFileHandler(this.ConfigFileName, this.PatchManager.ParseCommaDelimitedField, (pack, label, reason) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > {label}: {reason}")); foreach (IContentPack pack in this.Helper.GetContentPacks()) { this.VerboseLog($"Loading content pack '{pack.Manifest.Name}'..."); try { // read changes file ContentConfig content = pack.ReadJsonFile <ContentConfig>(this.PatchFileName); if (content == null) { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it has no {this.PatchFileName} file.", LogLevel.Error); continue; } if (content.Format == null || content.Changes == null) { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it doesn't specify the required {nameof(ContentConfig.Format)} or {nameof(ContentConfig.Changes)} fields.", LogLevel.Error); continue; } // validate version if (!this.SupportedFormatVersions.Contains(content.Format.ToString())) { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it uses unsupported format {content.Format} (supported version: {string.Join(", ", this.SupportedFormatVersions)}).", LogLevel.Error); continue; } // load config.json InvariantDictionary <ConfigField> config = configFileHandler.Read(pack, content.ConfigSchema); configFileHandler.Save(pack, config, this.Helper); if (config.Any()) { this.VerboseLog($" found config.json with {config.Count} fields..."); } // validate features if (content.Format.IsOlderThan("1.3")) { if (config.Any()) { this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the {nameof(ContentConfig.ConfigSchema)} field added in 1.3.", LogLevel.Error); continue; } if (content.Changes.Any(p => p.FromFile != null && p.FromFile.Contains("{{"))) { this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the {{{{token}}}} feature added in 1.3.", LogLevel.Error); continue; } if (content.Changes.Any(p => p.When != null && p.When.Any())) { this.Monitor.Log($"Loading content pack '{pack.Manifest.Name}' failed. It specifies format version {content.Format}, but uses the condition feature ({nameof(ContentConfig.Changes)}.{nameof(PatchConfig.When)} field) added in 1.3.", LogLevel.Error); continue; } } // load patches this.NamePatches(pack, content.Changes); foreach (PatchConfig patch in content.Changes) { this.VerboseLog($" loading {patch.LogName}..."); this.LoadPatch(pack, patch, config, logSkip: reasonPhrase => this.Monitor.Log($"Ignored {patch.LogName}: {reasonPhrase}", LogLevel.Warn)); } } catch (Exception ex) { this.Monitor.Log($"Error loading content pack '{pack.Manifest.Name}'. Technical details:\n{ex}", LogLevel.Error); } } }
/// <summary>Load one patch from a content pack's <c>content.json</c> file.</summary> /// <param name="pack">The content pack being loaded.</param> /// <param name="contentConfig">The content pack's config.</param> /// <param name="entry">The change to load.</param> /// <param name="config">The content pack's config values.</param> /// <param name="latestFormatVersion">The latest format version.</param> /// <param name="logSkip">The callback to invoke with the error reason if loading it fails.</param> private bool LoadPatch(ManagedContentPack pack, ContentConfig contentConfig, PatchConfig entry, InvariantDictionary <ConfigField> config, ISemanticVersion latestFormatVersion, Action <string> logSkip) { bool TrackSkip(string reason, bool warn = true) { this.PatchManager.AddPermanentlyDisabled(new DisabledPatch(entry.LogName, entry.Action, entry.Target, pack, reason)); if (warn) { logSkip(reason); } return(false); } try { // normalise patch fields if (entry.When == null) { entry.When = new InvariantDictionary <string>(); } // parse action if (!Enum.TryParse(entry.Action, true, out PatchType action)) { return(TrackSkip(string.IsNullOrWhiteSpace(entry.Action) ? $"must set the {nameof(PatchConfig.Action)} field." : $"invalid {nameof(PatchConfig.Action)} value '{entry.Action}', expected one of: {string.Join(", ", Enum.GetNames(typeof(PatchType)))}." )); } // parse target asset TokenString assetName; { if (string.IsNullOrWhiteSpace(entry.Target)) { return(TrackSkip($"must set the {nameof(PatchConfig.Target)} field.")); } if (!this.TryParseTokenString(entry.Target, config, out string error, out TokenStringBuilder builder)) { return(TrackSkip($"the {nameof(PatchConfig.Target)} is invalid: {error}")); } assetName = builder.Build(); } // parse 'enabled' bool enabled = true; { if (entry.Enabled != null && !this.TryParseBoolean(entry.Enabled, config, out string error, out enabled)) { return(TrackSkip($"invalid {nameof(PatchConfig.Enabled)} value '{entry.Enabled}': {error}")); } } // apply config foreach (string key in config.Keys) { if (entry.When.TryGetValue(key, out string values)) { InvariantHashSet expected = this.PatchManager.ParseCommaDelimitedField(values); if (!expected.Intersect(config[key].Value, StringComparer.InvariantCultureIgnoreCase).Any()) { return(TrackSkip($"disabled by config {key} (needs '{string.Join(", ", expected)}', found '{string.Join(", ", config[key].Value)}').", warn: false)); } entry.When.Remove(key); } } // parse conditions ConditionDictionary conditions; { if (!this.PatchManager.TryParseConditions(entry.When, contentConfig.Format, latestFormatVersion, out conditions, out string error)) { return(TrackSkip($"the {nameof(PatchConfig.When)} field is invalid: {error}.")); } } // validate conditions if (action == PatchType.Load) { ConditionKey[] tokenisable = this.ConditionFactory.GetTokenisableConditions().ToArray(); ConditionKey[] invalid = conditions.Keys.Except(tokenisable).ToArray(); if (invalid.Any()) { return(TrackSkip($"can't use these conditions with {nameof(PatchConfig.Action)} {PatchType.Load} ({string.Join(", ", invalid)})")); } } // get patch instance IPatch patch; switch (action) { // load asset case PatchType.Load: { // init patch if (!this.TryPrepareLocalAsset(pack, entry.FromFile, config, conditions, out string error, out TokenString fromAsset, shouldPreload: true)) { return(TrackSkip(error)); } patch = new LoadPatch(entry.LogName, pack, assetName, conditions, fromAsset, this.Helper.Content.NormaliseAssetName); // detect conflicting loaders if (enabled) { InvariantDictionary <IPatch> conflicts = this.PatchManager.GetConflictingLoaders(patch); if (conflicts.Any()) { IEnumerable <string> conflictNames = ( from conflict in conflicts orderby conflict.Key select $"'{conflict.Value.LogName}' already loads {conflict.Key}" ); return(TrackSkip( $"{nameof(entry.Target)} '{patch.TokenableAssetName.Raw}' conflicts with other load patches ({string.Join(", ", conflictNames)}). Each file can only be loaded by one patch, unless their conditions can never overlap.")); } } } break; // edit data case PatchType.EditData: { // validate if (entry.Entries == null && entry.Fields == null) { return(TrackSkip($"either {nameof(PatchConfig.Entries)} or {nameof(PatchConfig.Fields)} must be specified for a '{action}' change.")); } if (entry.Entries != null && entry.Entries.Any(p => p.Value != null && p.Value.Trim() == "")) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} can't contain empty values.")); } if (entry.Fields != null && entry.Fields.Any(p => p.Value == null || p.Value.Any(n => n.Value == null))) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} can't contain empty values.")); } // save patch = new EditDataPatch(entry.LogName, pack, assetName, conditions, entry.Entries, entry.Fields, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; // edit image case PatchType.EditImage: { // read patch mode PatchMode patchMode = PatchMode.Replace; if (!string.IsNullOrWhiteSpace(entry.PatchMode) && !Enum.TryParse(entry.PatchMode, true, out patchMode)) { return(TrackSkip($"the {nameof(PatchConfig.PatchMode)} is invalid. Expected one of these values: [{string.Join(", ", Enum.GetNames(typeof(PatchMode)))}].")); } // save if (!this.TryPrepareLocalAsset(pack, entry.FromFile, config, conditions, out string error, out TokenString fromAsset, shouldPreload: true)) { return(TrackSkip(error)); } patch = new EditImagePatch(entry.LogName, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, patchMode, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; default: return(TrackSkip($"unsupported patch type '{action}'.")); } // only apply patch when its tokens are available HashSet <ConditionKey> tokensUsed = new HashSet <ConditionKey>(patch.GetTokensUsed()); foreach (ConditionKey key in tokensUsed) { if (!patch.Conditions.ContainsKey(key)) { patch.Conditions.Add(key, patch.Conditions.GetValidValues(key)); } } // skip if not enabled // note: we process the patch even if it's disabled, so any errors are caught by the modder instead of only failing after the patch is enabled. if (!enabled) { return(TrackSkip($"{nameof(PatchConfig.Enabled)} is false.", warn: false)); } // save patch this.PatchManager.Add(patch); return(true); } catch (Exception ex) { return(TrackSkip($"error reading info. Technical details:\n{ex}")); } }
public ContentPackObject(string modPath, string contentPath, ContentPackManifest manifest, ContentConfig content, WhenDictionary config) { this.ModPath = modPath; this.ContentPath = contentPath; this.Manifest = manifest; this.Content = content; this.Config = config; }
/// <summary>Load one patch from a content pack's <c>content.json</c> file.</summary> /// <param name="pack">The content pack being loaded.</param> /// <param name="contentConfig">The content pack's config.</param> /// <param name="entry">The change to load.</param> /// <param name="tokenContext">The tokens available for this 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="logSkip">The callback to invoke with the error reason if loading it fails.</param> private bool LoadPatch(ManagedContentPack pack, ContentConfig contentConfig, PatchConfig entry, IContext tokenContext, ISemanticVersion latestFormatVersion, InvariantDictionary <ISemanticVersion> minumumTokenVersions, Action <string> logSkip) { bool TrackSkip(string reason, bool warn = true) { this.PatchManager.AddPermanentlyDisabled(new DisabledPatch(entry.LogName, entry.Action, entry.Target, pack, reason)); if (warn) { logSkip(reason); } return(false); } try { // normalise patch fields if (entry.When == null) { entry.When = new InvariantDictionary <string>(); } // parse action if (!Enum.TryParse(entry.Action, true, out PatchType action)) { return(TrackSkip(string.IsNullOrWhiteSpace(entry.Action) ? $"must set the {nameof(PatchConfig.Action)} field." : $"invalid {nameof(PatchConfig.Action)} value '{entry.Action}', expected one of: {string.Join(", ", Enum.GetNames(typeof(PatchType)))}." )); } // parse target asset TokenString assetName; { if (string.IsNullOrWhiteSpace(entry.Target)) { return(TrackSkip($"must set the {nameof(PatchConfig.Target)} field.")); } if (!this.TryParseTokenString(entry.Target, tokenContext, out string error, out assetName)) { return(TrackSkip($"the {nameof(PatchConfig.Target)} is invalid: {error}")); } } // parse 'enabled' bool enabled = true; { if (entry.Enabled != null && !this.TryParseEnabled(entry.Enabled, tokenContext, out string error, out enabled)) { return(TrackSkip($"invalid {nameof(PatchConfig.Enabled)} value '{entry.Enabled}': {error}")); } } // parse conditions ConditionDictionary conditions; { if (!this.TryParseConditions(entry.When, tokenContext, contentConfig.Format, latestFormatVersion, minumumTokenVersions, out conditions, out string error)) { return(TrackSkip($"the {nameof(PatchConfig.When)} field is invalid: {error}.")); } } // get patch instance IPatch patch; switch (action) { // load asset case PatchType.Load: { // init patch if (!this.TryPrepareLocalAsset(pack, entry.FromFile, tokenContext, out string error, out TokenString fromAsset)) { return(TrackSkip(error)); } patch = new LoadPatch(entry.LogName, pack, assetName, conditions, fromAsset, this.Helper.Content.NormaliseAssetName); } break; // edit data case PatchType.EditData: { // validate if (entry.Entries == null && entry.Fields == null) { return(TrackSkip($"either {nameof(PatchConfig.Entries)} or {nameof(PatchConfig.Fields)} must be specified for a '{action}' change.")); } if (entry.Entries != null && entry.Entries.Any(p => p.Value != null && p.Value.Trim() == "")) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} can't contain empty values.")); } if (entry.Fields != null && entry.Fields.Any(p => p.Value == null || p.Value.Any(n => n.Value == null))) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} can't contain empty values.")); } // parse entries IDictionary <string, TokenString> entries = new Dictionary <string, TokenString>(); if (entry.Entries != null) { foreach (KeyValuePair <string, string> pair in entry.Entries) { string key = pair.Key; if (!this.TryParseTokenString(pair.Value, tokenContext, out string error, out TokenString value)) { return(TrackSkip($"the {nameof(PatchConfig.Entries)} > '{key}' entry is invalid: {error}.")); } entries[key] = value; } } // parse fields IDictionary <string, IDictionary <int, TokenString> > fields = new Dictionary <string, IDictionary <int, TokenString> >(); if (entry.Fields != null) { foreach (var recordPair in entry.Fields) { string key = recordPair.Key; fields[key] = new Dictionary <int, TokenString>(); foreach (var fieldPair in recordPair.Value) { int field = fieldPair.Key; if (!this.TryParseTokenString(fieldPair.Value, tokenContext, out string error, out TokenString value)) { return(TrackSkip($"the {nameof(PatchConfig.Fields)} > '{key}' > {field} field is invalid: {error}.")); } fields[key][field] = value; } } } // save patch = new EditDataPatch(entry.LogName, pack, assetName, conditions, entries, fields, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; // edit image case PatchType.EditImage: { // read patch mode PatchMode patchMode = PatchMode.Replace; if (!string.IsNullOrWhiteSpace(entry.PatchMode) && !Enum.TryParse(entry.PatchMode, true, out patchMode)) { return(TrackSkip($"the {nameof(PatchConfig.PatchMode)} is invalid. Expected one of these values: [{string.Join(", ", Enum.GetNames(typeof(PatchMode)))}].")); } // save if (!this.TryPrepareLocalAsset(pack, entry.FromFile, tokenContext, out string error, out TokenString fromAsset)) { return(TrackSkip(error)); } patch = new EditImagePatch(entry.LogName, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, patchMode, this.Monitor, this.Helper.Content.NormaliseAssetName); } break; default: return(TrackSkip($"unsupported patch type '{action}'.")); } // skip if not enabled // note: we process the patch even if it's disabled, so any errors are caught by the modder instead of only failing after the patch is enabled. if (!enabled) { return(TrackSkip($"{nameof(PatchConfig.Enabled)} is false.", warn: false)); } // save patch this.PatchManager.Add(patch); return(true); } catch (Exception ex) { return(TrackSkip($"error reading info. Technical details:\n{ex}")); } }
public override void Entry(IModHelper help) { this.Config = this.Helper.ReadConfig <ModConfig>(); Logger = this.Monitor; EnabledAssets = new Dictionary <string, bool>(); ModHelper = help; EnableMod = this.Config.EnableMod; this.HDAssetManager = new HDAssetManager(help); this.ContentPackManager = new ContentPackManager(this.HDAssetManager); this.Helper.Events.Input.ButtonPressed += OnButtonPressed; this.Helper.Events.Player.Warped += OnWarped; this.Helper.Events.GameLoop.DayStarted += OnDayStarted; foreach (var asset in this.Config.LoadAssets) { string loadSection = asset.Key.Substring(0, asset.Key.LastIndexOf("/")); bool enabled = asset.Value && this.Config.LoadSections.GetValueSafe(loadSection); AddEnabledAsset(asset.Key, enabled); if (enabled) { string assetFile = Path.Combine(this.Config.AssetsPath, asset.Key) + ".png"; if (File.Exists(Path.Combine(help.DirectoryPath, assetFile))) { this.HDAssetManager.AddAssetFile(asset.Key, assetFile); } } } string[] contentPackDirs = Directory.GetDirectories(Path.Combine(help.DirectoryPath, "..")); foreach (string dir in contentPackDirs) { string manifestFile = Path.Combine(dir, "manifest.json"); if (Directory.GetParent(manifestFile).Name.StartsWith(".")) { continue; } ContentPackManifest manifest = null; try { manifest = this.Helper.ReadJsonFile <ContentPackManifest>(manifestFile); } catch (Exception e) { continue; } if (manifest != null && this.Config.LoadContentPacks.TryGetValue(manifest.UniqueID, out bool load) && load) { this.Monitor.Log($"Reading content pack: {manifest.Name} {manifest.Version}"); WhenDictionary configChoices = null; try { configChoices = this.Helper.ReadJsonFile <WhenDictionary>(Path.Combine(dir, "config.json")); } catch (Exception e) { this.Monitor.Log($"Failed to read config.json for {manifest.Name} {manifest.Version}"); continue; } if (configChoices == null) { configChoices = new WhenDictionary(); } ContentConfig contentConfig = null; try { contentConfig = this.Helper.ReadJsonFile <ContentConfig>(Path.Combine(dir, "content.json")); } catch (Exception e) { this.Monitor.Log($"Failed to read content.json for {manifest.Name} {manifest.Version}"); continue; } if (contentConfig == null) { continue; } string contentPath = Path.Combine(help.DirectoryPath, this.Config.ContentPacksPath, manifest.UniqueID); Directory.CreateDirectory(contentPath); ContentPackObject contentPack = new ContentPackObject(dir, contentPath, manifest, contentConfig, configChoices); this.ContentPackManager.AddContentPack(contentPack); } } HarmonyInstance instance = HarmonyInstance.Create("NinthWorld.HDSprites"); DrawFix.InitializePatch(instance); instance.PatchAll(Assembly.GetExecutingAssembly()); }
/********* ** Private methods *********/ /// <summary>Load the loaders and patchers from all registered content packs.</summary> private void LoadContentPacks() { foreach (IContentPack pack in this.Helper.GetContentPacks()) { // read config ContentConfig config = pack.ReadJsonFile <ContentConfig>(this.PatchFileName); if (config == null) { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it has no {this.PatchFileName} file.", LogLevel.Warn); continue; } if (config.Format == null || config.Changes == null) { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it doesn't specify the required {nameof(ContentConfig.Format)} or {nameof(ContentConfig.Changes)} fields.", LogLevel.Warn); continue; } if (config.Format.ToString() != "1.0") { this.Monitor.Log($"Ignored content pack '{pack.Manifest.Name}' because it uses unsupported format {config.Format} (supported version: 1.0).", LogLevel.Warn); continue; } // load patches int i = 0; foreach (PatchConfig entry in config.Changes) { i++; void LogSkip(string reasonPhrase) => this.Monitor.Log($"Ignored {pack.Manifest.Name} > entry #{i}: {reasonPhrase}", LogLevel.Warn); try { // skip if disabled if (!entry.Enabled) { continue; } // read action string action = entry.Action?.Trim().ToLower(); if (string.IsNullOrWhiteSpace(action)) { LogSkip($"must set the {nameof(PatchConfig.Action)} field."); continue; } // read target asset string assetName = !string.IsNullOrWhiteSpace(entry.Target) ? this.Helper.Content.NormaliseAssetName(entry.Target) : null; if (assetName == null) { LogSkip($"must set the {nameof(PatchConfig.Target)} field."); continue; } // read source asset string localAsset = !string.IsNullOrWhiteSpace(entry.FromFile) ? this.Helper.Content.NormaliseAssetName(entry.FromFile) : null; if (localAsset == null && (action == "load" || action == "editimage")) { LogSkip($"must set the {nameof(PatchConfig.FromFile)} field for a '{action}' patch."); continue; } if (localAsset != null && !this.AssetLoader.AssetExists(pack, localAsset)) { LogSkip($"the {nameof(PatchConfig.FromFile)} field specifies a file that doesn't exist: {localAsset}."); continue; } // read locale string locale = !string.IsNullOrWhiteSpace(entry.Locale) ? entry.Locale.Trim().ToLower() : null; // parse for type switch (action) { // load asset case "load": { // check for conflicting loaders if (this.Loaders.TryGetValue(assetName, out IList <LoadPatch> loaders)) { // can't add for all locales if any loaders already registered if (locale == null) { string[] localesLoadedBy = loaders.Select(p => $"{p.ContentPack.Manifest.Name}").ToArray(); LogSkip($"the {assetName} file is already being loaded by '{string.Join("', '", localesLoadedBy)}'. Each file can only be loaded once."); continue; } // can't add if already loaded for all locales { LoadPatch globalPatch = loaders.FirstOrDefault(p => p.Locale == null); if (globalPatch != null) { LogSkip($"the {assetName} file is already being loaded by {(pack == globalPatch.ContentPack ? "this content pack" : $"the '{globalPatch.ContentPack.Manifest.Name}' content pack")}. Each file can only be loaded once."); continue; } } // can't add if already loaded for selected locale { LoadPatch localePatch = loaders.FirstOrDefault(p => p.Locale != null && p.Locale.Equals(locale, StringComparison.CurrentCultureIgnoreCase)); if (localePatch != null) { LogSkip($"the {assetName} file is already being loaded for the {locale} locale by {(pack == localePatch.ContentPack ? "this content pack" : $"the '{localePatch.ContentPack.Manifest.Name}' content pack")}. Each file can only be loaded once per locale.");
public void ContentConfigToInformationTest2() { const string msaId = "msa-id-1"; ContentConfig config = new ContentConfig { DashboardTitle = "Title", ContentProviders = new List <ContentProviderConfig> { new ContentProviderConfig { Id = "id1", MediaServicesSets = new List <MediaServicesSetConfig> { new MediaServicesSetConfig { MediaServicesAccounts = new List <MediaServicesAccountConfig> { new MediaServicesAccountConfig { Id = msaId, MetaData = new MediaServicesMetaDataConfig { AzureSubscriptionId = "azuresubid", Location = "Amsterdam" } } }, DataStorageConnections = new List <AzureDataConfig> { new AzureDataConfig { AcctName = "accoutName1", AzureServer = "azure-db1-server-name", InitialCatalog = "azure-db1-name", UserName = "******", Password = "******" }, new AzureDataConfig { AcctName = "accoutName2", AzureServer = "azure-db2-server-name", InitialCatalog = "azure-db2-name", UserName = "******", Password = "******" } } } } } } }; MediaService mediaService = new MediaService { Id = msaId, Name = "Name1" }; MdCache.Instance.SetAs(MediaService.GetCacheKey(msaId), mediaService); Information info = ContentConfigToInformation.Map(config); Assert.NotNull(info); Assert.AreEqual(1, info.ContentProviders.Count); Assert.AreEqual(1, info.ContentProviders[0].MediaPipelines.Count); Assert.AreEqual(1, info.ContentProviders[0].MediaPipelines[0].Deployments.Count); Assert.AreEqual(msaId, info.ContentProviders[0].MediaPipelines[0].Deployments[0].Id); List <AzureDataConfig> dbConns = config.ContentProviders[0].MediaServicesSets[0].DataStorageConnections; Assert.IsNotNull(dbConns); Assert.IsNotEmpty(dbConns); Assert.AreEqual(2, dbConns.Count); Assert.AreNotSame(dbConns[0], dbConns[1]); }
public static IConfigurationLoader[] GlassLoaders() { var attributes = new UmbracoAttributeConfigurationLoader("Glass.Mapper.Sites.Umb"); return(new IConfigurationLoader[] { attributes, ContentConfig.Load() }); }