Модификации:Основные возможности
В этой статье описываются основные возможности SMAPI. Перед прочтением данной статьи, рекомендуется прочитать Modder Guide и Game Fundamentals.
Основы
Отслеживание изменений переменной
Модам часто приходится отслеживать изменение какого-либо значения. Если для параметра в SMAPI нет события, можно создать приватное поле, и обновлять его, используя событие обновления. Например, вот полностью работающий мод, который выводит сообщение в консоль, когда выносливость игрока изменяется:
/// <summary>Входная точка мода.</summary>
internal class ModEntry : Mod
{
/*********
** Свойства
*********/
/// <summary>Последнее значение выносливости игрока.</summary>
private float LastStamina;
/*********
** Публичные методы
*********/
/// <summary>При первой загрузке мода вызывается метод Entry</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;
}
/*********
** Приватные методы
*********/
/// <summary>Метод вызываемый при загрузке сохранения.</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>Метод вызывается после события обновления (около 60 раз в секунду).</summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event arguments.</param>
private void GameEvents_UpdateTick(object sender, EventArgs e)
{
// выходим, если сохранение еще не загружено
if (!Context.IsWorldReady)
return;
// выходим, если значение выносливости не изменилось
float currentStamina = Game1.player.Stamina;
if (currentStamina == this.LastStamina)
return;
// выводим сообщение и обновляем значение свойства LastStamina
this.Monitor.Log($"Player stamina changed from {currentStamina} to {this.LastStamina}");
this.LastStamina = currentStamina;
}
}
Предметы
Предметы - это объекты, которые могут быть помещены в инвентарь. Предметами являются инструменты, семена, урожай и т.п.
Создание предмета (Объекта)
Конструкторы класса для создания предмета:
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);
Где parentSheetIndex это ID предмета (может быть найдено в ObjectInformation.xnb).
Создание предмета на клетке
public virtual bool dropObject(Object obj, Vector2 dropLocation, xTile.Dimensions.Rectangle viewport, bool initialPlacement, Farmer who = null);
// Спавн предмета в локации:
Game1.getLocationFromName("Farm").dropObject(new StardewValley.Object(itemId, 1, false, -1, 0), new Vector2(x, y) * 64f, Game1.viewport, true, (Farmer)null);
Добавление предмета в инвентарь
//todo
Удаление предмета из инвентаря
Удаление зависит от инвентаря. Чаще всего вызывается метод класса Player, расположенного в пространстве имен Farmer.
Чтобы удалить предмет в большинстве ситуаций необходимо просто вызвать метод .removeItemFromInventory(Item)
Локации
Игровой локацией является любое место в игре (Ферма, Город, Шахта)
Game1.currentLocation является ссылкой на текущую локацию в которой находится игрок.
Важное предупреждение: Начиная с версии 1.3 Game1.currentLocation использовать в мультиплеере не рекомендуется.
Свойства карты
Многие свойства карты, а также клеток на ней можно изменить. Для детальной информации читайте свойства карты
Обработка клетки
Поле .terrainFeatures содержит свойство .Pairs, которое используется для получения доступа к элементу набора клеток (земля, трава, грядка).
Ниже приведен фрагмент для обработки клетки с культурой:
foreach (var tf in Game1.getFarm().terrainFeatures.Pairs)
{
if ((tf.Value is HoeDirt hd) && hd.crop != null)
{
// do crop like things here.
}
}
Для указания другой локации необходимо заменить Game1.getFarm(). Обратите внимание: в примере не производится проверка локации на существование (null check)!
Обработка объектов на клетке
Для обработки объектов есть несколько полей, но чаще всего используется .objects. В нем содержится информация об объектах (семена, камень, древесина).
Действия аналогичны TerrainFeatures, за исключением того, что нельзя размещать объекты за пределами карты.
Игрок
Местоположение
Позиция персонажа указывает координаты персонажа в текущей локации.
Позиция игрока в локации
Каждая локация представлена xTile картой, где левый верхний угол имеет координаты (0, 0), а правый нижний - (location.Map.DisplayWidth, location.Map.DisplayHeight) пикселей.
Существуют два способа узнать текущее местоположение персонажа в локации: по абсолютной позиции или по координатам клетки.
Position.X
и Position.Y
вернут координаты XY в пикселях.
getTileX()
и getTileY()
вернут координаты XY клетки в сетке тайл-листа.
Согласно Game1.tileSize
клетка имеет размер 64x64 пикселей, что дает возможность конвертировать абсолютную и относительную позиции:
// Абсолютная позиция => Позиция клетки
Math.Floor(Game1.player.Position.X / Game1.tileSize)
Math.Floor(Game1.player.Position.Y / Game1.tileSize)
// Позиция клетки => Абсолютная позиция
Game1.player.getTileX() * Game1.tileSize
Game1.player.getTileY() * Game1.tileSize
// Размеры клетки
Math.Floor(Game1.player.currentLocation.Map.DisplayWidth / Game1.tileSize)
Math.Floor(Game1.player.currentLocation.Map.DisplayHeight / Game1.tileSize)
Позиция игрока в видимой области
Размеры видимой области экрана можно узнать используя Game1.viewport.Width
и Game1.viewport.Height
. Размеры указаны в пикселях и соответствуют разрешению экрана игры.
Также видимая область имеет абсолютную позицию относительно карты, где левый верхний угол находится в (Game1.viewport.X, Game1.viewport.Y).
Позиция игрока относительно видимой области вычисляется следующим образом:
Game1.player.Position.X - Game1.viewport.X
Game1.player.Position.Y - Game1.viewport.Y
Неигровые персонажи
Создание пользовательского НИПа
Добавление нового НИПа включает в себя редактирование нескольких файлов:
- Создание файла: Characters\Dialogue\<имя>
- Создание файла: Characters\schedules\<имя>
- Создание файла: Portraits\<имя>
- Создание файла: Characters\<имя>
- Добавление записей Data\EngagementDialogue для НИПов, с которыми можно заключить брак
- Добавление записи в Data\NPCDispositions
- Добавление записи в Data\NPCGiftTastes
- Добавление записей в Characters\Dialogue\rainy
- Добавление записей в Data\animationDescriptions (если нужны пользовательские анимации в расписании)
Все это можно сделать с помощью IAssetLoaders/IAssetEditors или Content Patcher. В завершении, создается НИП, используя конструкторы SMAPI:
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);
Для спавна:
Game1.getLocationFromName("Town").addCharacter(npc);
Пользовательский интерфейс
Пользовательский интерфейс представляет собой набор отдельных элементов, которые составляют HUD и всплывающие окна.
//необходимо дополнить раздел.
Всплывающие сообщения
HUDMessage - это всплывающие сообщения в нижнем левом углу экрана. У них есть несколько конструкторов, которые указаны ниже (несколько не релевантных были пропущены):
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)
Ниже даны описания основных аргументов, которые требуются для создания сообщения. (Для полного описания рекомендуется читать информацию о классе, но в большинстве ситуации информации ниже достаточно)
WhatType:
- 1 - Достижение
- 2 - Новый квест
- 3 - Ошибка
- 4 - Выносливость
- 5 - Здоровье
Color: Цвет. В первых двух конструкторах используется цвет по умолчанию (Color:OrangeRed), а в четвертом - параметр 'leaveMeNull' отображается тем же цветом, что и текст игры.
Для примера:
- 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) - Выводит сообщение, которое исчезает в течении установленного времени.
Примечание:
- Все переменные являются публичными, кроме messageSubject, поэтому не стесняйтесь кастомизировать!
Пример: Всплывающее сообщение об ошибке:
Game1.addHUDMessage(new HUDMessage("MESSAGE", 3));
Меню
//добавить описание раздела
Получение информации о активной вкладки меню
Используйте Reflection, чтобы узнать какая вкладка GameMenu сейчас активна. GameMenu содержит вкладки Инвентарь, Навыки, Светское, Карта, Изготовление предметов, Коллекции, и Настройки в указанном порядке. То есть вкладка inventoryTab имеет индекс 0.
if (Game1.activeClickableMenu is GameMenu menu) {
IList<IClickableMenu> pages = this.Helper.Reflection.GetField<List<IClickableMenu>>(menu, "pages").GetValue();
IClickableMenu page = pages[menu.currentTab];
// Получение вкладки Карта
MapPage mapPage = (MapPage) pages[menu.currentTab];
// Два примера проверки, что вкладка Карта открыта
pages[menu.currentTab] is MapPage || menu.currentTab == GameMenu.mapTab;
}
Установка активной вкладки меню
Game1.activeClickableMenu = <Menu>
Harmony
“ | “Here be dragons. Thou art forewarned.” |
Harmony позволяет напрямую использовать методы игры. Это очень мощный инструмент, но есть серьезные оговорки:
- Очень легко вызвать сбои, ошибки или неотлавливаемые баги.
- Это может привести к затруднению диагностики ошибок повреждения памяти.
- Кроссплатформенная совместимость не гарантируется и должна быть протестирована на всех трех платформах.
- Может конфликтовать с другими модами Harmony (например, если два мода используют один и тот же метод, или два мода пытаются загрузить разные версии Harmony).
- Патчи Harmony могут иметь непредсказуемые эффекты для других модов, которые не используют Harmony.
Использование Harmony должно быть последним средством, и поэтому намеренно не документировано.
Другое
Добавление небольшой анимации
location.temporarySprites.Add(new TemporaryAnimatedSprite(...))
Смотрите TemporaryAnimatedSprite для детальной информации
Воспроизведение звука
location.playSound("SOUND");
(e.g., "junimoMeep1")
Отправка письма
Используйте IAssetEditor, чтобы добавить новое письмо в Data\Mail.xnb. Затем вызывайте метод Game1.addMailForTomorrow, чтобы отправить его игроку.
public static void addMailForTomorrow(string mailName, bool noLetter = false, bool sendToEveryone = false);
Где mailName ключ в Mail.xnb. Если использовать строку wizard где угодно в ключе, то письмо будет иметь волшебный фон[?] (начиная с 1.3) автоматически.
Исходные коды
Если что-то не получается, в большинстве случаев есть возможность подсмотреть реализацию в исходном коде другого мода. Смотрите столбец 'Исходный коды' в списке совместимых модификаций.