SMAPI - Stardew Modding API

SMAPI - Stardew Modding API

971k Downloads

Error loading Data\Movies if any Harmony 2 patch is applied to certain methods

Pathoschild opened this issue ยท 4 comments

commented

If certain methods (including GameLocation.performAction) are patched with Harmony 2.0+, loading Data\Movies fails with this vague error in the XNA content pipeline (see sample log):

ContentLoadException: Error loading "Data\Movies".

This blocks #711.

Known facts

  • The issue occurs with...
    • Harmony 2.1, 2.0.4, 2.0.1, and 2.0-RC1 (but not 1.2.0.1).
    • All patch types.
    • Patching GameLocation.performAction and some other instance methods. (No static methods seem to be affected.)
  • It happens even if it's the only patch and the patch does nothing, and before the patched method is ever called.
  • It happens whether you load it manually (see repro steps), or the game loads it later.
  • It only affects Data\Movies (whose type is Dictionary<string, MovieData>). It doesn't affect Data\HomeRenovations (whose type is a similar Dictionary<string, HomeRenovation>).

@Platonymous traced the deserializer while it was reading the file. It seems to work fine at first, then start reading invalid bytes:

@sophxm tracked it down to this specific line in MonoMod:

Repro steps

  1. Create a mod with this code:
    public class ModEntry : Mod
    {
        public override void Entry(IModHelper helper)
        {
            helper.Events.GameLoop.GameLaunched += (sender, e) =>
            {
                new Harmony(this.ModManifest.UniqueID)
                    .Patch(
                        original: AccessTools.Method(typeof(GameLocation), "performAction"),
                        prefix: new HarmonyMethod(typeof(ModEntry), nameof(ModEntry.Prefix))
                    );
    
                var data = Game1.content.Load<Dictionary<string, MovieData>>("Data\\Movies");
            };
        }
    
        public static void Prefix() { }
    }
  2. Launch the game.
  3. The error occurs as soon as the game launches (see sample log).
commented

This seems to be caused by something in HarmonyLib.MethodPatcher.CreateReplacement. In PatchFunctions.UpdateWrapper:

no error error occurs

In the second case, note that the error happens even if the method isn't detoured.

commented

This doesn't just affect GameLocation.performAction, it occurs with a variety of instance methods (static functions seem entirely unaffected). I initially thought it was isolated to methods that call MovieTheater.GetMovieData(), or call a static function that later calls that function, as the following methods also cannot be patched:

  • MovieTheaterScreeningEvent#getMovieEvent, MovieTheater#FormatString, MovieTheater#updateEvenIfFarmerIsntHere, MovieTheater#resetSharedState.

However, patching the following two methods works fine, even though they are also instance methods that call MovieTheater.GetMovieData():
MovieTheater#GetDialogueForCharacter, MovieTheater#OnStartMovieEvent.

There are also some other methods that result in the same error, but don't call that function at all, like MovieReaction#ShouldApplyToMovie.

The invalid data the deserializer reads changes depending on which function you're patching (at least, patching GameLocation.performAction results in different data to the others listed here). This might point towards the cause.

This bug still occurs with the latest versions of harmony (both the 2.0.4 release and the current master commit, ad6eba6).

commented

MovieData can no longer be decoded after evaluating the highlighted line shown in this screenshot. The call stack is shown in the debug window*, but the scenario occurs when MonoMod attempts to generate a method containing a FieldReference to a member of MovieData. I think this also explains the bizarre circumstances of which methods work and which don't (detailed above) - all of the methods that cannot be patched contain such a FieldReference.

I assume the ContentReader used to decode MovieData also uses this RuntimeTypeCache, and the cache is either poisoned or invalidated by the time decoding happens. MovieData can be decoded before this Harmony patch is applied, so I assume this is a Harmony/Monomod bug.

*This screenshot was taken using an edited Harmony build that simply copies the method instead of replacing it (so the call stack contains TryCreateILCopy instead of CreateReplacement), but the circumstances are the same.

commented

Fixed in the harmony-2 branch for the upcoming SMAPI 3.12.0.

@0x0ade tracked it down to an esoteric issue in XNA Framework where deserialization depends on the order of fields returned by Type.GetFields, but that order changes after Harmony/MonoMod use reflection to access the fields due to an issue in .NET Framework (see also this tweet). 0x0ade will add a workaround to MonoMod, and provided a temporary utility class we can use in SMAPI in the meantime to unblock the Harmony 2.x migration.

Thanks to 0x0ade and everyone else who helped track down and test the issue!