Изменения

Перейти к навигации Перейти к поиску
Новая страница: «← Index {{stub}} This page covers how to do common tasks in SMAPI mods. '''Before reading this page, see the Modding:Modder Guide|Modder Gu…»
← [[Modding:Index|Index]]
{{stub}}

This page covers how to do common tasks in SMAPI mods. '''Before reading this page, see the [[Modding:Modder Guide|Modder Guide]] and [[Modding:Modder Guide/Game Fundamentals|Game Fundamentals]].'''

==Basic techniques==
===Tracking changes to a value===
Mods often need to know when a value changed. If there's no SMAPI event for the value, you can create a private [https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/fields field] to track the value, and update it using the update tick event. For example, here's a fully functional mod which prints a console message when the player's stamina changes:

<source lang="c#">
/// <summary>The mod entry point.</summary>
internal class ModEntry : Mod
{
/*********
** Properties
*********/
/// <summary>The player's last stamina value.</summary>
private float LastStamina;


/*********
** Public methods
*********/
/// <summary>The mod entry point, called after the mod is first loaded.</summary>
/// <param name="helper">Provides simplified APIs for writing mods.</param>
public override void Entry(IModHelper helper)
{
SaveEvents.AfterLoad += this.SaveEvents_AfterLoad;
GameEvents.UpdateTick += this.GameEvents_UpdateTick;
}


/*********
** Private methods
*********/
/// <summary>The method invoked when the player loads a save.</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void SaveEvents_AfterLoad(object sender, EventArgs e)
{
this.LastStamina = Game1.player.Stamina;
}

/// <summary>The method invoked after the game updates (roughly 60 times per second).</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void GameEvents_UpdateTick(object sender, EventArgs e)
{
// skip if save not loaded yet
if (!Context.IsWorldReady)
return;

// skip if stamina not changed
float currentStamina = Game1.player.Stamina;
if (currentStamina == this.LastStamina)
return;

// print message & update stamina
this.Monitor.Log($"Player stamina changed from {currentStamina} to {this.LastStamina}");
this.LastStamina = currentStamina;
}
}
</source>

==Items==
Items are objects which represent things which can be put in an inventory. Tools, Crops, etc.

===Create an Item (Object)===
All constructors for Object:

<source lang='c#'>
public Object(Vector2 tileLocation, int parentSheetIndex, int initialStack);
public Object(Vector2 tileLocation, int parentSheetIndex, bool isRecipe = false);
public Object(int parentSheetIndex, int initialStack, bool isRecipe = false, int price = -1, int quality = 0);
public Object(Vector2 tileLocation, int parentSheetIndex, string Givenname, bool canBeSetDown, bool canBeGrabbed, bool isHoedirt, bool isSpawnedObject);
</source>

Where '''parentSheetIndex''' is the ID of the item (can be found in ObjectInformation.xnb).

===Spawn an item on the ground===

<source lang='c#'>
public virtual bool dropObject(Object obj, Vector2 dropLocation, xTile.Dimensions.Rectangle viewport, bool initialPlacement, Farmer who = null);

// Concrete code for spawning:
Game1.getLocationFromName("Farm").dropObject(new StardewValley.Object(itemId, 1, false, -1, 0), new Vector2(x, y) * 64f, Game1.viewport, true, (Farmer)null);
</source>

===Add an item to an inventory===
//todo

===Remove an item from an inventory===

This is dependent on the inventory - rarely will you be calling this directly, as the game has functions for this for the Player, located in Farmer (in the main namespace).

To do so, in most situations, just call .removeItemFromInventory(Item)

==Locations==
The ''GameLocation'' is a representation of any place in the game (e.g. Farm, Town)

The list of current locations is stored in ''Game1.currentLocations''

'''Important Caveat''': In 1.3 onwards, the ''Game1.currentLocations'' is not reliable for farmhands in MP.

===Map Properties===
You can edit many properties of the current map and the tiles in the map. This is already documented here [[Modding:Maps#Map_properties | Map Properties]]

===Tiles===
''terrainFeatures'' contains information about the tiles. (e.g. dirt, grass, crops, etc.)

''objects'' contains information about objects on top of tiles. (e.g. crops, stones)

===Handling TerrainFeatures===

As ''terrainFeatures'' is a NetField, always use .Pairs to access it for enumeration. The .Key value of this stores the location, and the .Value contains the terrainFeature itself. As a note, this includes items spawned off the map, which is usually cleared at end of day.

If you need to access just crops, this snippet will be of use:

<source lang="c#">

foreach (var tf in Game1.getFarm().terrainFeatures.Pairs)
{
if ((tf.Value is HoeDirt hd) && hd.crop != null)
{
// do crop like things here.
}
}
</source>

If you need some other location, sub that out for Game1.getFarm(). Note this doesn't null check Farm! Make sure Farm is loaded.

===Handling Objects===

There are several fields that handle objects, but always use the .objects one. The same rules apply as in TerrainFeatures, except that you really can't place objects beyond the edge of the map.

==Player==
//todo describe section
===Position===
A Character's position indicates the Character's coordinates in the current location.

====Position Relative to the Map====
Each location has an ``xTile`` map where the top-left corner of the map is ''(0, 0)'' and the bottom-right corner of the map is ''(location.Map.DisplayWidth, location.Map.DisplayHeight)'' in pixels.

There are two ways to get a Character's position in the current location: by absolute position and by tile position.

<code>Position.X</code> and <code>Position.Y</code> will give the XY coordinates in pixels.

<code>getTileX()</code> and <code>getTileY()</code> will give the XY coordinates in tiles.

Each tile is 64x64 pixels as specified by <code>Game1.tileSize</code>. The conversion between absolute and tile is as follows:
<source lang='c#'>
// Absolute position => Tile position
Math.Floor(Game1.player.Position.X / Game1.tileSize)
Math.Floor(Game1.player.Position.Y / Game1.tileSize)

// Tile position => Absolute position
Game1.player.getTileX() * Game1.tileSize
Game1.player.getTileY() * Game1.tileSize

// Tilemap dimensions
Math.Floor(Game1.player.currentLocation.Map.DisplayWidth / Game1.tileSize)
Math.Floor(Game1.player.currentLocation.Map.DisplayHeight / Game1.tileSize)
</source>

====Position Relative to the Viewport====
The viewport represents the visible area on the screen. Its dimensions are <code>Game1.viewport.Width</code> by <code>Game1.viewport.Height</code> in pixels; this is the same as the game's screen resolution.

The viewport also has an absolute position relative to the map, where the top-left corner of the viewport is at ''(Game1.viewport.X, Game1.viewport.Y)''.


The player's position in pixels relative to the viewport is as follows:
<source lang='c#'>
Game1.player.Position.X - Game1.viewport.X
Game1.player.Position.Y - Game1.viewport.Y
</source>

==NPC==
===Creating Custom NPCs===
Adding new NPCs involves editing a number of files:

* New file: Characters\Dialogue\<name>
* New file: Characters\schedules\<name>
* New file: Portraits\<name>
* New file: Characters\<name>
* Add entries Data\EngagementDialogue for NPCs that are marriable
* Add entry to Data\NPCDispositions
* Add entry to Data\NPCGiftTastes
* Add entries to Characters\Dialogue\rainy
* Add entries to Data\animationDescriptions (if you want custom animations in their schedule)

All of the above can be done with IAssetLoaders/IAssetEditors or Content Patcher. Finally, spawn the NPC with a SMAPI mod. The different constructors are:

<source lang='c#'>
public NPC(AnimatedSprite sprite, Vector2 position, int facingDir, string name, LocalizedContentManager content = null);
public NPC(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDir, string name, Dictionary<int, int[]> schedule, Texture2D portrait, bool eventActor);
public NPC(AnimatedSprite sprite, Vector2 position, string defaultMap, int facingDirection, string name, bool datable, Dictionary<int, int[]> schedule, Texture2D portrait);
</source>

For spawning:

<source lang='c#'>
Game1.getLocationFromName("Town").addCharacter(npc);
</source>

==UI==

The UI is a collection of separate elements which make up the HUD and occasional popups.

//todo expand section.


===Banner Message===
HUDMessage are those popups in the lower left hand screen. They have several constructors, which we will briefly go over here (a few non relevant ones have been snipped):

<source lang="c#">
public HUDMessage(string message);
public HUDMessage(string message, int whatType);
public HUDMessage(string type, int number, bool add, Color color, Item messageSubject = null);
public HUDMessage(string message, string leaveMeNull)
public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)
</source>


So before we go over when you'd use them, I'm going to briefly note how the class HUDMessage uses these. (I encourage people to read the class if they have further questions, but I doubt most of us will need to know more than this)


WhatType:
*1 - Achievement
*2 - New Quest
*3 - Error
*4 - Stamina
*5 - Health


Color: Fairly obvious. It should be noted that while the first two don't give an option (they default to ''Color:OrangeRed''), the fourth with the param 'leaveMeNull' displays as the same color as the game text.


For specifics:
*'' public HUDMessage(string type, int number, bool add, Color color, Item messageSubject = null);'' - This allows for expanded customization of the message. More often used for money.
*'' public HUDMessage(string message, string leaveMeNull)'' - Also displays no icon.
*'' public HUDMessage(string message, Color color, float timeLeft, bool fadeIn)'' - Displays a message that fades in for a set amount of time.


Note: For those of you who want a custom HUDMessage:
- Almost all of these variables are public, excluding messageSubject, so feel free to customize!



For example: add a new HUDMessage to show [insert image] toaster popup.
<source lang="c#">
Game1.addHUDMessage(new HUDMessage("MESSAGE", 3));
</source>

==Menus==
//todo describe section
===Get the Active Menu===
You can use ''Reflection'' to get the current active menu in ''GameMenu''. The ''GameMenu'' contains the Inventory, Skills, Social, Map, Crafting, Collections, and Options pages in this respective order, accessed by the tab index with ''inventoryTab'' at 0.

<source lang="c#">
if (Game1.activeClickableMenu is GameMenu menu) {
IList<IClickableMenu> pages = this.Helper.Reflection.GetField<List<IClickableMenu>>(menu, "pages").GetValue();
IClickableMenu page = pages[menu.currentTab];

// Example for getting the MapPage
MapPage mapPage = (MapPage) pages[menu.currentTab];

// Two examples of checking if MapPage is open
pages[menu.currentTab] is MapPage || menu.currentTab == GameMenu.mapTab;
}
</source>

===Set the Active Menu===
Game1.activeClickableMenu = <Menu>
===Simple Menu===
Copy the menu you want from the game and make it your own

==Harmony==
{{quote|Here be dragons. Thou art forewarned.}}

[https://github.com/pardeike/Harmony Harmony] lets you patch Stardew Valley methods directly. This is very powerful, but comes with major caveats:

* It's very easy to cause crashes, errors, or subtle bugs.
* It may cause difficult-to-diagnose memory corruption errors.
* Crossplatform compatibility is not guaranteed, and should be tested on all three platforms.
* May conflict with other Harmony mods (e.g. if two mods patch the same method, or two mods try to load different versions of Harmony).
* Harmony patch errors may have unpredictable effects on other mods that aren't using Harmony.

Using Harmony should be a last resort, and is deliberately not documented.

==Other==
===Add a small animation===
<source lang="c#">
location.temporarySprites.Add(new TemporaryAnimatedSprite(...))
</source>
See ''TemporaryAnimatedSprite'' for more details

===Play a sound===
<source lang="c#">
location.playSound("SOUND");
</source>
(e.g. "junimoMeep1")

===Send a letter===

Use IAssetEditor to inject new mail into Data\Mail.xnb. Then use Game1.addMailForTomorrow to send it to the player.

<source lang="c#">
public static void addMailForTomorrow(string mailName, bool noLetter = false, bool sendToEveryone = false);
</source>

Where mailName is the letter key in Mail.xnb. If you use the string '''wizard''' anywhere in the letter key, the letter will have the Wizard's new background (since 1.3) automatically.

==Open source==
When all else fails, when you've looked at the decompiled source too long and it makes no sense, take a look at some open-source mod code! See the 'source' column in the [[Modding:SMAPI compatibility|mod compatibility list]] for source code.

[[Category:Modding]]

[[en:Modding:Common tasks]]
5

правок

Навигация