Update for Stardew Valley 1.2
Pathoschild opened this issue ยท 3 comments
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 rootFarmer
namespace, so all references toFarmer
are suddenly ambiguous. - Rewrite
SGame.Draw
code.
SDV 1.2 significantly changed theGame1.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
inGameEvents.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).
-
@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
- Iterate through each of the loaded assemblies.
- Iterate through each
Method
of the current assembly. - Iterate through each
Instruction
from the currentMethod
. - Check for a valid
Instruction
that matchesGame1.activeClickableMenu
as a field. - Rewrite the current
Instruction
with a new one mapped to a property. - 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
andOpCodes.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 forGame1.activeClickableMenu
. -
instruction.Operand is FieldReference
is important as the Operand needs to be aFieldReference
, nuff said. -
The string
System.String StardewModdingAPI.Constants::activeDangus
is matched against theFieldReference.FullName
to ensure that the rightInstruction
is being matched. ForGame1.activeClickableMenu
the string will be something likeStardewValley.Menus.IClickableMenu StardewValley.Game1::activeClickableMenu
or something similar. -
OpCodes.Call
is used when constructing the newInstruction
to reference the property, the important pair to this is the actualMethodReference
which is generated by this line:
method.Module.Import(typeof(Constants).GetMethod("xxx_activeDrangus"))
. The call tomethod.Module.Import
returns aMethodReference
which is used when creating theInstruction
. Theget
andset
used in the property will generate the methodsget_{PropertyName}
andset_{PropertyName}
respectively. -
il.Replace(instruction, call)
is used to replace the current iteratingInstruction
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.
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.