/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="id">The unique key for the entry in the data file.</param> /// <param name="beforeID">The ID of another entry this one should be inserted before.</param> /// <param name="afterID">The ID of another entry this one should be inserted after.</param> /// <param name="toPosition">The position to set.</param> public EditDataPatchMoveRecord(ITokenString id, ITokenString beforeID, ITokenString afterID, MoveEntryPosition toPosition) { this.ID = id; this.BeforeID = beforeID; this.AfterID = afterID; this.ToPosition = toPosition; this.Contextuals = new AggregateContextual() .Add(id) .Add(beforeID) .Add(afterID); }
/// <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="entry">The change to load.</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="logSkip">The callback to invoke with the error reason if loading it fails.</param> private bool LoadPatch(ManagedContentPack pack, PatchConfig entry, IContext tokenContext, IMigration migrator, Action <string> logSkip) { bool TrackSkip(string reason, bool warn = true) { reason = reason.TrimEnd('.', ' '); 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 ITokenString assetName; { if (string.IsNullOrWhiteSpace(entry.Target)) { return(TrackSkip($"must set the {nameof(PatchConfig.Target)} field")); } if (!this.TryParseStringTokens(entry.Target, tokenContext, migrator, 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, migrator, out string error, out enabled)) { return(TrackSkip($"invalid {nameof(PatchConfig.Enabled)} value '{entry.Enabled}': {error}")); } } // parse conditions IList <Condition> conditions; { if (!this.TryParseConditions(entry.When, tokenContext, migrator, 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, migrator, out string error, out ITokenString 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 && entry.MoveEntries == null) { return(TrackSkip($"one of {nameof(PatchConfig.Entries)}, {nameof(PatchConfig.Fields)}, or {nameof(PatchConfig.MoveEntries)} must be specified for an '{action}' change")); } // parse entries List <EditDataPatchRecord> entries = new List <EditDataPatchRecord>(); if (entry.Entries != null) { foreach (KeyValuePair <string, JToken> pair in entry.Entries) { if (!this.TryParseStringTokens(pair.Key, tokenContext, migrator, out string keyError, out ITokenString key)) { return(TrackSkip($"{nameof(PatchConfig.Entries)} > '{key}' key is invalid: {keyError}")); } if (!this.TryParseJsonTokens(pair.Value, tokenContext, migrator, out string error, out TokenisableJToken value)) { return(TrackSkip($"{nameof(PatchConfig.Entries)} > '{key}' value is invalid: {error}")); } entries.Add(new EditDataPatchRecord(key, value)); } } // parse fields List <EditDataPatchField> fields = new List <EditDataPatchField>(); if (entry.Fields != null) { foreach (KeyValuePair <string, IDictionary <string, JToken> > recordPair in entry.Fields) { // parse entry key if (!this.TryParseStringTokens(recordPair.Key, tokenContext, migrator, out string keyError, out ITokenString key)) { return(TrackSkip($"{nameof(PatchConfig.Fields)} > entry {recordPair.Key} is invalid: {keyError}")); } // parse fields foreach (var fieldPair in recordPair.Value) { // parse field key if (!this.TryParseStringTokens(fieldPair.Key, tokenContext, migrator, out string fieldError, out ITokenString fieldKey)) { return(TrackSkip($"{nameof(PatchConfig.Fields)} > entry {recordPair.Key} > field {fieldPair.Key} key is invalid: {fieldError}")); } // parse value if (!this.TryParseJsonTokens(fieldPair.Value, tokenContext, migrator, out string valueError, out TokenisableJToken value)) { return(TrackSkip($"{nameof(PatchConfig.Fields)} > entry {recordPair.Key} > field {fieldKey} is invalid: {valueError}")); } if (value?.Value is JValue jValue && jValue.Value <string>()?.Contains("/") == true) { return(TrackSkip($"{nameof(PatchConfig.Fields)} > entry {recordPair.Key} > field {fieldKey} is invalid: value can't contain field delimiter character '/'")); } fields.Add(new EditDataPatchField(key, fieldKey, value)); } } } // parse move entries List <EditDataPatchMoveRecord> moveEntries = new List <EditDataPatchMoveRecord>(); if (entry.MoveEntries != null) { foreach (PatchMoveEntryConfig moveEntry in entry.MoveEntries) { // validate string[] targets = new[] { moveEntry.BeforeID, moveEntry.AfterID, moveEntry.ToPosition }; if (string.IsNullOrWhiteSpace(moveEntry.ID)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > move entry is invalid: must specify an {nameof(PatchMoveEntryConfig.ID)} value")); } if (targets.All(string.IsNullOrWhiteSpace)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' is invalid: must specify one of {nameof(PatchMoveEntryConfig.ToPosition)}, {nameof(PatchMoveEntryConfig.BeforeID)}, or {nameof(PatchMoveEntryConfig.AfterID)}")); } if (targets.Count(p => !string.IsNullOrWhiteSpace(p)) > 1) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' is invalid: must specify only one of {nameof(PatchMoveEntryConfig.ToPosition)}, {nameof(PatchMoveEntryConfig.BeforeID)}, and {nameof(PatchMoveEntryConfig.AfterID)}")); } // parse IDs if (!this.TryParseStringTokens(moveEntry.ID, tokenContext, migrator, out string idError, out ITokenString moveId)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' > {nameof(PatchMoveEntryConfig.ID)} is invalid: {idError}")); } if (!this.TryParseStringTokens(moveEntry.BeforeID, tokenContext, migrator, out string beforeIdError, out ITokenString beforeId)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' > {nameof(PatchMoveEntryConfig.BeforeID)} is invalid: {beforeIdError}")); } if (!this.TryParseStringTokens(moveEntry.AfterID, tokenContext, migrator, out string afterIdError, out ITokenString afterId)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' > {nameof(PatchMoveEntryConfig.AfterID)} is invalid: {afterIdError}")); } // parse position MoveEntryPosition toPosition = MoveEntryPosition.None; if (!string.IsNullOrWhiteSpace(moveEntry.ToPosition) && (!Enum.TryParse(moveEntry.ToPosition, true, out toPosition) || toPosition == MoveEntryPosition.None)) { return(TrackSkip($"{nameof(PatchConfig.MoveEntries)} > entry '{moveEntry.ID}' > {nameof(PatchMoveEntryConfig.ToPosition)} is invalid: must be one of {nameof(MoveEntryPosition.Bottom)} or {nameof(MoveEntryPosition.Top)}")); } // create move entry moveEntries.Add(new EditDataPatchMoveRecord(moveId, beforeId, afterId, toPosition)); } } // save patch = new EditDataPatch(entry.LogName, pack, assetName, conditions, entries, fields, moveEntries, 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, migrator, out string error, out ITokenString 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; // edit map case PatchType.EditMap: { // read map asset if (!this.TryPrepareLocalAsset(pack, entry.FromFile, tokenContext, migrator, out string error, out ITokenString fromAsset)) { return(TrackSkip(error)); } // validate if (entry.ToArea == Rectangle.Empty) { return(TrackSkip($"must specify {nameof(entry.ToArea)} (use \"Action\": \"Load\" if you want to replace the whole map file)")); } // save patch = new EditMapPatch(entry.LogName, pack, assetName, conditions, fromAsset, entry.FromArea, entry.ToArea, 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}")); } }
/// <inheritdoc /> public virtual MoveResult MoveEntry(object key, MoveEntryPosition toPosition) { throw new NotImplementedException(); }