SMAPI - Stardew Modding API

SMAPI - Stardew Modding API

971k Downloads

Better events

Pathoschild opened this issue · 28 comments

commented

SMAPI's events are pretty ad hoc with lots of gaps (e.g. KeyPressed but no MouseClicked). Plan out a consistent event system which covers most of the important things for a future version of SMAPI.

commented

An event that fires before showEndOfNightStuff, to fire immediately after player loses consciousness (voluntarily or not).

commented

We have to do a lot of menu overloading for .. a ton of things. It might not be a bad idea to have events instead.

commented

@JohnsonNicholas Can you list specific examples of events you'd want?

commented

Well. Namely, I'd prefer if we had seperate events for

  • Purchasing/Buying something, since not all of these are acutally in ShopMenu instances. (... sadly)
  • Dialogue events, in case you want to push a custom dialogue via mod (this is mainly so you don't have to use in game variables as much)

Someone already suggested night events, and it looks like you have item events in mind already. Stuff like TVs and such is pretty niche, and I don't see the point in custom events for that on the API level.

Quest stuff is very niche, and honestly, just having an IList so we can have SMAPI inject ObservableList in would be enough for me. A lot of some things I was thinking about (mail events, or just in-game event.. events.. probably would be served by that alone)

Edit: Okay, I'll be honest, I would actually really like events that fire when you talk or give an item to someone, when you hit or kill a monster, and possibly when you destroy or add an object. The last you can do now, as SerializableDirectories expose events. But having more low level events would be useful.

Edit2: merged them into one comment. Like a sane person.

commented

.. So from the discussions:
Something like PlayerEvents.OnDeath and PlayerEvents.OnCollapse would be neat too.

commented

.. Okay, so this might seem odd. But I should have considered it. I've been investigating the crop bug and am somewhat sure that an intermittent bug is triggered by the potential removal of elements during a for-loop iteration. (or possibly somewhere in here: https://www.dropbox.com/s/rkzfdkd3u846kr1/SDV_CropNewDay.txt?dl=0 ) but in either case, the more than likely patch fix would be doing pre-processing to minimize the chances of the game doing it.

But the problem is the only existing event SaveEvents.BeforeSave still triggers after the crops are processed. (Which I found out while testing what I thought might fix it.). The idea is that the following events might not be a bad idea:

  • CropEvents.OnHarvest
  • CropEvents.OnDeath
  • CropEvents.OnPlant
  • CropEvents.BeforeDayUpdate
  • CropEvents.AfterDayUpdate (not sure about this one, mind you)

Granted, the first three can be overriden already, but at least a BeforeDayUpdate for pre-processing couldn't hurt.

commented

A DrawWeather event at the following point in the code:
https://github.com/Pathoschild/SMAPI/blob/2.3/src/SMAPI/Framework/SGame.cs#L1084

commented

An example of the purchasing things without the shop menu is purchasing backpacks. Which brings me to another useful place for an event: GameLocation.answerDialogueAction

commented

One thing I've found useful is a hook for Game1.loadForNewGame. This is useful for adding custom locations back to the game so that the loading process will load their objects, terrain features, etc. correctly.
@spacechase0

That's supported in SMAPI 2.5 using this specialised event, which is still raised when other events are paused. (Advanced Location Loader uses it for custom locations.)

SpecialisedEvents.UnvalidatedUpdateTick += this.OnLoadForNewGame;

public void OnLoadForNewGame(object sender, EventArgs e)
{
   if (Game1.loadedGame)
   {
      // do stuff
      SpecialisedEvents.UnvalidatedUpdateTick -= this.OnLoadForNewGame;
   }
}
commented

Proposed design guidelines for SMAPI 3.0 events:

  • All events have a consistent name format: BeforeEventName or AfterEventName.
  • All events have their own EventArgs type with a name derived from the event name (even if it doesn't contain any data). That makes the signature predictable for new developers, and lets SMAPI add event data later without breaking changes.
    private void Location_AfterObjectsChanged(object sender, EventArgsLocationAfterObjectsChanged e) { }
  • All events are accessible through the mod helper:
    helper.Events.Locations.AfterObjectsChanged += ...;
commented

Location/world events have a lot of missing pieces, so I think that's a good place to start. I propose these helper.Events.World events:

event purpose
AfterLocationsChanged Raised when locations are added/removed (including indoor locations). Event data: added/removed locations.
AfterBuildingsChanged Raised when buildings are added/removed in any location. If several locations changed, it's raised once per location. Event data: location, added/removed buildings.
AfterLargeTerrainFeaturesChanged Raised when large terrain features (e.g. bushes) are added/removed in any location. If several locations changed, it's raised once per location. Event data: location, added/removed large terrain features.
AfterNpcsChanged Raised when NPCs (including villagers, monsters, pets, etc) are added/removed in any location. If several locations changed, it's raised once per location. Event data: location, added/removed NPCs.
AfterObjectsChanged Raised when objects are added/removed in any location. If several locations changed, it's raised once per location. Event data: location, added/removed objects.
AfterTerrainFeaturesChanged Raised when terrain features (e.g. trees or flooring) are added/removed in any location. If several locations changed, it's raised once per location. Event data: location, added/removed terrain features.

Those can all be done efficiently with SMAPI's current 'watcher' architecture. I want to add crop events too, but that'd be inefficient as-is (since SMAPI would need to check every tilled dirt in every location, 60 times per second). I might look into having SMAPI use Harmony internally to patch callbacks into net types to support that.

commented

An event that would be useful for me would be

event purpose
TimeEvents.BeforeTenMinuteUpdate Raised before the game handles the update tick that causes the clock to change.

My specific use case is for safe lightning: it needs to run code during that time, and there's no reliable way to do so currently.

Besides this specific case, it would be generally useful to have event raised after the clock changes in general, which could be something like

event purpose
TimeEvents.AfterTenMinuteUpdate Raised after the game handles the update tick that causes the clock to change.
commented

The first ten such events are ready in SMAPI 2.6 beta 16:

  • Input
    • ButtonPressed
    • ButtonReleased
    • CursorMoved
    • MouseWheelScrolled
  • World
    • LocationListChanged
    • BuildingListChanged
    • LargeTerrainFeatureListChanged
    • NpcListChanged
    • ObjectListChanged
    • TerrainFeatureListChanged

These are marked experimental and may change at any time. See migrate to SMAPI 3.0 > event changes on the wiki for more info, including how to transition old events to these.

commented

A few ideas:

  • Input events:
    • mouse clicked (get absolute position / tile / grab tile / tile action; suppress click from game).
    • button pressed;
    • button held;
    • button released.
  • Inventory events:
    • item picked up;
    • item consumed;
    • item dropped / destroyed / sold / lost;
    • inventory changed (generic version of the above);
    • tool used.
commented

Input events done via #316 and #317 in 2.0.

commented

One thing I've found useful is a hook for Game1.loadForNewGame. This is useful for adding custom locations back to the game so that the loading process will load their objects, terrain features, etc. correctly.

This can be done without #320. I'm currently doing it like this:

            if (Game1.currentLoader != null)
            {
                if (Game1.currentLoader.Current == 25 && prevLoaderNum != 25)
                {
                    SpaceEvents.InvokeOnBlankSave();
                }
                prevLoaderNum = Game1.currentLoader.Current;
            }

Relevant code in SaveGame.getLoadEnumerator:

      Game1.whichFarm = SaveGame.loaded.whichFarm;
      Game1.stats = SaveGame.loaded.stats;
      Game1.year = SaveGame.loaded.year;
      task = new Task(() =>
      {
        Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
        Game1.loadForNewGame(true);
      });
      task.Start();
      while (!task.IsCanceled && !task.IsCompleted && !task.IsFaulted)
      {
        yield return 24;
      }
      if (task.IsFaulted)
      {
        Game1.gameMode = 9;
        Exception exception = task.Exception.GetBaseException();
        Game1.debugOutput = Game1.parseText(exception.Message);
        Console.WriteLine(exception);
        SaveGame.IsProcessing = false;
        yield break;
      }
      if (SaveGame.CancelToTitle)
      {
        Game1.ExitToTitle();
      }
      yield return 25;
commented

Events that trigger when any Action on the building layer is triggered or any TouchAction on the back layer is triggered, right now EntoaroxFramework 1.x has the first, and 2.x has both, but these really would work a lot better as part of SMAPI itself.

commented

Per discord discussion, a event that allows mods to add onto the games light map would be very useful.

commented

Here's a recap of the events proposed in this ticket and their current status. These are listed in past tense, but they'd include before/after events where possible.

Crops

event proposer details
harvested @JohnsonNicholas Moved to #609.
died @JohnsonNicholas Moved to #609.
planted @JohnsonNicholas Moved to #609.
day updated @JohnsonNicholas Moved to #609.

Display

event proposer details
weather drawn @Entoarox Probably too specialised.

Input

event proposer details
mouse clicked @Pathoschild Done via Input.ButtonPressed.
button held @Pathoschild Done via Input.ButtonPressed + Input.ButtonReleased or the input API.
button released @Pathoschild Done via Input.ButtonReleased.
button pressed @Pathoschild Done via Input.ButtonPressed.

Player

event proposer details
item picked up @Pathoschild Moved to #610.
item consumed @Pathoschild Moved to #610.
item equipped @Pathoschild Moved to #610.
item dropped @Pathoschild Moved to #610.
item sold @Pathoschild Moved to #610.
item trashed @Pathoschild Moved to #610.
item purchased @JohnsonNicholas Moved to #610.
active item changed @Pathoschild Not really needed, easily to replicate with GameLoop.UpdateTicked if needed.
item dropped into machine @Pathoschild Not currently feasible.
inventory changed @Pathoschild Done via Player.InventoryChanged.
tool used @Pathoschild Moved to #610.
triggered map action @Entoarox TBD. Includes Action and TouchAction properties. Implemented by Entoarox Framework. Shelved, not currently feasible.
talked to NPC @JohnsonNicholas Moved to #611.
gifted to NPC @JohnsonNicholas Moved to #611. Implemented by Bookcase (see code + example usage).
attacked/killed NPC @JohnsonNicholas Moved to #612.
collapsed @JohnsonNicholas Moved to #613.
answered dialogue question @spacechase0 Moved to #611.

World

event proposer details
Added/removed location @Pathoschild Done via World.LocationListChanged.
Added/removed building @Pathoschild Done via World.BuildingListChanged.
Added/removed terrain @Pathoschild Done via World.TerrainFeatureListChanged.
Added/removed L terrain @Pathoschild Done via World.LargeTerrainFeatureListChanged.
Added/removed object @JohnsonNicholas
@Pathoschild
Done via World.ObjectListChanged et al.
Added/removed NPC @Pathoschild Done via World.NpcListChanged.

Specialised / other

event proposer details
Game1.loadForNewGame @spacechase0 Done via Specialised.LoadStageChanged or Specialised.UnvalidatedUpdateTicked.
showEndOfNightStuff @MercuriusXeno Done via GameLoop.DayEnding.
before save loaded @spacechase0 Done via Specialised.LoadStageChanged.
Ten-minute update @danvolchek TBD. Would happen before/after game changes the clock, runs lightning, etc. No longer needed.
cancel player warp @spacechase0 Can be done with GameLoop.UpdateTicking.
commented

I planned to add Player.CheckingForAction and Player.CheckedForAction in SMAPI 2.10, but unfortunately the game only calls Game1.hooks from the base method. Any checks handled by one of the location subclasses won't raise the event. Shelved to the archived/check-action-events branch for now; here's the wiki documentation for future reference.

{{/event
 |group = Player
 |name  = CheckingForAction, CheckedForAction
 |desc  = Raised before/after the game checks for an action in response to a player input. That includes activating an interactive object, opening a chest, putting an item in a machine, etc.

 |arg name 1 = <tt>e.Player</tt>
 |arg type 1 = <tt>Farmer</tt>
 |arg desc 1 = The player checking for an action.

 |arg name 2 = <tt>e.Tile</tt>
 |arg type 2 = <tt>Vector2</tt>
 |arg desc 2 = The [[Modding:Modder Guide/Game Fundamentals#Tiles|tile position]] being checked.

 |arg name 3 = <tt>e.Cursor</tt>
 |arg type 3 = <tt>[[Modding:Modder Guide/APIs/Input#ICursorPosition|ICursorPosition]]</tt>
 |arg desc 3 = The current cursor position. This may differ from the <tt>Tile</tt> position, due to how the game selects the target tile for actions in some cases.

 |arg name 4 = <tt>e.IsLocalPlayer</tt>
 |arg type 4 = <tt>bool</tt>
 |arg desc 4 = Whether the affected player is the local one.

 |arg name 5 = <tt>e.ActionPropertyValue</tt>
 |arg type 5 = <tt>string</tt>
 |arg desc 5 = The value of the <tt>Action</tt> tile property at the selected tile coordinate, if any.

 |arg name 6 = <tt>e.WasHandled</tt>
 |arg type 6 = <tt>bool</tt>
 |arg desc 6 = (<tt>CheckedForAction</tt> only.) Whether the game performed an action in response to the check.
}}
commented

@spacechase0

Another useful event would be being able to cancel Game1.warpFarmer. An example use is already present in the game: If something is happening in an area (such as a festival), you can prevent the warp from completing.

You can do that with the new GameLoop.UpdateTicking event:

private void OnUpdateTicking(object sender, UpdateTickingEventArgs e)
{
    // cancel warp to farm
    if (Game1.locationRequest?.Name == "Farm")
    {
        Game1.locationRequest = null;
        Game1.fadeIn = false;
        Game1.player.forceCanMove();
    }
}
commented

The event system is implemented and ready to use, with 44 events to start with. I split the remaining proposed events to #609 (crop events), #610 (item events), #611 (dialogue events), #612 (combat events), #613 (player collapsed event).

commented

I no longer need a ten minute update event - it was planned for a mod but I switched to an alternate method that uses Harmony that no longer relies on when the game time updates.

commented

Also consider player events:

  • equipped item;
  • active tool changed;
  • active item changed;
  • used tool;
  • used item;
  • dropped item into machine;
  • etc.
commented

I'd like to suggest a player event - GiftGiven, which tracks to who, what, and the delta of the disposition change.

commented

Moved from #378:

Request for SaveEvents.BeforeLoad
Custom Crops/Json Assets has to register items dynamically, so the IDs for items may change depending on the order the items were installed. Right now I have to save these in the mod folder. Ideally, these would be in the save folder, so that it is synced with the save. This allows you send the save to another person with the IDs intact, and also prevents this scenario from messing up:

Computer 1:

  • Installing crop mod Watermelon
  • Play a few days
  • Installing crop mod Cocoa
  • Play a few days
  • Installing crop mod Turnips
  • Play a few days
  • etc....

Computer 2:

  • Installing all crop mods used before
  • Play

In this case, IDs may not match since in the first computer, Watermelon is registered/assigned, and then Cocoa is registered/assigned, etc. However, on the second computer, all crop mods will be registered at once, and so IDs will be assigned in the order it sees them (so probably alphabetically) on a crop-by-crop basis.

Computer 1 would use IDs like this:

  • 101: Watermelon
  • 102: Cocoa
  • 103: Turnip

While computer 2 would use them like this:

  • 101: Cocoa
  • 102: Turnip
  • 103: Watermelon
commented

The Bookcase framework mod adds some custom events like NPC received gift (see example Bookcase mod). We should look through the events they provide to see which ones should be added in SMAPI 3.0.

commented

Another useful event would be being able to cancel Game1.warpFarmer. An example use is already present in the game: If something is happening in an area (such as a festival), you can prevent the warp from completing.