Sides/1.17

Revision as of 05:51, 6 December 2021 by ShrimpBot (talk | contribs) (Copy Sides to MC1.17 archive)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This page is under construction.

This page is incomplete, and needs more work. Feel free to edit and improve this page!

A very important concept to understand when modding Minecraft are the two sides: client and server. There are many, many common misconceptions and mistakes regarding siding, which can lead to bugs that might not crash the game, but can rather have unintended and often unpredictable effects.

Different Kinds of Sides

When we say "client" or "server", it usually follows with a fairly intuitive understanding of what part of the game we’re talking about. After all, a client is what the user interacts with, and a server is where the user connects for a multiplayer game. Easy, right?

But because of the structure of how Minecraft works, there can be some ambiguity even with two such terms. Here we disambiguate the four possible meanings of "client" and "server":

  • Physical Client - The physical client is the entire program that runs whenever you launch Minecraft from the launcher. All threads, processes, and services that run during the game’s graphical, interactable lifetime are part of the physical client.
  • Physical Server - Often known as the dedicated server, the physical server is the entire program that runs whenever you launch any dedicated server executable or JAR (minecraft_server.jar) that does not bring up a playable GUI.
  • Logical Server - The logical server is what runs game logic: mob spawning, weather, updating inventories, health, AI, and all other game mechanics. The logical server is present within the physical server, but is also can run inside a physical client together with a logical client, as a single player world. The logical server always runs in a thread named the Server Thread.
  • Logical Client - The logical client is what accepts input from the player and relays it to the logical server. In addition, it also receives information from the logical server and makes it available graphically to the player. The logical client runs in the Render Thread, though often several other threads are spawned to handle things like audio and chunk render batching.

In the Minecraft codebase, the physical sides are represented by an enum called Dist, while the logical sides are represented by an enum called LogicalSide.

It is guaranteed that the logical cient always runs on the physical client; however, the same cannot be said of the logical server.

Performing Side-Specific Operations

Level#isClientSide

This boolean check is the most common way (and the most recommended way) to check the currently running logical side. Querying this field on a Level object establishes the logical side that the level belongs to. That is, if this field is true, the level extends ClientLevel and is currently running on the logical client, while if the field is false, the level extends ServerLevel and is running on the logical server.

It follows that the physical/dedicated server will always contain false in this field, but we cannot assume that false implies a physical server, since this field can also be false for the logical server inside a physical client (in other words, a single player world).

Use this check whenever you need to determine if game logic and other mechanics should be run. For example, if you want to damage the player every time they click your block, or have your machine process dirt into diamonds, you should only do so after ensuring level#isClientSide is false. Applying game logic to the logical client can cause desynchronization (ghost entities, desynchronized stats, etc.) in the best case, and crashes in the worst case.

This check should be used as your go-to default. Aside from the sided events and DistExecutor, rarely will you need the other ways of determining sides and adjusting behavior.

Sided Setup Events

There are different events which are fired at different stages during the modloading process. Most of these events are fired on both physical sides, except for the sided setup events: FMLClientSetupEvent and FMLDedicatedServerSetupEvent, which is fired on the physical client and the physical/dedicated server respectively.

These events should be used for running side-specific initialization code. It is recommended to either put your sided event handler registration behind a DistExecutor call, or use the @Mod.EventBusSubscriber anntoation with 'value = Dist.CLIENT for clients (or value = Dist.DEDICATED_SERVER for the dedicated server) to conditionally register your event handlers and prevent the classes referenced within from crashing upon being loaded.

DistExecutor

Considering the use of a single "universal" jar for client and server mods, and the separation of the physical sides into two jars, an important question comes to mind: How do we use code that is only present on one physical side? All code in net.minecraft.client (such as anything rendering-related) is only present on the physical client, and all code in net.minecraft.server.dedicated is only present on the physical server.

If any class you write references those names in any way, they will crash the game when that respective class is loaded in an environment where those names do not exist. For example, a very common mistake in beginners is to call Minecraft.getMinecraft().<doStuff>() in block or block entity classes, which will crash any physical/dedicated server as soon as the class is loaded.

How do we resolve this? Forge provides the DistExecutor utility class, which provides various methods to run and call different code depending on the physical side. There are two versions of each method: safe* and unsafe. safe* methods accept a supplied method reference from another class; otherwise, an error will be thrown. unsafe* methods accept a doubly supplied instance instead. unsafe* methods could cause ClassNotFoundExceptions depending on how they are used, though in standard cases referencing an external class within the double supplier should be safe. In any case, make sure you understand how the class verifier works to load classes before using this.

Important

It is important to understand that DistExecutor checks the physical side. A single player world (logical server + logical client within a physical client) will always use Dist.CLIENT!

Thread Groups

If Thread.currentThread().getThreadGroup() == SidedThreadGroups.SERVER, it is likely the current thread is on the logical server. Otherwise, it is likely on the logical client. This is useful to retrieve the logical side when you do not have access to a Level object to check isClientSide. It guesses which logical side you are on by looking at the thread group of the currently running thread. Because it is a guess, this method should only be used when other options have been exhausted. In all other cases, you should prefer checking level#isClientSide to this check.

FMLEnvironment.dist

FMLEnvironment.dist holds the physical side your code is running on, as a value of Dist.CLIENT or Dist.DEDICATED_SERVER. This is determined by the mod loading code, so it always hold the correct value. However, there is little reason to directly query this variable, as most use-cases can use DistExecutor instead (which uses this value internally).

@OnlyIn

Annotating a method or field with the @OnlyIn(Dist) annotation indicates to the loader that the respective member should be completely stripped out of the definition not on the specified physical side. Usually, these are only seen when browsing through the decompiled Minecraft code, indicating methods that the Mojang obfuscator stripped out.

Important

There is NO reason for using this annotation directly. Use DistExecutor or a check on FMLEnvironment.dist instead.

Common Mistakes

Reaching Across Logical Sides

Whenever you want to send information from one logical side to another, you must always use network packets. It is incredibly tempting, when in a single player scenario, to directly transfer data from the logical server to the logical client.

This is actually very commonly inadvertently done through static fields. Since the logical client and logical server share the same JVM instance in a single player scenario, both threads writing to and reading from static fields will cause all sorts of race conditions and classical issues associated with threading.

This mistake can also be made explicitly by accessing physical client-only classes such as Minecraft from common code that runs or can run on the logical server. This mistake is easy to miss for beginners, who debug in a physical client. The code will work there, but will immediately crash on a physical server. For this reason, it is always recommended to test your mod with the physical/dedicated server.

Writing One-Sided Mods

Your mods are expected to always load, regardless of if they are loaded on the client or the server. For one-sided mods, this means that they must still run on the opposite physical side.

So for one-sided mods, you would typically register your event handlers using DistExecutor or @EventBusSubscriber(value = Dist.*), instead of directly calling the relevant registration methods in the constructor. The idea is that, if your mod is loaded on the wrong side, it should simply do nothing: listen to no events, do no special behaviors, and so on. A one-sided mod by nature should not register blocks, items, … since they would need to be available on the other side, too.

Additionally, if your mod is one-sided, it typically does not forbid the user from joining a server that is lacking that mod, but the server menu will display the server as being incompatible (with a red X at the side). Therefore, you should register an IExtensionPoint$DisplayTest extension point to make sure that Forge does not think your mod is required on the server: (this is usually done in the mod constructor)

// Make sure the mod being absent on the other network side does not cause the client to display the server as incompatible
ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class, () -> new IExtensionPoint.DisplayTest(() -> FMLNetworkConstants.IGNORESERVERONLY, (a, b) -> true));
// Make sure the mod being absent on the other network side does not cause the client to display the server as incompatible
ModLoadingContext.get().registerExtensionPoint(IExtensionPoint.DisplayTest.class) { IExtensionPoint.DisplayTest(Supplier { FMLNetworkConstants.IGNORESERVERONLY }, BiPredicate { _: String, _: Boolean -> true }) }
import scala.compat.java8.FunctionConverters._
// Make sure the mod being absent on the other network side does not cause the client to display the server as incompatible
ModLoadingContext.get.registerExtensionPoint(IExtensionPoint.DisplayTest.class, (() => new IExtensionPoint.DisplayTest((() => FMLNetworkConstants.IGNORESERVERONLY).asJava, asJavaBiPredicate((_: String, _: java.lang.Boolean) => true))).asJava)

This tells the client that it should ignore the server version being absent, and tells the server that it should not tell the client this mod should be present.