SeneaL API

SeneaL API

一个用于 Valheim UI 模组的小型公共库,以及一座将游戏自带窗口发送到你运行的任意 UI 模组的桥梁。

[b][size=200]SeneaL API[/size][/b]

[url=https://discord.gg/Nkgrq9cPPr][img]https://storage.yandexcloud.net/seneal-bucket/Valheim/Brand/link-discord.png[/img][/url][url=https://dalink.to/seneal_][img]https://storage.yandexcloud.net/seneal-bucket/Valheim/Brand/link-donate.png[/img][/url]

[b]页面:[/b][url=https://thunderstore.io/c/valheim/p/seneaL/SeneaL_API/]Thunderstore[/url] · [url=https://valheim.hexium.gg/mods/seneaL/SeneaL_API]Hexium[/url]

提供方:[url=https://thunderstore.io/c/valheim/p/seneaL/SeneaL_UI/]SeneaL UI[/url]([url=https://thunderstore.io/c/valheim/p/seneaL/SeneaL_UI/]Thunderstore[/url] · [url=https://www.nexusmods.com/valheim/mods/3875]Nexus Mods[/url] · [url=https://valheim.hexium.gg/mods/seneaL/SeneaL_UI]Hexium[/url])。

一个用于 Valheim UI 模组的公共小型库,同时也是一个桥接器,将游戏自身的窗口发送到你运行的任意 UI 模组。

[list]
[] [b]面向玩家:[/b]与支持它的 UI 模组(SeneaL UI)一起安装。之后显示游戏标准消息和窗口的模组看起来会与你界面的其余部分一致。没有此类 UI 模组时不会有任何变化:每个请求都会回退到原版窗口。[/]
[][b]面向模组作者:[/b]在一个地方显示通知、向玩家提问并注册你的 HUD 元素,且不依赖特定的 UI 模组。[/]
[][b]面向 UI 作者:[/b]为每个功能注册一个提供方,你就能收到所有请求,包括来自 API 的以及来自游戏自身调用的请求。[/]
[/list]
[b][size=170]工作原理[/size][/b]

[code]
你的模组 ── Notices.Show / Dialogs.Confirm / HudElements.Add ──┐
                                                              ├─► 提供方(一个 UI 模组)─► 它自己的窗口
游戏 / 任意模组 ── MessageHud.ShowMessage, UnifiedPopup.Push ───┘            │
                  TextInput.RequestText  (桥接)                              └─ 拒绝或缺失 ─► 原版窗口[/code]
所有调用都在 Unity 主线程上运行。

[b][size=170]在你的模组中使用它[/size][/b]

添加一个软依赖,并将所有 API 调用放在一个仅在 API 已加载时才运行的方法中:

[code]
[BepInDependency("seneaL.valheim.api", BepInDependency.DependencyFlags.SoftDependency)]
public class MyMod : BaseUnityPlugin
{
    private static bool HasApi => BepInEx.Bootstrap.Chainloader.PluginInfos.ContainsKey("seneaL.valheim.api");

    public static void Hello()
    {
        if (HasApi)
            ShowWithApi("Hello");
        else
            Player.m_localPlayer?.Message(MessageHud.MessageType.TopLeft, "Hello");
    }

    private static void ShowWithApi(string text) => SeneaL.API.Notices.Show(text);
}[/code]
如果你只使用游戏自身的方法,则无需任何操作:桥接器已经为它们做了路由。

[b][size=170]快速开始:一个完整的模组[/size][/b]

一个文件、一个软依赖、全部四个功能。所有涉及
[code]SeneaL.API[/code]的内容都放在一个单独的类中,因此当 API 未安装时,模组仍能加载并正常工作。

[code]
using BepInEx;
using UnityEngine;
using BepInEx.Bootstrap;

[BepInPlugin("my.mod.guid", "My Mod", "1.0.0")]
[BepInDependency("seneaL.valheim.api", BepInDependency.DependencyFlags.SoftDependency)]
public class MyMod : BaseUnityPlugin
{
    public static bool HasApi => Chainloader.PluginInfos.ContainsKey("seneaL.valheim.api");

    private void Start()
    {
        if (HasApi)
            ApiPart.Register();
    }

    private void Update()
    {
        if (HasApi && Input.GetKeyDown(KeyCode.F9))
            ApiPart.Ask();
    }
}

internal static class ApiPart
{
    private const string Owner = "my.mod.guid";

    public static void Register()
    {
        if (Api.Has("slots"))
            CustomSlots.Add(Owner, new SlotDef("charm") { Title = "Charm", Mode = SlotMode.Stored });

        if (Api.Has("notices"))
            Notices.Show("My Mod is ready", NoticeKind.TopLeft);
    }

    public static void Ask()
    {
        Dialogs.Confirm("Reset the counter?", "The value will be lost.", yes: () => Notices.Show("Counter reset", NoticeKind.Center));
    }
}[/code]
[code]using SeneaL.API;[/code]放在使用这些类型的文件顶部。
[code]Api.Has(...)[/code]仅当已安装的 API 版本中存在该功能时才为 true,因此它也能保护你免受旧版本 API 的影响。

[b][size=150]通知(Notices)[/size][/b]

[code]
Notices.Show("$msg_example", NoticeKind.Center);
Notices.Show("Picked up", NoticeKind.TopLeft, amount: 3, icon: sprite);
Notices.Unlock(icon, "Title", "Description");
Notices.Biome("Meadows", stinger: true);
Notices.Shown += notice => { };[/code]
[code]Notice.Text[/code]是本地化后的文本,
[code]Notice.Raw[/code]是传入的原始内容,
[code]Notice.Vanilla[/code]在它来自游戏调用时为 true。

[b][size=150]对话框(Dialogs)[/size][/b]

[code]
Dialogs.Confirm("Delete?", "This cannot be undone.", yes: () => Delete());
Dialogs.Message("Done", "Saved.");
Dialogs.Input("Name", "current", submit: name => Rename(name), validate: s => s.Length > 0);[/code]
每次调用都会返回该对话框。
[code]IsOpen[/code]在由原版窗口持有时为 false,
[code]Close()[/code]会关闭它。

多于两个答案:
[code]Dialogs.Choose[/code]接受最多四个按钮。第一个
[code]Primary[/code]按钮是主按钮,
[code]CancelIndex[/code](默认是最后一个按钮,-1 = 无)是 Esc 选中的按钮。在任何对话框上设置
[code]Centered = true[/code]可使其文本居中。

[code]
var d = Dialogs.Choose("Friend request", "Ragnar wants to be your friend.",
    new DialogButton("Accept", Accept, primary: true),
    new DialogButton("Decline", Decline),
    new DialogButton("Later", Later));

d.Centered = true;
d.CancelIndex = 2;[/code]
没有提供方(或提供方不认识
[code]DialogKind.Choice[/code])时,游戏的是/否窗口会显示前两个按钮(一个按钮:游戏的警告窗口)。用
[code]Api.Has("dialogs.choice")[/code]探测。对话框提供方会收到带有
[code]Buttons[/code]的
[code]ChoiceDialog[/code>,并用
[code]Choose(index)[/code]或
[code]Cancel()[/code]作答;无法绘制它的提供方从
[code]Show[/code]返回
[code]false[/code]。

[b][size=150]HUD 元素[/size][/b]

[code]
var element = HudElements.Add("my.mod.guid", "timer", "Timer", timerRect);
HudElements.Remove(element);[/code]
完整示例:一个小型计数器,由你自己构建并交给 UI。

[code]
using TMPro;
using UnityEngine;
using UnityEngine.UI;

internal static class KillCounter
{
    private static TextMeshProUGUI _text;
    private static HudElement _element;
    private static int _kills;

    public static void Create()
    {
        var root = new GameObject("MyKillCounter", typeof(RectTransform)).GetComponent<RectTransform>();

        root.SetParent(Hud.instance.m_rootObject.transform, false);
        root.anchorMin = root.anchorMax = root.pivot = new Vector2(0f, 1f);
        root.anchoredPosition = new Vector2(40f, -200f);
        root.sizeDelta = new Vector2(180f, 36f);

        root.gameObject.AddComponent<Image>().color = new Color(0f, 0f, 0f, .45f);

        var label = new GameObject("Text", typeof(RectTransform)).GetComponent<RectTransform>();

        label.SetParent(root, false);
        label.anchorMin = Vector2.zero;
        label.anchorMax = Vector2.one;
        label.offsetMin = label.offsetMax = Vector2.zero;

        _text = label.gameObject.AddComponent<TextMeshProUGUI>();
        _text.alignment = TextAlignmentOptions.Center;
        _text.fontSize = 18f;
        _text.raycastTarget = false;

        _element = HudElements.Add("my.mod.guid", "kills", "Kill counter", root);

        Refresh();
    }

    public static void Add()
    {
        _kills++;

        Refresh();
    }

    private static void Refresh() => _text.text = "Kills: " + _kills;
}[/code]
没有 SeneaL UI 时,计数器就放在你放置的位置。有了它,玩家可以在 HUD 编辑器中发现“Kill counter”(标记为来自模组),并且可以移动、缩放、淡化、附加和隐藏它。

UI 提供方随后可以移动、调整大小、淡化和附加该元素,并在玩家的配置文件中保留其位置。该元素仍存在于你的层级中:提供方只会改变它的位置、缩放和透明度。当元素内部有活动时设置
[code]element.Busy = true[/code],这样“空闲时自动隐藏”设置就不会在此时隐藏它。当元素消失时使用
[code]HudElements.Remove[/code];被销毁的
[code]RectTransform[/code]会自动移除。
[code]element.Placed[/code]在玩家于 HUD 编辑器中手动放置该元素时为 true:如果你的模组自己定位该元素(例如放在小地图旁边),则在
[code]Placed[/code]为 true 时跳过定位,让玩家的位置优先。
[code]HudElements.Find(owner, id)[/code]返回你或其他人注册的元素。用
[code]Api.Has("hud.placed")[/code]探测。

[b][size=150]主题(Theme)[/size][/b]

根据已安装 UI 模组的外观构建你自己的窗口或 HUD 部件:它的字体、面板、按钮、字段、行和标签页精灵以及它的颜色。没有主题提供方时,每个调用都返回空,因此请保留你自己的外观作为回退。

[code]
var sprite = Themes.Sprite(ThemePart.Window);

if (sprite != null) {
    image.sprite = sprite;
    image.type = Image.Type.Sliced;
    canvasScaler.referencePixelsPerUnit = Themes.PixelsPerUnit;
}

var font = Themes.Font(ThemeFont.Body);

if (Themes.TryColor(ThemeColor.Accent, out var accent))
    title.color = accent;

Themes.Changed += Rebuild;[/code]
部件:
[code]Window[/code]、
[code]Inner[/code]、
[code]Card[/code]、
[code]Button[/code]、
[code]ButtonHover[/code]、
[code]Primary[/code]、
[code]PrimaryHover[/code]、
[code]Field[/code]、
[code]FieldFocus[/code]、
[code]Row[/code]、
[code]RowHover[/code]、
[code]RowOn[/code]、
[code]Tab[/code]、
[code]TabHover[/code]、
[code]TabOn[/code]、
[code]Track[/code]、
[code]Chip[/code]、
[code]Separator[/code]。字体:
[code]Body[/code]、
[code]Strong[/code]、
[code]Title[/code]。颜色:
[code]Text[/code]、
[code]Muted[/code]、
[code]Accent[/code]、
[code]Good[/code]、
[code]Bad[/code]、
[code]Warn[/code]、
[code]OnPrimary[/code](
[code]Primary[/code]按钮上的文本)、
[code]Shade[/code](窗口后的暗淡层)。精灵是九宫格切片:设置
[code]Image.type = Sliced[/code]并为画布提供提供方的
[code]PixelsPerUnit[/code](或设置
[code]Image.pixelsPerUnitMultiplier[/code]以匹配),否则边角会变得太大或太小。
[code]Themes.Changed[/code]在提供方或其外观改变时触发。用
[code]Api.Has("theme")[/code]探测。
[code]Themes.Icon("map-pin")[/code]按 Phosphor 名称返回一个图标(白色,用
[code]Image.color[/code]着色),功能
[code]theme.icons[/code]。

[b][size=150]模组设置[/size][/b]

在 UI 模组自己的设置窗口中显示你的 BepInEx 设置(SeneaL UI:F7)。按键(
[code]KeyboardShortcut[/code])进入其按键列表并带有冲突警告,其余(bool、enum、带
[code]AcceptableValueRange[/code]的数字)进入其模组页面下你的模组名称处。这些条目仍归你所有:UI 只读写
[code]Entry.BoxedValue[/code]。

[code]
ModSettings.Add("my.mod.guid", "My Mod", openKey, () => Loc("Open the window"));
var s = ModSettings.Add("my.mod.guid", "My Mod", mode, () => Loc("Mode"), () => Loc("What the mode does"));
s.Options = () => new[] { Loc("Off"), Loc("Push to talk"), Loc("Open") };
s.Visible = () => serverAllowsIt;
s.Hold = true;[/code]
[code]Label[/code]、
[code]Tip[/code]和
[code]Options[/code]都是函数,因此它们会跟随游戏语言。
[code]Hold[/code]标记一个被按住的键(无冲突警告)。
[code]ModSettings.Changed[/code]在条目出现或消失时触发;在你的标签改变后调用
[code]ModSettings.Refresh()[/code]。用
[code]Api.Has("settings")[/code]探测。

[b][size=150]槽位(Slots)[/size][/b]

在装备旁边添加你自己的物品栏槽位。UI 会绘制它们,将物品与角色一起存储并处理拖放,而你决定什么能放入以及物品进出时发生什么。

[code]
var slot = CustomSlots.Add("my.mod.guid", new SlotDef("ring3")
{
    Title = "Third ring",
    Hint = "A ring from My Mod.",
    IconName = "diamond",
    Mode = SlotMode.Worn,
    Place = SlotPlace.Doll,
    EquipTime = -1f,
    Order = 10,
    Accepts = item => item.m_shared.m_name == "$item_my_ring",
    Visible = () => MyMod.RingsUnlocked,
    Changed = change => MyMod.Refresh(change.Old, change.New)
});

var worn = CustomSlots.Get("my.mod.guid", "ring3");
CustomSlots.Set("my.mod.guid", "ring3", null);[/code]
[list]
[][code]IconName[/code]:空槽位的图片,按 [url=https://phosphoricons.com]Phosphor[/url] 图标(regular 权重)的名称指定,例如
[code]"diamond"[/code]、
[code]"flask"[/code]、
[code]"sword"[/code]、
[code]"shield-chevron"[/code];全部 1512 个图标都内置于 UI 中,并[url=https://storage.yandexcloud.net/seneal-bucket/Valheim/SeneaL%20API/api-icons.avif]展示在一张图片中[/url],因此你无需分发任何内容。从网站复制名称(
[code]ph-[/code]和
[code].svg[/code]会被忽略)。
[code]Icon[/code]改为接受你自己的
[code]Sprite[/code],并优先于
[code]IconName[/code]。两者都没有时,UI 会根据 id 和标题猜测(ring、neck、bag、quiver),否则绘制一个火花。未知名称会保留默认值并记录日志。用
[code]Api.Has("slots.icon")[/code]探测。[/
]
[][code]Stored[/code]:槽位只存放物品,你用
[code]Get[/code]读取并自行处理。[/
]
[][code]Worn[/code]:物品在槽位中时被视为已装备:
[code]m_equipped[/code]会被设置,其装备状态效果和效果由宿主播放和移除。额外效果(视觉、属性)由你在
[code]Changed[/code]中实现。[/
]
[]使用槽位接受的物品(在物品栏中右键单击、快捷栏按键、手柄)会将其放入槽位,而不是让游戏装备它:先放入空的接受槽位,否则与第一个接受槽位中的物品交换。
[code]Worn[/code]槽位以此方式接受所有被接受的物品,
[code]Stored[/code]槽位只接受游戏能装备的物品。设置
[code]EquipOnUse = false[/code]以使该槽位保留游戏自身的用途。保持
[code]Accepts[/code]范围狭窄:接受所有
[code]Utility[/code]物品的槽位也会接受腰带。用
[code]Api.Has("slots.use")[/code]探测。[/
]
[][code]EquipTime[/code]:当玩家操作时(右键单击、快捷栏按键、手柄、拖放),将物品放入
[code]Worn[/code]槽位或从中取出所需的时间。
[code]-1[/code](默认)使用物品自身的
[code]m_equipDuration[/code],与游戏对护甲的处理方式相同;
[code]0[/code]为瞬时;正值为相应秒数。使用游戏自身的装备条、动画和取消。
[code]CustomSlots.Set[/code]始终为瞬时。用
[code]Api.Has("slots.time")[/code]探测。[/
]
[][code]Place[/code]:UI 绘制槽位的位置。
[code]Auto[/code](默认)将
[code]Stored[/code]和
[code]Worn[/code]槽位放入装备下方的[i]模组槽位[/i]行,并将
[code]Linked[/code]槽位放入装备栏的空闲位置;
[code]Row[/code]始终使用该行;
[code]Doll[/code]将槽位放入装备栏的空闲位置,装备栏在需要更多位置时会变宽;无法放在屏幕上的槽位保留在行中。玩家可以在 UI 设置中覆盖此设置。用
[code]Api.Has("slots.place")[/code]探测。[/
]
[]当游戏装备和卸下它自己的物品时,
[code]Worn[/code]物品会保留其装备状态效果,即使原版物品带有相同效果时也是如此。[/
]
[][code]Changed[/code]在每次填充、清空和交换时运行,并在角色加载时对已位于槽位中的每个物品各运行一次,因此一个处理程序即可覆盖两者。[/]
[]槽位属于本地玩家。最多显示 16 个槽位,按
[code]Order[/code]排序;槽位在行中的位置在会话之间保持不变。如果槽位被移除或其模组不存在,物品会返回物品栏。[/
]
[]物品存放在角色数据中单独的物品栏内,而不是主网格中:它们不计入负重,并在角色死亡时进入坟墓。[/]
[/list]
[b][size=150]链接槽位与 AzuEPI[/size][/b]

为你自己的新装备槽位(戒指、护身符)选择
[code]Worn[/code],而不是
[code]Linked[/code]。
[code]Linked[/code]不会为角色添加槽位:它显示游戏或其他模组已经装备的物品,将物品放入其中会调用游戏的
[code]EquipItem[/code]。物品类型为
[code]Utility[/code]的戒指随后会进入腰带槽位并取下腰带。

[code]Linked[/code]槽位不存储物品:它显示存放在别处的物品,通常是在玩家的物品栏中,并由你告诉 API 如何找到它。将物品放入槽位会通过游戏装备它,取出会卸下它,而
[code]Changed[/code]会在其中的物品变化时触发。

[code]
CustomSlots.Add("my.mod.guid", new SlotDef("tome")
{
    Title = "Tome",
    Mode = SlotMode.Linked,
    Source = player => player.GetInventory().GetEquippedItems().Find(i => i.m_shared.m_name == "$item_my_tome"),
    Accepts = item => item.m_shared.m_name == "$item_my_tome"
});[/code]
如果安装了 Azumatt 的 AzuEPI,SeneaL API 会每秒读取其他模组通过
[code]AzuEPI.API.AddSlot[/code]添加的装备槽位,并将它们每个镜像为归属于
[code]Azumatt.AzuExtendedPlayerInventory[/code]的
[code]Linked[/code]槽位。AzuEPI 继续决定什么能放入以及什么被装备;UI 提供方只显示槽位并移动物品。这不需要调用
[code]AddSlot[/code]的模组做任何事。配置中的
[code]Bridge / AzuEPI[/code]会关闭它。

将物品栏交给 AzuEPI 的 UI 提供方会设置
[code]CustomSlots.AzuManaged = true[/code]:桥接停止镜像,已镜像的槽位被移除,存根修补器只将
[code]AddSlot[/code]转发给真正的 AzuEPI 而不是模拟它,以便 AzuEPI 自己显示那些槽位。

将 AzuEPI API 存根嵌入自己的 dll 中的模组(存根方法什么都不做,因此它永远不会到达 AzuEPI)由此模组附带的一个小型修补器处理,该修补器位于
[code]BepInEx/patchers/SeneaL-API[/code]。它在所有插件之前加载,在任何已加载的程序集中查找嵌入的
[code]AzuEPI.API[/code]和
[code]AzuExtendedPlayerInventory.API[/code]类型,并将
[code]IsLoaded[/code]、
[code]AddSlot[/code](全部四个重载)和
[code]RemoveSlot[/code]路由到这里:

[list]
[][code]IsLoaded()[/code]在安装了真正的 AzuEPI 或 SeneaL UI 时返回
[code]true[/code],
否则返回
[code]false[/code](模组保留自己的回退)。[/
]
[]当安装了真正的 AzuEPI 时,
[code]AddSlot[/code]会转到它(然后像它的其他槽位一样被镜像),否则成为归属于
[code]AzuEPI.emulated[/code]的
[code]Linked[/code]槽位。添加相同槽位名称的两个模组共享一个槽位,就像在 AzuEPI 中一样。[/
]
[]UI 模组已自行支持的模组保持不变,以便其行为不变:
[code]Bridge / AzuEPIStubIgnore[/code]列出它们的程序集名称(默认为 Jewelcrafting、BowsBeforeHoes、BetterArchery、AdventureBackpacks 和 EpicLoot;仍保存旧默认列表的配置会迁移到此列表)。移除一个名称即可也路由该模组。
[code]Bridge / AzuEPIStubs = false[/code]会关闭修补器。两者都需要重启游戏。[/
]
[/list]
修补器文件是包的一部分;手动安装也必须复制它。

[b][size=170]编写提供方[/size][/b]

[code]
public sealed class MyNotices : NoticeProvider
{
    public override bool Show(Notice notice)
    {
        return true;
    }
}

Notices.Register("my.ui.guid", new MyNotices());[/code]
当你显示了它时返回
[code]true[/code],想交给原版窗口处理时返回
[code]false[/code]。你显示的通知会为你写入游戏的消息日志;如果你自己写入日志行,请先设置
[code]notice.Log = false[/code]。注册:最高优先级胜出,平局归最新者,
[code]Unregister(owner)[/code]回退到前一个提供方,再次注册同一所有者会替换其条目。在提供方中打开而后提供方消失的对话框会由下一个提供方再次显示,或由原版窗口显示。
[code]DialogProvider[/code]还会获得
[code]Hide[/code],在对话框关闭时调用;
[code]HudProvider[/code]获得
[code]Added[/code]和
[code]Removed[/code],并接收在其之前注册的每个元素。

[code]SlotProvider[/code]存储物品:
[code]Added[/code]和
[code]Removed[/code]告诉它关于槽位的信息,
[code]Get[/code]和
[code]Set[/code]读取和填充它们,并通过
[code]CustomSlots.Report[/code]报告更改。

[b][size=150]提供方示例[/size][/b]

一个对话框提供方:绘制你自己的窗口,在玩家回答时调用对话框自身的方法。

[code]
public sealed class MyDialogs : DialogProvider
{
    public override bool Show(Dialog dialog)
    {
        var window = MyWindow.Create(dialog.Title, dialog.Text);

        switch (dialog)
        {
            case ConfirmDialog confirm:
                window.AddButton(confirm.YesText ?? "Yes", confirm.Yes);
                window.AddButton(confirm.NoText ?? "No", confirm.No);

                break;
            case MessageDialog message:
                window.AddButton(message.OkText ?? "OK", message.Ok);

                break;
            case InputDialog input:
                window.AddField(input.Value, input.Placeholder, input.CharLimit, text =>
                {
                    if (input.IsValid(text))
                        input.Submit(text);
                });
                window.AddButton("Cancel", input.Cancel);

                break;
        }

        return true;
    }

    public override void Hide(Dialog dialog) => MyWindow.CloseFor(dialog);
}

Dialogs.Register("my.ui.guid", new MyDialogs(), priority: 10);[/code]
一个 HUD 提供方:你现在和以后都会收到每个元素,并可以按你的喜好移动它。该元素仍是所有者层级中的普通 Unity 对象。

[code]
public sealed class MyHud : HudProvider
{
    public override void Added(HudElement element)
    {
        MyEditor.Track(element.Key, element.Title, element.Rect);
    }

    public override void Removed(HudElement element)
    {
        MyEditor.Forget(element.Key);
    }
}

HudElements.Register("my.ui.guid", new MyHud());[/code]
一个主题提供方:给出你拥有的内容,其余返回
[code]null[/code]/
[code]false[/code]。当你的外观改变时调用
[code]Themes.Refresh()[/code]。

[code]
public sealed class MyTheme : ThemeProvider
{
    public override float PixelsPerUnit => 50f;

    public override Sprite Sprite(ThemePart part) => part == ThemePart.Window ? MySkin.Panel : null;

    public override TMP_FontAsset Font(ThemeFont font) => MyFonts.Regular;

    public override bool TryColor(ThemeColor color, out Color value)
    {
        value = color == ThemeColor.Accent ? MyColors.Gold : Color.white;

        return color == ThemeColor.Accent || color == ThemeColor.Text;
    }
}

Themes.Register("my.ui.guid", new MyTheme());[/code]
一个槽位提供方:保留物品,在发生变化时告诉 API。

[code]
public sealed class MySlots : SlotProvider
{
    private readonly Dictionary<string, ItemDrop.ItemData> _items = new Dictionary<string, ItemDrop.ItemData>();

    public override void Added(SlotDef slot) => MyBar.AddCell(slot);

    public override void Removed(SlotDef slot)
    {
        _items.Remove(slot.Key);

        MyBar.RemoveCell(slot);
    }

    public override ItemDrop.ItemData Get(SlotDef slot)
    {
        _items.TryGetValue(slot.Key, out var item);

        return item;
    }

    public override bool Set(SlotDef slot, ItemDrop.ItemData item)
    {
        if (item != null && !slot.Allows(item))
            return false;

        _items.TryGetValue(slot.Key, out var old);

        _items[slot.Key] = item;

        CustomSlots.Report(new SlotChange(slot, old, item));

        return true;
    }
}

CustomSlots.Register("my.ui.guid", new MySlots());[/code]
[b][size=150]常见错误[/size][/b]

[list]
[] 当 API 是可选时,从主类的方法中调用
[code]SeneaL.API[/code]类型:类型会在方法编译为本机代码时解析,缺少程序集会抛出异常。将所有 API 调用放在单独的类中,如快速开始所示。[/
]
[]从 Unity 主线程以外的线程使用
[code]Notices.Show[/code]。[/
]
[]在应仅在某个时刻存在的槽位上忘记
[code]Visible[/code]:始终可见的槽位会留在行中。[/
]
[]期望
[code]Accepts[/code]阻止玩家取出物品:它只决定可以放入什么。[/
]
[*]在所需对象存在之前注册提供方:提供方在注册时会接收先前的 HUD 元素和槽位,因此在 <br