SMAPI - Stardew Modding API

SMAPI - Stardew Modding API

971k Downloads

Update for Stardew Valley 1.2

Pathoschild opened this issue ยท 3 comments

commented

Update SMAPI to account for the changes in Stardew Valley 1.2.

To do

SMAPI compatibility

  • Remove low-hanging deprecations.
    Since Stardew Valley 1.2 breaks many mods anyway, remove the oldest deprecations and fix the issues that are easiest for mods to update.
  • Disambiguate references to Farmer.
    SDV 1.2 introduces a root Farmer namespace, so all references to Farmer are suddenly ambiguous.
  • Rewrite SGame.Draw code.
    SDV 1.2 significantly changed the Game1.Draw code; rewrite SMAPI's override to compensate.
  • Adjust events to account for changes in 1.2.
    In particular, players can now exit to the main menu which means we can no longer assume the game is only loaded once.
  • Fix world-ready events being raised before the game finishes loading the save in 1.2.
  • Bump minimum game version to 1.2.

Mod compatibility

  • Mark incompatible mods:

    mod up to version reason
    AccessChestAnywhere 1.1 "Method not found: 'Void StardewValley.Item.set_Name(System.String)'."
    Almighty Tool 1.1.1 uses obsolete StardewModdingAPI.Extensions.
    Better Sprinklers 2.1-EntoPatch.7 uses obsolete StardewModdingAPI.Extensions.
    Casks Anywhere 1.1 uses obsolete StardewModdingAPI.Inheritance.ItemStackChange.
    Chests Anywhere 1.8.2 "Method not found: 'Void StardewValley.Menus.TextBox.set_Highlighted(Boolean)'."
    CJB Automation 1.4 "Method not found: 'Void StardewValley.Item.set_Name(System.String)'."
    CJB Cheats Menu 1.13 uses removed Game1.borderFont.
    CJB Item Spawner 1.6 uses removed Game1.borderFont.
    Cooking Skill 1.0.3 "Method not found: 'Void StardewValley.Buff..ctor(Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, Int32, System.String)'".
    Enemy Health Bars 1.7 uses obsolete GraphicsEvents.DrawTick.
    Entoarox Framework 1.6.4
    (not latest)
    uses obsolete StardewModdingAPI.Inheritance.SObject until 1.6.1; then crashes until 1.6.4 ("Entoarox Framework requested an immediate game shutdown: Fatal error attempting to update player tick properties System.NullReferenceException: Object reference not set to an instance of an object. at Entoarox.Framework.PlayerHelper.Update(Object s, EventArgs e)".
    Extended Fridge 0.94
    (1.0 in manifest)
    "Field not found: 'StardewValley.Game1.mouseCursorTransparency'."
    Get Dressed 3.2 NullReferenceException in GameEvents.UpdateTick.
    Lookup Anything 1.10.1 FormatException when looking up NPCs.
    Makeshift Multiplayer 0.2.10 uses obsolete GraphicsEvents.OnPreRenderHudEventNoCheck.
    No Soil Decay 0.5 uses Assembly.GetExecutingAssembly().Location.
    Point-and-Plant 1.0.2 uses obsolete StardewModdingAPI.Extensions.
    Reusable Wallpapers 1.5 uses obsolete StardewModdingAPI.Inheritance.ItemStackChange.
    Save Anywhere 2.0 "Method not found: 'Void StardewModdingAPI.Command.CallCommand(System.String)'".
    StackSplitX 1.0 uses SMAPI's internal SGame class.
    Teleporter 1.0.2 'InvalidOperationException: The StardewValley.Menus.MapPage object doesn't have a private 'points' instance field".
    Zoryn's Better RNG 1.5 uses SMAPI's internal SGame class.
    Zoryn's Calendar Anywhere 1.5 uses SMAPI's internal SGame class.
    Zoryn's Health Bars 1.5 uses SMAPI's internal SGame class.
    Zoryn's Movement Mod 1.5 uses SMAPI's internal SGame class.
    Zoryn's Regen Mod 1.5 uses SMAPI's internal SGame class.
  • Rewrite references to...

    • Game1.activeClickableMenu as a field (now a property).
    • Game1.player as a field (now a property).
    • Game1.gameMode as a field (now a property).
commented

@Pathoschild Here's what I came up with for fixing the references to Game1.activeClickableMenu, as stated on Discord, I figured I'd just let you handle the implementation yourself.

Fixing the activeClickableMenu Problem

Steps to Success

  1. Iterate through each of the loaded assemblies.
  2. Iterate through each Method of the current assembly.
  3. Iterate through each Instruction from the current Method.
  4. Check for a valid Instruction that matches Game1.activeClickableMenu as a field.
  5. Rewrite the current Instruction with a new one mapped to a property.
  6. Write the assembly changes.

Some of the code can be used that's already in AssemblyLoader like shown here: https://github.com/Pathoschild/SMAPI/blob/develop/src/StardewModdingAPI/Framework/AssemblyLoader.cs#L196

Example Code

Constants.cs:24

public static string activeDangus = "dangus";
private static string _activeDrangus = "drangus";
public static string activeDrangus
{
    get { return _activeDrangus; }
    set { _activeDrangus = value; }
}

AssemblyLoader.cs:160

foreach (var type in module.Types)
{
    if (type.HasMethods)
    {
        foreach (var method in type.Methods)
        {
            if (method.HasBody)
            {
                var il = method.Body.GetILProcessor();

                foreach (var instruction in method.Body.Instructions.ToArray())
                {
                    if ((instruction.OpCode == OpCodes.Ldsfld || instruction.OpCode == OpCodes.Stsfld) &&
                        instruction.Operand is FieldReference &&
                        (instruction.Operand as FieldReference).FullName == "System.String StardewModdingAPI.Constants::activeDangus")
                    {
                        string which = (instruction.OpCode == OpCodes.Ldsfld) ? "get" : "set";
                        Instruction call = il.Create(
                            OpCodes.Call, 
                            method.Module.Import(typeof(Constants).GetMethod($"{which}_activeDrangus"))
                        );
                        il.Replace(instruction, call);
                    }
                }
            }
        }
    }
}

This code assuming that you allow the method to return with true will rewrite the assembly and give you the intended results. I tested this on trainer mod with the following code:

TrainerMod.cs:40~

private string refDrangus = Constants.activeDrangus;
private string refDangus = Constants.activeDangus;

public override void Entry(IModHelper helper)
{
    this.RegisterCommands();
    GameEvents.UpdateTick += this.ReceiveUpdateTick;

    Console.WriteLine("refDrangus: " + refDrangus);
    Console.WriteLine("refDangus: " + refDangus);

    Constants.activeDrangus = "sample";
    Constants.activeDangus = "simple";

    Console.WriteLine("Constants.activeDrangus: " + Constants.activeDrangus);
    Console.WriteLine("Constants.activeDangus: " + Constants.activeDangus);
}

The output of this will be:

refDrangus: drangus
refDangus: drangus
...
Constants.activeDrangus: simple
Constants.activeDangus: simple

Provided you can read my obtuse testing name differences of drangus with an r and dangus without the r, you can see that all occurrences of dangus get rewritten as drangus.

Important Notes

So, here are the important things to take from this test for implementation...

  • OpCodes.Ldsfld and OpCodes.Stsfld stand for Load Static Field and Set Static Field respectively. These opcodes are used for static fields (go figure) and are going to be the same ones references for Game1.activeClickableMenu.

  • instruction.Operand is FieldReference is important as the Operand needs to be a FieldReference, nuff said.

  • The string System.String StardewModdingAPI.Constants::activeDangus is matched against the FieldReference.FullName to ensure that the right Instruction is being matched. For Game1.activeClickableMenu the string will be something like StardewValley.Menus.IClickableMenu StardewValley.Game1::activeClickableMenu or something similar.

  • OpCodes.Call is used when constructing the new Instruction to reference the property, the important pair to this is the actual MethodReference which is generated by this line:
    method.Module.Import(typeof(Constants).GetMethod("xxx_activeDrangus")). The call to method.Module.Import returns a MethodReference which is used when creating the Instruction. The get and set used in the property will generate the methods get_{PropertyName} and set_{PropertyName} respectively.

  • il.Replace(instruction, call) is used to replace the current iterating Instruction with the new one generated. One line, easy to replace, the only thing of course is to not iterate over the actual enumerable, and instead create an array or something from it (something you're fully aware of and already doing later on in the file).

Finale

As for the implementation, I did try to do something with the Method Rewriter classes you had, but found that it didn't suit my needs and I figured I'd let you handle the implementation (sorry). More importantly however, I foresee this being a potential problem again in the future if SDV updates again and changes something like this. I believe that some sort of rewriter can be written to easily take these key points I mentioned above in the notes and rewrite Mod assemblies really easily and dynamically based on a new class added to SMAPI much like you're already doing with the SpriteBatchRewriter.

Problems with the current implementation are that for that you need to rewrite instructions, not methods, even though you technically are rewriting a method. More importantly, the ShouldRewrite call accepts a MethodReference and not a FieldReference which is what's needed here, plus the rewrite isn't exactly the same either.

Hopefully you have everything here needed to implement this in some elegant way that I can't be bothered to mess with right now and instead took the time to write you this lovely comment.

Ping me on Discord if you have any questions.

commented

Done in the upcoming SMAPI 1.9 release. It seems like Stardew Valley 1.2 development is winding down, so I don't expect any other major changes going forward.

commented

Split between SMAPI 1.9 (to be released for Stardew Valley 1.11) and SMAPI 1.10 (to be released for Stardew Valley 1.2 beta).