Модификации:Основные возможности

Материал из Stardew Valley Wiki
Перейти к навигации Перейти к поиску

Модификации:Индекс

В этой статье описываются основные возможности 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, поэтому не стесняйтесь кастомизировать!


Пример: Всплывающее сообщение об Error-image-ingame.png ошибке:

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) автоматически.

Исходные коды

Если что-то не получается, в большинстве случаев есть возможность подсмотреть реализацию в исходном коде другого мода. Смотрите столбец 'Исходный коды' в списке совместимых модификаций.