Better events
Pathoschild opened this issue · 28 comments
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.
An event that fires before showEndOfNightStuff, to fire immediately after player loses consciousness (voluntarily or not).
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.
@JohnsonNicholas Can you list specific examples of events you'd want?
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.
.. So from the discussions:
Something like PlayerEvents.OnDeath
and PlayerEvents.OnCollapse
would be neat too.
.. 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.
A DrawWeather event at the following point in the code:
https://github.com/Pathoschild/SMAPI/blob/2.3/src/SMAPI/Framework/SGame.cs#L1084
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
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;
}
}
Proposed design guidelines for SMAPI 3.0 events:
- All events have a consistent name format:
BeforeEventName
orAfterEventName
. - 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 += ...;
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.
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. |
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.
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.
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;
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.
Per discord discussion, a event that allows mods to add onto the games light map would be very useful.
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 |
---|---|---|
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. |
Not really needed, easily to replicate with GameLoop.UpdateTicked if needed. |
||
Not currently feasible. | ||
inventory changed | @Pathoschild | Done via Player.InventoryChanged . |
tool used | @Pathoschild | Moved to #610. |
Action and TouchAction properties. Implemented by Entoarox Framework. |
||
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 . |
Can be done with GameLoop.UpdateTicking . |
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.
}}
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();
}
}
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.
Also consider player events:
- equipped item;
- active tool changed;
- active item changed;
- used tool;
- used item;
- dropped item into machine;
- etc.
I'd like to suggest a player event - GiftGiven, which tracks to who, what, and the delta of the disposition change.
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
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.