From be610295c4767634efeff57c5d53e87abec85ed7 Mon Sep 17 00:00:00 2001 From: Linnea Gräf Date: Mon, 29 Jan 2024 14:08:25 +0100 Subject: Fix up the tutorials a bit --- .gitignore | 1 + docs/commands.md | 64 +++++++++++++++++++++++++++++++++---------- docs/events.md | 52 +++++++++++++++++++++++++---------- docs/mixins/accessors.md | 8 +++--- docs/mixins/adding-fields.md | 20 ++++++++++++-- docs/mixins/index.md | 15 +++++----- docs/mixins/simple-injects.md | 46 +++++++++++++++++++++++-------- docs/screens.md | 39 +++++++++++++------------- mkdocs.yml | 6 ++++ requirements.txt | 2 +- 10 files changed, 179 insertions(+), 74 deletions(-) diff --git a/.gitignore b/.gitignore index 9edfe63..afa2620 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ site/ +.cache/ venv/ diff --git a/docs/commands.md b/docs/commands.md index 4cd38d1..f258cd8 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -11,36 +11,51 @@ public class CrashCommand extends CommandBase { @Override public String getCommandName() { - return "crashme"; + return "crashme"; // (1)! } @Override public String getCommandUsage(ICommandSender sender) { - return ""; + return ""; // (2)! } @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - throw new RuntimeException("Not yet implemented!"); + throw new RuntimeException("Not yet implemented!"); // (3)! } @Override public boolean canCommandSenderUseCommand(ICommandSender sender) { - return true; + return true; // (4)! } + @Override + public List getCommandAliases() { + return Arrays.asList("dontcrashme"); // (5)! + } } ``` -I already implemented three methods into the command. +1. This is the name of your command. You can call your command in chat with `/crashme`. You should only use numbers and letters for this name, since a lot of other characters make it impossible to call your command. +2. This can be left empty. By default this is used by the vanilla `/help` command. But since we are on SkyBlock, where Hypixel uses a custom help menu that does not show client commands, there isn't really any point in filling that one out. +3. We will implement the actual code in the next section +4. This method simply allows anyone to call this command. Since this is a client command, "anyone" just means "the local player". +5. The `getCommandAliases` method allows you to specify additional names that your command can be called by. You can just not implement this method if you want to only use the name returned by `getCommandName`. !!! warning When writing a client command you will need to override `canCommandSenderUseCommand`. By default this method does not generate, but without it you will get a `You do not have permission to use this command` error (since you by default do not have any permissions on a server). Just always return `:::java true`, since the command is client side only anyway. +## Registering your command + +After all this work your command still just will not run. This is because right now you just have a random Java class Forge knows nothing about. You need to register your command. You typically do this in the `FMLInitializationEvent`: -First the method `getCommandName`, which returns the name of your command. The name is what you use in chat to run the command. The command name should be just numbers and letters (and maybe dashes, if you want). In chat you will need to run `/crashme` to run this command (the `/` gets added to the name automatically). -The second method is `getCommandUsage`. You need to override it because every commands needs to have a usage, but for most SkyBlock mods that usage doesn't matter — it only gets displayed in the `/help` menu, and Hypixel does not show client commands in it's help menu. +```java +@Mod.EventHandler +public void init(FMLInitializationEvent event) { + ClientCommandHandler.instance.registerCommand(new CrashCommand()); +} +``` ## Running your command @@ -66,7 +81,7 @@ But, this way of crashing the game might be a bit too easy to accidentally run. ```java @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { - // Make sure to check the array length before checking an argument + // Be sure to check the array length before checking an argument if (args.length == 1 && args[0].equals("confirm")) { LogManager.getLogger("CrashCommand").info("Intentionally crashing the Game!"); FMLCommonHandler.instance().exitJava(1, false); @@ -118,20 +133,39 @@ Minecraft uses `IChatComponent`s in chat (and a few other places). You can make } ``` - See [Events](/development/events) for more info on how to set up event handlers. + See [Events](events.md) for more info on how to set up event handlers. -## Registering your command -After all this work your command still just will not run. This is because the final step of client commands is still missing. You need to register your command. You typically do this in the `FMLInitializationEvent`: +## Tab Completion + +Minecraft allows you to press tab to auto complete arguments for commands. Your command will already be tab completable, but in order for this to also work with the arguments of your command, you need to override `addTabCompletionOptions`: ```java -@Mod.EventHandler -public void init(FMLInitializationEvent event) { - ClientCommandHandler.instance.registerCommand(new CrashCommand()); +@Override +public void processCommand(ICommandSender sender, String[] args) throws CommandException { + if (args.length == 0) { + sender.addChatMessage(new ChatComponentText("§cPlease use an argument")); + } else if (args[0].equals("weather")) { + sender.addChatMessage(new ChatComponentText("§bCurrent Weather: " + + (Minecraft.getMinecraft().theWorld.isRaining() ? "§7Rainy!" : "§eSunny!"))); + } else if (args[0].equals("coinflip")) { + sender.addChatMessage(new ChatComponentText("§bCoinflip: " + + (ThreadLocalRandom.current().nextBoolean() ? "§eHeads" : "§eTails"))); + } else { + sender.addChatMessage(new ChatComponentText("§cUnknown subcommand")); + } } -``` +@Override +public List addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { + if (args.length == 1) // (1)! + return getListOfStringsMatchingLastWord(args, "weather", "coinflip"); // (2)! + return Arrays.asList(); +} +``` +1. The args array contains all the arguments. The last argument is the one you should autocomplete. It contains the partial argument, or an empty string. Make sure to check the length of the array, so you know which argument you are autocompleting. +2. The `getListOfStringsMatchingLastWord` function automatically filters your autocompletion results based on the options you give it. The first argument is the `args` array, the second argument is either a `:::java List` or a vararg of `:::java String`s diff --git a/docs/events.md b/docs/events.md index 152ae62..8dc095b 100644 --- a/docs/events.md +++ b/docs/events.md @@ -4,20 +4,23 @@ Forge uses events to allow mods to communicate with Minecraft and each other. Mo ## Subscribing to events -If you are interested in an event you need to create an event handler. For this first create a method that has the `@SubscribeEvent` annotation, is `public`, return `void` and takes an event as an argument. The type of the event argument is what decides which events your method receives. You can also only have one argument on an event handler. +If you are interested in an event you need to create an event handler. For this first create a method that has the `:::java @SubscribeEvent` annotation, is `:::java public`, return `:::java void` and takes an event as an argument. The type of the event argument is what decides which events your method receives. You can also only have one argument on an event handler. ```java public class MyEventHandlerClass { int chatCount = 0; - @SubscribeEvent - public void onChat(ClientChatReceivedEvent event) { + @SubscribeEvent //(1)! + public void onChat(ClientChatReceivedEvent event) { //(2)! chatCount++; System.out.println("Chats received total: " + chatCount); } } ``` -This on it's own will not do anything yet. You must also register the event handler. To do that you register it on the corresponding event bus. For almost everything you will do, you need the `MinecraftForge.EVENT_BUS` (yes, even your own custom events should use this event bus). +1. This annotation informs Forge that your method is an event handler +2. The method parameter tells Forge which events this event handler listens to + +This on it's own will not do anything yet. You must also register the event handler. To do that you register it on the corresponding event bus. For almost everything you will do, you need the `:::java MinecraftForge.EVENT_BUS` (yes, even your own custom events should use this event bus). The best place to do this is in one of your `FML*InitializationEvent`s. ```java @@ -26,6 +29,7 @@ public class ExampleMod { @Mod.EventHandler public void init(FMLInitializationEvent event) { MinecraftForge.EVENT_BUS.register(new MyEventHandlerClass()); + MinecraftForge.EVENT_BUS.register(this); } } ``` @@ -39,11 +43,26 @@ Forge Events can be cancelled. What exactly that means depends on the event, but public void onChat(ClientChatReceivedEvent event) { // No more talking about cheese if (event.message.getFormattedText().contains("cheese")) - event.setCanceled(true); + event.setCanceled(true); // (1)! } ``` -Not all events can be cancelled. Check the event class in the decompilation for the `@Cancellable` annotation. +1. Cancel the event + +Not all events can be cancelled. Check the event class in the decompilation for the `:::java @Cancellable` annotation. + +If an event is cancelled, it not only changes what Minecraft's code does with the event, but also prevents all other event handlers that come afterwards from handling the event. If you want your event handler to even receive cancelled events, use `receiveCanceled = true`: + + +```java +@SubscribeEvent(receiveCanceled = true) // (1)! +public void onChat(ClientChatReceivedEvent event) { + event.setCanceled(false); // (2)! +} +``` + +1. Make sure our event handler receives cancelled events +2. Uncancel the event. This means the event will be handled by Minecrafts code normally again and you will see the chat. ## Custom Events @@ -51,12 +70,11 @@ Not all events can be cancelled. Check the event class in the decompilation for !!! note This is an advanced topic that most mod developers don't need to worry about. -Forge also allows you to create custom events. Each event needs to have it's own class extending `Event` (transitively or not). (Make sure you extend `net.minecraftforge.fml.common.eventhandler.Event`). +Forge also allows you to create custom events. Each event needs to have it's own class extending `:::java Event` (transitively or not). (Make sure you extend `:::java net.minecraftforge.fml.common.eventhandler.Event`, not any other event class). ```java -// If you don't want your event to be cancellable, remove this annotation -@Cancelable -public class CheeseEvent extends Event { +@Cancelable // (1)! +public class CheeseEvent extends Event { // (2)! public final int totalCheeseCount; public CheeseEvent(int totalCheeseCount) { @@ -65,9 +83,12 @@ public class CheeseEvent extends Event { } ``` +1. If you want your event to be cancellable, you need this annotation. Remove it for an uncancellable event. +2. Extend the Forge `:::java Event` class. The rest of your class is just normal Java. + That's it, you are done. You have a custom event! -I'm kidding of course. The next step is actually using your event. For now, let's put our own custom event inside the forge chat event: +I'm kidding of course. The next step is actually using your event. For now, let's put our own custom event inside the forge chat event (you will later learn how to use [mixins](./mixins/index.md) to create even more events): ```java int cheeseCount = 0; @@ -75,13 +96,16 @@ int cheeseCount = 0; @SubscribeEvent public void onChat(ClientChatReceivedEvent event) { if (event.message.getFormattedText().contains("cheese")) { - CheeseEvent cheeseEvent = new CheeseEvent(++cheeseCount); - MinecraftForge.EVENT_BUS.post(cheeseEvent); + CheeseEvent cheeseEvent = new CheeseEvent(++cheeseCount); // (1)! + MinecraftForge.EVENT_BUS.post(cheeseEvent); // (2)! } } ``` -And now we are done. Unless you want your event to be cancellable. We also need to add code to handle cancelled events (if you made your event `@Cancelable`). What that cancelling does is up to you, but in our example let's just cancel the original chat message event (hiding that chat message): +1. Creates a new `CheeseEvent` instance. This is just a normal java object construction, which does not interact with Forge at all. +2. Send our `CheeseEvent` to be sent to all event handlers by Forge. + +And now we are done, unless you want your event to be cancellable. For cancellable events we also need to add code to handle cancelled events. What that cancelling does is up to you, but in our example let's just cancel the original chat message event (hiding that chat message): ```java @SubscribeEvent diff --git a/docs/mixins/accessors.md b/docs/mixins/accessors.md index f9ad7b8..60a174d 100644 --- a/docs/mixins/accessors.md +++ b/docs/mixins/accessors.md @@ -24,13 +24,13 @@ public interface AccessorMinecraft { } ``` -First, notice that we need to use an `interface`. Most mixins are `class`es. Accessors are the exception, since we don't want to actually put any code into the `Minecraft` class. Accessor mixins can also not be mixed with other mixin styles, but since you should have multiple mixins even for the same class for different things anyway, this shouldn't be an issue. +First, notice that we need to use an `:::java interface`. Most mixins are `:::java class`es. Accessors are the exception, since we don't want to actually put any code into the `:::java Minecraft` class. Accessor mixins can also not be mixed with other mixin styles, but since you should have multiple mixins even for the same class for different things anyway, this shouldn't be an issue. -Next we put the `@Mixin` annotation on our Accessor to let it known which class we want to inject into. +Next we put the `:::java @Mixin` annotation on our Accessor to let it known which class we want to inject into. -Then for a field we use the `@Accessor` annotation, with the name of the field we want to access. Please give all your mixin methods a `_mymodid` indicator, to avoid name collissions with other mods. +Then for a field we use the `:::java @Accessor` annotation, with the name of the field we want to access. Please give all your mixin methods a `_mymodid` indicator, to avoid name collissions with other mods. -For a setter, the method returns void and takes one argument of the type of the field you are targetting. For a getter, the method returns the type of the field you are targeting and takes no arguments. +For a setter, the method returns `:::java void` and takes one argument of the type of the field you are targetting. For a getter, the method returns the type of the field you are targeting and takes no arguments. For an method invoker, you copy the method signature you want to call, but rename it to something unique with a `_mymodid` postfix. diff --git a/docs/mixins/adding-fields.md b/docs/mixins/adding-fields.md index c3533e9..ba54299 100644 --- a/docs/mixins/adding-fields.md +++ b/docs/mixins/adding-fields.md @@ -5,7 +5,7 @@ The next step up is injecting fields and methods into a class. This allows you t ```java @Mixin(EntityArmorStand.class) public class InjectCustomField { - Color colorOverride_mymodid; + Color colorOverride_mymodid = Color.RED; public void setColorOverride_mymodid(Color color) { colorOverride_mymodid = color; @@ -17,13 +17,13 @@ public class InjectCustomField { } ``` -This mixin is a `class`, like all mixin (except for accessors) are. You can make the class abstract if you want. +This mixin is a `:::java class`, like all mixin (except for accessors) are. You can make the class abstract if you want. First we add a new field (of course with modid postfix) into every armor stand. Then we also add a getter and a setter method for that field. -Right now we run into a problem. We can't access mixin classes directly, so we cannot simply cast the `EntityArmorStand` into a `InjectCustomField`. Instead we create an interface (inside of our regular code, not inside of the mixin package) and implement that interface in our mixin class. You can also implement other interfaces this way, not just your own. +Right now we run into a problem. We can't access mixin `:::java class`es directly, so we cannot simply cast the `EntityArmorStand` into a `InjectCustomField`. Instead we create an interface (inside of our regular code, not inside of the mixin package) and implement that interface in our mixin class. You can also implement other interfaces this way, not just your own. ```java // Inside our regular code. Not in the mixin package @@ -48,3 +48,17 @@ public class InjectCustomField implement ColorFieldAccessor { } } ``` + +Now we can just cast any instance of `EntityArmorStand` to `ColorFieldAccessor`: + +```java +public static Color getColorOverrideForArmorStand(EntityArmorStand armorStand) { + return ((ColorFieldAccessor) armorStand).getColorOverride_mymodid(); +} +``` + + + + + + diff --git a/docs/mixins/index.md b/docs/mixins/index.md index bf4aa67..400d0a8 100644 --- a/docs/mixins/index.md +++ b/docs/mixins/index.md @@ -1,10 +1,11 @@ # Mixins -Mixins allow you to change Minecraft code. This is massively powerful, but you need to be very careful when using them, especially when considering to work together with other mods. +Mixins allow you to change Minecraft code. This is massively powerful, but you need to be very careful when using them, especially when considering if you want to integrate well with other mods. !!! info The [MinecraftDev](https://mcdev.io/) plugin is pretty much non negotiable when coding Mixins. It enables auto completion, shows errors when your mixins are wrong in your IDE and allows you to directly navigate to the code you are changing. - It also has some other functions that allow for easier Minecraft development, but most of the functionality is aimed at higher versions. + + It also has some other functions that allow for easier Minecraft development, but most of that functionality is aimed at higher Minecraft versions. > Please forgive the the nonsensical examples. I try to make the examples as simple as possible. [Check](https://github.com/NotEnoughUpdates/NotEnoughUpdates/tree/master/src/main/java/io/github/moulberry/notenoughupdates/mixins) [out](https://github.com/hannibal002/SkyHanni/tree/beta/src/main/java/at/hannibal2/skyhanni/mixins/transformers) [some](https://github.com/Skytils/SkytilsMod/tree/1.x/src/main/java/gg/skytils/skytilsmod/mixins/transformers) [open](https://github.com/inglettronald/DulkirMod/tree/master/src/main/java/dulkirmod/mixins) source mods to check out some real world mixins. @@ -47,7 +48,7 @@ public void someMixinMethod_mymodid() {} public void someMixinMethod$mymodid() {} ``` -There are some exceptions for `@Inject`s, but in general it doesn't hurt to just add the postfix. +There are some exceptions for `:::java @Inject`s, but in general it doesn't hurt to just add the postfix. ### Non destructive mixins @@ -57,11 +58,11 @@ I.e. if you want a mixin to color mobs, and your mod decides not to color a mob, There are some general ground rules for achieving this behaviour: - - Only use `cir.setReturnValue()` or `ci.cancel()` if your mod decides to act on something. The default action should be to pass through to the next mixin or vanilla by doing nothing. - - Don't use `@Redirect`. Only one mixin can ever use a `@Redirect` on the same call. Only one redirect will ever work, even if your mod does nothing different with a given method call. - - Don't use `@Overwrite` (and don't overwrite without the annotation either, lol). Only one overwrite will ever work, even if your mod does nothing different with a given method call. + - Only use `:::java cir.setReturnValue()` or `:::java ci.cancel()` if your mod decides to act on something. The default action should be to pass through to the next mixin or vanilla by doing nothing. + - Don't use `:::java @Redirect`. Only one mixin can ever use a `:::java @Redirect` on the same call. Only one redirect will ever work, even if your mod does nothing different with a given method call. + - Don't use `:::java @Overwrite` (and don't overwrite without the annotation either, lol). Only one overwrite will ever work, even if your mod does nothing different with a given method call. -Of course you will have to break those rules from time to time. But before you do, think twice if you need to. And if you do, maybe consider exposing some sort of API for other mods to hook into your code? +Of course you will have to break those rules from time to time. But before you do, think twice if you *really* need to. And if you do, maybe consider exposing some sort of API for other mods to hook into your code? ## Troubleshooting diff --git a/docs/mixins/simple-injects.md b/docs/mixins/simple-injects.md index eeea65e..b635973 100644 --- a/docs/mixins/simple-injects.md +++ b/docs/mixins/simple-injects.md @@ -9,26 +9,41 @@ Now we will modify an existing method in Minecrafts code. This will allow us to ## The easiest of the easiest -Let's start with probably the easiest `@Inject` out there. The HEAD inject. This mixin will inject whatever code you have inside your method at the start of the method you target. +Let's start with probably the easiest `@Inject` out there. The `HEAD` inject. This mixin will inject whatever code you have inside your method at the start of the method you target. ```java -@Mixin(PlayerControllerMP.class) +@Mixin(PlayerControllerMP.class) // (1)! public class RightClickWithItemEvent { - @Inject(method = "sendUseItem", at = @At("HEAD")) - private void onSendUseItem_mymod(EntityPlayer playerIn, World worldIn, ItemStack itemStackIn, CallbackInfoReturnable cir) { + @Inject( // (2)! + method = "sendUseItem", // (3)! + at = @At("HEAD")) // (4)! + private void onSendUseItem_mymod( // (5)! + EntityPlayer playerIn, World worldIn, ItemStack itemStackIn, // (6)! + CallbackInfoReturnable cir // (7)! + ) { MinecraftForge.EVENT_BUS.post(new SendUseItemEvent(playerIn, worldIn, itemStackIn)); } } ``` +1. First we declare which class we want to change +2. `:::java @Inject` allows us to add code into an already existing method +3. This sets the method into which we want to inject something. Be careful of overloaded methods here. Check out the [advanced tutorial](./advanced-injects.md) for more info. +4. The `:::java @At` specifies where our code will be injected. `HEAD` just means the top of the method. +5. The injected code method should be `:::java private` and `:::java void` no matter what your target method is. You might also need to make your method `:::java static` +6. You need to copy over all the parameters from your original method into which you are injecting. +7. You need one extra parameter for the callback info. + + + First we want to inject into the `PlayerControllerMP` class. We create an `@Inject`. This tells us in which method we want to inject (`sendUseItem`) and where in that method (`HEAD`, meaning the very top of the method). The actual method signature for an inject is always to return a `void`. You can make them `private` or `public`. The arguments are the same arguments as the method you want to inject into, as well as a `CallbackInfo`. -For a method returning void, you just use a `CallbackInfo`, and if the method returns something, you use `CallbackInfoReturnable`. +For a method returning void, you just use a `:::java CallbackInfo`, and if the method returns something, you use `:::java CallbackInfoReturnable`. Your method will now be called every time the `sendUseItem` is called with the arguments to that method and the `CallbackInfo`. @@ -50,7 +65,8 @@ Let's take this example method: ```java public void methodA() { - // ... + // Let's pretend lots of code calls methodA, so we don't want to inject + // ourselves into methodA } public void methodB() { @@ -64,18 +80,26 @@ public void methodB() { We can inject ourselves into `methodB` as well. It is *just* a bit more complicated than the `HEAD` inject. ```java -@Inject(method = "methodB", at = @At(target = "Lnet/some/Class;methodA()V", value = "INVOKE")) +@Inject( + method = "methodB", // (3)! + at = @At( + target = "Lnet/some/Class;methodA()V", // (1)! + value = "INVOKE")) // (2)! private void onMethodBJustCalledMethodA(CallbackInfo ci) { } ``` +1. This is the method call for which we are searching. This is not the method into which our code will be injected. +2. This tells mixin that we want `target` to point to a method call (not a field or anything else). +3. This is the method into which we want our code to be injected. + > **HUUUUH, where does that come from???** -Don't worry! I won't explain you how to understand these `target`s in this tutorial, but you also don't need to understand that `target`. Instead you can simply use the Minecraft Development IntelliJ Plugin to help you. Simply type `@At(value = "INVOKE", target = "")`, place your cursor inside of the target and use auto completion (Ctrl + Space) and the plugin will recommend you a bunch of method calls. Find whichever seems right to you and press enter. You can now (also thanks to the plugin) Ctrl click on the `target` string, which will take you to the decompiled code exactly to where that target will inject. +Don't worry! I won't explain you how to understand these `target`s in this tutorial, but you also don't need to understand that `target`. Instead you can simply use the Minecraft Development IntelliJ Plugin to help you. Simply type `:::java @At(value = "INVOKE", target = "")`, place your cursor inside of the target and use auto completion (++ctrl+space++) and the plugin will recommend you a bunch of method calls. Find whichever seems right to you and press enter. You can now (also thanks to the plugin) ++ctrl++ click on the `target` string, which will take you to the decompiled code exactly to where that target will inject. ## Ordinals -Let's take the method injection example from before and change it a bit: +Let's take the `INVOKE` injection example from before and change it a bit: ```java public void methodA() { @@ -93,7 +117,7 @@ public void methodB() { } ``` -We can't simply use the same `@Inject` from before, since by default a `INVOKE` inject will inject just after *every* method call. Here, we can use the `ordinal` classifier to specify which method call we want to use. Keep in mind this is about where to place our injection, so many method calls in a loop will not increment the ordinal, only unique code locations that call the function will increase the ordinal. Keep in mind: we are programmers, we start counting with `0`. +We can't simply use the same `:::java @Inject` from before, since by default a `INVOKE` inject will inject just after *every* method call. Here, we can use the `ordinal` classifier to specify which method call we want to use. Keep in mind this is about where to place our injection, so many method calls in a loop will not increment the ordinal, only unique code locations that call the function will increase the ordinal. Remember: we are programmers, we start counting with `0`. ```java @Inject(method = "methodB", at = @At(target = "Lnet/some/Class;methodA()V", value = "INVOKE", ordinal = 1)) @@ -121,7 +145,7 @@ private void onIsHittingPosition_mymod(BlockPos pos, CallbackInfoReturnableF7 would not have a `typedChar`, but would have the `keyCode == Keyboard.KEY_F7`. We also call `super.keyTyped()` here so that the standard Escape to close works. +`keyTyped` is called whenever a key is pressed. You can check `typedChar` to see the character they typed, or you can check `keyCode` if you want to know the key they pressed. For example a key like ++f7++ would not have a `typedChar`, but would have the `keyCode == Keyboard.KEY_F7`. We also call `super.keyTyped()` here so that the standard ++esc++ to close works. -`mouseClicked` is called whenever the mouse is clicked in your screen. The `mouseButton` sadly doesn't have a vanilla class with namese for them like `Keyboard`, but you can use these constants instead: +`mouseClicked` is called whenever the mouse is clicked in your screen. The `mouseButton` sadly doesn't have a vanilla class with names for them like `Keyboard`, but you can use these constants instead: ```java public static final int MOUSE_LEFT = 0; @@ -135,7 +135,7 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { Images in Minecraft are rendered from the assets folder. In your project, you should have a folder called `src/main/resources`. In that folder you create two more folders called `assets//`. That is your asset root. In here you can put any file you want and load it into Minecraft. You probably want your textures to be in a folder like `textures/gui` *inside* your asset root however (`src/main/resources/assets//textures/gui/mytexture.png`). -For images you probably want to use png files. +For images you probably want to use png files, since Minecraft supports these out of the box, unlike most other image formats. For this tutorial you can use [this background](img/background.png). @@ -153,14 +153,10 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { // Render from your texture. drawModalRectWithCustomSizedTexture( - // The first two arguments are the position on the screen - width / 2 - 100, height / 2 - 20, - // The next arguments are the starting u and v - 0, 0, - // The next arguments are the size on the screen - 200, 40, - // The last two arguments are the size of the texture - 200, 40 + width / 2 - 100, height / 2 - 20, // (1)! + 0, 0, // (2)! + 200, 40, // (3)! + 200, 40 // (4)! ); FontRenderer fr = minecraft.fontRendererObj; @@ -178,6 +174,11 @@ public void drawScreen(int mouseX, int mouseY, float partialTicks) { } ``` +1. This is the top left position of where your texture should be rendered. +2. This is the starting u and v of your texture. If you don't have any custom [UVs](#uvs), you can just use `(0, 0)` +3. This is the size on the screen of what you want to render. If you don't use custom UVs, this needs to match your texture size. If you want to scale your texture, check out [Matrix Transformations](#transformations) or `drawScaledCustomSizeModalRect` (which is a bit more complicated). +4. This is the size of your texture, you need to hardcode this here, since texture packs can upload lower and higher resolution versions of your texture, and Minecraft needs to figure out how to scale the texture. + #### UVs Texture rendering in OpenGL (which is what Minecraft uses) uses UVs for reading from a texture. Instead of always rendering an entire texture, you can render only parts of a texture. This allows you to reuse a texture, without rebinding it, which can be beneficial for performance. For now I wouldn't worry about optimizing like that too much, but you'll still need to use UVs anyway. @@ -204,13 +205,13 @@ float u = 16 / 64; // start at x pixel coordinate 16 with a 64 wide image #### Translations -Translations move things around. `translate(10, 0, 0)` would cause all future render calls to instead render 10 pixels to the right. You may notice that `translate` takes 3 arguments. This is because the `z` direction is also translated. This is useful in 3D, but can be used in 2D GUI rendering as well to move things in front of other things. In a GUI a greater z value means that something renders in front of something that has a lower z. +Translations move things around. `:::java translate(10, 0, 0)` would cause all future render calls to instead render 10 pixels to the right. You may notice that `translate` takes 3 arguments. This is because the `z` direction is also translated. This is useful in 3D, but can be used in 2D GUI rendering as well to move things in front of other things. In a GUI a greater z value means that something renders in front of something that has a lower z. You can use this to render a tooltip for example. By default later rendering calls in your method would render on top of the tooltip, but if you first translate to a high z value and then back to normal after your tooltip rendering, the other method calls won't render on top of the tooltip. #### Scaling -Scalings, well, *scale* things. This means a `scale(2, 2, 1)` call would render everything after twice as big. But you need to be careful. Everything is twice as big, including the coordinates at which you render. +Scalings, well, *scale* things. This means a `:::java scale(2, 2, 1)` call would render everything after twice as big. But you need to be careful. Everything is twice as big, including the coordinates at which you render. ```java GlStateManager.scale(2, 2, 1); @@ -218,7 +219,7 @@ GlStateManager.scale(2, 2, 1); fontRenderer.drawString("Hello, World!", 10, 10, -1); ``` -This would normally render at `(10, 10)`. But since everything is scaled two times, it actually renders at `(20, 20)` and twice as big. To circumvent that you can instead first translate, then scale, and then render at `(0, 0)`. +This would normally render at `:::java (10, 10)`. But since everything is scaled two times, it actually renders at `:::java (20, 20)` and twice as big. To circumvent that you can instead first translate, then scale, and then render at `:::java (0, 0)`. ```java GlStateManager.translate(10, 10, 0); @@ -230,7 +231,7 @@ fontRenderer.drawString("Hello, World!", 0, 0, -1); Alternatively you can do the math and divide all coordinates by your current scale factor. !!! warning - Please always use `1` as a scale factor for the z direction. If you do not do this, a lot of rendering calls will break and parts of your GUI might just not render at all or in the wrong order. You can use other non-zero scale factors for z like `2` sometimes, but in almost all cases that is the **wrong behaviour** and you should instead use the scale factor `1`. + Please always use `:::java 1` as a scale factor for the z direction. If you do not do this, a lot of rendering calls will break and parts of your GUI might just not render at all or in the wrong order. You can use other non-zero scale factors for z like `:::java 2` sometimes, but in almost all cases that is the **wrong behaviour** and you should instead use the scale factor `:::java 1`. #### Stack Manipulation @@ -271,21 +272,21 @@ In addition to transformations you can also change "attributes" about the render #### Stack Manipulation -Like the matrix stack for transformations there is also an "attribute stack" for attributes. That one is severly broken in Minecraft, however. There is a bug in `GlStateManager` that means that if you set an attribute using `GlStateManager` inside of a `pushAttrib`-`popAttrib` block, you sometimes cannot set that attribute again. This means when inside of such a block you cannot call Minecrafts wrappers in `GlStateManager` and you need to instead call OpenGL directly. I recommend against using this however, since vanilla code you call might still use `GlStateManager`, therefore breaking attributes until the next frame. Use these two methods very carefully if at all. +Like the matrix stack for transformations there is also an "attribute stack" for attributes. That one is severly broken in Minecraft, however. There is a bug in `GlStateManager` that means that if you set an attribute using `GlStateManager` inside of a `pushAttrib`-`popAttrib` block, you sometimes cannot set that attribute again. This means when inside of such a block you cannot call Minecrafts wrappers in `GlStateManager` and you need to instead call OpenGL directly. I recommend against using this however, since vanilla code you call might still use `GlStateManager`, therefore breaking attributes until the next frame. Use these two methods very carefully, if at all. #### Color -`GlStateManager.color` is probably the easiest example of an attribute. You can use it to tint all your future render calls, including textures and text. You probably already set a color using `fr.drawString`, since that has a color argument, which in turn just calls `GlStateManager.color` after several layers of abstractions. If you want everything back to normal, just set the color to `color(1, 1, 1, 1)`. +`:::java GlStateManager.color` is probably the easiest example of an attribute. You can use it to tint all your future render calls, including textures and text. You probably already set a color using `:::java fr.drawString`, since that has a color argument, which in turn just calls `:::java GlStateManager.color` after several layers of abstractions. If you want everything back to normal, just set the color to `:::java color(1, 1, 1, 1)`. #### Depth `enableDepth` and `disableDepth` turn on and off depth testing. Meaning that the z value ignored and things with a high z value might render behind things with a low z value. Only the render order matters now, instead of the z value. -On top of that you can use `depthFunc` along with `GL11.GL_LESS`, `GL11.GL_LEQUAL`, `GL11.GL_GREATER`, `GL_ALWAYS` (which is equivalent to `disableDepth`) etc, to decide in which direction the depth test functions. +On top of that you can use `depthFunc` along with `:::java GL11.GL_LESS`, `:::java GL11.GL_LEQUAL`, `:::java GL11.GL_GREATER`, `:::java GL_ALWAYS` (which is equivalent to `disableDepth`) etc, to decide in which direction the depth test functions. #### Blending -Blending specifies how transparent images render on top of each other. Again you can `enableBlend` and `disableBlend`, as well as specify the function to use to blend two images. Again, you can use values from `GL11` like `GL11.GL_ONE_MINUS_SRC_ALPHA`, `GL11.GL_ONE_MINUS_DST_ALPHA`, `GL11.GL_SRC_ALPHA` and so on with `blendFunc` or `tryBlendFuncSeparate`. +Blending specifies how transparent images render on top of each other. Again you can `enableBlend` and `disableBlend`, as well as specify the function to use to blend two images. Again, you can use values from `:::java GL11` like `:::java GL11.GL_ONE_MINUS_SRC_ALPHA`, `:::java GL11.GL_ONE_MINUS_DST_ALPHA`, `:::java GL11.GL_SRC_ALPHA` and so on with `blendFunc` or `tryBlendFuncSeparate`. #### And many more diff --git a/mkdocs.yml b/mkdocs.yml index 036c187..05a35a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,12 +39,15 @@ theme: - navigation.tracking - navigation.indexes - content.action.edit + - search.suggest + - search.share icon: repo: fontawesome/brands/git-alt palette: # Palette toggle for light mode - media: "(prefers-color-scheme: light)" scheme: default + primary: lime toggle: icon: material/weather-sunny name: Switch to dark mode @@ -52,12 +55,15 @@ theme: # Palette toggle for dark mode - media: "(prefers-color-scheme: dark)" scheme: slate + primary: light blue toggle: icon: material/weather-night name: Switch to light mode plugins: + - search + - social - git-revision-date-localized: enable_creation_date: true fallback_to_build_date: true diff --git a/requirements.txt b/requirements.txt index b712b13..2691b7c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -mkdocs-material +mkdocs-material[imaging] mkdocs-git-revision-date-localized-plugin mkdocs-git-committers-plugin-2 -- cgit