Avoid relying on package name to parse CraftBukkit version
0dinD opened this issue · comments
One of the Paper developers (kennytv) made an announcement today on their Discord server, with a warning about relying on the version string in the CraftBukkit package name:
Important info for people reflecting on CraftBukkit internal code
This is very important if you for whatever reason use reflection to either
- parse the relocated package version.
- call CB internals.
At some point in the future, we might only provide jars without relocation, given it is a nonsensical practice resulting in unavoidably bad code design and unexpected incompatibilities in a large number of plugins. While we will be able to automagically remap both direct and reflective calls to the relocated package, parsing the package version is not supported and WILL break at some point in the future. The changes we have planned should make working with internals a lot easier, since we recognize that sometimes (though not as often as some might think) there is no better alternative.
...
This will affect AnvilGUI, because it relies on the CraftBukkit package name (e.g. v1_20_R2
) for the VersionMatcher:
Seeing as most people use Paper (or a fork thereof) these days, this is a pretty important issue that needs to be addressed sooner or later. I don't think Spigot has made any guarantees that the CraftBukkit package naming scheme will be kept around in the future either.
Relying on the CraftBukkit package name is an imperfect solution either way, because it's quite arbitrary and doesn't shield us from renamed method mappings (see #221, for example, which resulted in having to create a custom 1_19_1_R1
wrapper that doesn't map to any CraftBukkit version).
In terms of solutions, I think we could just base the wrapper versions on the Minecraft versions set by Mojang instead. All we really care about is "when do the mappings change?", and the answer to that question is "when Mojang releases a new version", as far as I can tell.
Unfortunately, there is no handy API in Bukkit/Spigot for getting the Minecraft version (I mean, you can get some version strings, but the format is not really set in stone). PaperLib is a good option (it works on CraftBukkit and Spigot as well, not just on Paper), it has a nice API for getting the Minecraft version: https://github.com/PaperMC/PaperLib/blob/69e76fc9036e7b5f2c967bd21db31043724ef9e3/src/main/java/io/papermc/lib/PaperLib.java#L224-L276
I am happy you are bringing this up. We'll need to base it on something exposed in the Bukkit API unfortunately though to maintain compatibility. Could we just use Bukkit#getVersion()
?
We'll need to base it on something exposed in the Bukkit API unfortunately though to maintain compatibility.
PaperLib is compatible with all CraftBukkit-based servers, it's just a compatibility library that exposes some useful Paper APIs to plugins that also want to support CraftBukkit and Spigot. If the server is running Paper, the proper API is used, and otherwise the API is emulated, for example, using the Bukkit API. The idea is that you would depend on PaperLib via Maven, and shade it into the AnvilGUI JAR (it's a pretty small library).
Could we just use
Bukkit#getVersion()
?
Yeah, I think that's basically what we have to do. PaperLib uses that method: https://github.com/PaperMC/PaperLib/blob/69e76fc9036e7b5f2c967bd21db31043724ef9e3/src/main/java/io/papermc/lib/environments/Environment.java#L53
The tricky part is parsing the version information from the string: https://github.com/PaperMC/PaperLib/blob/69e76fc9036e7b5f2c967bd21db31043724ef9e3/src/main/java/io/papermc/lib/environments/Environment.java#L57-L90
I mean, if all we need to do is parse the version string, I suppose it might be overkill to bring in PaperLib as a dependency. But it could be used as inspiration.
According to this post, AnvilGUI could break on Paper as soon as the next Minecraft update, 1.20.5: https://forums.papermc.io/threads/important-dev-psa-future-removal-of-cb-package-relocation.1106/
I tried running AnvilGUI with the no-relocation
build, and it definitely crashes for me, as expected (I see now that this has already been reported in #314). I would offer to help, if only I weren't so busy right now 🙁
Paper has moved on to use the mojang mappings for nms code as default from 1.20.5. This is not essentially a problem, since they have an in-built remapper in their server jar file to automatically map from spigot mappings to mojang mappings, and relocating imports of cb packages. However, to load the right Wrapper-Class for paper, we still need to somehow convert from the minecraft Version to the cb release version (e.g. 1.20.4-R0.1
to 1_20_R3
), since there will be no way to obtain the "R-Version" in newer paper builds. The R-Version doesn't matter for paper servers anymore.
I see two possible solutions:
-
Convert the minecraft version string to the cb release version string and let the paper remapper do the work
-
Create a new module for server jars that use mojang mappings and no cb package relocation.
I see one huge advantage in method 2), which is that the new wrapper class can be re-used for all upcoming paper versions, since it doesn't rely on imports with cb version numbers in it. Also mojang mappings doesn't change over releases. To make it compile, we need to use the paperweight gradle plugin which should generate such a jar with mojang mappings and without cb relocation. Unfortunately there is no maven plugin, meaning we would need to migrate to gradle.
In both cases, for keeping up spigot compatibility, we still need to create a new class file for every new cb release version, since both the cb package names and the spigot mappings change.
Paper has moved on to use the mojang mappings for nms code as default from 1.20.5. This is not essentially a problem, since they have an in-built remapper in their server jar file to automatically map from spigot mappings to mojang mappings, and relocating imports of cb packages. However, to load the right Wrapper-Class for paper, we still need to somehow convert from the minecraft Version to the cb release version (e.g.
1.20.4-R0.1
to1_20_R3
), since there will be no way to obtain the "R-Version" in newer paper builds. The R-Version doesn't matter for paper servers anymore.
Yeah the Mojang mappings are mostly irrelevant to this issue (#294) and shouldn't affect AnvilGUI in any way due to the automatic mapping being done by Paper at runtime. The real issue is that Paper no longer relocates the CraftBukkit package, so the CraftBukkit package version ("R-version") strings (e.g. 1_20_R3
) are now gone.
I'm not sure what you mean by "converting" from 1.20.4-R0.1
to 1_20_R3
, but I assume you mean via some kind of lookup table since these versions are incremented in a seemingly arbitrary manner, whenever the Spigot maintainers feel like it (sometimes it is incremented, sometimes it is not).
Honestly that feels kind of meaningless though. If we know the Minecraft version number, why not use that as the module name for the AnvilGUI adapters? Then we don't need to convert it to a CraftBukkit package version. That is to say, let's call the 1.20.5 adapter module 1_20_5
instead of 1_20_R4
.
- Convert the minecraft version string to the cb release version string and let the paper remapper do the work
I don't see what this has to do with the Paper remapper. Even if we convert to the CraftBukkit package version string, Paper won't remap it.
- Create a new module for server jars that use mojang mappings and no cb package relocation.
This would only work on Paper servers, so unless we decide to drop Spigot support entirely, this solution just sounds like a lot of extra work since we have to maintain two different adapters, one for Spigot and one for Paper.
@mastercake10 How about a third alternative:
- Parse the Bukkit version string and extract the Minecraft version from it (e.g. using PaperLib, as discussed earlier). Now use this information to load an appropriately named AnvilGUI adapter module (e.g.
1_8_8
or1_20_5
).
If renaming all the old AnvilGUI modules is too much of a hassle, we could even choose to fallback to the CraftBukkit package naming scheme on versions older than 1.20.5, and use the new naming scheme based on the Minecraft version only on 1.20.5 and up.
Either way, I think there are a number of advantages to this solution:
- Compatible with Spigot and Paper without any extra work (same adapter is used for both)
- We no longer have to apply ugly workarounds to fix issues like #221 (this issue in fact demonstrates perfectly why the CraftBukkit package version is meaningless and why the Minecraft version should be used instead)
- Should be simpler to implement than the other alternatives. Once the Minecraft version has been parsed, we can load the AnvilGUI adapter modules the exact same way they have always been loaded. Just make sure to rename the old modules from
1_8_R3
to1_8_8
, and so on (or don't, if we use the "fallback" approach).
What do you think of this? Let me know if there is something I have missed or misunderstood.
- Convert the minecraft version string to the cb release version string and let the paper remapper do the work
I don't see what this has to do with the Paper remapper. Even if we convert to the CraftBukkit package version string, Paper won't remap it.
Oh, wait, I just realized that Paper would in fact remap the relocated "R-version" CraftBukkit package, so you are right, it should work. But I still don't see the benefit of converting to the "R-version" string (via a lookup table that we have to keep maintaining) when we could just change the module naming system to correlate with actual Minecraft versions, e.g. 1_20_5
.
The 1_20_5
AnvilGUI module could still import from 1_20_R4
packages and it would still work on both Spigot and Paper, due to the automatic mappings. The only thing we can't do is parse the 1_20_R4
version from the package name. But as long as we change the AnvilGUI module loader to parse the actual Minecraft version instead, it should all work, I think.
I already did some work yesterday for solution 2), which is converting the project to gradle and making a module for paper. It falls back to the "R-version" string when there is a version tag in the craftbukkit package.
I think the only advantage is that it already makes AnvilGUI compatible with future mc releases for paper.
This would only work on Paper servers, so unless we decide to drop Spigot support entirely, this solution just sounds like a lot of extra work since we have to maintain two different adapters, one for Spigot and one for Paper.
In the last few release, the only thing that changed in the spigot mappings were the obfuscation and cb version numbers. That's why we need to make a new module once a new mc version comes out or the R-version is altered.
For paper with mojang mappings, this is no longer the case, so its just one class file, WrapperPaper.java. This class file can potentially be re-used for all upcoming paper versions, assuming methods wont change (even then we can use reflections).
Of course we still need to add wrapper classes for spigot, but in the long run who cares about spigot?
Oh, wait, I just realized that Paper would in fact remap the relocated "R-version" CraftBukkit package, so you are right, it should work. But I still don't see the benefit of converting to the "R-version" string (via a lookup table that we have to keep maintaining) when we could just change the module naming system to correlate with actual Minecraft versions, e.g. 1_20_5.
Yeah its not needed, your solution is better.
The 1_20_5 AnvilGUI module could still import from 1_20_R4 packages and it would still work on both Spigot and Paper, due to the automatic mappings. The only thing we can't do is parse the 1_20_R4 version from the package name. But as long as we change the AnvilGUI module loader to parse the actual Minecraft version instead, it should all work, I think.
Yes, I already tested this. It works.
I think your proposal (solution 3)) is the easiest thing to do without changing and potentially breaking much. I don't know how we feel about gradle projects, but I prefer them over maven. I already made it available in the spaceio repository in case you wanna test. If these are too many changes just for supporting paper 1.20.5+, or if we want to stick to maven, we can still implement solution 3.
I am not against switching to Gradle if that makes developing the project for the future easier. At this point, I haven't actively developed for MC in over 2-3 years, so I will trust your opinion here @mastercake10.
Also, if you're interested in taking the lead on this project, shoot me an email at me@wesleysmith.dev. It might be for the best to have someone who is more active take the lead on this.
For 1.20 and later we can simply use Bukkit.getServer().getBukkitVersion().split("-")[0]
, which gives us 1.20.4
for example. We can just switch between this and the old by trying the old one, catching the exception when it fails, and then assuming we're running >1.20. Using a map we can then remap those values back to the previous values (or in the other direction), and continue as usual. That way we also don't need to rely on PaperLib
. It's as simple as this:
private static final Map<String, String> VERSION_TO_REVISION = new HashMap<String, String>() {
{
this.put("1.20", "1_20_R1");
this.put("1.20.1", "1_20_R1");
this.put("1.20.2", "1_20_R2");
this.put("1.20.3", "1_20_R3");
this.put("1.20.4", "1_20_R3");
this.put("1.20.5", "1_20_R4");
this.put("1.20.6", "1_20_R4");
}
};
String serverVersion;
try {
serverVersion = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3].substring(1);
} catch (Exception ex) {
serverVersion = Bukkit.getServer().getBukkitVersion().split("-")[0];
}
if (serverVersion == null)
throw ...;
I can also send a quick PR implementing this, if necessary.
I already did some work yesterday for solution 2), which is converting the project to gradle and making a module for paper. It falls back to the "R-version" string when there is a version tag in the craftbukkit package.
I think the only advantage is that it already makes AnvilGUI compatible with future mc releases for paper.
I do see your point about "compatible with future mc releases". Since it would use Mojang mappings, the AnvilGUI module for Paper would only have to be updated whenever Mojang introduces breaking changes in the internals. Which is probably less often than the Spigot "R-versions". It would still be a slight duplication of efforts though, since now we have to test and maintain for Spigot and Paper separately. But in the end maybe it is worth it, I don't oppose this solution.
we can simply use
Bukkit.getServer().getBukkitVersion().split("-")[0]
Assuming that the first part of the Bukkit version is always the same as the Minecraft version (which I think has been the case so far) then yeah this would work. PaperLib (whether we bring in the whole library or just the version parsing part), which uses Bukkit#getVersion()
, was just one idea for how to get the Minecraft version. I do like your suggestion actually since it is very simple, and we probably don't care about pre-release, release candidate etc. versions.
Using a map we can then remap those values back to the previous values
This is the "lookup table" solution that we were discussing earlier. It does work, but again, why not ditch the "R-versions" entirely and just use the actual Minecraft version (with or without fallback)? Then we don't have to maintain the lookup table. I would imagine we can just replace .
with _
and call it a day (e.g. 1.20.5
converts to 1_20_5
).
I like the simplicity of @spnda solution, which is basically the same as my "solution 3" apart from some minor implementation details. But I also don't mind the solution of using Gradle and Paperweight proposed by @mastercake10 as long as someone is willing to maintain it. It can't hurt if both of you submit a PR, maybe it would then be easier to compare and evaluate the solutions? In the end I am not a maintainer here, just throwing some ideas around. Thank you all for the work that you put in!
I agree, I think @spnda's proposal is the most straightforward and least invasive solution without potentially introducing breaking changes.
why not ditch the "R-versions" entirely and just use the actual Minecraft version (with or without fallback)? Then we don't have to maintain the lookup table. I would imagine we can just replace . with _ and call it a day (e.g. 1.20.5 converts to 1_20_5).
To my understanding I see one problem here, which is that multiple MC versions share one class or "R-Version". For example, 1.20.5
and 1.20.6
can both use the module 1_20_5
, aka 1_20_R4
. I think this can only be solved through mapping. As long as spigot sticks to these meaningless R-versions, we still need to maintain them (the source files will reference the R-Version in the CB-Imports anyway).
I think for now we will use @spnda's solution and merge the changes in once the PR is created to facilitate the update for 1.20.5 / 1.20.6.
My Gradle + Paperweight solution still needs some testing and evaluation before I feel confident enough to submit a PR.
Also, if you're interested in taking the lead on this project, shoot me an email at me@wesleysmith.dev. It might be for the best to have someone who is more active take the lead on this.
Sure, I will think about it.
To my understanding I see one problem here, which is that multiple MC versions share one class or "R-Version". For example,
1.20.5
and1.20.6
can both use the module1_20_5
, aka1_20_R4
.
I don't see the problem with that. Why does the AnvilGUI module name need to be tied to the meaningless "R-versions"?
the source files will reference the R-Version in the CB-Imports anyway
Yes, and that's fine. They can do that regardless of whether we use "R-versions" in the AnvilGUI module names or not.
In this particular instance, we would only need a 1_20_5
module, which is loaded if the Minecraft version is 1.20.5
or higher. That is exactly the same thing as having a 1_20_R4
module, except 1_20_5
is a more meaningful name and would give us the possibility of introducing a 1_20_6
module if 1.20.6
were to contain breaking changes even though it has the same "R-version", 1_20_R4
. This exact problem has showed up before (see #221), and could show up again. Like, there is an AnvilGUI module called 1_19_1_R1
even though there is no such "R-version". My point is that it could just as well have been called 1_19_1
(which would make more sense). And the same goes for 1_17_1_R1
and 1_14_4_R1
.
But the mapping to "R-versions" via a lookup table works too, I'm not going to argue about that. Maybe there is something I have misunderstood (since I haven't done any coding) that makes it easier to go this route.
Yeah, you're right. Didn't see that. This is not hard to implement, tho it requires to rename the packages. We could start doing this from 1.20.5 on to avoid renaming old packages, but this will make it a bit inconsistent.
I think the easiest thing to do is to check if the cb package contains a "R", if thats the case load the wrapper for it. Else check for the major & minor version and load the corresponding wrapper in (or do it via hashmap).
If we find out this is to much of a hassle, we can still ditch the R.
We could start doing this from 1.20.5 on to avoid renaming old packages, but this will make it a bit inconsistent.
Yeah that's what I meant by the "fallback" approach I talked about earlier. I think it would be fine, renaming the old modules can be done retroactively if it is too much effort now.
I think the easiest thing to do is to check if the cb package contains a "R", if thats the case load the wrapper for it.
I think searching for _R
in the package name would be a fairly safe bet. Another solution is to try to load the class using the "R-version", and then catch the ClassNotFoundException
in which case we try again but with the new module naming scheme.
Else check for the major & minor version and load the corresponding wrapper in (or do it via hashmap).
Basically if (minecraftVersion >= 1.20.5) { loadModule("1_20_5"); }
, in terms of pseudocode. But yeah maybe it is easier to just have a map with an entry for each known Minecraft version, then we don't have to parse the version string into comparable version numbers.