public Patch(string pattern, string replacement, PatchMode mode = PatchMode.Any, int count = 0) { this.Mode = mode; this.Pattern = pattern; this.Replacement = replacement; this.ApplyTargetCount = count; }
// ------------------------------------------------------------------- // Constructor // ------------------------------------------------------------------- public Patch(string pattern, string replacement, PatchMode mode = PatchMode.Any, int count = 0, string[] fileFilters = null) { this.Mode = mode; this.Pattern = pattern; this.Replacement = replacement; if (fileFilters != null) { this.FileFilters = new List<string>(fileFilters); } }
public static AssetInjector <IAssetDataForImage, IAssetDataForImage> injectTileInto(this Texture2D t, string assetName, Range targetTileIndex, Range sourceTileIndex, int tileWidth = 16, int tileHeight = 16, PatchMode mode = PatchMode.Replace) { Func <IAssetDataForImage, IAssetDataForImage> merger = new Func <IAssetDataForImage, IAssetDataForImage>(delegate(IAssetDataForImage asset) { for (int i = 0; i < sourceTileIndex.length; i++) { Rectangle source = Game1.getSourceRectForStandardTileSheet(t, sourceTileIndex[i], tileWidth, tileHeight); Rectangle target = Game1.getSourceRectForStandardTileSheet(asset.Data, targetTileIndex[i], tileWidth, tileHeight); asset.PatchImage(t, source, target, mode); } return(asset); }); return(new AssetInjector <IAssetDataForImage, IAssetDataForImage>(assetName, merger).injectEdit()); }
public PatchExpression(string pattern, string replacement, PatchMode mode = PatchMode.Any, int count = 0, RegexOptions options = RegexOptions.None) : base(pattern, replacement, mode, count) { this.Expression = new Regex(pattern, options); this.TargetGroup = 1; }
/********* ** 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="fromAsset">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, ManagedContentPack contentPack, ITokenString assetName, IEnumerable <Condition> conditions, ITokenString fromAsset, Rectangle fromArea, Rectangle toArea, PatchMode patchMode, IMonitor monitor, Func <string, string> normaliseAssetName) : base(logName, PatchType.EditImage, contentPack, assetName, conditions, normaliseAssetName, fromAsset: fromAsset) { this.FromArea = fromArea != Rectangle.Empty ? fromArea : null as Rectangle?; this.ToArea = toArea != Rectangle.Empty ? toArea : null as Rectangle?; this.PatchMode = patchMode; this.Monitor = monitor; }
public static bool PatchImage(IAssetDataForImage __instance, Texture2D source, Rectangle?sourceArea, Rectangle?targetArea, PatchMode patchMode) { if (__instance.Data is Texture2DWrapper wrapper) { if (sourceArea.HasValue) { source = Texture2DWrapper.Crop(source, sourceArea.Value); } source = Texture2DWrapper.ScaleUp(source, wrapper.Scale); if (targetArea.HasValue) { targetArea = Texture2DWrapper.MultiplyRect(targetArea.Value, wrapper.Scale); } PropertyInfo Data = __instance.GetType().GetProperty("Data"); Data.SetValue(__instance, wrapper.Wrapped); __instance.PatchImage(source, null, targetArea, patchMode); Data.SetValue(__instance, wrapper); return(false); } return(true); }
/// <summary> /// Initializes a new instance of the <see cref="SInject"/> class. /// </summary> /// <param name="assemblyLocation">The assembly location.</param> /// <param name="snkCertificatePath">The location of snk certificate</param> /// <param name="mode">The mode.</param> /// <exception cref="System.Exception"></exception> public SInjection(string assemblyLocation, string snkCertificatePath = null, PatchMode mode = PatchMode.Release) { Log.Configure("SInject"); _assemblyLocation = assemblyLocation; _snkCertificatePath = snkCertificatePath; _mode = mode; Log.Write("Assembly being Injected {0}", assemblyLocation); // if (IsAssemblyInExcludedList(Path.GetFileName(assemblyLocation))) return; if (!File.Exists(assemblyLocation)) throw new Exception(string.Format("Assembly at location {0} doesn't exist", assemblyLocation)); _assemblyDefinition = AssemblyDefinition.ReadAssembly(assemblyLocation, GetReaderParameters()); }
/// <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}")); } }
public static IEnumerable <CodeInstruction> PatchBySequence(IEnumerable <CodeInstruction> instructions, IEnumerable <CodeInstruction> targetSequence, IEnumerable <CodeInstruction> patchSequence, PatchMode patchMode = PatchMode.AFTER, CheckMode checkMode = CheckMode.ALWAYS, bool showDebugOutput = false) { List <CodeInstruction> Instructions = instructions.ToList(); //create new list to be modified and returned. CodeInstruction targetStart = targetSequence.ElementAt(0); int targetSize = targetSequence.Count(); for (int i = 0; i < Instructions.Count; i++) //Check every Instruction in the given list if it is the correct Instruction set { bool targetSequenceStillFits = i + targetSize <= Instructions.Count; //calculate if target sequence fits in Instructions. if (targetSequenceStillFits) //stop if not enough lines capable of fitting target sequence { bool foundTargetSequence = true; for (int x = 0; x < targetSize && foundTargetSequence; x++) //compare each element of the new sequence to the old to see if it is the same. stop for loop early if the targetsequence { foundTargetSequence = Instructions[i + x].opcode.Equals(targetSequence.ElementAt(x).opcode); if (checkMode != CheckMode.NEVER)//if specified checking params are set appropriately, check opperand. CheckMode enum comes into play here. { foundTargetSequence = foundTargetSequence && ( ((Instructions[i + x].operand == null || checkMode == CheckMode.NONNULL) && targetSequence.ElementAt(x).operand == null) || Instructions[i + x].operand.Equals(targetSequence.ElementAt(x).operand) ); } if (showDebugOutput && foundTargetSequence) { Logger.Info($"Found {targetSequence.ElementAt(x).opcode} at {i + x}"); } } if (foundTargetSequence) //If the TargetSequence was found in the Instructions, Replace at the i index. { if (patchMode == PatchMode.BEFORE || patchMode == PatchMode.AFTER) { int indexToInsertAt = patchMode == PatchMode.AFTER ? i + targetSize : i; Instructions.InsertRange(indexToInsertAt, patchSequence.Select(c => c.FullClone())); } else if (patchMode == PatchMode.REPLACE) { Instructions.RemoveRange(i, targetSize); Instructions.InsertRange(i, patchSequence.Select(c => c.FullClone())); } else { throw new ArgumentException($"Argument PatchMode patchMode == {patchMode}; invalid value!"); } break; } } else //if targetsequence didn't fit in what was left of array (couldn't find target sequence) { StringBuilder sb = new StringBuilder(); sb.AppendLine($"Failed to patch by sequence: couldn't find target sequence. This might be okay in certain cases."); // Cut down the stack trace because it's 20 lines of unhelpful reflection internals. // Show enough to figure out which mod + transpiler method is causing this: sb.AppendLine($"Stack Trace:"); string[] stackTrace = new System.Diagnostics.StackTrace().ToString().Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); for (int lineNumber = 0; lineNumber < 2; lineNumber++) { sb.AppendLine(stackTrace[lineNumber]); } Logger.Info(sb.ToString()); break; } } return(Instructions.AsEnumerable()); }
/// <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="config">The content pack's config values.</param> /// <param name="logSkip">The callback to invoke with the error reason if loading it fails.</param> private bool LoadPatch(IContentPack pack, PatchConfig entry, InvariantDictionary <ConfigField> config, 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).Any()) { return(TrackSkip($"disabled: config field '{key}' must have one of '{string.Join(", ", expected)}', but found '{string.Join(", ", config[key].Value)}'.", warn: false)); } entry.When.Remove(key); } } // parse conditions ConditionDictionary conditions; { if (!this.PatchManager.TryParseConditions(entry.When, 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, config, conditions, out string error, out TokenString fromAsset, checkOnly: !enabled)) { return(TrackSkip(error)); } patch = new LoadPatch(entry.LogName, this.AssetLoader, 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 => string.IsNullOrWhiteSpace(p.Value))) { 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, this.AssetLoader, 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, checkOnly: !enabled)) { return(TrackSkip(error)); } patch = new EditImagePatch(entry.LogName, this.AssetLoader, 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 TargetMethodInfo(MethodBase oldMethod, MethodInfo newMethod, PatchMode mode) { this.oldMethod = oldMethod; this.newMethod = newMethod; this.mode = mode; }
public static AssetInjector <IAssetDataForImage, IAssetDataForImage> injectInto(this Texture2D t, string assetName, Rectangle?source, Rectangle?target, PatchMode mode = PatchMode.Replace) { Func <IAssetDataForImage, IAssetDataForImage> merger = new Func <IAssetDataForImage, IAssetDataForImage>(delegate(IAssetDataForImage asset) { asset.PatchImage(t, source, target, mode); return(asset); }); return(new AssetInjector <IAssetDataForImage, IAssetDataForImage>(assetName, merger).injectEdit()); }
public static AssetInjector <IAssetDataForImage, IAssetDataForImage> injectInto(this Texture2D t, string assetName, Point position, PatchMode mode = PatchMode.Replace) { return(t.injectInto(assetName, null, new Rectangle(position.X, position.Y, t.Width, t.Height), mode)); }
/// <summary>Overwrite part of the image.</summary> /// <param name="source">The image to patch into the content.</param> /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param> /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param> /// <param name="patchMode">Indicates how an image should be patched.</param> /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> public void PatchImage(Texture2D source, Rectangle?sourceArea = null, Rectangle?targetArea = null, PatchMode patchMode = PatchMode.Replace) { // get texture Texture2D target = this.Data; // get areas sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate if (source == null) { throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); } if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) { throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); } if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) { throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); } if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) { throw new InvalidOperationException("The source and target areas must be the same size."); } // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = new Color[pixelCount]; source.GetData(0, sourceArea, sourceData, 0, pixelCount); // merge data in overlay mode if (patchMode == PatchMode.Overlay) { Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; target.GetData(0, targetArea, newData, 0, newData.Length); for (int i = 0; i < sourceData.Length; i++) { Color pixel = sourceData[i]; if (pixel.A != 0) // not transparent { newData[i] = pixel; } } sourceData = newData; } // patch target texture target.SetData(0, targetArea, sourceData, 0, pixelCount); }
public static void PatchExtendedTileSheet(this IAssetDataForImage asset, Texture2D source, Rectangle?sourceArea = null, Rectangle?targetArea = null, PatchMode patchMode = PatchMode.Replace) { string assetName = asset.AssetName.Replace('/', '\\'); if (!extendedTextureAssets.ContainsKey(assetName) || !targetArea.HasValue) { asset.PatchImage(source, sourceArea, targetArea, patchMode); return; } extendedTextures.Remove(extendedTextureAssets[assetName].BaseTileSheet); extendedTextureAssets[assetName].BaseTileSheet = asset.Data; extendedTextures.Add(extendedTextureAssets[assetName].BaseTileSheet, extendedTextureAssets[assetName]); var adjustedTarget = GetAdjustedTileSheetTarget(asset.Data, targetArea.Value); //Log.trace("Tilesheet target:" + adjustedTarget.TileSheet + " " + adjustedTarget.Y); if (adjustedTarget.TileSheet == 0) { asset.PatchImage(source, sourceArea, targetArea, patchMode); return; } // Cheaty hack so I don't have to reimplement patch var oldData = asset.Data; var dataProp = asset.GetType().GetProperty("Data"); try { dataProp.SetValue(asset, GetTileSheet(oldData, adjustedTarget.TileSheet)); Rectangle r = targetArea.Value; r.Y = adjustedTarget.Y; //Log.trace($"Ext-patching on {assetName}={extendedTextureAssets[assetName].AssetPath}: {r}/{asset.Data.Width}x{asset.Data.Height}"); asset.PatchImage(source, sourceArea, r, patchMode); } finally { dataProp.SetValue(asset, oldData); } }
public static void PatchImage(IAssetDataForImage __instance, ref Texture2D source, ref Rectangle?sourceArea, Rectangle?targetArea, PatchMode patchMode) { if (source is ScaledTexture2D scaled) { var a = new Rectangle(0, 0, __instance.Data.Width, __instance.Data.Height); var s = new Rectangle(0, 0, source.Width, source.Height); var sr = !sourceArea.HasValue ? s : sourceArea.Value; var tr = !targetArea.HasValue ? sr : targetArea.Value; if (a == tr && patchMode == PatchMode.Replace) { __instance.ReplaceWith(source); return; } if (patchMode == PatchMode.Overlay) { scaled.AsOverlay = true; } if (scaled.AsOverlay) { Color[] data = new Color[(int)(tr.Width) * (int)(tr.Height)]; __instance.Data.getArea(tr).GetData(data); scaled.SetData <Color>(data); } if (__instance.Data is MappedTexture2D map) { map.Set(tr, source); } else { __instance.ReplaceWith(new MappedTexture2D(__instance.Data, new Dictionary <Rectangle?, Texture2D>() { { tr, source } })); } } }
/// <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) { 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.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 ConditionDictionary 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 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.")); } // 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 TokenString 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 TokenString 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 TokenString 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)); } } } // 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, migrator, 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 void Import(string fileName, PatchMode mode) { Clear(); string[] fileNameSplit = fileName.Split(new char[] { '/', '\\' }); string fileRelativePath = fileName.Remove(fileName.Length - (fileNameSplit[fileNameSplit.Length - 1].Length), fileNameSplit[fileNameSplit.Length - 1].Length); int minIndex = allVertices.Length; List <Vector3> positions = new List <Vector3>(); CollisionPatch currentPatch = new CollisionPatch("<Undefined>"); StreamReader rd = new StreamReader(fileName); while (!rd.EndOfStream) { string line = rd.ReadLine(); string[] split = line.Split(' '); if (split[0] == "mtllib") //Material Library { string currentDir = Environment.CurrentDirectory; Environment.CurrentDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(fileName)); CreateMaterialLibrary(line.Remove(0, split[0].Length).Trim()); Environment.CurrentDirectory = currentDir; } else if (split[0] == "o") //Object { if (mode == PatchMode.ByObject) { if (currentPatch.triangles.Count > 0) { patches.Add(currentPatch); } currentPatch = new CollisionPatch(split[1]); } } else if (split[0] == "v") //Vertex Position { Vector3 v = cvt.ParseVector3(split, 1); v.X *= -1; positions.Add(v * Settings.importScale); } else if (split[0] == "usemtl") //Material usage declaration { if (mode == PatchMode.ByMaterial) { if (currentPatch.triangles.Count > 0) { foreach (CollisionPatch patch in patches) { if (patch.name == currentPatch.name) { goto skipMakePatch; } } patches.Add(currentPatch); skipMakePatch :; } foreach (CollisionPatch patch in patches) { if (patch.name == split[1]) { currentPatch = patch; goto skipNewPatch; } } currentPatch = new CollisionPatch(split[1]); if (!materials.TryGetValue(split[1], out currentPatch.materialImage)) { currentPatch.materialImage = failImage; } skipNewPatch :; } } else if (split[0] == "f") // Face { short i0 = 0, ix = 0; for (int i = 1; i < split.Length; i++) { short index = 0; string[] vertexIndices = split[i].Split('/'); short.TryParse(vertexIndices[0], out index); index -= 1; if (i == 1) { i0 = index; } if (i >= 3) { CollisionTriangle t; currentPatch.triangles.Add(t = new CollisionTriangle((short)(i0 + minIndex), (short)(ix + minIndex), (short)(index + minIndex))); } ix = index; } } } rd.BaseStream.Close(); Array.Resize(ref allVertices, allVertices.Length + positions.Count); Array.Copy(positions.ToArray(), 0, allVertices, allVertices.Length - positions.Count, positions.Count); if (currentPatch.triangles.Count > 0) { if (mode == PatchMode.ByObject) { goto makePatch; } foreach (CollisionPatch patch in patches) { if (patch.name == currentPatch.name) { goto skipMakePatch; } } makePatch: patches.Add(currentPatch); skipMakePatch :; } Optimize(); }
public TargetMethodAttribute(Type targetClass, string targetMethodName, Type[] genericArguemtns, PatchMode patchMode, params Type[] argumentTypes) : this(targetClass, targetMethodName, patchMode, argumentTypes) =>
// /// <summary> // /// Hooks the console title. // /// </summary> TODO: this might actually be needed to avoid using the XNA shims // public void HookConsoleTitle() // { // var method = Terraria.Main.Methods.Single(x => x.Name == "DedServ"); // var callback = API.GameWindow.Methods.First(m => m.Name == "SetTitle"); // //// var il = method.Body.GetILProcessor(); // // var replacement = _asm.MainModule.Import(callback); // foreach (var ins in method.Body.Instructions // .Where(x => x.OpCode == OpCodes.Call // && x.Operand is MethodReference // && (x.Operand as MethodReference).DeclaringType.FullName == "System.Console" // && (x.Operand as MethodReference).Name == "set_Title")) // { // ins.Operand = replacement; // } // } /// <summary> /// Hooks start of the application for plugins to be loaded /// </summary> /// <param name="mode">Mode.</param> public void HookProgramStart(PatchMode mode) { if (mode == PatchMode.Server) { var method = Terraria.WindowsLaunch.Methods.Single(x => x.Name == "Main"); var callback = API.MainCallback.Methods.First(m => m.Name == "OnProgramStarted"); var il = method.Body.GetILProcessor(); var ret = il.Create(OpCodes.Ret); var call = il.Create(OpCodes.Call, _asm.MainModule.Import(callback)); var first = method.Body.Instructions.First(); il.InsertBefore(first, il.Create(OpCodes.Ldarg_0)); il.InsertBefore(first, call); il.InsertBefore(first, il.Create(OpCodes.Brtrue_S, first)); il.InsertBefore(first, ret); } else if (mode == PatchMode.Client) { var method = Terraria.Program.Methods.Single(x => x.Name == "LaunchGame"); var callback = API.MainCallback.Methods.First(m => m.Name == "OnProgramStarted"); var il = method.Body.GetILProcessor(); var ret = il.Create(OpCodes.Ret); var call = il.Create(OpCodes.Call, _asm.MainModule.Import(callback)); var first = method.Body.Instructions.First(); il.InsertBefore(first, il.Create(OpCodes.Ldarg_0)); il.InsertBefore(first, call); il.InsertBefore(first, il.Create(OpCodes.Brtrue_S, first)); il.InsertBefore(first, ret); } }
/********* ** 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 normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">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="normalizeAssetName">Normalize an asset name.</param> public EditImagePatch(string logName, ManagedContentPack contentPack, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle fromArea, TokenRectangle toArea, PatchMode patchMode, IMonitor monitor, Func <string, string> normalizeAssetName) : base(logName, PatchType.EditImage, contentPack, assetName, conditions, normalizeAssetName, fromAsset: fromAsset) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.Monitor = monitor; this.Contextuals .Add(fromArea) .Add(toArea); }
// /// <summary> // /// Allows eclipse to be started at any time // /// </summary> // //TODO mark as a feature, not a requirement for a functional OTA // public void HookEclipse() // { // var mth = Terraria.Main.Methods.Single(x => x.Name == "UpdateTime"); // var field = API.MainCallback.Fields.Single(x => x.Name == "StartEclipse"); // // var il = mth.Body.GetILProcessor(); // var start = il.Body.Instructions.Single(x => // x.OpCode == OpCodes.Ldsfld // && x.Operand is FieldReference // && (x.Operand as FieldReference).Name == "hardMode" // && x.Previous.OpCode == OpCodes.Call // && x.Previous.Operand is MethodReference // && (x.Previous.Operand as MethodReference).Name == "StartInvasion" // ); // // //Preserve // var old = start.Operand as FieldReference; // // //Replace with ours to keep the IL inline // start.Operand = _asm.MainModule.Import(field); // //Readd the preserved // il.InsertAfter(start, il.Create(OpCodes.Ldsfld, old)); // // //Now find the target instruction if the value is true // var startEclipse = il.Body.Instructions.Single(x => // x.OpCode == OpCodes.Stsfld // && x.Operand is FieldReference // && (x.Operand as FieldReference).Name == "eclipse" // && x.Next.OpCode == OpCodes.Ldsfld // && x.Next.Operand is FieldReference // && (x.Next.Operand as FieldReference).Name == "eclipse" // ).Previous; // il.InsertAfter(start, il.Create(OpCodes.Brtrue, startEclipse)); // // //Since all we care about is setting the StartEclipse to TRUE; we need to be able to disable once done. // il.InsertAfter(startEclipse.Next, il.Create(OpCodes.Stsfld, start.Operand as FieldReference)); // il.InsertAfter(startEclipse.Next, il.Create(OpCodes.Ldc_I4_0)); // } // public void HookBloodMoon() // { // var mth = Terraria.Main.Methods.Single(x => x.Name == "UpdateTime"); // var field = API.MainCallback.Fields.Single(x => x.Name == "StartBloodMoon"); // //return; // var il = mth.Body.GetILProcessor(); // var start = il.Body.Instructions.Single(x => // x.OpCode == OpCodes.Ldsfld // && x.Operand is FieldReference // && (x.Operand as FieldReference).Name == "spawnEye" // && x.Next.Next.OpCode == OpCodes.Ldsfld // && x.Next.Next.Operand is FieldReference // && (x.Next.Next.Operand as FieldReference).Name == "moonPhase" // ); // // //Preserve // var old = start.Operand as FieldReference; // var target = start.Next as Instruction; // // //Replace with ours to keep the IL inline // start.Operand = _asm.MainModule.Import(field); // //Readd the preserved // il.InsertAfter(start, il.Create(OpCodes.Ldsfld, old)); // // //Now find the target instruction if the value is true // Instruction begin = start.Next; // var i = 12; // while (i > 0) // { // i--; // begin = begin.Next; // } // il.InsertAfter(start, il.Create(OpCodes.Brtrue, begin)); // // //Since all we care about is setting the StartBloodMoon to TRUE; we need to be able to disable once done. // var startBloodMoon = il.Body.Instructions.Single(x => // x.OpCode == OpCodes.Ldsfld // && x.Operand is FieldReference // && (x.Operand as FieldReference).Name == "bloodMoon" // && x.Next.Next.OpCode == OpCodes.Ldsfld // && x.Next.Next.Operand is FieldReference // && (x.Next.Next.Operand as FieldReference).Name == "netMode" // ); // il.InsertAfter(startBloodMoon.Next, il.Create(OpCodes.Stsfld, start.Operand as FieldReference)); // il.InsertAfter(startBloodMoon.Next, il.Create(OpCodes.Ldc_I4_0)); // } /// <summary> /// Saves the patched assembly /// </summary> /// <param name="mode">Mode.</param> /// <param name="fileName">File name.</param> /// <param name="apiBuild">API build.</param> /// <param name="tdsmUID">Tdsm user interface.</param> /// <param name="name">Name.</param> /// <param name="swapOTA">If set to <c>true</c> swap OT.</param> public void Save(PatchMode mode, string fileName, int apiBuild, string tdsmUID, string name, bool swapOTA = false) { if (mode == PatchMode.Server) { //Ensure the name is updated to the new one _asm.Name = new AssemblyNameDefinition(name, new Version(0, 0, apiBuild, 0)); _asm.MainModule.Name = fileName; //Change the uniqueness from what Terraria has, to something different (that way vanilla isn't picked up by assembly resolutions) // var g = _asm.CustomAttributes.Single(x => x.AttributeType.Name == "GuidAttribute"); for (var x = 0; x < _asm.CustomAttributes.Count; x++) { if (_asm.CustomAttributes[x].AttributeType.Name == "GuidAttribute") { _asm.CustomAttributes[x].ConstructorArguments[0] = new CustomAttributeArgument(_asm.CustomAttributes[x].ConstructorArguments[0].Type, tdsmUID); } else if (_asm.CustomAttributes[x].AttributeType.Name == "AssemblyTitleAttribute") { _asm.CustomAttributes[x].ConstructorArguments[0] = new CustomAttributeArgument(_asm.CustomAttributes[x].ConstructorArguments[0].Type, name); } else if (_asm.CustomAttributes[x].AttributeType.Name == "AssemblyProductAttribute") { _asm.CustomAttributes[x].ConstructorArguments[0] = new CustomAttributeArgument(_asm.CustomAttributes[x].ConstructorArguments[0].Type, name); } //else if (_asm.CustomAttributes[x].AttributeType.Name == "AssemblyFileVersionAttribute") //{ // _asm.CustomAttributes[x].ConstructorArguments[0] = // new CustomAttributeArgument(_asm.CustomAttributes[x].ConstructorArguments[0].Type, "1.0.0.0"); //} } } // foreach (var mod in _asm.Modules) // { // for (var x = mod.Resources.Count - 1; x >= 0; x--) // { // mod.Resources.RemoveAt(x); // } // } //_asm.Write(fileName); using (var fs = File.OpenWrite(fileName)) { _asm.Write(fs); fs.Flush(); fs.Close(); } if (swapOTA) SwapOTAReferences(); }
/********* ** 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> /// Onward from this specific instruction is client code, and code afterwards returns for the server. (Odd i know) /// However, if we dont return and skipMenu is set, the server will crash /// </summary> public void SkipMenu(PatchMode mode) { if (mode != PatchMode.Server) throw new Exception("SkipMenu is a server-only fix"); var initialise = Terraria.Main.Methods.Single(x => x.Name == "Initialize"); var loc = initialise.Body.Instructions .Where(x => x.OpCode == OpCodes.Ldsfld && x.Operand is FieldDefinition) //.Select(x => x.Operand as FieldDefinition) .Single(x => (x.Operand as FieldDefinition).Name == "skipMenu"); var il = initialise.Body.GetILProcessor(); il.InsertBefore(loc, il.Create(OpCodes.Ret)); }
public static bool PatchImage(IAssetDataForImage __instance, XTexture2D source, XRectangle?sourceArea, XRectangle?targetArea, PatchMode patchMode) { if (!Config.SMAPI.ApplyPatchEnabled) { return(true); } // get texture if (source is null) { throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); } XTexture2D target = __instance.Data; // get areas sourceArea ??= new(0, 0, source.Width, source.Height); targetArea ??= new(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate if (!source.Bounds.Contains(sourceArea.Value)) { throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); } if (!target.Bounds.Contains(targetArea.Value)) { throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); } if (sourceArea.Value.Size != targetArea.Value.Size) { throw new InvalidOperationException("The source and target areas must be the same size."); } if (GL.Texture2DExt.CopyTexture(source, sourceArea.Value, target, targetArea.Value, patchMode)) { return(false); } // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; var sourceData = GC.AllocateUninitializedArray <XColor>(pixelCount); source.GetData(0, sourceArea, sourceData, 0, pixelCount); // merge data in overlay mode if (patchMode == PatchMode.Overlay) { // get target data var targetData = GC.AllocateUninitializedArray <XColor>(pixelCount); target.GetData(0, targetArea, targetData, 0, pixelCount); // merge pixels for (int i = 0; i < sourceData.Length; i++) { var above = sourceData[i]; var below = targetData[i]; // shortcut transparency if (above.A < MinOpacity) { sourceData[i] = below; continue; } if (below.A < MinOpacity) { sourceData[i] = above; continue; } // merge pixels // This performs a conventional alpha blend for the pixels, which are already // premultiplied by the content pipeline. The formula is derived from // https://shawnhargreaves.com/blog/premultiplied-alpha.html. float alphaBelow = 1 - (above.A / 255f); sourceData[i] = new XColor( (int)(above.R + (below.R * alphaBelow)), // r (int)(above.G + (below.G * alphaBelow)), // g (int)(above.B + (below.B * alphaBelow)), // b Math.Max(above.A, below.A) // a ); } } // patch target texture target.SetData(0, targetArea, sourceData, 0, pixelCount); return(false); }
public void ApplyPatch(string filePath, PatchMode mode) { var strs = File.ReadAllLines(filePath, Encoding.UTF8); string lastSection = null; for (int i = 0; i < strs.Length; i++) { var str = strs[i]; if (str.Length < 1) { continue; } var comm = str.IndexOf("//"); if (comm > -1) { str = str.Substring(0, comm); if (str.Length < 1) { continue; } } var sub = str.TrimStart(); if (sub.Length < 1) { continue; } if (sub[0] == '[') { var cbi = sub.IndexOf(']', 1); if (cbi > 0) { var section = sub.Substring(0, cbi + 1); lastSection = section; if (!Data.ContainsKey(section)) { Data[section] = new List <Parameter>(); } } } else if (lastSection != null) { var esi = str.IndexOf('=', 1); if (esi > 0) { string name = str.Substring(0, esi), value = str.Substring(esi + 1); Parameter param; if ((mode == PatchMode.Addon) || ((param = SeekParam(Data[lastSection], name)) == null)) { Data[lastSection].Add(new Parameter { Name = name, Value = value }); } else { var strRep = value.IndexOf("%s%"); if (strRep > -1) { var hasBraces = ((param.Value[0] == '"') && (param.Value[param.Value.Length - 1] == '"')); if (hasBraces) { param.Value = param.Value.Substring(0, param.Value.Length - 1); } param.Value = $"{value.Substring(0, strRep)}{param.Value}{value.Substring(strRep + 3)}{(hasBraces ? "\"" : "")}"; } else { param.Value = value; } } } } } }
/// <summary>Overwrite part of the image.</summary> /// <param name="source">The image to patch into the content.</param> /// <param name="sourceArea">The part of the <paramref name="source"/> to copy (or <c>null</c> to take the whole texture). This must be within the bounds of the <paramref name="source"/> texture.</param> /// <param name="targetArea">The part of the content to patch (or <c>null</c> to patch the whole texture). The original content within this area will be erased. This must be within the bounds of the existing spritesheet.</param> /// <param name="patchMode">Indicates how an image should be patched.</param> /// <exception cref="ArgumentNullException">One of the arguments is null.</exception> /// <exception cref="ArgumentOutOfRangeException">The <paramref name="targetArea"/> is outside the bounds of the spritesheet.</exception> public void PatchImage(Texture2D source, Rectangle?sourceArea = null, Rectangle?targetArea = null, PatchMode patchMode = PatchMode.Replace) { // get texture if (source == null) { throw new ArgumentNullException(nameof(source), "Can't patch from a null source texture."); } Texture2D target = this.Data; // get areas sourceArea = sourceArea ?? new Rectangle(0, 0, source.Width, source.Height); targetArea = targetArea ?? new Rectangle(0, 0, Math.Min(sourceArea.Value.Width, target.Width), Math.Min(sourceArea.Value.Height, target.Height)); // validate if (sourceArea.Value.X < 0 || sourceArea.Value.Y < 0 || sourceArea.Value.Right > source.Width || sourceArea.Value.Bottom > source.Height) { throw new ArgumentOutOfRangeException(nameof(sourceArea), "The source area is outside the bounds of the source texture."); } if (targetArea.Value.X < 0 || targetArea.Value.Y < 0 || targetArea.Value.Right > target.Width || targetArea.Value.Bottom > target.Height) { throw new ArgumentOutOfRangeException(nameof(targetArea), "The target area is outside the bounds of the target texture."); } if (sourceArea.Value.Width != targetArea.Value.Width || sourceArea.Value.Height != targetArea.Value.Height) { throw new InvalidOperationException("The source and target areas must be the same size."); } // get source data int pixelCount = sourceArea.Value.Width * sourceArea.Value.Height; Color[] sourceData = new Color[pixelCount]; source.GetData(0, sourceArea, sourceData, 0, pixelCount); // merge data in overlay mode if (patchMode == PatchMode.Overlay) { // get target data Color[] targetData = new Color[pixelCount]; target.GetData(0, targetArea, targetData, 0, pixelCount); // merge pixels Color[] newData = new Color[targetArea.Value.Width * targetArea.Value.Height]; target.GetData(0, targetArea, newData, 0, newData.Length); for (int i = 0; i < sourceData.Length; i++) { Color above = sourceData[i]; Color below = targetData[i]; // shortcut transparency if (above.A < AssetDataForImage.MinOpacity) { continue; } if (below.A < AssetDataForImage.MinOpacity) { newData[i] = above; continue; } // merge pixels // This performs a conventional alpha blend for the pixels, which are already // premultiplied by the content pipeline. The formula is derived from // https://blogs.msdn.microsoft.com/shawnhar/2009/11/06/premultiplied-alpha/. // Note: don't use named arguments here since they're different between // Linux/Mac and Windows. float alphaBelow = 1 - (above.A / 255f); newData[i] = new Color( (int)(above.R + (below.R * alphaBelow)), // r (int)(above.G + (below.G * alphaBelow)), // g (int)(above.B + (below.B * alphaBelow)), // b Math.Max(above.A, below.A) // a ); } sourceData = newData; } // patch target texture target.SetData(0, targetArea, sourceData, 0, pixelCount); }
/********* ** Public methods *********/ /// <summary>Construct an instance.</summary> /// <param name="path">The path to the patch from the root content file.</param> /// <param name="assetName">The normalized asset name to intercept.</param> /// <param name="conditions">The conditions which determine whether this patch should be applied.</param> /// <param name="fromAsset">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="updateRate">When the patch should be updated.</param> /// <param name="contentPack">The content pack which requested the patch.</param> /// <param name="parentPatch">The parent patch for which this patch was loaded, if any.</param> /// <param name="monitor">Encapsulates monitoring and logging.</param> /// <param name="normalizeAssetName">Normalize an asset name.</param> public EditImagePatch(LogPathBuilder path, IManagedTokenString assetName, IEnumerable <Condition> conditions, IManagedTokenString fromAsset, TokenRectangle fromArea, TokenRectangle toArea, PatchMode patchMode, UpdateRate updateRate, ManagedContentPack contentPack, IPatch parentPatch, IMonitor monitor, Func <string, string> normalizeAssetName) : base( path: path, type: PatchType.EditImage, assetName: assetName, conditions: conditions, normalizeAssetName: normalizeAssetName, fromAsset: fromAsset, updateRate: updateRate, contentPack: contentPack, parentPatch: parentPatch ) { this.FromArea = fromArea; this.ToArea = toArea; this.PatchMode = patchMode; this.Monitor = monitor; this.Contextuals .Add(fromArea) .Add(toArea); }
/// <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 Task PatchAsync(IProgress <int> progress, PatchMode mode, float multiplier) { return(Task.Run(() => { var _multiplier = BitConverter.GetBytes(multiplier); for (var i = 0; i < _candidateIds.Count(); i++) { var id = _candidateIds[i]; var binary = _archive.Get(id); var disassembly = D3DCompiler.Disassemble(binary); if (disassembly.Contains("FP_FadePower")) { // actor - patching these causes glitches with camera-overlap fade progress.Report(i + 1); continue; } bool found = false; byte[] newBinary = (byte[])binary.Clone(); foreach (var pos in binary.SigScan(block_sig)) { switch (mode) { case PatchMode.DisableDithering: Buffer.BlockCopy(nop_12x, 0, newBinary, pos + block_discard_offset, 12); break; case PatchMode.NarrowDithering: Buffer.BlockCopy(_multiplier, 0, newBinary, pos + block_float_offset, 4); break; } found = true; } foreach (var pos in binary.SigScan(block2_sig)) { switch (mode) { case PatchMode.DisableDithering: Buffer.BlockCopy(nop_12x, 0, newBinary, pos + block2_discard_offset, 12); break; case PatchMode.NarrowDithering: Buffer.BlockCopy(_multiplier, 0, newBinary, pos + block2_float_offset, 4); break; } found = true; } if (found) { int[] checksum = DXBCChecksum.DXBCChecksum.CalculateDXBCChecksum(newBinary); Buffer.BlockCopy(checksum, 0, newBinary, 4, 16); _archive.Replace(id, newBinary); } progress.Report(i + 1); } })); }