├── .github └── workflows │ ├── deploy.yml │ └── manual-publish.sh ├── .gitignore ├── README.md ├── docs ├── commands.md ├── events.md ├── hotswap.md ├── https.md ├── ide-setup.md ├── img │ ├── background-screen.png │ ├── background.png │ ├── color-selector-screen.png │ ├── text-alignment.png │ └── uvs.jpg ├── index.md ├── inventories.md ├── mixins │ ├── accessors.md │ ├── adding-fields.md │ ├── advanced-injects.md │ ├── index.md │ └── simple-injects.md ├── screens.md ├── tweakers.md └── vanilla.md ├── includes └── shared_links.md ├── mkdocs.yml ├── requirements.txt ├── serve.sh └── shell.nix /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: 3 | - push 4 | 5 | 6 | permissions: 7 | contents: write 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | with: 14 | fetch-depth: 0 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: 3.x 18 | - name: Get pip cache dir 19 | id: pip-cache 20 | run: echo "dir=$(pip cache dir)" >> $GITHUB_OUTPUT 21 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 22 | - name: Cache dependencies 23 | uses: actions/cache@v3 24 | with: 25 | path: ${{ steps.pip-cache.outputs.dir }} 26 | key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}${{ hashFiles('**/mkdocs.yml') }} 27 | restore-keys: | 28 | ${{ runner.os }}-pip- 29 | - run: pip install -r requirements.txt 30 | - run: mkdocs build 31 | - uses: peaceiris/actions-gh-pages@v3 32 | if: github.ref == 'refs/heads/master' 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | cname: moddev.nea.moe 36 | publish_dir: ./site 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish.sh: -------------------------------------------------------------------------------- 1 | export SSH_USER="site-deploy" 2 | export SSH_HOST="nea.moe" 3 | export GITHUB_SHA="$(git rev-parse HEAD)-$(openssl rand -hex 10)" 4 | 5 | venv/bin/mkdocs build 6 | scp -r site "$SSH_USER"@"$SSH_HOST":~/new-site-content-$GITHUB_SHA 7 | ssh "$SSH_USER"@"$SSH_HOST" < getCommandAliases() { 34 | return Arrays.asList("dontcrashme"); // (5)! 35 | } 36 | } 37 | ``` 38 | 39 | 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. 40 | 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. 41 | 3. We will implement the actual code in the next section 42 | 4. This method simply allows anyone to call this command. Since this is a client command, "anyone" just means "the local player". 43 | 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`. 44 | 45 | !!! warning 46 | 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. 47 | 48 | ## Registering your command 49 | 50 | 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`: 51 | 52 | 53 | ```java 54 | @Mod.EventHandler 55 | public void init(FMLInitializationEvent event) { 56 | ClientCommandHandler.instance.registerCommand(new CrashCommand()); 57 | } 58 | ``` 59 | 60 | ## Running your command 61 | 62 | The `processCommand` method is run when your command is executed: 63 | 64 | 65 | ```java 66 | @Override 67 | public void processCommand(ICommandSender sender, String[] args) throws CommandException { 68 | LogManager.getLogger("CrashCommand").info("Intentionally crashing the Game!"); 69 | FMLCommonHandler.instance().exitJava(1, false); 70 | } 71 | ``` 72 | 73 | !!! info 74 | When using a Logger, make sure to use the `LogManager` from `org.apache.logging.log4j.LogManager`. Using the other log managers won't work. 75 | 76 | !!! info 77 | If you want to close the game, you need to use `:::java FMLCommonHandler.instance().exitJava(exitCode, false)` instead of `:::java System.exit()`. Forge disables the normal `:::java System.exit()` calls. 78 | 79 | But, this way of crashing the game might be a bit too easy to accidentally run. So let's add a confirmation system. When your `processCommand` is called, you are given two arguments: the `sender` is always the current player (since this is a client command), and the `args` array gives you all the arguments you are being called with. If a player runs the command `/crashme foo bar`, args will be `:::java new String[] {"foo", "bar"}`. 80 | 81 | ```java 82 | @Override 83 | public void processCommand(ICommandSender sender, String[] args) throws CommandException { 84 | // Be sure to check the array length before checking an argument 85 | if (args.length == 1 && args[0].equals("confirm")) { 86 | LogManager.getLogger("CrashCommand").info("Intentionally crashing the Game!"); 87 | FMLCommonHandler.instance().exitJava(1, false); 88 | } else { 89 | sender.addChatMessage(new ChatComponentText("§aAre you sure you want to crash the game? Click to confirm!") 90 | .setChatStyle(new ChatStyle() 91 | .setChatClickEvent(new ClickEvent(ClickEvent.Action.RUN_COMMAND, "/crashme confirm")))); 92 | } 93 | } 94 | ``` 95 | 96 | !!! info 97 | Because `sender` is always the current player, you can also use 98 | ```java 99 | Minecraft.getMinecraft().thePlayer.addChatMessage(/* ... */); 100 | ``` 101 | 102 | Minecraft uses `IChatComponent`s in chat (and a few other places). You can make those by calling `:::java new ChatComponentText("")`. In there you can use format codes like `§a`. If you want, you can also use `:::java EnumChatFormatting.GREEN.toString()` instead of `§a`. You can change the chat style of a `ChatComponentText` in order to give it hover or click effects. 103 | 104 | 105 | !!! warning 106 | You might be tempted to open a gui from your command like this: 107 | ```java 108 | @Override 109 | public void processCommand(ICommandSender sender, String[] args) throws CommandException { 110 | Minecraft.getMinecraft().displayGuiScreen(new MyGuiScreen()); 111 | } 112 | ``` 113 | This will not work, since your command gets executed from the chat gui and sending a chat line schedules the chat gui to be closed in the same tick (accidentally closing your gui instead). 114 | 115 | In order to make this work, you need to instead wait a tick and then open your gui. You can do this by having a tick event handler in your main mod class like this: 116 | ```java 117 | // In your main mod class 118 | public static GuiScreen screenToOpenNextTick = null; 119 | 120 | @SubscribeEvent 121 | public void onTick(TickEvent.ClientTickEvent event) { 122 | if (event.phase == TickEvent.Phase.END) return; 123 | if (screenToOpenNextTick != null) { 124 | Minecraft.getMinecraft().displayGuiScreen(screenToOpenNextTick); 125 | screenToOpenNextTick = null; 126 | } 127 | } 128 | 129 | // In your command class: 130 | @Override 131 | public void processCommand(ICommandSender sender, String[] args) throws CommandException { 132 | ExampleMod.screenToOpenNextTick = new MyGuiScreen(); 133 | } 134 | ``` 135 | 136 | See [Events](events.md) for more info on how to set up event handlers. 137 | 138 | 139 | ## Tab Completion 140 | 141 | 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`: 142 | 143 | 144 | ```java 145 | @Override 146 | public void processCommand(ICommandSender sender, String[] args) throws CommandException { 147 | if (args.length == 0) { 148 | sender.addChatMessage(new ChatComponentText("§cPlease use an argument")); 149 | } else if (args[0].equals("weather")) { 150 | sender.addChatMessage(new ChatComponentText("§bCurrent Weather: " + 151 | (Minecraft.getMinecraft().theWorld.isRaining() ? "§7Rainy!" : "§eSunny!"))); 152 | } else if (args[0].equals("coinflip")) { 153 | sender.addChatMessage(new ChatComponentText("§bCoinflip: " + 154 | (ThreadLocalRandom.current().nextBoolean() ? "§eHeads" : "§eTails"))); 155 | } else { 156 | sender.addChatMessage(new ChatComponentText("§cUnknown subcommand")); 157 | } 158 | } 159 | 160 | @Override 161 | public List addTabCompletionOptions(ICommandSender sender, String[] args, BlockPos pos) { 162 | if (args.length == 1) // (1)! 163 | return getListOfStringsMatchingLastWord(args, "weather", "coinflip"); // (2)! 164 | return Arrays.asList(); 165 | } 166 | ``` 167 | 168 | 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. 169 | 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 170 | 171 | 172 | -------------------------------------------------------------------------------- /docs/events.md: -------------------------------------------------------------------------------- 1 | # Events in Forge 2 | 3 | Forge uses events to allow mods to communicate with Minecraft and each other. Most of the events you will need to use come from Forge, but you can also create your own events if you need more. 4 | 5 | ## Subscribing to events 6 | 7 | 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. 8 | 9 | ```java 10 | public class MyEventHandlerClass { 11 | int chatCount = 0; 12 | @SubscribeEvent //(1)! 13 | public void onChat(ClientChatReceivedEvent event) { //(2)! 14 | chatCount++; 15 | System.out.println("Chats received total: " + chatCount); 16 | } 17 | } 18 | ``` 19 | 20 | 1. This annotation informs Forge that your method is an event handler 21 | 2. The method parameter tells Forge which events this event handler listens to 22 | 23 | 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. 24 | 25 | 26 | ```java 27 | @Mod(modid = "examplemod", useMetadata = true) 28 | public class ExampleMod { 29 | @Mod.EventHandler 30 | public void init(FMLInitializationEvent event) { 31 | MinecraftForge.EVENT_BUS.register(new MyEventHandlerClass()); 32 | MinecraftForge.EVENT_BUS.register(this); 33 | } 34 | } 35 | ``` 36 | 37 | ## Cancelling Events 38 | 39 | Forge Events can be cancelled. What exactly that means depends on the event, but it usually stops the action the event indicates from happening. 40 | 41 | ```java 42 | @SubscribeEvent 43 | public void onChat(ClientChatReceivedEvent event) { 44 | // No more talking about cheese 45 | if (event.message.getFormattedText().contains("cheese")) 46 | event.setCanceled(true); // (1)! 47 | } 48 | ``` 49 | 50 | 1. Cancel the event 51 | 52 | Not all events can be cancelled. Check the event class in the decompilation for the `:::java @Cancellable` annotation. 53 | 54 | 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`: 55 | 56 | 57 | ```java 58 | @SubscribeEvent(receiveCanceled = true) // (1)! 59 | public void onChat(ClientChatReceivedEvent event) { 60 | event.setCanceled(false); // (2)! 61 | } 62 | ``` 63 | 64 | 1. Make sure our event handler receives cancelled events 65 | 2. Uncancel the event. This means the event will be handled by Minecrafts code normally again and you will see the chat. 66 | 67 | 68 | ## Custom Events 69 | 70 | !!! note 71 | This is an advanced topic that most mod developers don't need to worry about. 72 | 73 | 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). 74 | 75 | ```java 76 | @Cancelable // (1)! 77 | public class CheeseEvent extends Event { // (2)! 78 | public final int totalCheeseCount; 79 | 80 | public CheeseEvent(int totalCheeseCount) { 81 | this.totalCheeseCount = totalCheeseCount; 82 | } 83 | } 84 | ``` 85 | 86 | 1. If you want your event to be cancellable, you need this annotation. Remove it for an uncancellable event. 87 | 2. Extend the Forge `:::java Event` class. The rest of your class is just normal Java. 88 | 89 | That's it, you are done. You have a custom event! 90 | 91 | 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): 92 | 93 | ```java 94 | int cheeseCount = 0; 95 | 96 | @SubscribeEvent 97 | public void onChat(ClientChatReceivedEvent event) { 98 | if (event.message.getFormattedText().contains("cheese")) { 99 | CheeseEvent cheeseEvent = new CheeseEvent(++cheeseCount); // (1)! 100 | MinecraftForge.EVENT_BUS.post(cheeseEvent); // (2)! 101 | } 102 | } 103 | ``` 104 | 105 | 1. Creates a new `CheeseEvent` instance. This is just a normal java object construction, which does not interact with Forge at all. 106 | 2. Send our `CheeseEvent` to be sent to all event handlers by Forge. 107 | 108 | 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): 109 | 110 | ```java 111 | @SubscribeEvent 112 | public void onChat(ClientChatReceivedEvent event) { 113 | if (event.message.getFormattedText().contains("cheese")) { 114 | CheeseEvent cheeseEvent = new CheeseEvent(++cheeseCount); 115 | MinecraftForge.EVENT_BUS.post(cheeseEvent); 116 | if (cheeseEvent.isCanceled()) { 117 | event.setCanceled(true); 118 | } 119 | } 120 | } 121 | ``` 122 | 123 | You can now subscribe to your custom event like you would to any other event: 124 | 125 | ```java 126 | @SubscribeEvent 127 | public void onCheese(CheeseEvent event) { 128 | if (event.totalCheeseCount > 10) { 129 | // Only 10 cheese messages are allowed per restart 130 | event.setCanceled(true); 131 | } 132 | } 133 | ``` 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/hotswap.md: -------------------------------------------------------------------------------- 1 | # Hotswap 2 | 3 | When talking about programming, hotswap refers to switching out code at runtime, without restarting your program. 4 | 5 | By default, Java only has this capability in a very limited fashion, and this only works inside of a development environment. 6 | 7 | ## DCEVM 8 | 9 | DCEVM is a custom JVM that allows you to do a lot more hotswapping. Not only can you change method bodies, but also add new methods, remove methods, add new fields and classes, and more. 10 | 11 | To install DCEVM, first download the installer from https://dcevm.github.io/. Get the installer for the latest Java 8 version (181). The DCEVM installer modifies an existing JVM, and does not work on its own. 12 | 13 | Next you need to download [the corresponding JVM itself](https://www.oracle.com/java/technologies/javase/javase8-archive-downloads.html). For versions as old as 1.8u181 you might need to create an Oracle account. Newer versions of Java *will not work*. 14 | 15 | Once you got the normal 1.8u181 JVM installed, you can launch the DCEVM installer you downloaded earlier. You should use another version of Java to launch the JAR, otherwise you might get issues on windows. DCEVM will prompt you where to install itself to. Choose your 1.8u181 installation and **replace the JVM**. Do **not** install as alt VM. 16 | 17 | Once that is done (which should be fairly quick), you can exit the installer. 18 | 19 | ## Using DCEVM 20 | 21 | In order to use DCEVM (or any other hotswap enabled JVM), you need to launch using that JVM. Edit your run configuration in your IDE. Note that you don't need to change the JVM for your entire Module, only for your run configuration. You may also need to manually add the JVM in IntelliJ by choosing "Select alternative JVM" (this is not the same as the alt VM from DCEVM, that one is unrelated). 22 | 23 | Then you need to launch in debug mode. You can do that by selecting the :beetle: next to your normal :material-play: run button. 24 | 25 | Once your game is started, you can reload your changes by pressing ++ctrl+f9++ (or using the build project keybind). Once your project is built, you should automatically get a prompt to reload your changes. If not, you might need to change "reload classes after compilation" in the IntelliJ settings. 26 | 27 | ## Caveats 28 | 29 | Even with DCEVM you cannot change all the things. Initialization code is not run again, so things like event listers, registered commands, mixins and similar configuration that is done once will not be updated. Even some things like static fields don't get properly initialized. Sometimes changes cannot be properly detected and all classes get reloaded, resulting in most of your event listeners being unregistered. There isn't much that can be done in those situations. Watch your IntelliJ notifications to see how many classes are being reloaded and restart if you see numbers that are too big or too small (0). 30 | 31 | ## Hotswap Agent 32 | 33 | !!! note 34 | This is an advanced topic, and this section is incomplete. You probably don't need to use hotswap agent and it can be quite a bit confusing to set up. 35 | 36 | Hotswap Agent is a software that is run on top of DCEVM that allows for additional reloading of classes. It adds some built-in functionality for things like running static initializers, reloading logging configurations and a lot of other open source frameworks reloading. 37 | 38 | For most people installing hotswap agent is easiest by using the IntelliJ plugin. Once the plugin is installed, go into your global settings, into "Tools > Hotswap" and check the checkbox next to your run configuration. 39 | 40 | Check your logs for `Loading Hotswap agent {VERSION} - unlimited runtime class redefinition.` to see if hotswap agent is running. If you don't see that line, you probably only run DCEVM. 41 | 42 | By default hotswap agent does not do anything about forge. You can check out [hotswapagent-forge](https://github.com/nea89o/hotswapagent-forge/) to receive runtime information about class reloads in forge. 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /docs/https.md: -------------------------------------------------------------------------------- 1 | # Web Requests and HTTPS 2 | 3 | Sooner or later you might want to interact with the Web from your mod. 4 | 5 | Normal methods of making HTTP requests work, this is still Java after all. 6 | There are however some pitfalls. 7 | 8 | ## Our subject of study 9 | 10 | Think of a command like this: 11 | 12 | 13 | ```java 14 | @Override 15 | public void processCommand(ICommandSender sender, String[] args) { 16 | try { 17 | URL url = new URL("https://some-leaderboard-api.nea.moe/getleaderboardposition?username=" + args[0]); 18 | JsonObject apiResponse = gson.fromJson(url.openStream(), JsonObject.class); 19 | sender.addChatMessage(new ChatComponentText("§a" + args[0] + " is rank #" + apiResponse.get("rank").getAsInt())); 20 | } catch (Exception e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | ``` 25 | 26 | This will work, but it has a few problems. There are a few smaller problems (like not checking for missing arguments, not handling http failures, not handling invalid JSON), but the one big problem is blocking requests. 27 | 28 | Whenever you make an HTTP request, it will take some time to complete that request. During that time other code is blocked from running, since it needs to wait on data from that HTTP request. In this example `:::java gson.fromJson(...)` will wait until the HTTP request is completed, since the returned JsonObject needs that data. 29 | 30 | This is an issue, since Minecraft is waiting on your command to complete before it can continue working, meaning while that HTTP request is running, you cannot render any new frames, cannot handle any input, and you cannot handle any of the servers packets, meaning you can even time out on longer requests. The solution to this is threads. 31 | 32 | ## Threads 33 | 34 | A thread allows you to basically create another mini program inside of your existing program. A thread can run at the same time as all your other code, but because it is a thread instead of another program, it can access some of the same data. 35 | 36 | 37 | ```java 38 | @Override 39 | public void processCommand(ICommandSender sender, String[] args) { 40 | Thread thread = new Thread(() -> { 41 | try { 42 | URL url = new URL("https://some-leaderboard-api.nea.moe/getleaderboardposition?username=" + args[0]); 43 | JsonObject apiResponse = gson.fromJson(url.openStream(), JsonObject.class); 44 | sender.addChatMessage(new ChatComponentText("§a" + args[0] + " is rank #" + apiResponse.get("rank").getAsInt())); 45 | } catch (Exception e) { 46 | throw new RuntimeException(e); 47 | } 48 | }); 49 | thread.start(); 50 | } 51 | ``` 52 | 53 | In Java, the `:::java new Thread(...)` constructor allows you to create a new thread. You pass in a lambda that gets run inside that new `Thread` as soon as you call `:::java thread.start()`. 54 | 55 | Running this command now no longer blocks your game. All the processing and waiting of the network request is done on another `Thread`, so the main Minecraft thread can run freely. 56 | 57 | 58 | ### Thread safety 59 | 60 | Threads aren't all sunshines and roses however. One thing you might notice is your game occasionally crashing after you run your command. You get a `ConcurrentModificationException` from the chat window. Maybe you just notice some inconsistent data that comes from a network request. All this comes from one basic issue. Two threads can run at the same time, therefore two threads can access and modify the same data at the same time. 61 | 62 | Let's look at a simpler example. In this code snippet the two functions `threadA` and `threadB` are run by different threads at the same time. 63 | 64 | ```java 65 | String someSharedVariable = "hello"; 66 | 67 | void threadA() { 68 | someSharedVariable = doCalculation(someSharedVariable); 69 | } 70 | 71 | void threadB() { 72 | someSharedVariable = doCalculation(someSharedVariable); 73 | } 74 | ``` 75 | 76 | If we run `threadA` and `threadB` in a conventional program, we would expect `doCalculation` to be called twice: 77 | 78 | ```java 79 | 80 | void runInConventionalProgram() { 81 | threadA(); 82 | threadB(); 83 | // This above is equivalent to 84 | someSharedVariable = doCalculation(doCalculation(someSharedVariable)); 85 | } 86 | ``` 87 | 88 | However, ran with threads, the statements intermingle. `threadA` and `threadB` might both access the shared variable at the start, getting the initial value. Both apply the calculation to that same initial value. `threadB` finishes first and saves the calculation result in the variable. `threadA` finishes second and overwrites `threadB`s calculations with its new result. This becomes even more complicated when different threads do different calculations. 89 | 90 | In order to make threads work, we need something called synchronization. Only one thread can access the same data at the same time, or things get problematic. 91 | 92 | !!! note 93 | Keep in mind that this does not just apply to direct variable access. Calling a function which then later on accesses a variable or a field in a variable will still cause the same issues. 94 | 95 | !!! note 96 | There are of course exceptions. Some classes and objects have internal synchronization built in, specifically built to support multiple threads accessing them, but most classes will break if you access or modify them from multiple threads at the same time. In fact even simple variable access can (silently) break, as demonstrated above. 97 | 98 | ### Synchronization 99 | 100 | Synchronization blocks a thread while another thread is accessing the same data as it. This is not done automatically, but instead needs to be explicitly specified. Synchronizing is done on an object (not on the name of that object, but that exact instance). If you are inside of a `:::java synchronized (someObject)` block you are said to be "holding *that objects* lock". 101 | 102 | ```java 103 | 104 | Object lockObject = new Object(); 105 | 106 | String someSharedVariable; 107 | 108 | 109 | void threadA() { 110 | String localVariable = doSomeCalculation(); 111 | doSomeExpensiveAndSlowNetworkCall(); 112 | synchronized (lockObject) { 113 | someSharedVariable = doSomeCalculationWithTheSharedVariable(localVariable, someSharedVariable); 114 | } 115 | doSomeMoreCalculation(localVariable); 116 | } 117 | ``` 118 | 119 | A few notes: 120 | 121 | - the first `doSomeCalculation` call is not synchronized. If you access `someSharedVariable` here there will be no guarantees over other threads accessing it. 122 | - the `:::java synchronized` block needs an argument. In this case i pass in `lockObject`, *not* `someSharedVariable`. This is because `:::java synchronized (x)` will only work if `x` is never changed. You can change things *inside* of the locked variable (so `x.b = 10;` is okay), but replacing the entire variable with a new one (like we are doing with `:::java someSharedVariable = /*...*/`) would mean that we are synchronizing against a completely different object, which will therefore not be synchronized with other threads. 123 | If you want to synchronize a variable across threads that you want to change out entirely, you should have a second lock object you never change. 124 | 125 | - Once i finish the synchronized block i should no longer access the `someSharedVariable`. After i end that block, other threads can access the object. 126 | - I do the expensive operations *outside* of the `:::java synchronized` block. If you hold the lock on an object all other threads that want to lock on the same object will wait, so doing a network request (for example) while holding a lock will result in those other threads waiting, which is exactly the thing we wanted to avoid by using threads. 127 | - I lock on a variable that i made up. `:::java synchronized` blocks only ever matter if *all* the code accessing that variable use those same blocks. If other code just accesses that variable without synchronizing first, then your synchronization does not matter at all. 128 | 129 | Synchronization is a necessary tool, and often the most efficient/performant way of doing things, but it is quite complicated, and it does not work if you want to access Minecrafts variables, since Minecraft does not do any `synchronized` blocks itself. 130 | 131 | So if synchronization does not work or is too complicated, we can instead do something else: Just work on one thread. 132 | 133 | ### Callbacks 134 | 135 | Expensive calculations need to be done on other threads. Stalling out Minecraft every time you want to look up some bazaar data in the background is not okay if it kicks you out of whatever server you are on, and also freezes the entire game. But since most things you want to do with network requests end up with displaying some version of that newly gotten info to the player, we also need to get back info into the main thread. This can be done either by synchronizing on a variable with the reply and waiting in a (synchronized) tick event to check for that variables contents, or you can use callbacks. 136 | 137 | Minecraft has a function called `:::java Minecraft.getMinecraft().addScheduledTask()`. This function takes in a `:::java Runnable`, which is a lambda of the form `:::java () -> {}`. This lambda will be called on the main thread in the next tick. You can call this function from your thread in order to call Minecrafts functions safely. Keep in mind that this new code will run on the Minecraft main thread, meaning that you should not access data from your other thread, unless you know that other thread won't change the variable anymore, or are properly synchronizing it. 138 | 139 | ```java 140 | @Override 141 | public void processCommand(ICommandSender sender, String[] args) { 142 | Thread thread = new Thread(() -> { 143 | try { 144 | URL url = new URL("https://some-leaderboard-api.nea.moe/getleaderboardposition?username=" + args[0]); 145 | JsonObject apiResponse = gson.fromJson(url.openStream(), JsonObject.class); 146 | Minecraft.getMinecraft().addScheduledTask(() -> { 147 | sender.addChatMessage(new ChatComponentText("§a" + args[0] + " is rank #" + apiResponse.get("rank").getAsInt())); 148 | }); 149 | } catch (Exception e) { 150 | throw new RuntimeException(e); 151 | } 152 | }); 153 | thread.start(); 154 | } 155 | ``` 156 | 157 | In this example all minecraft code (aside from `addScheduledTask`, which is specifically designed to work when accessed from another thread) is only accessed from the main minecraft thread, but all of our calculation is done inside of a worker thread. 158 | 159 | 160 | ### More on threading 161 | 162 | Threads are necessary, but they are also clumsy and hard to work with. There are ways to mitigate this, using `CompletableFuture` and utility methods and events. There are also things about threading that i am skimming over here. There are easier to synchronize primitives (for example `List`s that automatically synchronize) and thread pools, but those are all complicated and nuanced and also not Minecraft specific. Threading is a really complex topic, and even my very basic overview here probably has some mistakes already, so i urge you to seek out other tutorials on it, if you plan on doing complicated threading work. 163 | 164 | ## SSL 165 | 166 | After all that ordeal with threading you probably are hoping that you are done, but sadly there is another problem. Running your code inside of your development environment is fine, but some of your users report issues when running the game normally. After a while of debugging you find the root cause: Java somehow thinks that the https server has an invalid certificate. 167 | 168 | ### Certificates and how does SSL work 169 | 170 | !!! note 171 | This section explains how SSL works and why we need to do what we need to do. If you just want to fix SSL issues, you can skip this rather technical section. 172 | 173 | !!! note 174 | This section intentionally oversimplifies things such as key exchanges, signing vs encryption, private/public key, TLS handshakes, certificate chains and more in order to provide some limited understanding (as is applicable to a minecraft mod), without requiring much effort. 175 | 176 | HTTPS works using something called TLS, which is a newer form of SSL. For most purposes (including this text) SSL and TLS can be used interchangibly. SSL encrypts everything inside of your HTTPS request in accordance with an SSL public key so that only the corresponding SSL private key can decrypt the data. This works both ways, so both sides of the exchange have a private and a public key that they use so that only the other person can read any of the data in transit. 177 | 178 | ```mermaid 179 | sequenceDiagram 180 | My Mod ->> My Server: Here is my SSL public key. Please respond back with your SSL public key. 181 | My Server -->> My Mod: Here is my SSL public key. 182 | My Mod -->> My Server: Here is an HTTP request. 183 | My Server -->> My Mod: Here is an HTTP response. 184 | ``` 185 | 186 | Solid lines are unencrypted, dashed lines are encrypted. [Note that this diagram is intentionally oversimplified and **wrong**](https://en.wikipedia.org/wiki/TLS_handshake). 187 | 188 | One problem you might notice is that even tho all the interesting communication is encrypted, nothing prevents an evil actor to just pretend to be the server, intercepting our first message and replacing it with their own fake SSL public key. This way they can read all of our messages. One way to fix this is using certificates. 189 | 190 | In our client we have a list of so called "root certificate authorities" (root CA). These are essentially just public keys that we *trust* to be good. Since we don't want to store a public key for every website, we just store those few root CAs. Then, in addition to their public key, every website we visit also sends along a signature for their public key, essentially an encryption using the root CAs public key that can cryptographically only be done by someone with that root CAs private key (some signature schemes operate differently, but the basic idea is the same. You have some way of mathematically proving that the certificate was signed in some way by someone in your root CA list). 191 | 192 | ```mermaid 193 | sequenceDiagram 194 | My Mod ->> My Server: Here is my SSL public key. Please respond back with your SSL public key. 195 | My Server -->> My Mod: Here is my SSL public key. 196 | Note over My Mod: Here we verify the servers SSL public key against our root CA list 197 | My Mod -->> My Server: Here is an HTTP request. 198 | My Server -->> My Mod: Here is an HTTP response. 199 | ``` 200 | 201 | Obviously an [evil root CA can still be a problem](https://certificate.transparency.dev/), but a lot of attacks can be prevented this way. 202 | 203 | ### Root CAs in Java 204 | 205 | Java comes with a list of root CAs (certificate authority), basically a list of signatures that can be used to verify if you want to trust a website. Whenever you make an HTTPS request Java checks the connection against that list of CAs, and throws an error if it cannot trust that website. By default Java is fairly up to date and adds new root CAs whenever they become officially/widely accepted. 206 | 207 | However, the java version used by the default minecraft launcher is quite old. Java version 1.8u51 to be exact. Your IDE probably uses a newer version of Java, probably something around ~1.8u400. In between those many versions new root CAs get added, so it is quite easy to not notice if a website will not be trusted by the normal minecraft launcher. 208 | 209 | Now how do we fix this? 210 | 211 | #### Avoiding the problem altogether 212 | 213 | There are two easy (almost code less) fixes. Use an older certificate authority that has been around for a while, or use a more modern Java version. Note that a lot of those older CAs may charge a fee, and even when using an old provider, there is no guarantee that they will give you a certificate signed with one of their older root CAs. Maybe you also want to access a website you don't have access to (like GitHub or the Hypixel API). Also getting all your users to change the default minecraft java installation is a challenge so obvious, I hope I don't have to explain to anyone. 214 | 215 | #### False hope 216 | 217 | There are two ways that I see often used to fix this problem. Just using HTTP and disabling the CA trusting mechanism altogether. Both of them allow anyone to access all of the data that is being transmitted. For something simple such as bazaar data that might be fine, but even if there is nothing private or secretive you need to be careful. If (for example) you are disabling HTTPS for your update checker, it is quite easy for a malicious actor to inject a fake update, making them download a potentially malicious binary that will be executed on the next startup. 218 | 219 | In fact disabling HTTPS or the CA root mechanisms is considered so bad of a practice that all major browsers have a mechanism called [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) which allows websites to prevent accessing them via HTTP. 220 | 221 | I will call out some mods here that I know are doing this. 222 | 223 | - [Partly Sane Skies](https://github.com/PartlySaneStudios/partly-sane-skies/blob/4d4eb0f30d52e74762439b2114f09c13af166819/src/main/kotlin/me/partlysanestudios/partlysaneskies/data/api/Request.kt#L116) 224 | - [Bazaar Notifier](https://github.com/symt/BazaarNotifier/blob/988b2728538c45aba6ca3f240c3c718b8608fd12/src/main/java/dev/meyi/bn/utilities/Utils.java#L43-L59) 225 | - and many more... 226 | 227 | The mods listed here only use them for [non-confidential/non-integral](https://en.wikipedia.org/wiki/CIA_triad) requests, but some others out there use them for their updater notifications, potentially allowing attackers to run arbitrary code on their users computer or other dangerous things. If you would like to add or remove from that list, feel free to [make an issue](https://github.com/nea89o/modDevWiki/issues/new?title=[SSL%20Violator%20List]). 228 | 229 | Some mods go even further beyond and not only use those broken trust factories for their own mods request, but also set them as the global default, making the requests done by all other mods also be insecure. 230 | 231 | For obvious reasons, I won't tell you how to set up these custom trust chain checkers and I am urging you to never use HTTP over HTTPS for anything remotely sensitive. 232 | 233 | #### Bringing your own certificates 234 | 235 | The nice thing about Java is that, while it does not have all the modern certificates built in, you can just ship your own. This is a bit involved, but it allows you to make HTTPS requests seamlessly and without security issues. You can even add your own root CA, in case you don't want to use any of the existing root CAs (but I won't teach you how to do that here). 236 | 237 | Generally when using Java, you store your certificates inside of a key store. So let's first create our custom key store. If possible, you should be using an old JDK (1.8u51) for this. You will also need a new JDK that you want to take the keys from. The JKS default formats have changed a bit throughout the years, and while they all should roughly be compatible with each other, it helps if we create the file with an old JDK. 238 | 239 | Every keystore in java needs a password. By default the password is `changeit` whenever you download a JDK. Don't change that password. 240 | 241 | 242 | ```bash 243 | YOUR_OLD_JDK/bin/keytool -importkeystore -srckeystore YOUR_NEW_JDK/lib/security/cacerts -destkeystore mykeystore.jks 244 | ``` 245 | 246 | This will prompt you for a password for your destination keystore. You should just keep it as "changeit", since that is the default for all java keystores that do not store secret information. This new `mykeystore.jks` file now needs to be added to your `src/main/resources` folder. Add it to that folder directly, instead of the `assets` folder like with other resources. Feel also free to rename it to something else including your mod id. 247 | 248 | Now we come to using this certificate in your code. First you need to load your new keystore: 249 | 250 | ```java 251 | static SSLContext ctx; 252 | static { 253 | try { 254 | KeyStore myKeyStore = KeyStore.getInstance("JKS"); 255 | // Change SomeClass to be the class you are currently in 256 | // The resource name needs to be prefixed with a / 257 | // The password should be changeit, unless you are using a different password for your keystore (but you shouldn't) 258 | myKeyStore.load(SomeClass.class.getResourceAsStream("/mykeystore.jks"), "changeit".toCharArray()); 259 | KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 260 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 261 | kmf.init(myKeyStore, null); 262 | tmf.init(myKeyStore); 263 | ctx = SSLContext.getInstance("TLS"); 264 | ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); 265 | } catch (KeyStoreException | NoSuchAlgorithmException | KeyManagementException | UnrecoverableKeyException | 266 | IOException | CertificateException e) { 267 | System.out.println("Failed to load keystore. A lot of API requests won't work"); 268 | e.printStackTrace(); 269 | ctx = null; 270 | } 271 | } 272 | ``` 273 | 274 | This should be done only once, and you can store this `:::java SSLContext` in a static variable. 275 | 276 | Now, whenever you make an HTTPs request, you simply set the requests context to that SSL context: 277 | 278 | ```java 279 | static SSLContext ctx; 280 | static { 281 | // That code from earlier 282 | } 283 | public static URLConnection openHTTPSConnection(URL url) throws Exception { 284 | URLConnection connection = url.openConnection(); 285 | if (connection instanceof HttpURLConnection && ctx != null) { 286 | ((HttpURLConnection) connection).setSSLSocketFactory(ctx.getSocketFactory()); 287 | } 288 | return connection; 289 | // Or you can return an input stream directly: 290 | // return connection.getInputStream(); 291 | } 292 | ``` 293 | 294 | 295 | So with all this, how does our final HTTPS request look like: 296 | 297 | ```java 298 | @Override 299 | public void processCommand(ICommandSender sender, String[] args) { 300 | Thread thread = new Thread(() -> { 301 | try { 302 | URL url = new URL("https://some-leaderboard-api.nea.moe/getleaderboardposition?username=" + args[0]); 303 | JsonObject apiResponse = gson.fromJson(SSLUtil.openHTTPSConnection(url).getInputStream(), JsonObject.class); 304 | Minecraft.getMinecraft().addScheduledTask(() -> { 305 | sender.addChatMessage(new ChatComponentText("§a" + args[0] + " is rank #" + apiResponse.get("rank").getAsInt())); 306 | }); 307 | } catch (Exception e) { 308 | throw new RuntimeException(e); 309 | } 310 | }); 311 | thread.start(); 312 | } 313 | ``` 314 | 315 | There is still lots to improve here, and you will probably write your own utility methods to make this easier, but the basic structure will boil down to something roughly looking like this. 316 | 317 | 318 | -------------------------------------------------------------------------------- /docs/ide-setup.md: -------------------------------------------------------------------------------- 1 | # Setting up 2 | 3 | This is a slightly rewritten version of [SBMW's Getting started article](https://web.archive.org/web/20240214104717/https://sbmw.ca/development/getting-started/). That one was also written by me, and contains roughly the same info. 4 | 5 | !!!note 6 | Downloads in this tutorial are provided as a convenience, if you know what you are doing you can find these downloads somewhere else (in a package manager like chocolatey, pacman or [sdkman](https://sdkman.io/)). If you decide to go that route, please make sure that you download the exact same Software and not something similar. 7 | 8 | ## Setting up a Java Development Environment 9 | 10 | Minecraft mods are written in Java and as such you will need a Java Development Setup. You will need *both* 11 | 12 | - [A Java JDK (*not JRE*) for version 17](https://adoptium.net/temurin/releases?version=17) 13 | - [A Java JDK (*not JRE*) for version 8](https://adoptium.net/temurin/releases?version=8) 14 | 15 | ## Setting up an Integrated Development Environment 16 | 17 | There is pretty much only one IDE for mod development, which is [IntelliJ](https://www.jetbrains.com/idea/). In theory it is also possible to code in VSCode, Vim or Eclipse, but doing so is difficult to set up and not recommended. Even if you are already familiar with one of these IDEs, switching to IntelliJ is pretty much mandatory. 18 | 19 | IntelliJ has a free community edition, as well as a paid ultimate edition, both available for download [here](https://www.jetbrains.com/idea/download/other.html). 20 | 21 | 22 | ## Setting up GitHub 23 | 24 | Although not strictly necessary, it is recommended that you create a [GitHub](https://github.com) Account. This tutorial will assume you have one. If you do not have a GitHub Account you might need to find some more manual work arounds for some things. 25 | 26 | ## Deciding on a Project 27 | 28 | ### Contributing to an existing Project 29 | 30 | If you want to contribute to an existing project, find that project on GitHub and Discord. You can usually find both linked on their page on the [SkyBlock Mod Wiki][mod-list]. You can typically ask around in the Discord for help, which will help you to get up to speed with whether they will accept a feature you are planning to contribute. Once all of that is cleared, you can create a Fork on GitHub. You will then do everything with the forked repository as you would do with your own repository, and once you are done create a pull request in the original repository you are planning to contribute to. 31 | 32 | ### Creating a new Project 33 | 34 | Go to [Forge1.8.9Template](https://github.com/nea89o/Forge1.8.9Template/). Click on "Use this template" and "Create a new Repository". Find a good name for your mod. If you want to code your mod in Kotlin or make a 1.12 mod you will need to tick "Include all branches". Don't worry about it too much, you can change this later with a little bit of effort. 35 | 36 | !!! warning 37 | Do not clone the template repository directly (or download a zip of the template repository). When using the "Use this template" option on GitHub, there is some additional processing being done to insure your repository is fully set up. 38 | 39 | If you *must* you can also manually use the `make-my-own.sh` script, but only on Linux, and there are no guarantees for it to work. 40 | 41 | !!! info 42 | In the past [GitHub decided to temporarily delete my account](https://nea.moe/blog/github-suspension). I have since been unbanned, but if you want, you can still use the [zip generator](https://nea.moe/tools/processor/forge1.8.9) that works without using GitHub. Once you have downloaded and unzipped the folder you can continue as you usually would with this tutorial. 43 | 44 | ## Setting up your IDE 45 | 46 | IntelliJ has a built in option for cloning a project. Chose "New" then "Project from Version Control". Log into GitHub and clone the project. 47 | 48 | Once the project is done cloning, you need to head into your :simple-gradle: gradle settings (the elephant on the right of your workspace, then click on the cogwheel) and change the gradle JVM to be a version *17* JDK. 49 | 50 | Next go into your Project Settings (++ctrl+alt+shift+s++) and set the Project SDK to be a version 8 JDK. 51 | 52 | Finally click on the reload button in the gradle tab on the right from earlier. 53 | 54 | You should now have generated a Run Config in IntelliJ. You can find those next to the :material-play:. If you can't find it, check the :material-folder:`.idea/runConfigurations` folder. If you can find a file there, you just need to restart IntelliJ. 55 | 56 | Always use these run configs instead the `runClient` task in the gradle tab. The gradle task ***does not work***. 57 | 58 | ## Common Issues 59 | 60 | ### No matching variant of dev.architectury:architectury-pack200:0.1.3 was found. 61 | 62 | This error indicates that your Java Version does not support architectury. Fix this by setting your gradle JDK to 17 63 | 64 | ### Unsupported class file major version 65 65 | 66 | This error indicates that your Java version is too new. Fix this by setting your gradle JDK to use exactly java version 17, not something newer like Java 21. 67 | 68 | ### Error: Could not find or load main class `@C:\Some\Long\Path\1283814818228418813-argFile` 69 | 70 | This error (with the `@` at the beginning of the missing main class and then some random path) happens when IntelliJ (or another IDE) tries to use Java 9+ exclusive argument shorting. In Java 9 and later you can use `@` to add arguments from a file. In Java 8 this will lead to errors, so you need to disable argfile shortening in your run configuration ("Shorten command line: none" in IntelliJ). 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/img/background-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea89o/ModDevWiki/5de2c0aa3b6313591202c13a99f7ff93497b026a/docs/img/background-screen.png -------------------------------------------------------------------------------- /docs/img/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea89o/ModDevWiki/5de2c0aa3b6313591202c13a99f7ff93497b026a/docs/img/background.png -------------------------------------------------------------------------------- /docs/img/color-selector-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea89o/ModDevWiki/5de2c0aa3b6313591202c13a99f7ff93497b026a/docs/img/color-selector-screen.png -------------------------------------------------------------------------------- /docs/img/text-alignment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea89o/ModDevWiki/5de2c0aa3b6313591202c13a99f7ff93497b026a/docs/img/text-alignment.png -------------------------------------------------------------------------------- /docs/img/uvs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nea89o/ModDevWiki/5de2c0aa3b6313591202c13a99f7ff93497b026a/docs/img/uvs.jpg -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Welcome to nea89s Modding Wiki 2 | 3 | This wiki is aimed at mod developers for Minecraft 1.8.9 and those who want to be. More specifically, this wiki is here for client mods for Hypixels [SkyBlock](https://wiki.hypixel.net/Main_Page). 4 | 5 | First a word of warning: Modding isn't always for the faint of heart. Mojang doesn't really intend for people to mod their game, so there isn't exactly a lot of documentation laying around. What little documentation you will find is probably for newer versions and most mod dev forums either can't or don't want to help you with a [9 year old minecraft version](https://howoldisminecraft189.today/). But while starting with a mod as your first programming project isn't great and it helps to know your ways around Java as well, if you are passionate then I believe you can do it! 6 | 7 | Despite all that, there is still a community around modding for SkyBlock. You don't have to go into this alone, you can always join up with an existing modding team, which also helps beating the ratting allegations. Check out [existing mods and their communities][mod-list]! Almost everything is more fun with other people, and learning from other people will never be replaced by a cold, heartless wiki such as this one. 8 | 9 | If you get all that and you are still in, then let's [start with the IDE set up](ide-setup.md). 10 | 11 | -------------------------------------------------------------------------------- /docs/inventories.md: -------------------------------------------------------------------------------- 1 | # Inspecting Inventories 2 | 3 | Whether you want to sum up item prices in a chest, highlight bazaar orders that have been filled, or want to check if you are currently in an npc shop to prevent the player from selling their hyperion, almost every single SkyBlock mod will eventually want to read from an inventory. 4 | 5 | ## Getting the inventory 6 | 7 | Let's just for now be quick and dirty and use a `ClientTickEvent`. The `ClientTickEvent` gets run multiple times each tick, so having code in there is generally not super recommended. But for now this will be fine. 8 | 9 | ```java 10 | @SubscribeEvent 11 | public void onTick(TickEvent.ClientTickEvent event) { 12 | if (event.phase != TickEvent.Phase.END) return; // (1)! 13 | GuiScreen currentScreen = Minecraft.getMinecraft().currentScreen; 14 | if (!(currentScreen instanceof GuiChest)) return; // (2)! 15 | GuiChest currentScreen1 = (GuiChest) currentScreen; 16 | ContainerChest container = (ContainerChest) currentScreen1.inventorySlots; // (3)! 17 | LogManager.getLogger("ExampleMod").info("Container Name: " 18 | + container.getLowerChestInventory().getDisplayName().getFormattedText()); // (4)! 19 | } 20 | ``` 21 | 22 | 1. First, we only want to check in one phase. The `ClientTickEvent` is called multiple times per tick, but we only need to check the GUI once per tick. 23 | 2. Check if we are in a chest 24 | 3. Cast the screen chest, get the container coresponding to the chest and cast that to be a ContainerChest. 25 | 4. Log out the display name of the chest inventory 26 | 27 | So there are a few interesting things going on here. And maybe we should start with the question what is a `GuiChest` versus a `ContainerChest` versus a `IInventory`. 28 | 29 | The `GuiChest` is probably the easiest one. It represents the actual GUI you see, so it contains information about user actions (such as dragged item stacks, hovered slots) and does the actual drawing and click handling. 30 | 31 | However, `GuiChest` does not store any items. Instead it has a reference to a `Container`. The `Container` represents the logical state of that chest interface. It contains informations about all the items on screen as well as logical counterparts to GUI state (such as the dragged item stacks). 32 | 33 | But the `Container` is actually not the master of all things items either, instead it aggregates an `IInventory`. Those are the actual definitive sources of state for the items. In case of a `ChestContainer` there are actually two `IInventory` instance, one for the player inventory, and one for the chest contents. Confusingly the chest contents are called `lowerChestInventory` despite being in the upper part of the screen. The `IInventory` also contains information about the name of the chest. 34 | 35 | Another thing to note here is that we cast a `Container` to a `ContainerChest`. We can do this, because the `GuiChest` always has a `ContainerChest` as container, even if that information isn't present in the type system. 36 | 37 | ## Accessing Items 38 | 39 | Now that we know the basics of inventory logistics of Minecraft, we can actually make use of our opened chest gui. 40 | 41 | ```java 42 | // Continuing the TickEvent from before 43 | for (int i = 0; i < container.getLowerChestInventory().getSizeInventory(); i++) { 44 | ItemStack stack = container.getLowerChestInventory().getStackInSlot(i); 45 | if (stack != null) 46 | LogManager.getLogger("ExampleMod").info("Slot " + i + ": " + stack); 47 | } 48 | ``` 49 | 50 | ``` title="Output" 51 | [22:19:28] [main/INFO] (ExampleMod) Container Name: Large Chest§r 52 | [22:19:28] [main/INFO] (ExampleMod) Slot 0: 12xitem.enderPearl@0 53 | [22:19:28] [main/INFO] (ExampleMod) Slot 1: 51xtile.tnt@0 54 | ``` 55 | 56 | Note that we access the `lowerChestInventory` here. Accessing the `ContainerChest` directly gives us not only the chest contents, but the player inventory also, as all slots get merged into one uber ItemStack storage. This behaviour might actually be wanted sometimes (for example, you might want to highlight slots in both your as well as the chest inventory). Also note that when we access the `IInventory` we directly access `ItemStack`s. Accessing the `ContainerChest` directly is a bit easier and more powerful, but also implicitly includes the player inventory, so extra measure need to be taken: 57 | 58 | 59 | ```java 60 | for (int i = 0; i < container.inventorySlots.size(); i++) { 61 | Slot slot = container.inventorySlots.get(i); 62 | if (slot.getHasStack() /* equivalent to slot.getStack() != null */) 63 | LogManager.getLogger("ExampleMod").info("Slot " + i + ": " + slot.getStack()); 64 | } 65 | 66 | ``` 67 | 68 | ``` title="Output" 69 | [22:23:03] [main/INFO] (ExampleMod) Container Name: Large Chest§r 70 | [22:23:03] [main/INFO] (ExampleMod) Slot 0: 12xitem.enderPearl@0 71 | [22:23:03] [main/INFO] (ExampleMod) Slot 1: 51xtile.tnt@0 72 | [22:23:03] [main/INFO] (ExampleMod) Slot 54: 1xitem.potion@0 73 | [22:23:03] [main/INFO] (ExampleMod) Slot 55: 1xitem.potion@0 74 | [22:23:03] [main/INFO] (ExampleMod) Slot 56: 1xitem.potion@0 75 | [22:23:03] [main/INFO] (ExampleMod) Slot 57: 1xitem.potion@0 76 | [22:23:03] [main/INFO] (ExampleMod) Slot 58: 1xitem.potion@0 77 | [22:23:03] [main/INFO] (ExampleMod) Slot 59: 1xitem.bootsChain@0 78 | [22:23:03] [main/INFO] (ExampleMod) Slot 60: 1xitem.skull@3 79 | [22:23:03] [main/INFO] (ExampleMod) Slot 61: 6xitem.skull@3 80 | [22:23:03] [main/INFO] (ExampleMod) Slot 62: 1xitem.swordIron@0 81 | [22:23:03] [main/INFO] (ExampleMod) Slot 73: 1xitem.blazeRod@0 82 | [22:23:03] [main/INFO] (ExampleMod) Slot 81: 1xitem.bow@0 83 | [22:23:03] [main/INFO] (ExampleMod) Slot 82: 1xitem.swordDiamond@0 84 | [22:23:03] [main/INFO] (ExampleMod) Slot 83: 1xitem.stick@0 85 | [22:23:03] [main/INFO] (ExampleMod) Slot 84: 1xitem.pickaxeDiamond@0 86 | [22:23:03] [main/INFO] (ExampleMod) Slot 85: 1xitem.swordGold@0 87 | [22:23:03] [main/INFO] (ExampleMod) Slot 88: 1xitem.horsearmorgold@0 88 | [22:23:03] [main/INFO] (ExampleMod) Slot 89: 1xitem.netherStar@0 89 | ``` 90 | 91 | First note that we can now use `Slot`s instead of plain `ItemStack`. A `Slot` contains extra information such as `xDisplayPosition` and `yDisplayPosition` which you might need in case you want to draw something around certain items. 92 | 93 | If you want to use `Slot`s without worrying about potential player slots sneaking in, you can use two methods for finding out where a slot belongs: 94 | 95 | ```java 96 | Slot slot = container.inventorySlots.get(i); 97 | boolean isChestSlotA = !(slot.inventory instanceof InventoryPlayer); 98 | boolean isChestSlotB = i < container.getLowerChestInventory().getSizeInventory(); 99 | boolean WRONG_METHOD = slot.getSlotIndex() < container.getLowerChestInventory().getSizeInventory(); 100 | ``` 101 | 102 | !!! important 103 | You need to use `slot.slotNumber` or the `i` index you used for iterating here. Using the `getSlotIndex` is meaningless, since that index is the index *inside* of the `IInventory` (so the first hotbar slot is always index `0`, just like the first slot of a chest) 104 | 105 | Overall I think that the index based method is a lot less pretty. Not only is it not a real invariant of the `Container` class for those two inventories to exist in this order (In theory you could have a `Container` that puts the `slotNumber` of the `InventoryPlayer` first, and then the chest contents. This is not the case in vanilla code, however), but it also very prone to mistakes, such as messing up `<` and `<=`. We also lose all help from the type system. That `int` has no types associated with it, so especially when passing arounds `int`s like that, they use meaning very quickly, so we have to write a lot more documentation to keep our code understandable. The `inventory instanceof InventoryPlayer` is very explicit and our code reads almost like documentation itself: "is this slot inside of the players inventory or not". 106 | 107 | ## Inside of Items 108 | 109 | Just logging out items to the command line is neat and all, but in most cases you will want to programatically inspect items. 110 | 111 | So, for the final time in this article, let's do a disambiguation: `ItemStack` versus `Item`. This one is hopefully a simple one. 112 | 113 | `ItemStack` represents a concrete stack. It has a size, metadata (custom name, custom lore, ExtraAttributes). If you have two item stacks in a chest somewhere, you will have two instances of `ItemStack` that reference those *exact* two item stacks. 114 | 115 | `Item` on the other hand represents a *type* of an item. For example, a `diamond_sword` or a `dirt`. Some things that you might think of as an "item type" is actually grouped together under one `Item` instance. Different coloured objects, such as wool or dyes are all just one `Item.dye` and which dye you are referencing is part of the `ItemStack` metadata. You will find most `Item`s inside of the `Items` class (`Items.apple`). However, items that correspond to a `Block` are usually not found in there. Instead, you can use `Item.getItemFromBlock(Blocks.dirt)` to get those `Item` types. Note that you will always get the same exact object instance from this method, so you can use `==` on those returned objects. Also be aware that some more exotic blocks (such as doors) might have individual `Item`s that end up placing a completely unrelated `Block`. For example: there is a `Items.wheat_seeds` which places a `Blocks.wheat` when right clicked on farmland, but calling `Item.getItemFromBlock(Blocks.wheat)` will get you a null instead of your `Items.wheat_seed`. For those "placer" `Item`s you will usually want to work in whatever medium is native for what you are doing (`Item` for inventories and entities, `Block` for reading world data). 116 | 117 | How do you get data out of an `ItemStack` now. There are two ways of going about this: APIs or NBT. 118 | 119 | ### Item APIs 120 | 121 | Item APIs are arguably easier to use, so you might be tempted to just always use the, but they have some disadvantages I will talk about soon. 122 | 123 | ```java 124 | logger.info("Slot " + i + ":"); 125 | logger.info(" Item: " + stack.getItem()); 126 | logger.info(" Display Name: " + stack.getDisplayName()); 127 | logger.info(" Stack Size: " + stack.stackSize); 128 | logger.info(" Lore:"); 129 | for (String loreLine : stack.getTooltip(Minecraft.getMinecraft().thePlayer, false)) { 130 | logger.info(" - " + loreLine); 131 | } 132 | ``` 133 | 134 | This prints out all the information very nicely: 135 | 136 | ``` title="Output" 137 | [22:50:07] [main/INFO] (ExampleMod) Slot 1: 138 | [22:50:07] [main/INFO] (ExampleMod) Item: net.minecraft.item.ItemBlock@68a94e58 139 | [22:50:07] [main/INFO] (ExampleMod) Display Name: §9Superboom TNT 140 | [22:50:07] [main/INFO] (ExampleMod) Stack Size: 51 141 | [22:50:07] [main/INFO] (ExampleMod) Lore: 142 | [22:50:07] [main/INFO] (ExampleMod) - §o§9Superboom TNT§r 143 | [22:50:07] [main/INFO] (ExampleMod) - §5§o§7Breaks weak walls. Can be used to 144 | [22:50:07] [main/INFO] (ExampleMod) - §5§o§7blow up Crypts in §cThe Catacombs §7and 145 | [22:50:07] [main/INFO] (ExampleMod) - §5§o§7§5Crystal Hollows§7. 146 | [22:50:07] [main/INFO] (ExampleMod) - §5§o 147 | [22:50:07] [main/INFO] (ExampleMod) - §5§o§9§lRARE 148 | ``` 149 | 150 | We get a bit of a hiccup with the `Item`. Turns out just system out printing a `Item` isn't great. You can call `.getRegistryName()` to fix this however: 151 | 152 | ``` title="Output" 153 | [22:52:30] [main/INFO] (ExampleMod) Item: minecraft:tnt 154 | ``` 155 | 156 | ### NBT APIs 157 | 158 | But, we soon run into problems. Two kinds of problems: logical and performance. Using the standard APIs for lore and display name invoke Forge events, which causes a *lot* of other code to run, exponentially more code the more mods you have. This is not only slow (since those other mods might do some expensive calculations), but will also obscure information. Some mods might append some information to the bottom of the tooltip, thereby not making the rarity the last line of the lore anymore, for example. 159 | 160 | So we turn to the API that doesn't call mods: NBT. NBT (Named Binary Tag) is a data format for storing essentially complex key value objects, similar to JSON. Instead of using verbose (human readable) representation for numbers, strings, bytes, booleans, lists, dictionaries NBT uses binary. There is a format called SNBT that represents NBT data in a human readable way, which looks like slightly modified JSON, which i will also use for NBT in here. 161 | 162 | Minecraft uses NBT to store all the information about items, blocks and entities in the background. Most of that data is only available on the server (thereby inaccessible inside of a client mod) and sent to the client via some other mean. The big exception to that are items. `ItemStack`s are sent (almost) entirely via NBT. 163 | 164 | ```java 165 | byte STRING_NBT_TAG = new NBTTagString().getId(); // (1)! 166 | NBTTagCompound tagCompound = stack.getTagCompound();// (2)! 167 | if (tagCompound == null) continue; // (3)! 168 | String displayName = tagCompound.getCompoundTag("display").getString("Name"); // (4)! 169 | NBTTagList loreList = tagCompound.getCompoundTag("display").getTagList("Lore", STRING_NBT_TAG); // (5)! 170 | for (int i1 = 0; i1 < loreList.tagCount(); i1++) { // (6)! 171 | String loreLine = loreList.getStringTagAt(i1); // (7)! 172 | } 173 | ``` 174 | 175 | 1. First let's save the tag id of a string. This is essentially the "type" of a string when using NBTs. 176 | 2. Access the NBT associated with an `ItemStack`. This will always be a `NBTTagCompound` which is equivalent to a JSON `object` 177 | 3. The `NBTTagCompound` of an `ItemStack` can be null. 178 | 4. First access the `NBTTagCompound` that is the "display". Then in that sub object access the string at "Name". 179 | 5. First access the `NBTTagCompound` that is the "display". Then in that sub object access a list with the elements of type `NBTTagString` 180 | 6. We can get the length of the list with `tagCount()` 181 | 7. Now we can access each line of lore from the list using `getStringTagAt` 182 | Given how long this code is, I usually have a helper method for these types of operations in my code: 183 | ```java 184 | public static List listFromNBT(NBTTagList nbtList, Function reader) { 185 | List ts = new ArrayList<>(nbtList.tagCount()); 186 | for (int i = 0; i < nbtList.tagCount(); i++) { 187 | ts.add(reader.apply((U) nbtList.get(i))); 188 | } 189 | return ts; 190 | } 191 | ``` 192 | ```java 193 | NBTTagList loreList = tagCompound.getCompoundTag("display").getTagList("Lore", STRING_NBT_TAG); 194 | List loreStrings = listFromNBT(loreList, NBTTagString::getString); 195 | ``` 196 | This is a very powerful method that makes working with nbt lists a lot easier, but it also very easy to cause RuntimeExceptions this way. In the end I personally think that NBT is always a mess of potential runtime exceptions. There are some ways to make it more bearable, but it will always be error prone. 197 | 198 | 199 | You can already see how our code is getting longer. And this isn't the only problem with NBTs. Some NBT elements might not be there even tho you expect them to be. There are two ways how this manifests. A `null` in case of the root `stack.getTagCompound()` or just missing properties inside of a `TagCompound`. In case of missing properties this will just silently default construct a matching object. This is already a problem here, since we will get an empty string if we don't have a display name set (instead of null, or a fallback to the item name). It would be great if we could have a more explicit "absent" value, but sadly NBT does not offer that. Instead you will need to manually and error pronely check with `hasKey`. 200 | 201 | Another problem are those many string keys. Not only is it hard to remember them and look them up, but there is also 0 feedback at compile time for typos or any other faults in those strings. You will instead either crash at runtime, or more likely silently get empty (faulty) data. 202 | 203 | All of this makes NBT extremely unattractive to work with. But if we want our code to work correctly, even with other mod installed, or if we want our code to run fast, then we will need to use NBT more often than we would like. 204 | 205 | And it is not all bad. NBT also allows us access to bonus data that normal `ItemStack` APIs don't have access to. Enter `ExtraAttributes`. 206 | 207 | ### ExtraAttributes 208 | 209 | `ExtraAttributes` is a set of extra NBT data that is sent along with most `ItemStack`s on Hypixel. It contains a lot of things from item ids to pet exp to enchants and reforges. It is essentially the machine readable counter part to the lore. Much like the lore it is not suuuper consistent, but usually survives more versions without changes. In the end we are always at the mercy of hypixel. 210 | 211 | 212 | ```json 213 | { 214 | id: "minecraft:diamond_pickaxe", 215 | Count: 1b, 216 | tag: { 217 | ench: [{ 218 | lvl: 9s, 219 | id: 32s 220 | }], 221 | Unbreakable: 1b, 222 | HideFlags: 254, 223 | display: { 224 | Lore: [ ... ], 225 | Name: "§aDiamond Pickaxe" 226 | }, 227 | ExtraAttributes: { 228 | id: "DIAMOND_PICKAXE", 229 | enchantments: { 230 | efficiency: 9 231 | }, 232 | uuid: "28d1c00d-2112-453a-82c1-c35a28bebf6f", 233 | timestamp: 1691931000000L 234 | } 235 | }, 236 | Damage: 0s 237 | } 238 | ``` 239 | 240 | We can see at the root the actual item metadata (the `Count`, `id` and `Damage`). Those are part of the `ItemStack` and are always parsed by the `ItemStack` APIs. The NBT we get access to with `getTagCompound` is the `tag` part of this SNBT. 241 | 242 | The `ench` tag contains the *vanilla* enchantments. Those are used by vanilla code. Hypixel does not send all enchantments this way, only the ones that affect client behaviour, such as efficiency and depth strider. 243 | 244 | The `Unbreakable` tag hides the durability bar. This makes your items not show the durability bar for a split second whenever you mine a block. 245 | 246 | The `HideFlags` tag prevents minecraft from adding information to the lore. Each bit represents something different, like "Hide the fact that is item is marked as Unbreakable" or "hide the enchantments on this item". 247 | 248 | We looked at the `display` tag earlier. 249 | 250 | Lastly there is `ExtraAttributes`. This section is not vanilla at all. It is instead Hypixel's own internal data structures. This is used by Hypixels code to represent information about the item that go beyond what Minecraft can express. It contains an `id` that is the official hypixel id (which may be different from the vanilla id), `enchantments` (which is a tag compound mapping enchantment ids to levels), uuids which are a unique identifier for each item that exist (for example: no two ASPECT_OF_THE_END have the same uuid, you get a new one every time you craft) and so much more. 251 | 252 | Some of this information is found on almost every item (such as `id`), some of this data is item specific and some of it is shared between only a few items. 253 | 254 | Generally you can find out quite a lot about an item by looking at its `ExtraAttributes`. I can't go over everything here, but let's look at one more example: 255 | 256 | 257 | ```json 258 | { 259 | id: "minecraft:skull", 260 | Count: 1b, 261 | tag: { 262 | SkullOwner: { 263 | Id: "ecc8937f-a09e-4f06-a10a-efadfaff1e3b", 264 | hypixelPopulated: 1b, 265 | Properties: { 266 | textures: [{ 267 | Value: "eyJ0ZXh0dXJlcyI6eyJTS0lOIjp7InVybCI6Imh0dHA6Ly90ZXh0dXJlcy5taW5lY3JhZnQubmV0L3RleHR1cmUvNzA3MWE3NmY2NjlkYjVlZDZkMzJiNDhiYjJkYmE1NWQ1MzE3ZDdmNDUyMjVjYjMyNjdlYzQzNWNmYTUxNCJ9fX0=" 268 | }] 269 | }, 270 | Name: "§ecc8937f-a09e-4f06-a10a-efadfaff1e3b" 271 | }, 272 | display: { 273 | Lore: [ ... ], 274 | Name: "§7[Lvl 100] §6Elephant" 275 | }, 276 | ExtraAttributes: { 277 | petInfo: "{\"type\":\"ELEPHANT\",\"active\":false,\"exp\":4.0048701025808394E7,\"tier\":\"LEGENDARY\",\"hideInfo\":false,\"heldItem\":\"GREEN_BANDANA\",\"candyUsed\":0,\"uuid\":\"8d35c8fe-1351-47f6-8609-e0b0fbcb077d\",\"uniqueId\":\"5cdc7008-009e-4730-8f52-cd970491ecc4\",\"hideRightClick\":false,\"noMove\":false}", 278 | id: "PET", 279 | uuid: "8d35c8fe-1351-47f6-8609-e0b0fbcb077d" 280 | } 281 | }, 282 | Damage: 3s 283 | } 284 | ``` 285 | 286 | This pet is a skull, which has some interesting information in the `SkullOwner` tag. That tag contains the texture for the skull. This can be used occassionally when identifying custom items that don't have a `ExtraAttributes.id` tag. 287 | 288 | 289 | Also we can tell that the `id` of this pet is just `PET`. You might expect it to be `ELEPHANT_PET` from mods such as NEU, but those ids are just made up extensions to the `id` by Hypixel. The pet type is actually stored in the `petInfo` string, which is actually a string containing a normal JSON object that needs to be decoded on top of the NBT data parsing. 290 | 291 | You can see all kinds of useful info in there, like `candyUsed` which is normally a stat hidden on level 100 pets. It contains information about the `type`, obviously, but also the total `exp` and the `heldItem` (with an item `id` instead of just a name). 292 | 293 | This kind of way of inspecting data is really powerful and makes a lot of mods possible in the first place. But NBTs are messy and I would highly recommend you transfer NBTs into normal Java objects [as soon as possible](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/). 294 | 295 | ## Closing out - The GuiOpenEvent 296 | 297 | And finally let's circle back to our first section, in which we started with the `ClientTickEvent`. In the end `ClientTickEvent` works just fine. But also, it leaves us lacking. The performance isn't great and we sometimes just miss items. Normally we could use a `GuiOpenEvent`. This fires whenever a gui is opened, so we could in theory just read all of our data once, when we open a chest. Sadly that doesn't work out, since Hypixel only sends the items after the GUI has opened. We might get some or all of the items in a single tick, but we can't be so sure about that when there are people with a 300, 400 or even 500 ms ping to Hypixel. There are many solutions to this problem: simply waiting a set amount of ticks after a `GuiOpenEvent` is probably the easiest one. That one is obviously a bit sloppy, but is also very simple to implement. Another solutions, would be mixing into `NetHandlerPlayClient.handleSetSlot` and listening for the bottommost rightmost item to be set (slotCount - 1) (this one is a lot cleaner, works faster and almost never gives us any partial inventory states), which still fails if Hypixel decides to use empty item stacks for that slot. Almost always there will be a glass pane or another item, but when a Hypixel GUI decides against using glass panes your code might not just run at all. You could maybe decide to mix those methods: either the last index is set, or we waited 5 ticks after the `GuiOpenEvent`. There are probably more options out there to explore and it is up to you how ridiculous you want to make your system for detecting inventory opens. In the end Hypixel doesn't specifically provide an API for mod developers, so we have to make due with what happens to work for us. 298 | 299 | 300 | 301 | -------------------------------------------------------------------------------- /docs/mixins/accessors.md: -------------------------------------------------------------------------------- 1 | # Accessor Mixins 2 | 3 | > a/k/a Reverse Patch 4 | 5 | Let's start with the easiest form of Mixins — Accessors. Accessors allow you to access functions that are otherwise private in a Minecraft class. You can also do that using reflection, but you might notice that your reflection call will not easily work in both devenv and a live env. This is because the method names are different in a devenv compared to a normal Forge installation. You can still specify both names and just look through all the names using reflection, but Accessor Mixins are a lot easier to use, with less accidental pitfalls, and better performance. 6 | 7 | ```java 8 | // This Mixin targets the Minecraft class 9 | @Mixin(Minecraft.class) 10 | public interface AccessorMinecraft { 11 | 12 | // Getter for the field theIntegratedServer 13 | // Notice the _mymodid at the end. 14 | @Accessor("theIntegratedServer") 15 | IntegratedServer getIntegratedServe_mymodid(); 16 | 17 | // Setter for serverPort 18 | @Accessor("serverPort") 19 | void setServerPort_mymodid(int port); 20 | 21 | // Invoker for rightClickMouse. 22 | @Invoker("rightClickMouse") 23 | void rightClickMouse_mymodid(); 24 | } 25 | ``` 26 | 27 | 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. 28 | 29 | Next we put the `:::java @Mixin` annotation on our Accessor to let it known which class we want to inject into. 30 | 31 | 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. 32 | 33 | 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. 34 | 35 | For an method invoker, you copy the method signature you want to call, but rename it to something unique with a `_mymodid` postfix. 36 | 37 | Now if you want to use those methods, you can simply cast any instance of your targeted class to your accessor mixin class: 38 | 39 | ```java 40 | Minecraft mc = Minecraft.getMinecraft(); 41 | AccessorMinecraft accessorMc = (AccessorMinecraft) mc; 42 | accessorMc.rightClickMouse_mymodid(); 43 | ``` 44 | 45 | If you get a class cast exception here, it means your mixin was not applied. This can happen if you forgot to register your mixin, or if your mixin contains errors. 46 | -------------------------------------------------------------------------------- /docs/mixins/adding-fields.md: -------------------------------------------------------------------------------- 1 | # Adding new fields and methods 2 | 3 | The next step up is injecting fields and methods into a class. This allows you to store additional state in objects, or can serve as an alternative to accessors for more complex operations that need to access private state of a class. 4 | 5 | ```java 6 | @Mixin(EntityArmorStand.class) 7 | public class InjectCustomField { 8 | Color colorOverride_mymodid = Color.RED; 9 | 10 | public void setColorOverride_mymodid(Color color) { 11 | colorOverride_mymodid = color; 12 | } 13 | 14 | public Color getColorOverride_mymodid() { 15 | return colorOverride_mymodid; 16 | } 17 | } 18 | ``` 19 | 20 | This mixin is a `:::java class`, like all mixin (except for accessors) are. You can make the class abstract if you want. 21 | 22 | First we add a new field (of course with modid postfix) into every armor stand. 23 | 24 | Then we also add a getter and a setter method for that field. 25 | 26 | 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. 27 | 28 | ```java 29 | // Inside our regular code. Not in the mixin package 30 | public interface ColorFieldAccessor { 31 | void setColorOverride_mymodid(Color color); 32 | Color getColorOverride_mymodid(); 33 | } 34 | 35 | // And the updated mixin 36 | @Mixin(EntityArmorStand.class) 37 | public class InjectCustomField implement ColorFieldAccessor { 38 | Color colorOverride_mymodid; 39 | 40 | @Override 41 | public void setColorOverride_mymodid(Color color) { 42 | colorOverride_mymodid = color; 43 | } 44 | 45 | @Override 46 | public Color getColorOverride_mymodid() { 47 | return colorOverride_mymodid; 48 | } 49 | } 50 | ``` 51 | 52 | Now we can just cast any instance of `EntityArmorStand` to `ColorFieldAccessor`: 53 | 54 | ```java 55 | public static Color getColorOverrideForArmorStand(EntityArmorStand armorStand) { 56 | return ((ColorFieldAccessor) armorStand).getColorOverride_mymodid(); 57 | } 58 | ``` 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/mixins/advanced-injects.md: -------------------------------------------------------------------------------- 1 | # Advanced Injects 2 | 3 | So you wanna learn how to *really* use Injects? It is gonna be a tough road, and I won't lead you all the way there (mostly because eventually there are diminishing returns on a tutorial like this), but eventually most SkyBlock devs fall down the rabbit hole. 4 | 5 | This will be pretty dry compared to the other mixin tutorials, so feel free to skip reading this and just use this as a glossary. 6 | 7 | ## Remapping 8 | 9 | Let's start with names. Names are important. If you call a method with the wrong name, You get a crash at best, and at worse you cause undefined behavior. But, most methods go by unpronounceable names like `v` or `method_12934`. This is because Mojang obfuscated Minecraft, replacing every class name, every method name, etc. with a randomly generated short name to prevent people from reverse engineering it (which has the nice side effect of bit of a smaller binary). Now if we develop mods, we don't want to work with names like those. So we use mappings. Those are long lists telling us which obfuscated method name corresponds to a readable method name. In modern versions you have [yarn](https://github.com/FabricMC/yarn/) (which is a community project), as well as official names from [Mojang](https://nea.moe/minecraft.html) themselves, but in older versions, we just have MCP. 10 | 11 | Let's go through the process of how your normal Forge mod gets compiled: 12 | 13 | - Download Minecraft (obfuscated by mojang) 14 | - Actually download another copy of Minecraft (the server, also obfuscated) 15 | - Merge the two JARs into one, so you can reference both server and client classes from the same mod 16 | - Apply the MCP mappings to the JAR, turning Mojangs names into readable ones. 17 | - Apply some patches to the JAR, to inject Forge events and custom registries and such. 18 | - the order of those first 5 steps isn't always the same. minecraft version and liveenv/devenv differences can rearrange them sometimes 19 | - Now you compile your mod source against this new Minecraft JAR (as well as some extra libraries) 20 | - Forge in a live environment uses an intermediary between the completely obfuscated and the completely readable names, so now we need to turn our readable names back into intermediary ones 21 | - For this, Forge goes through your generated JAR and applies the mappings from earlier, but in reverse 22 | 23 | This process has it's drawbacks. Especially that last step isn't perfect, and not everything you do will be remapped (and sometimes that is desired). 24 | 25 | Let's look at some examples: 26 | 27 | 28 | ```java 29 | public void myFunc() throws Throwable { 30 | ItemStack itemStack = new ItemStack(/* ... */); 31 | itemStack.getDisplayName(); 32 | ItemStack.class.getMethod("getDisplayName").invoke(itemStack); 33 | System.out.println("net.minecraft.item.ItemStack"); 34 | System.out.println("ItemStack"); 35 | } 36 | ``` 37 | 38 | Now the forge remapper will take that code and get you something like this in the actual compiled mod: 39 | 40 | ```java 41 | public void myFunc() throws Throwable { 42 | azq itemStack = new azq(/* ... */); 43 | itemStack.b(); 44 | azq.class.getMethod("getDisplayName").invoke(itemStack); 45 | System.out.println("net.minecraft.item.ItemStack"); 46 | System.out.println("ItemStack"); 47 | } 48 | ``` 49 | 50 | There are a few things that work and a few things don't in this snippet. 51 | 52 | The normal usage of `ItemStack` gets correctly replaced with `azq` the correct obfuscated name (well, in reality the obfuscated name would be a different one, but the basic idea holds) and the `getDisplayName` call gets replaced with `b`. 53 | 54 | But the reflection didn't work out so great. While the `.class` literal did get remapped, the `getMethod` argument didn't. And if we used `Class.forName` that would also not get remapped. This is because those values are just strings that just so happen to have the same name as a class or method. For this simple case, you might think we could just do some flow analysis and remap those values, but for more complicated cases (maybe the method name gets passed as an argument, or stored in a variable) the flow analysis is not that clear. Those cases *could* be covered, but doing so would lead to a lot of inconsistencies around the edges of our flow analysis. A simple refactor could lead to your code not being remapped correctly. In that light it is better to just not remap strings at all. 55 | 56 | The `println` is not changed either, but most likely those debug prints are not meant to change. If you later get an error relating to this method and you search for "ItemStack", you want to find those log entries in your log. So in this case the "failed" remap is actually the correct behaviour. 57 | 58 | Now given all this information, let's see how mixins handle remaps. 59 | 60 | ## Refmaps 61 | 62 | Refmaps are mixins way around the forge compilation step. Mixins uses a lot of string identifiers. From method names in `@Inject(method = "")` to method descriptors in `@At(target = "", value = "INVOKE")`, to many more. All those strings are not recognized by Forge as something to be remapped, and even if Forge did remapping on strings, those strings are often in complicated formats that are wildly different from how Forge expects them. Because of this mixins instead use their own extra compilation step to remap all that information. 63 | 64 | The mixin refmap strategly looks like this: 65 | 66 | 67 | - Compile against the deobfuscated (readable name) Minecraft JAR, like the normal mod. 68 | - Let Forge take care of all the real java code (the method bodies, method arguments and return types, class references in annotations) 69 | - Afterwards, take a look at all mixin annotations and resolve the things they refer to using the readable names. 70 | - Then, since we are still in the development environment where those mappings are available, create a JSON file that contains all mappings relevant to all the mixin annotations. 71 | - Mixin doesn't just ship all the mappings because they are quite large and 99% not needed. 72 | - This JSON file is called the "refmap" 73 | - Later, at runtime, when the Mixin class transformer parses the annotations to apply class transformations it reads the refmap and resolves annotation arguments using those names. 74 | 75 | You might run into a problem sometimes when referring to a non remapped method however. Not all methods in Minecrafts code are obfuscated. Some need to keep their original name in order to interact with other Java code. For example the `equals` method of an object needs to always be called `equals`. Obfuscating that method breaks comparisons used by a lot of Java standard library functions. When Mixin encounters those unobfuscated names during the refmap collection step, it notices the lack of a remapped name. This could mean that something is just named the same, but it could also mean that there is an error (the developmer mistyped a name). If you want to inform mixin that you are aware of a lacking mapping, you can do so by specifying `remap = false` on that annotation. It only applies to that specific annotation, so you might need to apply it to your `@Inject` and your `@At` separately. 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/mixins/index.md: -------------------------------------------------------------------------------- 1 | # Mixins 2 | 3 | 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. 4 | 5 | !!! info 6 | 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. 7 | 8 | It also has some other functions that allow for easier Minecraft development, but most of that functionality is aimed at higher Minecraft versions. 9 | 10 | > 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. 11 | 12 | ## Layout 13 | 14 | Mixins need to be in their own package. You should have a dedicated mixin package in the template already. You can have multiple subpackages, but your normal code and your Mixin code need to be separate. This is because Mixins are instructions for how to change the program, rather than actual program code itself. Mixins also need to be registered in your `mixin.example.json`. In there you only need to put the class name, not including the mixin package. Mixins also need to be written in Java, not in Kotlin. 15 | 16 | ```json 17 | { 18 | "package": "${mixinGroup}", 19 | "refmap": "mixins.${modid}.refmap.json", 20 | "minVersion": "0.7", 21 | "compatibilityLevel": "JAVA_8", 22 | "mixins": [ 23 | "MixinGuiMainMenu", 24 | "subpackage.MixinSomeOtherClass" 25 | ] 26 | } 27 | ``` 28 | 29 | !!! info 30 | Depending on the template you used for your mod, you may have an auto mixin plugin installed already. That kind of plugin automatically finds all mixins that are inside of your mixin package, meaning you can just ignore the `mixin.modid.json`. You still need to put your mixins inside of the correct package, but you don't need to register them explicitly anymore. 31 | 32 | If you have such a plugin, you can find it by looking for something like this: 33 | ```json 34 | "plugin": "${basePackage}.init.AutoDiscoveryMixinPlugin", 35 | ``` 36 | in your mixin json. 37 | 38 | You can also have multiple mixins for the same Minecraft class. 39 | 40 | ## Mixin Use Cases 41 | 42 | I recommend you start learning with accessor mixins, since those are the easiest, and go down the list from there. 43 | 44 | - [Accessors](./accessors.md) 45 | - [Adding Fields and Methods](./adding-fields.md) 46 | - [Simple Injects](./simple-injects.md) 47 | 48 | ## Compatibility 49 | 50 | ### Modid postfix 51 | 52 | In order for your mod to be compatible with other mods it is *highly* recommend (if not borderline mandatory) to prefix or postfix all of your methods with your modid: 53 | 54 | ```java 55 | public void someMixinMethod_mymodid() {} 56 | // or 57 | public void someMixinMethod$mymodid() {} 58 | ``` 59 | 60 | There are some exceptions for `:::java @Inject`s, but in general it doesn't hurt to just add the postfix. 61 | 62 | ### Non destructive mixins 63 | 64 | When mixing into a class you would generally want that, if another mod has the exact same mixin, both of your mixins would work. Especially if your mixin only works sometimes (like being toggleable using a config option). 65 | 66 | I.e. if you want a mixin to color mobs, and your mod decides not to color a mob, another mod should be able to use the exact same mixin (just in their mod) to color those mobs. 67 | 68 | There are some general ground rules for achieving this behaviour: 69 | 70 | - 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 (`:::java return`ing from your inject). 71 | - 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. 72 | - 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. 73 | 74 | 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? 75 | 76 | ## Troubleshooting 77 | 78 | The first step in troubleshooting mixins is to enable `-Dmixin.debug=true` in your run configurations jvm arguments. This will print out all the Mixins as they are applied and show you exactly what is wrong with each mixin, and why it wasn't applied. 79 | 80 | Another common issue is to forget to register a mixin in the `mixins.modid.json` 81 | 82 | You can also get exceptions when trying to load a mixin class directly. Accessing any mixin class except for an accessor from non mixin code will crash your game. If you want to call a method inside a mixin, have that mixin implement an interface instead. 83 | 84 | 85 | ## Other resources 86 | 87 | - [2xsaiko](https://dblsaiko.net/)'s [Mixin Cheatsheet](https://github.com/2xsaiko/mixin-cheatsheet/blob/master/README.md) 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/mixins/simple-injects.md: -------------------------------------------------------------------------------- 1 | # Simple Injects 2 | 3 | Let's get into method modifications. The real interesting part of mixins. Hopefully you know the basics from the first two mixin tutorials by now, because now we get into a whole another layer of complexity. 4 | 5 | Now we will modify an existing method in Minecrafts code. This will allow us to react to changes in Minecrafts state. This is how almost all custom [events](../events.md) are done, but we are of course not limited to just events that observe state changes. Using method modifying mixins we can change almost any behaviour in Minecrafts code. 6 | 7 | !!! note 8 | This is the simple tutorial, I will tell you *how* to use `@Inject`s and co, but I won't tell you the *why*. Check out the [advanced tutorial](./advanced-injects.md) for that. 9 | 10 | ## The easiest of the easiest 11 | 12 | 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. 13 | 14 | ```java 15 | @Mixin(PlayerControllerMP.class) // (1)! 16 | public class RightClickWithItemEvent { 17 | 18 | @Inject( // (2)! 19 | method = "sendUseItem", // (3)! 20 | at = @At("HEAD")) // (4)! 21 | private void onSendUseItem_mymod( // (5)! 22 | EntityPlayer playerIn, World worldIn, ItemStack itemStackIn, // (6)! 23 | CallbackInfoReturnable cir // (7)! 24 | ) { 25 | MinecraftForge.EVENT_BUS.post(new SendUseItemEvent(playerIn, worldIn, itemStackIn)); 26 | } 27 | } 28 | ``` 29 | 30 | 1. First we declare which class we want to change 31 | 2. `:::java @Inject` allows us to add code into an already existing method 32 | 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. 33 | 4. The `:::java @At` specifies where our code will be injected. `HEAD` just means the top of the method. 34 | 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` 35 | 6. You need to copy over all the parameters from your original method into which you are injecting. 36 | 7. You need one extra parameter for the callback info. 37 | 38 | 39 | 40 | First we want to inject into the `PlayerControllerMP` class. 41 | 42 | 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). 43 | 44 | 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`. 45 | 46 | For a method returning void, you just use a `:::java CallbackInfo`, and if the method returns something, you use `:::java CallbackInfoReturnable`. 47 | 48 | Your method will now be called every time the `sendUseItem` is called with the arguments to that method and the `CallbackInfo`. 49 | 50 | !!! important 51 | Your method will be *called* at the beginning of the injected into method like this: 52 | 53 | ```java 54 | public boolean sendUseItem(EntityPlayer playerIn, World worldIn, ItemStack itemStackIn) { 55 | onSendUseItem_mymod(playerIn, worldIn, itemStackIn, new CallbackInfo(/* ... */)); 56 | // All the other code that is normally in the method 57 | } 58 | ``` 59 | 60 | This means returning from your method will just continue as normal. See [cancelling](#cancelling) for info on how to return from the outer method. 61 | 62 | ## At a method call 63 | 64 | Let's take this example method: 65 | 66 | ```java 67 | public void methodA() { 68 | // Let's pretend lots of code calls methodA, so we don't want to inject 69 | // ourselves into methodA 70 | } 71 | 72 | public void methodB() { 73 | System.out.println("Here 1"); 74 | methodA(); 75 | // We want to inject our method call right here. 76 | System.out.println("Here 2"); 77 | } 78 | ``` 79 | 80 | We can inject ourselves into `methodB` as well. It is *just* a bit more complicated than the `HEAD` inject. 81 | 82 | ```java 83 | @Inject( 84 | method = "methodB", // (3)! 85 | at = @At( 86 | target = "Lnet/some/Class;methodA()V", // (1)! 87 | value = "INVOKE")) // (2)! 88 | private void onMethodBJustCalledMethodA(CallbackInfo ci) { 89 | } 90 | ``` 91 | 92 | 1. This is the method call for which we are searching. This is not the method into which our code will be injected. 93 | 2. This tells mixin that we want `target` to point to a method call (not a field or anything else). 94 | 3. This is the method into which we want our code to be injected. 95 | 96 | > **HUUUUH, where does that come from???** 97 | 98 | 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. 99 | 100 | ## Ordinals 101 | 102 | Let's take the `INVOKE` injection example from before and change it a bit: 103 | 104 | ```java 105 | public void methodA() { 106 | // ... 107 | } 108 | 109 | public void methodB() { 110 | System.out.println("Here 1"); 111 | if (Math.random() < 0.4) 112 | methodA(); 113 | System.out.println("Here 2"); 114 | methodA(); 115 | // We want to inject our method call right here. 116 | System.out.println("Here 3"); 117 | } 118 | ``` 119 | 120 | 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`. 121 | 122 | ```java 123 | @Inject(method = "methodB", at = @At(target = "Lnet/some/Class;methodA()V", value = "INVOKE", ordinal = 1)) 124 | private void onMethodBJustCalledMethodA(CallbackInfo ci) { 125 | } 126 | ``` 127 | 128 | ## Cancelling 129 | 130 | Cancelling a method means you return from the method you are injected to as soon as your injector method is done. In order to be able to use the cancelling methods, you need to mark your injection as cancellable. 131 | 132 | ```java 133 | @Inject(method = "syncCurrentPlayItem", at = @At("HEAD"), cancellable = true) 134 | private void onSyncCurrentPlayItem_mymod(CallbackInfo ci) { 135 | System.out.println("This code will be executed"); 136 | if (Math.random() < 0.5) 137 | ci.cancel(); 138 | System.out.println("This code will *also* be executed"); 139 | // As soon as this method returns, the outer method will see that it was cancelled and *also* return 140 | } 141 | 142 | @Inject(method = "isHittingPosition", at = @At("HEAD"), cancellable = true) 143 | private void onIsHittingPosition_mymod(BlockPos pos, CallbackInfoReturnable cir) { 144 | cir.setReturnValue(true); 145 | } 146 | ``` 147 | 148 | For `void` methods you need to use `:::java callbackInfo.cancel()` which acts the same as a normal `:::java return;` would in the method you are injecting into. For all other methods you need to use `:::java callbackInfoReturnable.setReturnValue(returnValue)` which corresponds to `:::java return returnValue;`. 149 | 150 | 151 | !!! important 152 | Cancelling a `CallbackInfo` will only have an effect as soon as you return from your injector method. 153 | The rest of your method will run as normal. 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /docs/screens.md: -------------------------------------------------------------------------------- 1 | # Screens and You 2 | 3 | Creating a custom screen manually is quite a big effort. Instead you can consider using an existing GUI library: 4 | 5 | - [MoulConfig](https://notenoughupdates.org/MoulConfig/) is a config library with some features for custom GUIs based on NEUs GUIs. 6 | - [Elementa](https://github.com/EssentialGG/Elementa) is a gui library made by Sk1er. It mainly targets Kotlin, an alternative programming language to Java, but can in theory also be used from Java. 7 | - [Vigilance](https://github.com/EssentialGG/Vigilance) is fully automated Elementa for config GUIs only. 8 | - [OneConfig](https://docs.polyfrost.org/oneconfig/) is a config library made by Polyfrost. 9 | - or just do it yourself. Writing a gui is not the easiest thing in modding, but nothing allows you more customization. 10 | 11 | ## Basic Scaffold 12 | 13 | A basic gui screen has 3 methods: 14 | 15 | ```java 16 | public class MyGuiScreen extends GuiScreen { 17 | @Override 18 | public void drawScreen(int mouseX, int mouseY, float partialTicks) { 19 | drawDefaultBackground(); 20 | super.drawScreen(mouseX, mouseY, partialTicks); 21 | } 22 | 23 | @Override 24 | protected void keyTyped(char typedChar, int keyCode) throws IOException { 25 | super.keyTyped(typedChar, keyCode); 26 | } 27 | 28 | @Override 29 | protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { 30 | super.mouseClicked(mouseX, mouseY, mouseButton); 31 | } 32 | } 33 | ``` 34 | 35 | `drawScreen` is called every frame and is used to render things onto the screen. Note that you *first* call `drawDefaultBackground()` (which tints the background dark) and then call `super.drawScreen()` (which renders widgets on your screen). 36 | 37 | `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. 38 | 39 | `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: 40 | 41 | ```java 42 | public static final int MOUSE_LEFT = 0; 43 | public static final int MOUSE_RIGHT = 1; 44 | public static final int MOUSE_MIDDLE = 2; 45 | public static final int MOUSE_BACKWARD = 3; 46 | public static final int MOUSE_FORWARD = 4; 47 | ``` 48 | 49 | You can also always access the `width` and `height` fields to get the screen width and height to layout your components. 50 | 51 | ## Adding buttons 52 | 53 | Vanilla has a system for buttons already built in, which I am using for a simple color selector: 54 | 55 | ```java 56 | int lastClickedButton = 0; 57 | 58 | @Override 59 | public void initGui() { 60 | super.initGui(); 61 | // Add buttons to the gui list during gui initialization 62 | this.buttonList.add(new GuiButton(0, width / 2 - 55, height / 2 - 10, 30, 20, "§cRED")); 63 | this.buttonList.add(new GuiButton(1, width / 2 - 15, height / 2 - 10, 30, 20, "§9BLUE")); 64 | this.buttonList.add(new GuiButton(2, width / 2 + 25, height / 2 - 10, 30, 20, "§2GREEN")); 65 | } 66 | 67 | @Override 68 | protected void actionPerformed(GuiButton button) throws IOException { 69 | // When a button is clicked saved that last id (or do something else based on the id) 70 | // You could change a setting here for example 71 | lastClickedButton = button.id; 72 | } 73 | 74 | @Override 75 | public void drawScreen(int mouseX, int mouseY, float partialTicks) { 76 | // Draw the background tint 77 | drawDefaultBackground(); 78 | 79 | // Find the last selected color 80 | int color = 0; 81 | if (lastClickedButton == 0) { 82 | color = 0xFFFF0000; 83 | } else if (lastClickedButton == 1) { 84 | color = 0xFF0000FF; 85 | } else if (lastClickedButton == 2) { 86 | color = 0xFF00FF00; 87 | } 88 | 89 | // Draw a colorful rectangle 90 | drawGradientRect(width / 2 - 65, height / 2 - 20, width / 2 + 65, height / 2 + 20, color, color); 91 | 92 | // Draw buttons 93 | super.drawScreen(mouseX, mouseY, partialTicks); 94 | } 95 | 96 | ``` 97 | 98 | ![A screen with 3 buttons and blue background. The button labels are blue, red and green](img/color-selector-screen.png) 99 | 100 | ## Rendering text and images 101 | 102 | Instead of using built in buttons and basic rectangles, you can also render more complex things. 103 | 104 | ### Rendering text 105 | 106 | You can easily use Minecraft's built in font renderer to render any text you like. 107 | 108 | ```java 109 | @Override 110 | public void drawScreen(int mouseX, int mouseY, float partialTicks) { 111 | // Draw tinted background 112 | drawDefaultBackground(); 113 | 114 | // Draw an outline rectangle 115 | drawGradientRect(width / 2 - 100, height / 2 - 20, width / 2 + 100, height / 2 + 20, 0xFF808080, 0xFF808080); 116 | 117 | FontRenderer fr = Minecraft.getMinecraft().fontRendererObj; 118 | String text = "Hello, World!"; 119 | int textWidth = fr.getStringWidth(text); 120 | 121 | // Draw a string left aligned 122 | fr.drawString(text, width / 2 - 95, height / 2 - 18, -1); 123 | 124 | // Draw a string center aligned 125 | fr.drawString(text, width / 2 - textWidth / 2, height / 2 - 8, -1); 126 | 127 | // Draw a string right aligned 128 | fr.drawString(text, width / 2 + 95 - textWidth, height / 2 + 2, -1); 129 | } 130 | ``` 131 | 132 | ![A screen with the text "Hello, World!" 3 times. Once left aligned, once center aligned, once right aligned](img/text-alignment.png) 133 | 134 | ### Rendering images 135 | 136 | 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`). 137 | 138 | For images you probably want to use png files, since Minecraft supports these out of the box, unlike most other image formats. 139 | 140 | For this tutorial you can use [this background](img/background.png). 141 | 142 | 143 | ```java 144 | @Override 145 | public void drawScreen(int mouseX, int mouseY, float partialTicks) { 146 | // Draw tinted background 147 | drawDefaultBackground(); 148 | 149 | Minecraft minecraft = Minecraft.getMinecraft(); 150 | 151 | // First we need to bind the texture 152 | minecraft.getTextureManager().bindTexture(new ResourceLocation("examplemod"/* or your modid */, "textures/gui/background.png")); 153 | 154 | // Render from your texture. 155 | drawModalRectWithCustomSizedTexture( 156 | width / 2 - 100, height / 2 - 20, // (1)! 157 | 0, 0, // (2)! 158 | 200, 40, // (3)! 159 | 200, 40 // (4)! 160 | ); 161 | 162 | FontRenderer fr = minecraft.fontRendererObj; 163 | String text = "Hello, World!"; 164 | int textWidth = fr.getStringWidth(text); 165 | 166 | // Draw a string left aligned 167 | fr.drawString(text, width / 2 - 95, height / 2 - 15, 0xFF000000); 168 | 169 | // Draw a string center aligned 170 | fr.drawString(text, width / 2 - textWidth / 2, height / 2 - 5, 0xFF000000); 171 | 172 | // Draw a string right aligned 173 | fr.drawString(text, width / 2 + 95 - textWidth, height / 2 + 5, 0xFF000000); 174 | } 175 | ``` 176 | 177 | 1. This is the top left position of where your texture should be rendered. 178 | 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)` 179 | 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). 180 | 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. 181 | 182 | #### UVs 183 | 184 | 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. 185 | 186 | The U component goes along the x axis of the image and starts with 0 at the left and ends with 1 at the right. The V component goes along the y axis of the image and starts with 0 at the top and ends with 1 at the bottom. This is specific to OpenGL, other game engines might have different UV coordinate spaces. 187 | 188 | ![UV graph](img/uvs.jpg) 189 | 190 | To calculate the actual uv you want to start with in game, just divide the pixel coordinate by the texture size: 191 | 192 | ```java 193 | float u = 16 / 64; // start at x pixel coordinate 16 with a 64 wide image 194 | ``` 195 | 196 | 197 | 198 | ## GlStateManager 199 | 200 | `GlStateManager` is a class that changes the behaviour of all other rendering calls. 201 | 202 | ### Transformations 203 | 204 | `GlStateManager` has a so called "matrix stack" which stores a set of transformations that get applied to all render calls you do. Only the top layer of the matrix stack affects your render calls. 205 | 206 | #### Translations 207 | 208 | 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. 209 | 210 | 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. 211 | 212 | #### Scaling 213 | 214 | 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. 215 | 216 | ```java 217 | GlStateManager.scale(2, 2, 1); 218 | 219 | fontRenderer.drawString("Hello, World!", 10, 10, -1); 220 | ``` 221 | 222 | 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)`. 223 | 224 | ```java 225 | GlStateManager.translate(10, 10, 0); 226 | GlStateManager.scale(2, 2, 1); 227 | 228 | fontRenderer.drawString("Hello, World!", 0, 0, -1); 229 | ``` 230 | 231 | Alternatively you can do the math and divide all coordinates by your current scale factor. 232 | 233 | !!! warning 234 | 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`. 235 | 236 | #### Stack Manipulation 237 | 238 | I mentioned earlier that the GlStateManager has a matrix stack. Your transformations and render calls only ever use the topmost matrix on that stack. Those other layers have a purpose however. Since you always want to hand back the GlStateManager in the same state you got it (otherwise all of Minecraft renders 10 pixels to the side. *oops*) you might be tempted to manually undo all your `translate` and `scale` calls. While this is doable, a much easier way is to instead use the matrix stack. 239 | 240 | You can push a new matrix to the stack by using `pushMatrix`. This copies the current top matrix (with all the transformations applied by earlier code) and makes that new matrix the top matrix. Then after your code is done you can just call `popMatrix` and that top matrix is discarded and the old matrix with all the old transformations is used for the rest of the code. Just put your rendering and transformation calls inside of those two method calls and all should work out. (Of course inside of the `pushMatrix` `popMatrix` environment transformations still only apply to code after the transformation is applied.) 241 | 242 | ```java 243 | GlStateManager.pushMatrix(); 244 | GlStateManager.translate(Math.random() * 100, Math.random() * -30, 200000); 245 | // more wacky transformation calls here 246 | // then comes your rendering code 247 | fr.drawString("Hi", 0, 0, -1); 248 | GlStateManager.popMatrix(); 249 | // And here everything is normal again 250 | ``` 251 | 252 | 253 | #### Rotation 254 | 255 | Rarely you might also want to rotate things. Rotation works a bit differently than how you might expect it. A common way of doing rotation is with euler angles, so basically just rotation around x, y and z as 3 values. This has a lot of drawbacks, but is quite easy to visualize and is commonly used in 3D modeling software. 256 | 257 | Minecraft instead uses rotations around a vector. This means you choose the axis you want to rotate around, and then you choose the angle. If you want to rotate around x, y and z after one another, you need to make 3 rotate calls this way. 258 | 259 | Remember, like with the other transformations, the Z direction points out of the screen towards the "front". 260 | 261 | ```java 262 | GlStateManager.pushMatrix(); 263 | GlStateManager.translate(width / 2, height / 2 - 5 + fr.FONT_HEIGHT / 2, 0); 264 | GlStateManager.rotate((float) ((System.currentTimeMillis() / 200.0) % (360)), 0, 0, 1); 265 | fr.drawString(text, -textWidth / 2, -fr.FONT_HEIGHT / 2, 0xFF000000); 266 | GlStateManager.popMatrix(); 267 | ``` 268 | 269 | ### Attributes 270 | 271 | In addition to transformations you can also change "attributes" about the render calls. These can be occasionally useful, but are a bit more complicated. 272 | 273 | #### Stack Manipulation 274 | 275 | 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. 276 | 277 | #### Color 278 | 279 | `:::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)`. 280 | 281 | #### Depth 282 | 283 | `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. 284 | 285 | 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. 286 | 287 | #### Blending 288 | 289 | 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`. 290 | 291 | #### And many more 292 | 293 | There are a ton of attributes, some of them more useful, some of them less. Most of them map to regular OpenGL attributes, so you can always look up OpenGL tutorials on how to use them. Or check out other mods or vanilla code to see them in action. Just remember to always reset attributes to how you got them. 294 | 295 | 296 | ## Going beyond 297 | 298 | Minecraft has a few built in methods for drawing that I showed you in here. But Minecraft has many more, for rendering Items in UIs, and other things, but these basics should get you started. Be sure to always check out vanilla code for example usages. And if vanilla is missing something you want, you can always call the OpenGL primitives that Minecraft itself calls. 299 | 300 | -------------------------------------------------------------------------------- /docs/tweakers.md: -------------------------------------------------------------------------------- 1 | # Tweakers and FMLLoadingPlugins 2 | 3 | Forge offers a ton of capabilities to modders. A lot of things are hooked into events and nearly all methods in the Minecraft are callable from mod code. 4 | 5 | But there are also limits. A lot of early loading (such as automatically loading dependencies) cannot be done using the Forge APIs. Similarly if a method or class is private or a certain method you would like an event for isn't hooked you can quickly run into problems. 6 | 7 | Tweakers allow you to hook into early code, changing the way things are loaded, modifying classes to inject arbitrary calls and attributes into classes you don't own. They are immensely powerful, but also quite confusing and difficult to get right. 8 | 9 | Note that I will talk about Tweakers for the most part here. `IFMLLoadingPlugin`s have similar capabilities for the most part and are more difficult to set up. They also have some unique capabilities which I'll cover at the end, but for now Tweakers are fine. I might also cover some other details of the FML launch process and the launchwrapper that aren't *strictly* part of the tweaker API. 10 | 11 | ## Getting Started 12 | 13 | You probably have used a tweaker already. [Mixins](./mixins/index.md) are loaded using a tweaker as well. You might not have realized since it is included in a bunch of templates already, but the tweaker system is exactly what allows mixin to modify all kinds of classes on your behalf. 14 | 15 | Running [multiple tweakers](#delegating-tweakers) is usually a bit difficult, but while we are in a development environment we can just keep adding more `--tweakClass ` arguments. Those can be set in your loom settings in the `build.gradle.kts` file: 16 | 17 | ```kotlin 18 | loom { 19 | launchConfigs { 20 | "client" { 21 | // You should already have a loom block, you can just add it in there 22 | arg("--tweakClass", "com.mymod.init.MyTweaker") 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | !!! note 29 | Note that we make use of a fully classified class name here (including the package). We also need to choose a dedicated package and *cannot* put it in a package with anything except tweaker related classes. Typically those packages are named either `init` or `tweaker`, although you can find other names too. 30 | 31 | Classes outside of that package are not typically available to the tweaker. While there are some ways to access those classes, most of them result in multiple classes being loaded down the line or other similar crashes, so put all of your tweaker classes and the classes those classes access into their own package. 32 | 33 | Referring to classes by `SomeClass.class.getName()` would be nicer, but sadly this does load a class, so try to avoid that if possible and use normal fully qualified strings instead. 34 | 35 | Next we create the actual tweaker. For now that just means implementing the `:::java ITweaker`. 36 | 37 | ```java 38 | package com.mymod.init; 39 | 40 | public class TestTweaker implements ITweaker { 41 | @Override 42 | public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { 43 | 44 | } 45 | 46 | @Override 47 | public void injectIntoClassLoader(LaunchClassLoader classLoader) { 48 | 49 | } 50 | 51 | @Override 52 | public String getLaunchTarget() { 53 | return null; 54 | } 55 | 56 | @Override 57 | public String[] getLaunchArguments() { 58 | return new String[0]; 59 | } 60 | } 61 | ``` 62 | 63 | These are the default implementations of all the methods, so you can just implement the `:::java ITweaker` interface and auto implement the methods. 64 | 65 | Feel free to add a few print statements into those methods and see what happens. 66 | 67 | But what do those methods *actually* do (aside from very early print statements): 68 | 69 | - `getLaunchTarget` returns the main class of Minecraft. Since there can be multiple tweakers only the first "primary" tweaker is called to get the main class. For mods (like we are writing) this method never gets called. 70 | 71 | - `getLaunchArguments` gets called for each tweaker and has to return an array. Those arrays get concatenated and are used as arguments for the Minecraft main method. You can set any option here that *Minecraft* expects. Note that options expected by other tweakers or Forge itself are ignored here. So extra `--tweakClass` args here are ignored. 72 | 73 | Some interesting arguments are `--uuid`, `--username` and `--accessToken`. Those are how DevAuth are implemented (which is another tweaker under the hood). Check out `net.minecraft.client.main.Main` for more options, but most of the time this stays empty. 74 | 75 | - `acceptOptions` allows you to process arguments passed into Minecraft. The `args` array contains all unrecognized options, so it does not include the other options passed into that method (`--gameDir`, `--assetsDir`, `--version` which is called `profile`). It also does not include the `--tweakClass` arguments. We will learn how to access other tweakers later on. Most mods also ignore that method. 76 | 77 | Lastly we have `injectIntoClassLoader`. So let's go over how to use `LaunchClassLoader` that is provided. 78 | 79 | ## Loading dependencies 80 | 81 | Before we get into the juicy stuff, let's talk about something basic. How about adding some dependencies? 82 | 83 | Usually you just include all your dependencies into a JAR, but sometimes you don't want that. There are a couple of possible reasons for this: 84 | 85 | - Dynamically not loading a dependency if another mod is installed. Maybe that mod already bundles (or is) the dependency. 86 | - Dynamically loading a dependency from a remote location. Maybe you have some big dependencies you don't want users to update every time they download a new version. 87 | - Dynamically loading a dependency depending on some other variable, like depending on the operating system or even the Minecraft version. 88 | - Downloading external code for auto updates. (Please don't do this btw. Just use a [regular updater](https://github.com/nea89o/libautoupdate) instead, it will be easier to set up, more transparent to users when they get a prompt and not slow down startup as much.) 89 | 90 | If you have done dynamic class loading in the past you might know that you can achieve a lot of these things using a `URLClassLoader`. We can use the `LaunchClassLoader` in much the same way, except with some extra gadgets related to loading Minecraft classes. 91 | 92 | ```java 93 | @Override 94 | public void injectIntoClassLoader(LaunchClassLoader classLoader) { 95 | try { 96 | File downloadedFile = downloadSomeDependencyIfNotAlreadyOnDisk(); 97 | classLoader.addURL(downloadedFile.toURI().toURL()); 98 | } catch (IOException e) { 99 | throw new RuntimeException(e); 100 | } 101 | } 102 | ``` 103 | 104 | ## Blackboard 105 | 106 | Sometimes you will want to communicate with other mods and their tweakers. This is most easily done through the blackboard. Directly accessing another mods tweaker is dangerous (although possible). Usually there is a lot of reflection involved and because we are so early in loading a lot of classes are not available yet (especially the ones outside of tweaker packages). 107 | 108 | This is where blackboards come in. Blackboards allow mods to easily share data. 109 | 110 | The blackboard is available using `Launch.blackboard` and is a simple `Map`. I would generally encourage you to only put objects in there that are made up entirely of simple java objects (`java.*` objects). 111 | 112 | Similarly I would encourage you to use fully qualified names if possible: `:::java "com.mymod.init.Tweaker.someProp"`. This way name collisions can be avoided. 113 | 114 | Let's go over some use cases of the blackboard. 115 | 116 | ### Delegating tweakers 117 | 118 | While you can have many tweakers in a devenv, loading multiple tweakers from a JAR is not possible. Instead you can load one tweaker which will then instruct the launch process to load a second tweaker. This is done via `(List) Launch.blackboard.get("TweakClasses")` 119 | 120 | ```java 121 | @Override 122 | public void injectIntoClassLoader(LaunchClassLoader classLoader) { 123 | List tweakClasses = (List) Launch.blackboard.get("TweakClasses"); 124 | tweakClasses.add("com.mymod.init.SomeOtherTweaker"); 125 | } 126 | ``` 127 | 128 | This can be combined with new dependencies added via `addURL` to load tweakers from dependencies as well. 129 | 130 | These types of tweakers are sometimes also called cascading tweakers. 131 | 132 | If instead of tweaker class names, you want full on objects, you can use `:::java (List) Launch.blackboard.get("Tweaks")`. This list is read only (while you can write elements into that list, doing so will not run any events on them). 133 | 134 | Another pit fall is when you can cascade new tweakers. You can only do so during `acceptOptions` and `injectIntoClassLoader`. At any other point in time the new tweakers can be either ignored or cause a crash. 135 | 136 | ### Negotiating 137 | 138 | When negotiating some kind of version or other token that needs to be inspected by all tweakers before doing a final decision it pays to know how tweakers are processed. So, now a word about that. 139 | 140 | The first tweaker that is loaded is the FML tweaker. This might be surprising, but the whole tweaker system is not actually done by Forge itself, but instead is part of what is called the launchwrapper. FML is just one big user of this system. The first FML tweaker now looks through your mod folder and loads a bunch of mods from there. First all `IFMLLoadingPlugin`s are loaded. Those are then wrapped into tweaker wrappers to hand them back to the launchwrapper. This way `IFMLLoadingPlugin`s can interact with tweakers in all the same way a normal `ITweaker` could (but notably with a more complex setup). 141 | 142 | After all those plugins are loaded the tweakers from those same mods are loaded also, both of those get added to the `TweakClasses` list from earlier as cascading tweakers. 143 | 144 | The tweakers and plugins are also sorted, based on the priority given to them using annotations (for `IFMLLoadingPlugin`) or the `TweakOrder` manifest attribute in a JAR. Note that the tweak order only applies for the methods called, not for the constructor or the `:::java static` init blocks. 145 | 146 | Most of this does not matter for most users. What does matter is in what order methods are executed. 147 | 148 | 1. Run the static init and the constructor for each tweaker, one after the after. 149 | 2. Then run `acceptOptions` and `injectIntoClassLoader` for each tweaker. 150 | 3. If any new tweakers got added to `TweakClasses`, go back to step 1. (Note that duplicate class names get removed here, in case two tweakers add the same tweaker to be cascaded) 151 | 4. Once there are no more tweakers to be constructed and processed we continue 152 | 5. Now we collect all the arguments using `getLaunchArguments` from every tweaker 153 | 6. After all that has been collected the primary tweaker gets asked to provide a main class using `getLaunchTarget`. 154 | 155 | As you can see there is a break in the middle, so you can synchronize all your tweakers by doing negotiation during `acceptOptions`, and then executing a final action in `getLaunchArguments`. For example you can put a version number into the blackboard in `acceptOptions` if you are higher than the current number in that blackboard variable. Then in `getLaunchArguments` you know that every other tweaker has put their number in the variable, so if you are still the highest variable, you are the tweaker with the most up to date version who should inject their dependency. Note here that you can still access the `LaunchClassLoader` after the `injectIntoClassLoader` method has been called using `Launch.classLoader`. 156 | 157 | ## Transformers 158 | 159 | This is the big one. Class transformers allow you to change arbitrary code in your own mod, other mods, Forge and even Minecraft itself. There are some exclusions (such as not allowing to modify other tweakers and some core libraries), but almost all code can be changed. 160 | 161 | !!!note 162 | This section presumes you have some familiarity with the java class file format. That kind of a tutorial would be a big undertaking, but if someone wants to pay me handsomely for it, i will do it. 163 | 164 | At it's core a class transformer operates on JVM class files. Every class loaded can be transformed before by simply changing the class files contents. To do this the class transformer is given some information on which class it is operating on, as well as the bytes making up the original file. It is then expected to hand back a new byte array containing the modified file. This `.class` file is then loaded by the JVM. 165 | 166 | Operating on raw bytes is rarely advisable and so Forge ships with the [asm library](https://asm.ow2.io/). Asm allows parsing a bytearray into a structure called a `ClassNode` (i will not be covering visitors here, even tho they can be more efficient). Accordingly a simple transformer scaffold to construct a `ClassNode` could look something like this: 167 | 168 | ```java 169 | public class TestTransformer implements IClassTransformer { 170 | @Override 171 | public byte[] transform(String name, String transformedName, byte[] basicClass) { 172 | if (!name.equals("net.minecraft.client.Minecraft")) return basicClass; 173 | ClassNode node = new ClassNode(); 174 | ClassReader reader = new ClassReader(basicClass); 175 | reader.accept(node, 0); 176 | 177 | doSomethingToTheClassNode(node); 178 | 179 | ClassWriter writer = new ClassWriter(0); 180 | node.accept(writer); 181 | return writer.toByteArray(); 182 | } 183 | } 184 | ``` 185 | 186 | Note that we check for the name first and immediately return the class bytes if the name does not match what we expect. Class transformers should be fast, if possible, since they are executed for every class and parsing a `ClassNode` is not necessarily cheap. 187 | 188 | Next we create a class node and hand it off to another method. Finally we write the transformed class node back to a byte array and return it. 189 | 190 | Also note how i used the name `net.minecraft.client.Minecraft`. Class names and method names can change depending on the environment. Forge generally runs under `MCP` names in the development environment and `searge` names in the live environment. While those have different names for methods, fields and variables, they do share the same class names, so checking the name this way is okay (but checking method names requires checking for both the searge and MCP names). 191 | 192 | If you plan on doing lots of class transformations you might want to create a base class of sorts: 193 | 194 | ```java 195 | public abstract class BasePatch implements IClassTransformer { 196 | 197 | protected abstract String getTargetedName(); 198 | 199 | protected abstract ClassNode transformClassNode(ClassNode classNode); 200 | 201 | @Override 202 | public byte[] transform(String name, String transformedName, byte[] basicClass) { 203 | if (!name.equals(getTargetedName())) return basicClass; 204 | ClassNode node = new ClassNode(); 205 | ClassReader reader = new ClassReader(basicClass); 206 | reader.accept(node, 0); 207 | 208 | node = transformClassNode(node); 209 | 210 | ClassWriter writer = new ClassWriter(0); 211 | node.accept(writer); 212 | return writer.toByteArray(); 213 | } 214 | } 215 | ``` 216 | 217 | This way you will also have a place to store all the helpful utilities you will most likely create. 218 | 219 | How about we actually change something then? 220 | 221 | 222 | ```java 223 | // Note that while i use the base patch class from earlier here, you could also just do this directly in a class transformer without utilities 224 | public class MinecraftPublicMaker extends BasePatch { 225 | @Override 226 | protected String getTargetedName() { 227 | return "net.minecraft.client.Minecraft"; 228 | } 229 | 230 | @Override 231 | protected ClassNode transformClassNode(ClassNode classNode) { 232 | for (MethodNode method : classNode.methods) { 233 | // for every method we set the access flag to public using bit wise operations 234 | // and remove the private and protected bits 235 | // Note that if you check for method.name here, you will need to check two strings - searge and MCP 236 | method.access = (method.access | Modifier.PUBLIC) & ~(Modifier.PRIVATE | Modifier.PROTECTED); 237 | } 238 | return classNode; 239 | } 240 | } 241 | ``` 242 | 243 | Once you have access to a class node you can change anything. I won't do a full tutorial on bytecode and how to change the code of methods here, but you are welcome to movitate me to do so. Class transformers are vastly more powerful than mixins. 244 | 245 | 246 | One final note about debugging. Since changing the class file can cause a lot of unexpected results, especially when (not if) you mess up and create broken bytecode it can be helpful to dump transformed classes. That can be done by specifying `-Dlegacy.debugClassLoading=true -Dlegacy.debugClassLoadingSave=true` as jvm arguments. This will create a folder called `CLASSLOADER_TEMP(and some number)`. In there you will find the bytes after all transformations have been done. This will allow you to decompile your transformed classes and debug your transformers. Make sure to clear out space for more class loader dump folders every 10 runs or it will stop working. And make sure to always test your transformers on a live environment as well as a development one! 247 | 248 | ## `IFMLLoadingPlugin`s 249 | 250 | Why are there `ITweaker`s and `IFMLLoadingPlugin`s you might ask yourself. The simple answer is: everybody wanted to do their own standard. `ITweaker`s are by the launch wrapper which is not a Forge product itself, but is used to launch Forge. `IFMLLoadingPlugin`s are done by Forge and as such are more tightly integrated into Forge, but since they are done by Forge they are also needlessly complicated. 251 | 252 | Some of the custom functionality provided by Forge is quite nice, such as providing a custom mod container, but for the most part `IFMLLoadingPlugin`s are used for one reason: Unlike tweakers (which are loaded from the `mods` folder), they are loaded from the classpath. This is useful for libraries, since it allows them to specify a loading plugin for class transformations or as an additional entrypoint since libraries dont have an init event like a mod does. A lot of libraries will just use that `IFMLLoadingPlugin` to load a `ITweaker` using [cascading tweakers](#delegating-tweakers), since `ITweaker`s generally have nicer semantics. 253 | 254 | `IFMLLoadingPlugin`s also allow to specify a sorting order using annotations which can be nice if you need some early spot to add dependencies. 255 | 256 | ## Mod loading 257 | 258 | One day you might want to leave the dev environment and once you do you will run into some problems. You specify your tweaker like you used to do with mixins (the `"TweakClass"` manifest attribute) and now when you run your mod it executes your tweaker. But - *just* your tweaker. Forge has made the executive decision to exclude any JAR that has a tweaker or an `IFMLLoadingPlugin` from participating in regular mod discovery. In order to make yourself eligible again for normal mod execution, you will need to remove yourself from that list. Similarly, if you still want to use mixins, you will not only need to load the mixin tweaker via delegation, but you will also need to add yourself to the mixin container list, since mixin checks that your `TweakClass` entry is equal to the expected mixin tweaker. 259 | 260 | ```java 261 | @Override 262 | public void acceptOptions(List args, File gameDir, File assetsDir, String profile) { 263 | // Exercise for the reader: add delegation to the mixin tweaker 264 | URL location = getClass().getProtectionDomain().getCodeSource().getLocation(); 265 | if (location == null) return; 266 | if (!"file".equals(location.getProtocol())) return; 267 | try { 268 | // Add yourself as mixin container 269 | MixinBootstrap.getPlatform().addContainer(location.toURI()); 270 | String file = new File(location.toURI()).getName(); 271 | // Remove yourself from both the ignore list in order to be eligible to be loaded as a mod. 272 | CoreModManager.getIgnoredMods().remove(file); 273 | CoreModManager.getReparseableCoremods().add(file); 274 | } catch (URISyntaxException e) { 275 | e.printStackTrace(); 276 | } 277 | } 278 | ``` 279 | 280 | ## Future prospects 281 | 282 | This tutorial already took a fairly long time and this is a really advanced topic not many people will read about. If you want more tutorials for advanced stuff like this, such as docs about FML loading internals or how you could create your own Minecraft client using launchwrapper (and why you shouldn't), or even a full JVM bytecode manipulation tutorial, then let me know. While I would love to create more tutorials like this, I do need some incentive to make these advanced/expert level tutorials. 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | -------------------------------------------------------------------------------- /docs/vanilla.md: -------------------------------------------------------------------------------- 1 | # Looking through Vanilla Code 2 | 3 | Looking through Minecraft's code is often the only way to figure out how things work, so let's go on a little code safari through Vanilla code. 4 | 5 | ## Setup 6 | 7 | You might have already looked through some vanilla code by just ++ctrl++ clicking on a method and just having IntelliJ decompile the code. While this works for basic stuff, a lot of features are broken in that decompiled code. For example, you can't use a debugger to break on those lines, there is less Javadoc, and more. A lot of that is fixed by manually decompiling Minecraft. 8 | 9 | First, go into your :simple-gradle: gradle tab and select the `gradle genSources` task. This will take a while to decompile the entire Minecraft JAR. Once this task is done you can navigate into a Minecraft class the normal way. This might still show you the IntelliJ decompiled code. You can tell, because there will be a banner at the top labeled "Decompiled class file". In that same banner you should either find a "Show source file" button, or a "Select sources" button. The "Select sources" button will allow you to select the `-sources.jar`. It should be in the same folder that your normal Minecraft JAR is already. Once you selected one of these buttons, you should automatically use the generated sources, instead of the decompiled ones. 10 | 11 | ## Minecraft 12 | 13 | The `Minecraft` class is the entrypoint for almost all things you do as a client mod. The `Minecraft` is the actual Minecraft client instance. It contains references to nearly anything going on in the client, either directly (for example the `thePlayer` field) or transitively (for example the network handler `thePlayer.sendQueue`). You can get the reference to the `Minecraft` class using `:::java Minecraft.getMinecraft()`. I recommend you build your own set of utility methods for accessing things inside of the `Minecraft` class. 14 | 15 | Since the `Minecraft` class is quite big, I'll give you a few of the more interesting things to look at: 16 | 17 | - `theWorld` allows you to access information about the current world the player is in, like blocks and entities. 18 | - `thePlayer` allows you to get all kinds of information about the current player, like the position, inventory, and more, just like with any other player entity. 19 | - `thePlayer.sendQueue` allows you to send any kind of packet you want to the server. I wouldn't recommend you do this directly and instead use the `PlayerControllerMP`. Remember to closely follow the hypixel rules when doing this. The other useful thing in here is to fake packets from the server by calling the `handle*` methods. Oftentimes you would rather call the methods directly that that packet will eventually call, but injecting packets here allows other mods to react to those packets. Be careful however! Modifying Minecrafts internal state can cause it to send illegal packets to the server. Methods in here are a prime target for mixins, allowing you to react to individual packets. Make sure to follow hypixel rules and to not provide the player with information that they couldn't otherwise get in the vanilla game. 20 | - `playerController` allows you to do things as the player in a more user friendly way than with packets. Minecraft often has checks here that prevent you from sending illegal packets or just a nicer described API. Remember to follow the Hypixel rules however. The other, far more practical, use of this class is as a mixin target. Whenever you want to react to the player doing something that doesn't already have a client side event, you can probably mix into a method in here to work it out. 21 | - `currentScreen` allows you to read (only read) the currently open GUI screen. 22 | - `displayGuiScreen` allows you to open screens (or close them, by calling this with `:::java null`) 23 | - `mcResourceManager` allows you to `registerReloadListener` which will call a function of yours after a resource pack reload. This allows you to re-read shaders, textures, or config files that you read from the minecraft asset folder. 24 | - `addScheduledTask` allows you to delay an action by a tick, or to reschedule something to be run on the Minecraft thread, if you are on another thread. 25 | 26 | 27 | 28 | ## GuiScreen 29 | 30 | For our next subject of study, instead of looking how we can use a class from the outside, let's see how Minecraft itself uses the class on the inside. Let's look at how `GuiIngameMenu` (the escape menu) uses `GuiScreen`. 31 | 32 | Of course the generated code isn't super beautiful, but we can gain some insights. We can see how `initGui` is used to add a bunch of `GuiButton`s to the `buttonList`. We can also see how `actionPerformed` is used to handle clicks on these buttons. We can also see a `GuiButton` be disabled. 33 | 34 | ## Scoreboard 35 | 36 | Let's look at one last example: the scoreboard. The earlier sections all showed how you can discover new things, but what about investigating something specific you need. Let's say you want to read out what the scoreboad contains. It has plenty of info that we might want to use for our SkyBlock mod, like the Purse, Bits, current location, Jacobs Events, Season, whether we are in SkyBlock or not. 37 | 38 | Let's start out with a blank slate. We don't know where any code is, but we can guess that it probably mentions "Scoreboard" somewhere. If we use IntelliJs Symbol Search feature (++shift+shift++) to search for "Scoreboard" (with "Include non-project items" turned on) we can quickly find a class called `Scoreboard`. In here we can find a lot of information. A bit too much info, actually. 39 | 40 | Here we can find information about teams, objectives, scores, slots and criteria. All of this sounds a bit more confusing than the simple `:::java List` we would like. A next step might be to go to the [minecraft wiki and read up on scoreboard terminology](https://minecraft.wiki/w/Scoreboard). And while i can recommend you to read that article if you want a deeper understanding, for us there is an easier path. Since we don't care about most things, we can just look up the code that is used to render the scoreboard on the side of the screen and figure out which methods to call from there. 41 | 42 | At this point it helps to know that the class `GuiIngame` is responsible for rendering a lot of HUD elements (like the scoreboard). From there you can find the method `renderScoreboard` quite easily by searching for "scoreboard" in that class. 43 | 44 | If we pretend for a second that we don't have this information already, we can find the `renderScoreboard` method an other way: we know that the info for rendering the scoreboard is inside the `Scoreboard` class. Now we can use "Right click" -> "Show usages" (or ++ctrl+alt+7++) to look up usages of `Scoreboard`. We might need to configure IntelliJ to look through usages in libraries as well in the popup. From there we can find two usages roughly related to rendering: `GuiPlayerTabOverlay` and `GuiIngame` (as well as `GuiIngameForge`, which just overrides `GuiIngame`). Since we care about the sidebar scoreboard, and not about the scores displayed in the tablist we have once again arrived at our `GuiIngame.renderScoreboard` method. 45 | 46 | Once we have found the `renderScoreboard` method we need to figure out what it does. There are a lot of convoluted render calls going on, but if we ignore all the actual rendering for a second and focus on just the calls to the `Scoreboard` and adjacent class we can figure out what it does: 47 | 48 | - First, get the scoreboard, for a given `ScoreObjective`. We might need to figure out where to get that from in a second. 49 | - Next we call `scoreboard.getSortedScore(objective)` to extract the scores. Those scores have player names attached, and we filter out all players who start with `#`. 50 | - Next we remove the beginning of the list until we only have 15 elements left. This gives us a hint that the list might be sorted from lowest to highest. 51 | - Next we have a loop iterating over all scores, and using `ScorePlayerTeam.formatPlayerName` to format the player names and then appending the score to the right to find the longest string. This is probably here to align all of our scores. 52 | - At this point we have all the info we need to infer the entire process. We know how to format the player names, where to get them from and in which order they are found. 53 | 54 | The rest of that method is dedicated to more rendering. If we go through the rest of the code, we can see some of our suspicions confirmed (such as the rendering starting from the bottom and going upwards as we iterate over the list), but we already have a hunch of how we could get those strings ourselves. 55 | 56 | Now the only mystery left is where we get our `ScoreObjective` from. Earlier we saw a method in `Scoreboard` called `getObjective` which returned a `ScoreObjective`, but it needed a name. So how about we look which method calls `renderScoreboard`. If we use ++ctrl+alt+7++ again to look up usages, we can see that `GuiIngameForge` and `GuiIngame` both call this function. Let's first look at the forge code, since that might override some vanilla behaviour. Here we can see `scoreobjective1` which is obtained from `getObjectiveInDisplaySlot` with either `1` or `3 + getTeamColor(currentPlayer)`. We might step through with our debugger to find out which of these paths Hypixel uses to set our scoreboard, or we might just try out the simpler case of always using `1` or we might reimplement this entire logic. We can also see that `theWorld.getScoreboard()` is used to get the scoreboard instance. If we investigate a bit more we might even find the `Scoreboard.getObjectiveDisplaySlot` confirming our suspicions that `1` means sidebar. 57 | 58 | Now we can combine all that knowledge to write our own sidebar scoreboard parser: 59 | 60 | ```java 61 | final int SIDEBAR_SLOT = 1; 62 | Scoreboard scoreboard = Minecraft.getMinecraft().theWorld.getScoreboard(); 63 | ScoreObjective objective = scoreboard.getObjectiveInDisplaySlot(SIDEBAR_SLOT); 64 | List scoreList = scoreboard.getSortedScores(objective) 65 | .stream() 66 | .limit(15) 67 | .map(score -> 68 | ScorePlayerTeam.formatPlayerName( 69 | scoreboard.getPlayersTeam(score.getPlayerName()), 70 | score.getPlayerName())) 71 | .collect(Collectors.toList()); 72 | Collections.reverse(scoreList); 73 | for (String s : scoreList) { 74 | LogManager.getLogger("Scoreboard").info(s); 75 | sender.addChatMessage(new ChatComponentText(s)); 76 | } 77 | ``` 78 | 79 | After writing this command and testing it in a single player world everything seems to work out! But not so fast! When we actually try this command on Hypixel something bad happens. 80 | 81 | The printout in chat seems all right. But if we also print out the string into the console we see a bunch of weird emojis: 82 | ``` title="Output" 83 | [17:03:46] [main/INFO] (Scoreboard) §701/29/24 §8m22💣§8AA 84 | [17:03:46] [main/INFO] (Scoreboard) 👽 85 | [17:03:46] [main/INFO] (Scoreboard) Autumn 30th🔮 86 | [17:03:46] [main/INFO] (Scoreboard) §710:30am §e☀🐍 87 | [17:03:46] [main/INFO] (Scoreboard) §7⏣ §bVillage👾 88 | [17:03:46] [main/INFO] (Scoreboard) §7♲ §7Ironman🌠 89 | [17:03:46] [main/INFO] (Scoreboard) 🍭 90 | [17:03:46] [main/INFO] (Scoreboard) Piggy: §668,463⚽ 91 | [17:03:46] [main/INFO] (Scoreboard) Bits: §b46,180🏀 92 | [17:03:46] [main/INFO] (Scoreboard) 👹 93 | [17:03:46] [main/INFO] (Scoreboard) §6Spooky Festiva🎁§6l§f 31:14 94 | [17:03:46] [main/INFO] (Scoreboard) 🎉 95 | [17:03:46] [main/INFO] (Scoreboard) §ewww.hypixel.ne🎂§et 96 | ``` 97 | 98 | Turns out, hypixel uses Emojis as player name in order to never conflict with anyones player name. Those don't get rendered by vanillas text renderer, but our code of course doesn't know this yet. 99 | 100 | At this point we are a bit tired, so instead of investigating which characters don't or do get rendered by Minecraft we might settle for the easy way out. Simply checking how wide Minecraft thinks a char is. If it is 0 wide, it is probably not being rendered: 101 | 102 | 103 | ```java 104 | String stripAliens(String text) { 105 | StringBuilder sb = new StringBuilder(); 106 | for (char c : text.toCharArray()) { 107 | if (Minecraft.getMinecraft().fontRendererObj.getCharWidth(c) > 0 || c == '§') 108 | sb.append(c); 109 | } 110 | return sb.toString(); 111 | } 112 | ``` 113 | 114 | Or you could go even easier and just manually have a blacklist of emojis (since they always seem to be the same). In either case you arrive at a beautiful, clean scoreboard string list: 115 | 116 | ``` title="Output" 117 | [17:09:57] [main/INFO] (Scoreboard) §701/29/24 §8m23§8AP 118 | [17:09:57] [main/INFO] (Scoreboard) 119 | [17:09:57] [main/INFO] (Scoreboard) Autumn 30th 120 | [17:09:57] [main/INFO] (Scoreboard) §75:50pm §e☀ 121 | [17:09:57] [main/INFO] (Scoreboard) §7⏣ §bVillage 122 | [17:09:57] [main/INFO] (Scoreboard) §7♲ §7Ironman 123 | [17:09:57] [main/INFO] (Scoreboard) 124 | [17:09:57] [main/INFO] (Scoreboard) Piggy: §668,468 125 | [17:09:57] [main/INFO] (Scoreboard) Bits: §b46,180 126 | [17:09:57] [main/INFO] (Scoreboard) 127 | [17:09:57] [main/INFO] (Scoreboard) §6Spooky Festiva§6l§f 25:04 128 | [17:09:57] [main/INFO] (Scoreboard) 129 | [17:09:57] [main/INFO] (Scoreboard) §ewww.hypixel.ne§et 130 | ``` 131 | 132 | Hopefully this last excurs has showed you how you might go about finding information in Minecraft's source code. Most things you will encounter are not going to be documented, and this scoreboard example was just one of the many example I could've chosen for this excurs. So don't be frustrated, because finding these kinds of things is exactly what the fun of modding is all about. 133 | 134 | And lastly: If you do get stuck — seek help. Other people probably have walked this path before. Finding existing [open source projects][mod-list] and using their techniques for extracting information out of Minecraft can really speed up your work. Just make sure to properly check licenses and credit your code. Preferably both in a comment in the code, as well as the README of your mod. Or even better: ask before you take code. Most developers have been in your position before and will be sympathetic to someone just starting out with modding; Stealing code will leave you without that community and possible in legal troubles, once you encounter your next road block. 135 | 136 | 137 | -------------------------------------------------------------------------------- /includes/shared_links.md: -------------------------------------------------------------------------------- 1 | [mod-list]: https://sbmw.ca/mod-lists/skyblock-mod-list/ 2 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Legacy Modding Wiki by nea89 2 | site_url: https://moddev.nea.moe/ 3 | site_description: | 4 | My wiki for 1.8.9 Forge modding beginners and experts alike. 5 | repo_name: nea89o/ModDevWiki 6 | repo_url: https://github.com/nea89o/ModDevWiki 7 | edit_uri: blob/master/docs 8 | 9 | 10 | 11 | nav: 12 | - index.md 13 | - ide-setup.md 14 | - events.md 15 | - commands.md 16 | - screens.md 17 | - inventories.md 18 | - vanilla.md 19 | - Mixins: 20 | - mixins/index.md 21 | - mixins/accessors.md 22 | - mixins/adding-fields.md 23 | - mixins/simple-injects.md 24 | - hotswap.md 25 | - tweakers.md 26 | - https.md 27 | 28 | 29 | validation: 30 | omitted_files: warn 31 | absolute_links: warn 32 | unrecognized_links: warn 33 | 34 | theme: 35 | name: material 36 | features: 37 | - content.code.annotate 38 | - content.code.copy 39 | - content.tooltips 40 | - navigation.top 41 | - navigation.instant 42 | - navigation.instant.prefetch 43 | - navigation.instant.progress 44 | - navigation.tracking 45 | - navigation.indexes 46 | - content.action.edit 47 | - search.suggest 48 | - search.share 49 | icon: 50 | repo: fontawesome/brands/git-alt 51 | palette: 52 | # Palette toggle for light mode 53 | - media: "(prefers-color-scheme: light)" 54 | scheme: default 55 | primary: lime 56 | toggle: 57 | icon: material/weather-sunny 58 | name: Switch to dark mode 59 | 60 | # Palette toggle for dark mode 61 | - media: "(prefers-color-scheme: dark)" 62 | scheme: slate 63 | primary: light blue 64 | toggle: 65 | icon: material/weather-night 66 | name: Switch to light mode 67 | 68 | 69 | plugins: 70 | - search 71 | - social 72 | - material-plausible 73 | - git-revision-date-localized: 74 | enable_creation_date: true 75 | fallback_to_build_date: true 76 | 77 | extra: 78 | analytics: 79 | provider: plausible 80 | domain: moddev.nea.moe 81 | src: "https://pla.nea.moe/js/plausible.js" 82 | 83 | markdown_extensions: 84 | - abbr 85 | - admonition 86 | - attr_list 87 | - def_list 88 | - footnotes 89 | - md_in_html 90 | - toc: 91 | permalink: true 92 | - pymdownx.arithmatex: 93 | generic: true 94 | - pymdownx.betterem: 95 | smart_enable: all 96 | - pymdownx.caret 97 | - pymdownx.details 98 | - pymdownx.emoji: 99 | emoji_generator: !!python/name:material.extensions.emoji.to_svg 100 | emoji_index: !!python/name:material.extensions.emoji.twemoji 101 | - pymdownx.highlight: 102 | anchor_linenums: true 103 | line_spans: __span 104 | pygments_lang_class: true 105 | - pymdownx.inlinehilite 106 | - pymdownx.keys 107 | - pymdownx.magiclink: 108 | normalize_issue_symbols: true 109 | repo_url_shorthand: true 110 | user: squidfunk 111 | repo: mkdocs-material 112 | - pymdownx.mark 113 | - pymdownx.saneheaders 114 | - pymdownx.smartsymbols 115 | - pymdownx.snippets: 116 | auto_append: 117 | - includes/shared_links.md 118 | - pymdownx.superfences: 119 | custom_fences: 120 | - name: mermaid 121 | class: mermaid 122 | format: !!python/name:pymdownx.superfences.fence_code_format 123 | - pymdownx.tabbed: 124 | alternate_style: true 125 | combine_header_slug: true 126 | slugify: !!python/object/apply:pymdownx.slugs.slugify 127 | kwds: 128 | case: lower 129 | - pymdownx.tasklist: 130 | custom_checkbox: true 131 | - pymdownx.tilde 132 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs-material[imaging] 2 | mkdocs-git-revision-date-localized-plugin 3 | mkdocs-git-committers-plugin-2 4 | material-plausible-plugin 5 | -------------------------------------------------------------------------------- /serve.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ( cd "$(dirname "$0")" 3 | if ! [[ -d venv ]]; then 4 | python3 -m venv venv 5 | fi 6 | source venv/bin/activate 7 | pip install -r requirements.txt 8 | mkdocs serve 9 | ) 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import {}; 3 | in pkgs.mkShell { 4 | buildInputs = [ 5 | (pkgs.python3.withPackages (python-pkgs: [python-pkgs.virtualenv])) 6 | pkgs.cairo 7 | ]; 8 | shellHook = '' 9 | export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:${pkgs.cairo}/lib" 10 | python -m venv "$PWD"/venv 11 | source "$PWD"/venv/bin/activate 12 | ''; 13 | } 14 | --------------------------------------------------------------------------------