2026年6月5日

技术教程 如何在运行时获取当前MinecraftServer实例的引用

作者 TheWhiteDog9487

观前提示:本篇文章的演示版本为26.1,从这个正式版开始Mojang完全移除了源码混淆,因此等价于使用Mojang Mapping反混淆表。

众所周知,Mod开发过程中时常需要获取Minercraft(Yarn反混淆表叫MinecraftClient)和MinecraftServer的当前实例。
用途挺多的。比如你可以直接通过服务器实例的getPlayerList()函数拿到当前的玩家列表。

这俩中的客户端实例是很好获取的,在client源集内直接使用Minecraft.getInstance()指令就可以,比如下面这样:

package com.example.client;

import net.fabricmc.api.ClientModInitializer;
import net.minecraft.client.Minecraft;

public class TemplateModClient implements ClientModInitializer {
	@Override
	public void onInitializeClient() {
		// This entrypoint is suitable for setting up client-specific logic, such as rendering.
		Minecraft Client = Minecraft.getInstance();
	}
}

但是服务器这边就没这么容易了,根本没有getInstance()这样的函数。
虽然很多API的函数式接口里会为你提供当前服务器实例,但是想要随时随地使用还是有些难度的。
因此,更好的方法是在服务器初始化完成正常启动之后保存一份引用,而Fabric API的ServerLifecycleEvents事件类就可以注册在服务器启动之后触发的事件。
所以我们可以像下面这样:

package com.example;

import net.fabricmc.api.ModInitializer;

import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TemplateMod implements ModInitializer {
	public static final String MOD_ID = "template-mod";

	// This logger is used to write text to the console and the log file.
	// It is considered best practice to use your mod id as the logger's name.
	// That way, it's clear which mod wrote info, warnings, and errors.
	public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);
	
	public static MinecraftServer ServerInstance;
	
	@Override
	public void onInitialize() {
		// This code runs as soon as Minecraft is in a mod-load-ready state.
		// However, some things (like resources) may still be uninitialized.
		// Proceed with mild caution.

		LOGGER.info("Hello Fabric world!");
		ServerLifecycleEvents.SERVER_STARTED.register(new ServerLifecycleEvents.ServerStarted() {
			@Override
			public void onServerStarted(MinecraftServer server) {
				ServerInstance = server;
			}
		} );
	}
}

之后需要使用服务器实例的时候直接用ServerInstance即可。

等等,还没完。
如果是客户端单人游戏,是可能出现客户端未关闭但是服务器已经关闭失效的情况的,只需要保存世界退出到游戏主界面即可。故此,在出现这种情况之后继续使用旧的服务器实例显然是不合适的,不仅不能用而且因为旧的实例仍然可以通过变量访问而避免垃圾回收从而造成内存泄漏。
所以,这个变量一定会是可空类型,在服务器失效之后置空,像下面这样:

package com.example;

import net.fabricmc.api.ModInitializer;

import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TemplateMod implements ModInitializer {
	public static final String MOD_ID = "template-mod";

	// This logger is used to write text to the console and the log file.
	// It is considered best practice to use your mod id as the logger's name.
	// That way, it's clear which mod wrote info, warnings, and errors.
	public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);

	public static MinecraftServer ServerInstance;

	@Override
	public void onInitialize() {
		// This code runs as soon as Minecraft is in a mod-load-ready state.
		// However, some things (like resources) may still be uninitialized.
		// Proceed with mild caution.

		LOGGER.info("Hello Fabric world!");
		ServerLifecycleEvents.SERVER_STARTED.register(new ServerLifecycleEvents.ServerStarted() {
			@Override
			public void onServerStarted(MinecraftServer server) {
				ServerInstance = server;
			}
		} );
		ServerLifecycleEvents.SERVER_STOPPED.register(new ServerLifecycleEvents.ServerStopped() {
			@Override
			public void onServerStopped(MinecraftServer server) {
				ServerInstance = null;
			}
		} );
	}
}

如果你使用的是Kotlin,情况会略微复杂一点点。
因为Kotlin具有空安全特性,可空类型的调用必须使用空安全调用或者非空断言,比如像下面这样:

package com.example

import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.minecraft.server.MinecraftServer
import org.slf4j.LoggerFactory

object TemplateMod : ModInitializer {
    private val logger = LoggerFactory.getLogger("template-mod")
	
	var ServerInstance: MinecraftServer? = null
	
	override fun onInitialize() {
		// This code runs as soon as Minecraft is in a mod-load-ready state.
		// However, some things (like resources) may still be uninitialized.
		// Proceed with mild caution.
		logger.info("Hello Fabric world!")

		ServerLifecycleEvents.SERVER_STARTED.register{server ->
			ServerInstance = server }
		ServerLifecycleEvents.SERVER_STOPPED.register{server ->
			ServerInstance = null }
        
		ServerInstance?.playerList
//		可空类型需要使用空安全调用运算符?.
		ServerInstance!!.port
//		或者像这样,使用非空断言运算符!!
	}
}

这样使用起来实际上是很不方便的,而且空安全调用有可能会导致本该被抛出的空指针异常被静默处理延后到后面导致其他问题增加调试难度,所以我们可以使用自定义get指令配合空合并运算符,比如像这样:

package com.example

import net.fabricmc.api.ModInitializer
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents
import net.minecraft.server.MinecraftServer
import net.minecraft.world.level.Level.OVERWORLD
import org.slf4j.LoggerFactory

object TemplateMod : ModInitializer {
    private val logger = LoggerFactory.getLogger("template-mod")

	internal var ServerInstanceBackendField: MinecraftServer? = null
	public val ServerInstance: MinecraftServer get() {
		return ServerInstanceBackendField ?: throw NullPointerException("服务器实例当前不可用,结果为null") }

	override fun onInitialize() {
		// This code runs as soon as Minecraft is in a mod-load-ready state.
		// However, some things (like resources) may still be uninitialized.
		// Proceed with mild caution.
		logger.info("Hello Fabric world!")

		ServerLifecycleEvents.SERVER_STARTED.register{server ->
			ServerInstanceBackendField = server }
		ServerLifecycleEvents.SERVER_STOPPED.register{server ->
			ServerInstanceBackendField = null }
        
		ServerInstance.getLevel(OVERWORLD)
	}
}

这样就不需要处理空安全,如果空指针爆炸了也能通过堆栈跟踪看到哪里出了问题
只不过多个后备字段会变丑,这个我也没办法,这已经是我目前已知最好的解决方案了。