Suggestion API

Suggestion API

用于处理Minecraft建议的API。它允许你添加建议,并可以更改其渲染方式。使用这个库,你可以静态或动态地添加它们

基础库

Suggestions API v1.0.5

该库被注入到 Minecraft 源代码中,负责处理聊天中 Minecraft 建议(Suggestion)的逻辑,以便为添加或修改建议提供更方便的封装。目前,该库包含以下接口:

  • 根据用户在文本输入框中输入的文本,同步/异步地添加建议
  • 更改建议的渲染
  • 处理与建议相关的事件:
    • 会话初始化时
    • 建议被选中时
  • 替换由非 Suggestions API 创建的建议

为此,该库以以下形式提供了现成的实现:

  • 始终显示的建议(带有始终显示条件的文本建议)
  • 简单建议(带有默认或自定义显示条件的文本建议)
  • 图标建议(带有左侧或右侧图标、以及默认或自定义显示条件的文本建议)
  • 同步/异步建议注入器(根据匹配指定的正则表达式模式,在向输入框输入文本时动态添加建议)

哪些模组使用了 Suggestions API?

以下模组使用了 Suggestions API:

快速开始

将以下内容合并到你的文件中。下面是一个适用于 Fabric 或 Quilt 版本的示例。如果你是为 Forge 或 NeoForge 开发,请将 fabric 替换为 forge(自 Suggestions API v1.0.4 起以此方式工作)

build.gradle
repositories {
    maven {
        url = "https://api.modrinth.com/maven"
    }
}

dependencies {
    modImplementation "maven.modrinth:suggestions-api:1.0.6+fabric"
}
build.gradle.kts
repositories {
    maven("https://api.modrinth.com/maven")
}

dependencies {
    modImplementation("maven.modrinth", "suggestions-api", "1.0.6+fabric")
}

快速文档

包含该库基础知识的快速文档。

内置建议有哪些类型?

Suggestion 接口位于 io.github.aratakileo.suggestionsapi.suggestion。

该库有两种内置的建议类型。以下是用于初始化它们的函数:

  • Suggestion.simple(...) —— 简单建议(仅文本)
  • Suggestion.withIcon(...) —— 带图标建议(图标将渲染在左侧)

简单建议初始化函数的主要参数将是该建议的文本。例如:

final var simpleSuggestion = Suggestion.simple("bonjour!");

要初始化带图标的建议,你需要指定两个主要参数:建议的文本和图标资源。此函数必须在游戏所有纹理加载完成后严格调用,否则游戏会崩溃。以下以屏障(barrier)纹理为例:

final var suggestionWithIcon = Suggestion.withIcon("barrier", new ResourceLocation("minecraft", "textures/item/barrier.png"));

默认情况下,建议会在开始输入建议文本后开始显示。你可以将自定义的匹配条件作为最后一个参数指定为 lambda 函数。该 lambda 的第一个参数是建议的文本,第二个参数是当前表达式(输入框中从最近的左侧空格到光标之间的文本)。在下面的示例中,当用户完整输入建议文本时,该建议会显示:

final var anotherSimpleSuggetion = Suggestion.simple(
        "bonjour!",
        (suggestionText, currentExpression) -> suggestionText.toLowerCase().equals(currentExpression.toLowerCase())
);

或

final var anotherSimpleSuggetion = Suggestion.simple("bonjour!", String::equalsIgnoreCase);

函数 Suggestion.simple(...) 和 Suggestion.withIcon(...) 的默认条件为 (suggestionText, currentExpression) -> suggestionText.toLowerCase().startsWith(currentExpression.toLowerCase()) 或 Suggestion::DEFAULT_CONDITION。

如果你希望无论输入的文本是什么,建议都始终显示,可以指定条件 (suggestionText, currentExpression) -> true 或 Suggestion::ALWAYS_SHOW_CONDITION;在函数 Suggestion.alwaysShown(...)(作为函数 Suggestion.simple(...) 的替代)和 Suggestion.alwaysShownWithIcon(...)(作为函数 Suggestion.withIcon(...) 的替代)中初始化建议时,默认使用的就是该条件:

final var alwaysShownSuggestion = Suggestion.alwaysShown("bonjour!");

如何向游戏添加新建议?

你可以使用位于 io.github.aratakileo.suggestionsapi.injector 中的 Injector 接口向游戏添加新建议。

注入器有两种基本类型:简单注入器和异步注入器。为了初始化它们,该库同样提供了函数。其第一个参数将是一个正则表达式模式。

要注册一个注入器,必须将其作为单个参数传递给函数 SuggestionsAPI.registerInjector(...)。

要创建一个简单注入器,可以使用函数 Injector.simple(...),它返回 SuggestionsInjector。让我们添加两个新的简单建议(Injector.ANYTHING_WITHOUT_SPACES_PATTERN 为 Pattern.compile("\\S+$")):

SuggestionsAPI.registerInjector(Injector.simple(
        Injector.ANYTHING_WITHOUT_SPACES_PATTERN,
        (stringContainer, startOffset) -> List.of(
                Suggestion.alwaysShownWithIcon("barrier", new ResourceLocation("minecraft", "textures/item/barrier.png")),
                alwaysShownSuggestion
        )  // variables from the example above
));

结果将如下所示:

如果你希望自己的建议在输入命令时不显示,可以按如下方式更改代码:

SuggestionsAPI.registerInjector(Injector.simple(
        Injector.ANYTHING_WITHOUT_SPACES_PATTERN,
        (stringContainer, startOffset) -> stringContainer.getContext().isNotCommand() ? List.of(
                Suggestion.alwaysShownWithIcon("barrier", new ResourceLocation("minecraft", "textures/item/barrier.png")),
                alwaysShownSuggestion
        ) : null  // variables from the example above
));

作为第二个参数,函数 Injector.simple(...) 接收一个 lambda,用于描述生成建议列表的过程并返回该列表。同时,该 lambda 有它自己的两个参数:第一个参数包含当前表达式的字符串(输入框中右边缘碰到光标、并按指定模式找到的文本),第二个参数是一个数字,表示当前表达式开头与原始表达式(输入框中从最近的左侧空格到光标之间的文本)之间的偏移量。作为示例,下面展示了在尝试输入数字时添加数字建议的情况:

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile(":[A-Za-z0-9]*(:)?$"),
        (stringContainer, startOffset) -> Stream.of(
            "67487",
            "nothing",
            "bedrock",
            "bedrock_2"
        ).map(value -> Suggestion.simple(':' + value + ':')).toList()
));

或

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile("[A-Za-z0-9]+$"),
        (stringContainer, startOffset) -> Stream.of(
            "Hi, " + stringContainer.getContent().substring(startOffset) + '!',
            '"' + stringContainer.getContent().substring(startOffset) + '"'
        ).map(Suggestion::alwaysShown).toList()
));

或

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile(":[0-9]*(:)?$"),
        (stringContainer, startOffset) -> IntStream.rangeClosed(1000, 1010)
            .boxed()
            .map(Objects::toString)
            .map(Suggestion::alwaysShown)
            .toList()
));

// The suggestions of this injector will not appear if the suggestions from the injector above appear, 
// because the interaction string of this injector is included in the interaction string of the injector above

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile("[0-9]$"),
        (stringContainer, startOffset) -> IntStream.rangeClosed(0, 9)
            .boxed()
            .map(Objects::toString)
            .map(Suggestion::alwaysShown)
            .toList()
));

默认情况下,如果根据某个注入器的正则表达式模式检测到的字符串是另一个注入器根据其正则表达式模式检测到的字符串的一部分,那么字符串被嵌套的那个注入器的建议将被忽略。可以通过将 InputRelatedInjector.NestingStatus.ALL_NESTABLE 指定为第三个(最后一个)参数来为特定注入器禁用此机制。例如:

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile(":[0-9]*(:)?$"),
        (stringContainer, startOffset) -> IntStream.rangeClosed(1000, 1010)
            .boxed()
            .map(Objects::toString)
            .map(Suggestion::alwaysShown)
            .toList(),
        InputRelatedInjector.NestingStatus.ALL_NESTABLE
));

// The suggestions of this injector will appear if the suggestions from the injector above appear

SuggestionsAPI.registerInjector(Injector.simple(
        Pattern.compile("[0-9]$"),
        (stringContainer, startOffset) -> IntStream.rangeClosed(0, 9)
            .boxed()
            .map(Objects::toString)
            .map(Suggestion::alwaysShown)
            .toList(),
        InputRelatedInjector.NestingStatus.ALL_NESTABLE
));

该参数还有其他可能的值:

  • InputRelatedInjector.NestingStatus.NOT_NESTABLE —— 默认使用
  • InputRelatedInjector.NestingStatus.ONLY_API_NESTABLE —— 仅允许 Suggestions API 添加的建议嵌套
  • InputRelatedInjector.NestingStatus.ONLY_NON_API_NESTABLE —— 仅允许在 Suggestions API 之外添加的建议嵌套【已弃用】

如果你需要建议异步出现,可以使用函数 Injector.async(...) 来初始化异步注入器。使用该函数初始化的注入器提供了一种机制:如果收到了新的请求,而当前异步过程此时尚未完成,则可以取消当前异步过程。该函数与上一个函数类似,但这次第二个参数(即 lambda)返回一个无参数的 lambda,该无参数 lambda 将作为异步过程启动;并且它有第三个参数,最后一个参数是一个 lambda,它接受一个包含新建议的列表,并应在异步过程的 lambda 内部使用。例如:

SuggestionsAPI.registerInjector(Injector.async(
        /* insert your pattern here */,
        (stringContainer, startOffset) -> {
            /* insert your async processing code here */
            
            return /* insert list of suggestion here */;
        }
));

函数 Injector.async(...) 返回 AsyncInjector。与上一个函数的情况一样,该函数中也可以指定第三个参数。

我可以将建议替换为新实例吗?【已弃用】

Suggestions API 禁止隐式替换建议,但允许你替换并非由它添加的建议(即由 Minecraft 或其他不使用 Suggestions API 的模组添加的建议)。如果你需要的建议尚未被其他模组替换,你可以使用位于 io.github.aratakileo.suggestionsapi.injector 中的 ReplacementInjector 来替换建议。例如:

// To check this, start entering the command `give @s minecraft:barrier` in the chat or in the command block
SuggestionsAPI.registerInjector(Injector.replacement(
        nonApiSuggestion -> nonApiSuggestion.equals("minecraft:barrier") ? Suggestion.withIcon(
                nonApiSuggestion,
                new ResourceLocation("minecraft", "textures/item/barrier.png")
        ) : null
));

结果将如下所示: