aboutsummaryrefslogtreecommitdiff
path: root/CONTRIBUTING.md
diff options
context:
space:
mode:
Diffstat (limited to 'CONTRIBUTING.md')
-rw-r--r--CONTRIBUTING.md191
1 files changed, 191 insertions, 0 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 62ffd979e..57f22f2e2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -180,3 +180,194 @@ Plugin [HotSwap Agent](https://plugins.jetbrains.com/plugin/9552-hotswapagent) t
Follow [this](https://forums.Minecraftforge.net/topic/82228-1152-3110-intellij-and-gradlew-forge-hotswap-and-dcevm-tutorial/)
tutorial.
+
+## 1.21 / Modern version development
+
+You might have noticed that while the SkyHanni source code is found in `src/`, the actual tasks for compiling, building and running the mod
+are located in a subproject called `1.8.9`. This is because SkyHanni is preparing for the eventual fall of 1.8.9 (via the foraging update or
+otherwise).
+
+To do so (while not disrupting regular development) we use [preprocessor](https://github.com/Deftu/RM-Preprocessor). Preprocessor
+automatically transforms code based on mappings as well as comment directives to create multiple variants of your source code for
+different Minecraft versions.
+
+Note also that the only targets we consider are 1.8.9 and 1.21 (or whatever the latest version we may target). The other versions are only there
+to make mappings translate more easily (more on that later).
+
+### Goals
+
+It is the explicit goal of this operation to passively generate a 1.21 version of SH using preprocessor. To this end, contributors are
+encouraged to add mappings and preprocessing directives to their features to make them compile on 1.21. *However*, this is considered a very
+low priority. Due to the confusing nature (and the slower initial setup time due to decompiling four versions of Minecraft), this feature
+is disabled by default. Similarly, it is up to each contributor to decide if they want to learn how to use preprocessor mappings and
+directives. An explicit non-goal is to maintain two SH versions continuously; instead, we only want to make the eventual transition to 1.21 a task
+that can be slowly worked on over a long span of time.
+
+### Set Up
+
+The modern version variants can be set using `skyhanni.multi-version` in `.gradle/private.properties` to three levels.
+
+`off` completely disables any preprocessor action or alternative versions. There will be only one project (although still at the `:1.8.9`
+subproject path), and alternative version sources will not be generated (although old generated sources **will not be deleted**). To make
+setting up a dev environment as fast and discernible as possible, this is the default option.
+
+`preprocess-only` adds the `preprocessCode` task as well as all the version subprojects. Compiling or running newer versions is not
+possible, but the `preprocessCode` task can be run manually to inspect the generated source code. This mode is what should most often be
+used when making alterations to the mappings or modifying preprocessor directives. Note that while this setting generally ignores any failed
+renaming attempts, if something is so badly mangled that it cannot even guess a name for a function, it will still break the build. Those
+situations should be rare, however. (In the entire SH codebase prior to me introducing this system I only found <10 such cases). You can
+specifically compile 1.8.9 using `./gradlew :1.8.9:build`. This does not affect the regular execution of the client which will only compile
+1.8.9.
+
+`compile` enables compilation for the `:1.21` subproject. This means that a `build` or `assemble` task will try (and fail) to compile a
+1.21 (as well as 1.8.9) JAR. This mode may be useful for someone seeking out issues to fix, but is generally not useful in day to day
+operations since the compile will never succeed and will block things like hotswap compilations (via <kbd>CTRL+F9</kbd>) from completing.
+
+### Improving mappings
+
+The different project versions are set up in such a way that each version depends on a slightly older version from which it is then adapted.
+There are two main versions (1.8.9 and 1.21), but there are also a few bridge versions. These exist to make remapping easier since automatic
+name mappings between 1.8.9 and 1.21 do not really exist. This is the current layout for our remaps: First, we remap to 1.12. This still
+largely uses the old rendering system and has a lot of similar names to 1.8.9. As such, only very little needs to be adapted in terms of
+behaviour, and at best, a couple of names need to be updated manually. The big jump is from 1.12 to 1.16. We use 1.16 since we don't want to
+make too large of a jump (which could lead to a lot more missing names), but we still want to jump to a version with Fabric (and specifically
+with Fabric intermediary mappings). We also can't really jump to an earlier version since 1.14 and 1.15 have a really poor Fabric API and
+still have some of the old rendering code, meaning we would need to adapt to two slightly different rendering engines instead of just one
+big rendering change. Despite the preprocessor's best efforts, this version will likely have the most manual mapping changes. Note that we
+actually have two projects on 1.16. There is the Forge project, which is the one we remap to first. Then we remap to the corresponding
+Fabric/Yarn mappings. This is because remapping between Searge and Yarn is very inconsistent unless it is done on one and the same version.
+Finally, we remap from 1.16 to 1.21. This is a fairly small change, especially since Fabric intermediary mappings make different names
+between versions very rare. The only real changes that need to be done in this jump are behavioural ones.
+
+The preprocessor does some built-in remapping (changing names), based on obfuscated names, but sometimes the automatic matching fails. If it
+cannot find a new name (or the name it automatically determines was wrong), you can change the corresponding mapping. In order to make
+this as smooth as possible, it is generally recommended to find the earliest spot at which the mappings deviate. So fixing a mapping on the
+hop from 1.16 to 1.21 is generally not recommended. This is because, while we do not care about 1.12 or 1.16 compiling for its own merit,
+we do care about the automatically inferred name changes from 1.12 to 1.16 and so on, which only work if those versions already have the
+correct names available.
+
+#### A missing/incorrect name
+
+This is the easiest part. If a name for a function simply could not be automatically remapped, all you need to do is to add an entry in the
+corresponding mapping.txt file. These can be found at `versions/mapping-<newVersion>-<oldVersion>.txt`.
+
+```
+# You can use # to comment lines
+
+# You can rename a class simply by writing the two names
+# The first name is the name on the newer version (the first one in the file name); the second one is the name in the old version.
+net.minecraft.util.math.MathHelper net.minecraft.util.MathHelper
+# If you want to rename an inner class, remember to use $s
+net.minecraft.world.scores.Team$Visibility net.minecraft.scoreboard.Team$EnumVisible
+
+# You can rename a field by writing the name of the containing class in the new version, and then the new and old name of the field.
+# Again, the first field name is the one in the newer version (first in the file name).
+net.minecraft.world.entity.Entity xOld prevPosX
+
+# Finally, you can also rename methods. To do so, you need to first specify the name of the containing class in the new version, then
+# the name of the new and old method name. The first method name is the newer version name (first in the file name).
+net.minecraft.util.text.Style getHoverEvent() getChatHoverEvent()
+```
+
+Adding a mapping like this is the easiest way to fix a broken method call, field access, or class reference. It will also apply to all
+files, so you might be fixing issues in files you didn't even look at. It will even work in mixin targets, as long as they are unambiguous
+(consider using the method descriptor instead of just the method name for your mixin). However, if something aside from the name changed,
+this will not suffice.
+
+#### Conditional compilation
+
+In addition to the built-in remapping, there is also the more complicated art of preprocessor directives. Directives allow you to comment or
+uncomment sections of the code depending on the version you are on. Uncommented sections are renamed as usual, so even within those directives,
+you only need to write code for the *lowest* version that your comment is active in. As such, I once again highly recommend to target your
+directive to the lowest version in which it applies, so that other sections that call into that code as well as your code can make use of
+as many automatic renames as possible.
+
+There is only really one directive, which is `if`. Take this function, for example:
+
+```kt
+private fun WorldClient.getAllEntities(): Iterable<Entity> =
+//#if MC < 1.16
+ loadedEntityList
+//#else
+//$$ entitiesForRendering()
+//#endif
+```
+
+The first `#if` instructs the preprocessor to only uncomment the following code if the Minecraft version is less than 1.16. Then, the `#else`
+uncomments the other section on versions 1.16 and above. Finally, the `#endif` ends the else block and lets the following functions always remain
+active. To distinguish regular comments from preprocessor comments, preprocessor only works with comments that start with `//$$`. So let's
+walk through what is happening here.
+
+In 1.8.9, the code remains unchanged. **Note that this means the programmer is responsible for commenting out the unused parts.
+The preprocessor will never change the `src/` directory**.
+
+Next, the preprocessor converts the code to 1.12. 1.12 still has the `loadedEntityList` as well as the same name for the `WorldClient` and
+`Entity` classes, so nothing is changed.
+
+Next, the code gets converted to 1.16 Forge. Since 1.16 is not less than 1.16, it will comment out the first line and uncomment the second line.
+1.16 Forge also uses a different name for `WorldClient` and a different package for `Entity`, so those are also changed (the package change
+is only visible in the imports):
+
+```kt
+private fun ClientLevel.getAllEntities(): Iterable<Entity> =
+//#if MC < 1.14
+//$$ loadedEntityList
+//#else
+ entitiesForRendering()
+//#endif
+```
+
+Now the code gets converted to 1.16 Fabric. Since those two targets are on the same Minecraft version name changes almost never fail to be
+done automatically. Notice the different names for `ClientWorld` as well as `entities`. The method is called `getEntities()` on Fabric, but
+since this is Kotlin code, the preprocessor automatically cleans up `getEntities()` using
+[Kotlin property access syntax](https://kotlinlang.org/docs/java-interop.html#getters-and-setters).
+
+```kt
+private fun ClientWorld.getAllEntities(): Iterable<Entity> =
+//#if MC < 1.14
+//$$ loadedEntityList
+//#else
+ entities
+//#endif
+```
+
+Finally, the code gets converted to 1.21 using intermediary mappings. This last step does not bring any new challenges, so we end up with:
+
+```kt
+private fun ClientWorld.getAllEntities(): Iterable<Entity> =
+//#if MC < 1.14
+//$$ loadedEntityList
+//#else
+ entities
+//#endif
+```
+
+#### If expressions
+
+Let's look at the syntax of those `#if` expressions.
+
+First of all, the `#else` block is optional. If you just want code on some versions (for example for adding a method call that is implicitly
+done on newer versions, or simply because the corresponding code for newer versions has to be done in some other place), you can just omit
+the `#else` section and you will simply not compile any code at that spot.
+
+There is also an `#elseif` in case you want to switch behaviour based on multiple version brackets. Again, while we don't actually target
+1.12 or 1.16, making those versions compile will help other parts of the code to upgrade to 1.21 more cleanly and easily. So, making those
+versions work (or at least providing a stub like `error("Not implemented on this version") as List<Entity>` to make types infer correctly)
+should be something you look out for.
+
+`#if` and `#elseif` also do not support complicated expressions. The only operations supported are `!=`, `==`, `>=`, `<=`, `<` and `>`. You
+cannot join two checks using `&&` or similar, instead needing to use nested `#if`s.
+
+The actual versions being worked with here are not actually semantically compared Minecraft versions, but instead integers in the form
+`major * 10000 + minor * 100 + patch`. So, for example, `1.12` turns into `11200`. Both `11200` and `1.12` can be used in directives, but
+`1.12` style values are generally easier to understand.
+
+You can also check if you are on Forge using the `FORGE` variable. It is set to either 1 or 0. Similarly, there is also a `JAVA` variable to
+check the Java version this Minecraft version is on. For the `FORGE` variable there is an implicit `!= 0` to check added if you just check
+for the variable using `#if FORGE`.
+
+#### Helpers
+
+Sadly, `#if` expressions cannot be applied globally (unlike name changes), so it is often very helpful to create a helper method and call
+that method from various places in the codebase. This is generally already policy in SH for a lot of things. For more complex types that
+change beyond just their name (for example different generics), a `typealias` can be used in combination with `#if` expressions.