全站通知:

模组:迁移至SMAPI 4.0

来自星露谷物语维基
跳到导航 跳到搜索

目录

当前页面编写的内容面向模组的开发者。模组玩家请阅读模组:模组兼容性


此页面解释如何升级您的 C# 模组以与 SMAPI 4.0.0 兼容(内容包不受影响)。您现在即可升级模组,无需等待 4.0 发布。

概览

该版本有何变化

SMAPI 兼容性走势图。2017 年 19 月 SMAPI 2.0 的发布造成了兼容性的小下降。而 SMAPI 3.0 紧随 Stardew Valley 1.4 发布。

五年前,SMAPI 2.0.0引入了内容 API(即 IAssetLoaderIAssetEditor)。自此,它成为了 SMAPI 最重要的部件之一;比如,它是 Content Patcher 的基础,而现在大约有 41.1% 的模组依赖于 Content Patcher。然而,自发布以来,此 API 并没有本质变化;时至今日,它已经无法支持所有用例。

SMAPI 4.0.0 的发布致力于解决此问题。新版本完全翻新了内容 API:

  • 现在可以通过 helper 完全发现此 API,与其他 API 保持一致。这更符合模组开发者的直觉。
  • 加载操作不再总是互斥,以免频繁出现模组冲突。现在可以为每个加载操作指定优先级了。
  • API 不再隐式处理本地化:现在 Data/BundlesData/Bundles.fr-FR 不再被认为是相同文件(尽管仍然可以根据所需进行本地化无关的更改)。
  • 添加了内容包标签,用于指示正在加载/编辑素材的内容包。这反映于日志消息中,以简化故障排除,避免内容包故障被报告给框架模组作者。
  • 添加了编辑优先级,可用于微调与其他模组或编辑的兼容性,

SMAPI 4.0.0 支持 Stardew Valley 1.6 且去除了所有弃用 API。

这是一次巨变吗

不是。尽管这是一次主要更新,我们会极力减小其影响:

  • 旧版的内容 API 会在很长一段时间里继续得到支持,但会在 SMAPI 控制台中引起愈发明显的警告,以提示旧版 API 已过时、应当被移除;
  • 会提交拉取请求以更新受影响的模组;
  • SMAPI 4.0 发布时,会为尚未提供官方更新的模组提供非官方更新;
  • 会积极沟通和记录更改以帮助开发者。

上述措施意味着 SMAPI 4.0.0 将会最小程度地影响模组兼容性,尽管其更改范围较大。

如何升级模组

您无需手动梳理代码。SMAPI 会自动提示您的代码中的弃用 API。

  1. SMAPI 会在控制台窗口中输出弃用信息(具体格式取决于弃用级别,但您可以搜索您的模组名称):
    Modding - updating deprecated SMAPI code - deprecation warnings.png
  2. 当您在 Visual Studio 中查看代码时,会看见构建错误,以及相应的修复提示:
    Modding - updating deprecated SMAPI code - deprecation intellisense.png
  3. 您可以阅读如下章节以了解如何更新特定的 API。

破坏性更改

内容拦截API

IAssetLoaderIAssetEditor 接口不再存在。它们已被 AssetRequested 事件取代,该事件用法如下:

public class ModEntry : Mod
{
    /// <inheritdoc />
    public override void Entry(IModHelper helper)
    {
        this.Helper.Events.Content.AssetRequested += this.OnAssetRequested;
    }


    /// <inheritdoc cref="IContentEvents.AssetRequested" />
    /// <param name="sender">The event sender.</param>
    /// <param name="e">The event arguments.</param>
    private void OnAssetRequested(object sender, AssetRequestedEventArgs e)
    {
        if (e.Name.IsEquivalentTo("Portraits/Abigail"))
        {
            e.LoadFromModFile<Texture2D>("assets/portrait.png", AssetLoadPriority.Medium);
        }
    }
}

迁移提示:

  • 素材名称不再本地化无关。例如,Data/BundlesData/Bundles.fr-FR 不再指示同一素材。如果希望应用本地化无关的更改,请查看 e.NameWithoutLocale 而非 e.Name
  • 组合了旧版的 CanLoadCanEdit/Edit 方法,因此您只需检查一次条件逻辑。
  • 现在加载素材时必须指定 AssetLoadPriority 参数以指定加载的优先级。AssetLoadPriority.Exclusive 相当于旧版的行为,但可能降低模组兼容性。参见 IntelliSense 文档以了解更多。

参见内容事件内容 API 文档以了解用法。

内容加载API

旧版的 helper.Content API 令人困惑,因为它处理原版素材和模组素材的方式并不相同。有些方法接受可选的 ContentSource 参数(但很容易遗漏),而有些方法仅支持原版素材或模组素材,而那些希望同时覆盖二者的文档十分抽象。而且旧版 API 加载的素材没有缓存,可能会影响性能,并阻碍某些功能的实现,例如新内容事件

新版中将其分割为两个 API 以解决此问题:

字段 说明
helper.ModContent 加载模组素材,没有缓存(类似于 helper.Content),因此每次加载时都会从文件中重新读取。
helper.GameContent Content 文件夹或内容拦截中加载素材,有缓存新版内容事件需要缓存才能工作)。

下面是迁移旧版方法和属性的教程:

旧版代码 迁移方法
helper.Content.AssetEditors
helper.Content.AssetLoaders
使用内容事件
helper.Content.CurrentLocale
helper.Content.CurrentLocaleConstant
helper.Content.InvalidateCache
使用 helper.GameContent.
helper.Content.GetActualAssetKey 使用 helper.ModContent.GetInternalAssetName,并移除 ContentSource 参数。新版方法会返回 IAssetName 类型值;您可以更新代码以使用它,也可使用其 Name 属性,相当于旧版的字符串值。
helper.Content.GetPatchHelper 使用 helper.GameContenthelper.ModContent
helper.Content.Load 使用 helper.GameContenthelper.ModContent,并移除 ContentSource 参数。

迁移说明:

  • 使用 helper.GameContent 加载素材时,不要附带 .xnb</samp 扩展名(例如,使用 "Portraits/Abigail" 而非 "Portraits/Abigail.xnb")。您请求的是素材名称,而非文件路径。
  • 使用 helper.ModContent 加载 XNB 文件时,务必附加 .xnb 扩展名。新版中将不再自动添加。
helper.Content.NormalizeAssetName 使用 helper.GameContent.ParseAssetName。新版方法会返回 IAssetName 类型值;您可以更新代码以使用它,也可使用其 Name 属性,相当于旧版的字符串值。

其他API更改

旧版代码 迁移方法
Constants.ExecutionPath 使用 Constants.GamePath
GameFramework.Xna XNA 不再用于任何平台;您可以安全地移除任何针对 XNA 的逻辑。
helper.ConsoleCommands.Trigger 不再支持。您可以使用模组API来与其他模组进行整合。
IAssetInfo.AssetName 使用 Name 代替,新属性有内建的工具方法用于处理素材名称。
IAssetInfo.AssetNameEquals(name) 使用 Name.IsEquivalentTo(name)
IContentPack.LoadAsset 使用 ModContent.Load
IContentPack.GetActualAssetKey 使用 ModContent.GetInternalAssetName,并移除 ContentSource 参数。新版方法会返回 IAssetName 类型值;您可以更新代码以使用它,也可使用其 Name 属性,相当于旧版的字符串值。
PerScreen<T>(null) 向构造函数传入空做法已被弃用。现在应当调用 PerScreen<T>() 以使用默认值。
SDate.Season SDate.Season 现在为 Season 枚举,以与游戏保持一致。如果您确实需要使用字符串,请使用 SDate.SeasonKey

可空引用类型注解

现在 SMAPI 提供对 C# 可空引用类型的完整注解。您需要在模组代码中启用它才能生效。如果您的模组使用了它们,您将从 Visual Studio 获得有用的代码分析警告,以避免在可空/不可空时发生错误。例如:

// warning: dereference of a possibly null reference
var api = this.Helper.ModRegistry.GetApi<IExampleApi>("SomeExample.ModId");
api.DoSomething();

// warning: possible null reference argument for parameter 'message'
string? message = null;
this.Monitor.Log(message);

因为 C# 可空引用类型注解的限制,无法覆盖三种边界情况。这些情况也被写进了 IntelliSense 文档。

API 边界情况
helper.Reflection GetFieldGetMethodGetProperty 方法被标记为返回不可空值,因为它们在目标不存在时会抛出错误。即使您显式地指定了 required: false ,这一情况也不会改变;因此,请无论如何也要对返回值判空。
helper.Translation 翻译被标记为不可空,因为可能会回退到 "missing translation: key" 缺省值。即使您显式地调用了 translation.UsePlaceholder(false),这一情况也不会改变;因此,请无论如何也要对返回值判空(如果需要)。
PerScreen<T> 其可空性依赖于您的设置。例如,PerScreen<string> 代表不可空字符串,PerScreen<string?> 代表可空字符串。然而,调用不可空引用类型的空构造函数仍然会返回空,因为这是类型默认值。例如:
var perScreen = new PerScreen<string>();
string value = perScreen.Value; // 尽管标记为非空,仍会返回空。

为避免这种情况,可以指定默认的非空值:

var perScreen = new PerScreen<string>(() => string.Empty);
string value = perScreen.Value; // 默认返回空字符串。

移除依赖

SMAPI 4.0.0 不再使用如下依赖,因此不会再自动加载它们。如果您手动引用了它们之一,请将其复制到您的模组发布文件夹,或参见如下的迁移建议。

依赖 建议
System.Configuration.ConfigurationManager.dll 使用标准的配置 API 代替之。
System.Runtime.Caching.dll 避免使用此 DLL 的 MemoryCacheObjectCache,因为可能影响性能。如果您需要缓存到期(cache expiry),请考虑使用更快速的 Microsoft.Extensions.Caching.Memory 包(但仍然很繁重)。否则,可以考虑直接使用 Dictionary<TKey, TValue> 字段。
System.Security.Permissions.dll 通常只有 System.Configuration.ConfigurationManager.dllSystem.Runtime.Caching.dll 才需要它,因此可以删除。

其他更改

贴图原始数据

创建 Texture2D 实例开销较大,且需要调用显卡。现在,如果您不需要加载整张贴图,可以使用 IRawTextureData 加载贴图,然后将其传入接收贴图的 SMAPI API 中。

例如,您无需再创建 Texture2D 实例,而可以像这样叠加图像:

private void OnAssetRequested(object? sender, AssetRequestedEventArgs e)
{
    if (e.Name.IsEquivalentTo("Portraits/Abigail"))
    {
        e.Edit(asset =>
        {
            IRawTextureData ribbon = this.Helper.ModContent.Load<IRawTextureData>("assets/ribbon.png");
            asset.AsImage().PatchImage(source: ribbon);
        });
    }
}