Fabric 服务端插件开发简述与聊天事件监听转发

news2025/5/17 19:39:32

原文链接:Fabric 服务端插件开发简述与聊天事件监听转发 < Ping通途说

0. 引言

以前写过Spigot的插件,非常简单,仅需调用官方封装好的Event类即可。但Fabric这边在开发时由于官方文档和现有互联网资料来看,可能会具有一定的误导性,使得开发者使用更加底层的Mixin注入开发。但后来才发现官方已经封装了这些实现方法,为了避免后续开发者踩坑,也能让开发者明白Fabric的技术实现,于是才写本文。

本文以构建一个监听游戏内玩家聊天事件、玩家进入服务器事件、玩家离开服务器事件,并遵循OnebotV11协议转发到对应服务器的Fabric服务端插件。

环境搭建与项目创建本文不再阐述,有关资料可参考:

  1. 设置开发环境 | Fabric 文档 - Fabric 的环境搭建
  2. Template mod generator | Fabric -  Fabric 模板模组生成器
  3. Minecraft 模组编写基础 [Fabric Wiki] - Fabric的基本介绍
  4. 教程:更新Java - 中文 Minecraft Wiki - 各版本Minecraft最低JAVA版本要求

1. 直接调用Event方法

1.1 获取Event API方法

初学者可能会被官方文档迷惑,在监听事件 [Fabric Wiki]中,只看到了几个官方封装的方法,自己想要的类并不在以下列表中:

点击链接后又发现跳转到了项目源码中,如果对Java不熟悉的开发者可能就不明白为何会跳转到源码,然后我应该看什么。虽然 事件 | Fabric 文档 该文档中简单描述了事件监听的基本过程,但依然没有列出想要事件列表。而官方描述的 net.fabricmc.fabric.api.event ,在进入源代码后发现也只不过是所有Event的抽象方法。

那么真正的Event API到底在哪,官方文档没有像Bukkit/Spigot那样详细的列出来,我们只能借助Deepseek来查询。通过查询后,本文中我们需要监听的聊天事件,就在net.fabricmc.fabric.api.message.v1.ServerMessageEvents.CHAT_MESSAGE中。

进入 ServerMessageEvents查看,可以看到除玩家聊天事件外,还有游戏消息事件和执行指令事件。

再通过AI我们可以找到玩家进入服务器(ServerPlayConnectionEvents.JOIN

)和玩家离开服务器(ServerPlayConnectionEvents.DISCONNECT

)的事件。而该包存在于net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents 类中,因此可以猜测,官方似乎放弃了对net.fabricmc.fabric.api.event的维护,将所有Event分散到不同的模块中。

对于API的查询,官方可能已经放出了相关文档,但普通的开发者无法通过正常的方式通过互联网搜索或从官网中进入。在此官方改善该问题前只能通过AI辅助查找方法实现。

1.2 插件开发

找到API那开发起来就非常简单了,直接在插件初始化中注册事件:

public class EventForwardobv11 implements ModInitializer {
    public static final String MOD_ID = "event-forward-obv11";
    public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
    public static ModConfig CONFIG;

    @Override
    public void onInitialize() {
        CONFIG = ConfigManager.loadConfig();

        ServerMessageEvents.CHAT_MESSAGE.register((message, sender, params) -> {
            HttpUtil.sendGet("[" + sender.getName().getString() + "] " + message.getContent().getString());
        });
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            ServerPlayerEntity player = handler.player;
            HttpUtil.sendGet(player.getName().getString() + "加入服务器 (" + (server.getCurrentPlayerCount() + 1) + "/" + server.getMaxPlayerCount() + ")");
        });
        ServerPlayConnectionEvents.DISCONNECT.register((handler, sender) -> {
            ServerPlayerEntity player = handler.player;
            HttpUtil.sendGet(player.getName().getString() + "退出服务器");
        });
    }
}

注册完成后,我们需要将事件通过OnebotV11协议转发至群中,查看协议API,发送群消息的接口是 /send_group_msg 。对于单个小型Minecraft 服务器,请求应该不会大到哪里去,因此我们选择HTTP GET的方式向服务器发送请求,这里需要封装一个请求方法:

// GET 方法实现
class HttpUtil {
    public static void sendGet(String message) {
        ModConfig config = EventForwardobv11.CONFIG;
        String url = "http://"+config.obServer + ":" + config.obPort + "/send_group_msg?access_token=" + config.obToken + "&group_id=" + config.forwardGroup + "&message=" + URLEncoder.encode(message, StandardCharsets.UTF_8);
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            HttpGet get = new HttpGet(url);
            HttpResponse response = client.execute(get);
            switch (response.getStatusLine().getStatusCode()) {
                case 401:
                    EventForwardobv11.LOGGER.error("鉴权失败,当前请求需要Token");
                    break;
                case 403:
                    EventForwardobv11.LOGGER.error("鉴权失败,Token无效");
                    break;
                case 404:
                    EventForwardobv11.LOGGER.error("请求失败,无效接口:{}",url);
            }
        // 主要是根据状态码判断错误,正常返回结果直接忽略
        } catch (Exception e) {
            EventForwardobv11.LOGGER.error("发送消息失败:{}", e.getMessage());
        }
    }
}

关于代码中的异常处理,根据 HTTP | OneBot 11 标准只有在请求失败时才会返回200以外的状态码。服务器返回200对插件没有任何作用,因此只需关注其他状态码并处理异常即可。

服务器地址与端口是不确定的,因此需要用户来自己配置,我们来自己实现一个配置类。本插件是服务端使用的,因此无需像客户端那样展示GUI配置界面,因此可以利用GSON来生成读取配置:


class ModConfig { // 遵循Onebotv11协议发送事件
    public String obServer = "127.0.0.1"; // Onebotv11服务器地址
    public int obPort = 8080; // http端口
    public String obToken = ""; // 鉴权token
    public String forwardGroup = ""; // 要转发的群聊
}

class ConfigManager {
    private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
    private static final Path CONFIG_PATH = Paths.get("config", "event-forward-obv11.json");

    public static ModConfig loadConfig() {
        try {
            File configFile = CONFIG_PATH.toFile();
            if (!configFile.exists()) {
                return createDefaultConfig(); // 如果文件不存在,创建默认配置
            }
            try (FileReader reader = new FileReader(configFile)) {
                return GSON.fromJson(reader, ModConfig.class); // 读取配置
            }
        } catch (Exception e) {
            System.err.println();
            EventForwardobv11.LOGGER.error("无法加载配置文件,使用默认配置:{}", e.getMessage());
            return new ModConfig();
        }
    }

    public static ModConfig createDefaultConfig() {
        try {
            File configFile = CONFIG_PATH.toFile();
            configFile.getParentFile().mkdirs(); // 确保目录存在
            ModConfig defaultConfig = new ModConfig();
            try (FileWriter writer = new FileWriter(configFile)) {
                GSON.toJson(defaultConfig, writer); // 写入默认配置
            }
            return defaultConfig;
        } catch (Exception e) {
            EventForwardobv11.LOGGER.error("无法创建默认配置文件:{}", e.getMessage());
            return new ModConfig();
        }
    }

现在,插件全部功能已经实现。将构建后的插件装载至服务端的mod文件夹,当玩家进出服务器和聊天时就会转发事件:

2. Mixin注入简述与示例

Mixin是我刚接触Fabric就误入歧途了解的东西。Mixin的主要用途是修改基本游戏中已存在的代码,可以是通过注入自定义逻辑移除机制或者修改值

我是完全参考该项目进行学习与开发的: GitHub - IotaBread/player-events: Fabric mod. Sends a configurable message/command when a player joins the server.

这个项目实现了对玩家事件的监听,例如玩家进服、玩家出服、玩家死亡等事件。正好缺少监听聊天事件,我们可以通过学习项目中玩家进出服的实现,来注册一个我们自己实现的聊天事件监听器!

先来到插件入口,可以看到自定义回调在这里注册了,回调参数通过读取Config文件来获取并传递到函数中。

2.1 玩家进入服务器事件监听器

以玩家加入服务器事件监听器为例子,找到PlayerJoinCallback(api/src/main/java/me/bymartrixx/playerevents/api/event/PlayerJoinCallback.java)。这里是自定义事件的处理模块,

随后继续查阅文件,可以发现在PlayerManagerMixin(api/src/main/java/me/bymartrixx/playerevents/api/mixin/PlayerManagerMixin.java)中引用了以上方法。可以看到Mixin在代码中出现了,其中使用了@Injects向Minecraft源代码中注入了捕获玩家连接的事件。

关于注入(Injects):允许你在已存在的模组中的特定位置放置自定义的代码。

其中,at参数代表当前代码段将会在Minecraft函数中何时执行,method参数则代表代码段会在Minecraft哪个函数中执行。可以看到代码中使用了 TAIL,即代表在Minecraft中onPlayerConnect函数在即将返回值时运行注入的代码。

另外的if判断如果玩家离开服务器的次数少于1次,那就可以判定该玩家是第一次进入服务器,随后再将事件传递到专门的监听器中。

再讲讲代码中的 CallbackInfoCallbackInfo(或 CallbackInfoReturnable)用于控制 原方法的执行流程。它是 Mixin 注入点时的一个参数,可以:

  • 取消原方法执行ci.cancel())。
  • 提前返回ci.setReturnValue(...),仅限 CallbackInfoReturnable)。
  • 获取方法执行状态(如是否已被取消)

什么时候用 CallbackInfoCallbackInfoReturnable<T>

  • 用 CallbackInfo:当目标方法是 void 或无返回值修改需求时。
  • 用 CallbackInfoReturnable<T>:当需要修改返回值时(如 public boolean someMethod())。

2.2 玩家离开服务器事件监听器

再来看看玩家离开服务器事件监听器的代码:

可以看到,除调用方法名称有区别外,与玩家进入服务器的代码基本一致。

同理,再前往调用了PlayerLeaveCallback函数的Mixin中查看:

哦豁,跟玩家进入服务器注入器代码有很大区别,多了很多东西。第一个注入器监听的是指令被执行后的事件,第二个注入器监听的则是玩家离开服务器事件。

首先是 @Shadow ,@Shadow 用于 “声明” 目标类(被 Mixin 的类)的 字段或方法,但并不实际修改它们。它只是让 Mixin 知道:“这个字段/方法在目标类中存在,我可以访问它”。不会生成任何实际代码,仅仅是一个引用标记。

在实际代码中,ServerPlayNetworkHandler 类(原版 Minecraft)有一个 player 字段,存储当前连接的玩家。@Shadow public ServerPlayerEntity player; 的作用是,告诉 Mixin:“ServerPlayNetworkHandler 里有一个 player 字段,我要用它”。这样你就可以在 Mixin 中直接通过 this.player 访问原版字段。

两个函数的at都使用了INVOKE,这代表当前方法将会在Minecraft对应函数被调用前执行。那target参数又是什么呢,为何他的值又是一长串?method参数为什么也是一长串?

首先@At 的 target是指定注入点的目标方法,它指向的是 目标类中的某个方法或字段(通常是原版 Minecraft 的方法)。指定的是 “在哪个方法调用或字段访问时触发注入”。直接看target地址:

Lnet/minecraft/server/network/ServerPlayNetworkHandler;checkForSpam()V

  • 当 ServerPlayNetworkHandler.checkForSpam() 被调用时,触发注入。
  • 格式:L包路径/类名;方法名(参数类型)返回值类型(JVM 字节码描述符)。

这段的知识面涉及JVM的标准设计第 4 章.类 File Format,字段描述符可以参考:

字段描述符原名描述
Bbyte带符号的字节
CcharBasic Multilingual Plane 中的 Unicode 字符代码点,使用 UTF-16 编码
Ddouble双精度浮点值
Ffloat单精度浮点值
Iint正数
Jlong长整数
L类名称;reference类名称的实例
Sshort带符号的短整型
Zbooleantrue 或 false
[reference单数组维度

而4.3.3的方法描述符基本与字段描述符一致,只是多了一个 V 用于表示 void

以下是Fabric文档中的官方描述:

再来看看 method,它指定的是要注入的目标方法,用于告诉 Mixin “你要修改的是哪个方法”,指向的是 被注入的目标方法(即你要修改的方法)。

method = "method_44356(Lnet/minecraft/class_7472;Ljava/util/Optional;)V"

因此这里的 method 表示:

  • 要修改的方法是 method_44356(可能是混淆后的名称)。
  • 它的参数是 ChatCommandC2SPacket(class_7472)和 Optional<MessageSignatureList>。
  • 返回值是 void(V)。

那么如何找到以上我们需要的地址?这就需要反编译Minecraft代码了:配置模组开发环境 [Fabric Wiki]

Fabric已经配置好了Gradle任务,直接运行就可以了,非常的方便。

一般情况下仅需指定target类地址就可以了,如果在实际运行中出现无法找到对应方法的问题,就需要再去指定method的地址。比如例子中的checkForSpam,通过搜索源代码(客户端版本:1.21.5)发现,该方法存在于:

c ate net/minecraft/class_3244 net/minecraft/server/network/ServerPlayNetworkHandler
...
m ()V r method_43669 checkForSpam

因此如果想要让执行指令后监听器生效,那就需要将代码修改为:
@Inject(at=@At(value="INVOKE", target="Lnet/minecraft/server/network/ServerPlayNetworkHandler;checkForSpam()V"),method = "method_43669(Lnet/minecraft/class_3244;Ljava/util/Optional;)V", require = 0)
所以,指定地址的弊端就出现了,客户端一更新,插件就需要重新反编译Minecraft代码来获取对应方法地址。

2.3 一起来实现聊天事件监听!

根据以上我们了解的知识,可以模仿来创作一个新的自定义Mixin!

@Mixin(ServerPlayNetworkHandler.class)
public abstract class ServerPlayNetworkHandlerMixin {
    @Inject(method = "handleMessage", at = @At("HEAD"))
    private void onChatMessage(Text message, CallbackInfo ci) {
        ServerPlayerEntity player = ((ServerPlayNetworkHandler)(Object)this).player;
        MinecraftServer server = player.getServer();
        
        // 触发你的回调事件
        ActionResult result = PlayerChatCallback.EVENT.invoker().playerChat(player, server);
        
    }
}

以及自定义回调事件:

public interface PlayerChatCallback {
    Event<PlayerChatCallback> EVENT = EventFactory.createArrayBacked(PlayerChatCallback.class,(listeners)->(player, server)->{
        for (PlayerChatCallback listener: listeners){
            listener.playerChat(player,server);
        }
    });
    void playerChat(ServerPlayerEntity player, MinecraftServer server);
}

最后在初始化类中注册这个监听器:

public class MyMod implements ModInitializer {
    @Override
    public void onInitialize() {
        // 注册聊天事件处理器
        PlayerChatHandler.register();
    }
}

可不要忘记 mixin 配置,让插件知道你创建了新的Mixin

在 src/main/resources/mixins.modid.json 中添加你的 mixin,package为存放Mixin的目录。

{
  "package": "com.example.mymod.api.mixin",
  "mixins": [
    "ServerPlayNetworkHandlerMixin"
  ]
}

3. Fabric 与 Bukkit区别

1. 架构设计差异

Bukkit/Spigot/PaperFabric
目标定位为服务器插件提供高层 API,屏蔽底层实现提供底层 Mod 支持,允许直接修改 Minecraft 代码
事件系统提供完整的 事件总线,插件直接注册监听器部分事件需通过 Mixin 注入 或 Fabric API 提供的事件
底层访问权限限制较多,只能通过 Bukkit API 操作允许直接修改游戏代码(通过 Mixin)
兼容性维护由 Bukkit/Spigot 团队维护 API 兼容性依赖社区和 Fabric 团队,部分 API 可能变动

2. 底层实现对比

Bukkit 的实现方式

  1. Bukkit 在服务端启动时,通过反射/CraftBukkit 修改 Minecraft 代码,插入事件触发点。
  2. 插件开发者无需关心注入逻辑,直接使用封装好的 Event 类。

// 伪代码:Bukkit 在底层注入事件调用
public void handleChatPacket(Packet packet) {
    Player player = getPlayerFromPacket(packet);
    PlayerChatEvent event = new PlayerChatEvent(player, packet.getMessage());
    Bukkit.getPluginManager().callEvent(event); // 触发事件
    if (!event.isCancelled()) {
        originalHandlePacket(packet); // 继续原逻辑
    }
}

Fabric 的实现方式

  1. Fabric 本身不修改原版代码,而是通过 Mixin 在编译时动态注入。
  2. 事件系统需要手动实现(或依赖 Fabric API 提供的事件)。
  3. 开发者可以完全控制事件行为,但需自行维护兼容性。

// 伪代码:Fabric 通过 Mixin 注入事件
@Inject(method = "handleChat", at = @At("HEAD"), cancellable = true)
private void onChat(MessagePacket packet, CallbackInfo ci) {
    PlayerChatEvent event = new PlayerChatEvent(player, packet.getMessage());
    EventManager.fireEvent(event); // 自定义事件总线
    if (event.isCancelled()) {
        ci.cancel(); // 取消原逻辑
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2377930.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电商物流管理优化:从网络重构到成本管控的全链路解析

大家好&#xff0c;我是沛哥儿。作为电商行业&#xff0c;我始终认为物流是电商体验的“最后一公里”&#xff0c;更是成本控制的核心战场。随着行业竞争加剧&#xff0c;如何通过物流网络优化实现降本增效&#xff0c;已成为电商企业的必修课。本文将从物流网络的各个环节切入…

Unity:延迟执行函数:Invoke()

目录 Unity 中的 Invoke() 方法详解 什么是 Invoke()&#xff1f; 基本使用方法 使用要点 延伸功能 ❗️Invoke 的局限与注意事项 在Unity中&#xff0c;延迟执行函数是游戏逻辑中常见的需求&#xff0c;比如&#xff1a; 延迟切换场景 延迟播放音效或动画 给玩家时间…

移植RTOS,发现任务栈溢出怎么办?

目录 1、硬件检测方法 2、软件检测方法 3、预防堆栈溢出 4、处理堆栈溢出 在嵌入式系统中&#xff0c;RTOS通过管理多个任务来满足严格的时序要求。任务堆栈管理是RTOS开发中的关键环节&#xff0c;尤其是在将RTOS移植到新硬件平台时。堆栈溢出是嵌入式开发中常见的错误&am…

【设计模式】- 结构型模式

代理模式 给目标对象提供一个代理以控制对该对象的访问。外界如果需要访问目标对象&#xff0c;需要去访问代理对象。 分类&#xff1a; 静态代理&#xff1a;代理类在编译时期生成动态代理&#xff1a;代理类在java运行时生成 JDK代理CGLib代理 【主要角色】&#xff1a; 抽…

数据服务共享平台方案

该文档聚焦数据服务共享平台方案,指出传统大数据管理存在数据定义不统一、开发困难、共享不足等挑战,提出通过自服务大数据平台实现数据 “采、存、管、用” 全流程优化,涵盖数据资产管理、自助数据准备、服务开发与共享、全链路监控等功能,并通过国家电网、东方航空、政府…

skywalking使用教程

skywalking使用教程 一、介绍 skywalking 1.1 概念 skywalking是分布式系统的应用程序性能监视工具&#xff0c;专为微服务、云原生架构和基于容器&#xff08;Docker、K8s、Mesos&#xff09;架构而设计。SkyWalking 是观察性分析平台和应用性能管理系统&#xff0c;提供分布…

C 语 言 - - - 简 易 通 讯 录

C 语 言 - - - 简 易 通 讯 录 代 码 全 貌 与 功 能 介 绍通 讯 录 的 功 能 说 明通 讯 录 效 果 展 示代 码 详 解contact.hcontact.ctest.c 总 结 &#x1f4bb;作 者 简 介&#xff1a;曾 与 你 一 样 迷 茫&#xff0c;现 以 经 验 助 你 入 门 C 语 言 &#x1f4a1;个 …

机器学习知识自然语言处理入门

一、引言&#xff1a;当文字遇上数学 —— 自然语言的数字化革命 在自然语言处理&#xff08;NLP&#xff09;的世界里&#xff0c;计算机要理解人类语言&#xff0c;首先需要将文字转化为数学向量。早期的 One-Hot 编码如同给每个词语分配一个唯一的 “房间号”&#xff0c;例…

MySQL数据库——支持远程IP访问的设置方法总结

【系列专栏】&#xff1a;博主结合工作实践输出的&#xff0c;解决实际问题的专栏&#xff0c;朋友们看过来&#xff01; 《项目案例分享》 《极客DIY开源分享》 《嵌入式通用开发实战》 《C语言开发基础总结》 《从0到1学习嵌入式Linux开发》 《QT开发实战》 《Android开发实…

Pageassist安装(ollama+deepseek-r1)

page-assist网站&#xff1a;https://github.com/n4ze3m/page-assist 首先电脑配置node.js&#xff0c;管理员打开命令窗口输入下面命令下载bun npm install -g buncd 到你想要安装page-assist的地方&#xff08;推荐桌面&#xff09; 输入下列命令 git clone https://gith…

2025年渗透测试面试题总结-安恒[实习]安全服务工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 安恒[实习]安全服务工程师 1. SQLMap爆出当前库名的参数是什么&#xff1f; 2. Nmap探测系统的参数&am…

五月份嵌入式面试总结

目录 1、札记 1.1、芯片的bring up 主要做哪些工作&#xff1a; 2、Linux驱动八股文 中断与同步互斥 2.1.1 内核同步互斥的几种方式 2.1.2 互斥锁和自旋锁的区别 2.1.3 spin_lock 和 spin_lock_irqsave 的区别 2.1.4 进程上下文和中断上下文有什么区别 2.1.5 进行上下…

数据库行业竞争加剧,MySQL 9.3.0 企业版开始支持个人下载

最新发现&#xff0c;Oracle 官方网站放开了 MySQL 9.3.0 企业版下载链接&#xff0c;个人用户也可以免费下载&#xff0c;不过只能用于学习、开发或者原型测试&#xff0c;不能用于生产环境。 通常我们都是下载 MySQL 社区版&#xff0c;不过 MySQL 企业版可以支持更多高级功能…

Tcping详细使用教程

Tcping详细使用教程 下载地址 https://download.elifulkerson.com/files/tcping/0.39/在windows环境下安装tcping 在以上的下载地中找到exe可执行文件&#xff0c;其中tcping.exe适用于32位Windows系统&#xff0c;tcping64.exe适用于64位Windows操作系统。 其实tcping是个…

【GAN网络入门系列】一,手写字MINST图片生成

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 博主简介&#xff1a;努力学习的22级本科生一枚 &#x1f31f;​&#xff1b;探索AI算法&#xff0c;C&#xff0c;go语言的世界&#xff1b;在迷茫中寻找光芒…

ubuntu22鼠键失灵恢复记录笔记chatgpt解决

ChatGPT 说&#xff1a; 你提到“Ubuntu 22 鼠键失灵”&#xff0c;这个问题可能涉及以下几方面&#xff1a; &#x1f9ed; 先确认问题 是鼠标问题还是键盘问题&#xff0c;还是触控板&#xff1f; “鼠键”一般理解为“鼠标键”&#xff0c;请确认你是指鼠标左键/右键失灵&a…

智能呼入:云蝠大模型赋能政府热线

政府热线作为连接政府与民众的重要桥梁&#xff0c;提升智能化水平&#xff0c;成为政府热线亟待解决的问题。 大模型呼入 大模型呼入技术基于先进的自然语言处理和机器学习算法&#xff0c;能够实现对海量语音数据的处理和理解。通过构建大规模的语言模型&#xff0c;系统可…

STM32 ADC+DMA+TIM触发采样实战:避坑指南与源码解析

知识点1【TRGO的介绍】 1、TRGO的概述 TRGO&#xff1a;Trigger Output&#xff08;触发输出&#xff09;&#xff0c;是定时器的一种功能。 它可以作为外设的启动信号&#xff0c;比如ADC转换&#xff0c;DAC输出&#xff0c;DMA请求等。 对于ADC来说&#xff0c;可以通过…

(1-4)Java Object类、Final、注解、设计模式、抽象类、接口、内部类

目录 1. Object类 1.1 equals 1.2 toString&#xff08;&#xff09; 2.final关键字 3.注解 4. 设计模式 4.1 单例模式 4.1.1 饿汉式 4.1.3 饿汉式 VS 懒汉式 5. 抽象类&抽象方法 6. 接口 7.内部类 7.1 成员内部类 7.2 静态内部类 7.3 方法内部类 7.4 匿名内…

在服务器上安装AlphaFold2遇到的问题(3)_cat: /usr/include/cudnn_version.h: 没有那个文件或目录

[rootlocalhost ~]# cat /usr/include/cudnn_version.h cat: /usr/include/cudnn_version.h: 没有那个文件或目录这个错误表明系统找不到 cudnn_version.h 头文件&#xff0c;说明 cuDNN 的开发文件&#xff08;头文件&#xff09;没有正确安装。以下是完整的解决方案&#xff…