1
0
mirror of https://github.com/Suiranoil/SkinRestorer.git synced 2026-01-16 04:42:12 +00:00

migrate mappings to official

This commit is contained in:
2024-06-28 20:16:00 +03:00
parent f567996ced
commit 82f078bfdb
12 changed files with 146 additions and 138 deletions

View File

@@ -39,11 +39,18 @@ subprojects {
build.finalizedBy(mergeJars)
assemble.finalizedBy(mergeJars)
repositories {
maven {
name = 'ParchmentMC'
url = 'https://maven.parchmentmc.org'
}
}
dependencies {
minecraft "net.minecraft:minecraft:$rootProject.minecraft_version"
mappings loom.layered {
it.mappings("net.fabricmc:yarn:$rootProject.yarn_mappings:v2")
it.mappings("dev.architectury:yarn-mappings-patch-neoforge:$rootProject.yarn_mappings_patch_neoforge_version")
officialMojangMappings()
parchment("org.parchmentmc.data:parchment-$rootProject.minecraft_version:$rootProject.parchment_mappings@zip")
}
}

View File

@@ -12,8 +12,8 @@ import net.lionarius.skinrestorer.skin.provider.SkinProvider;
import net.lionarius.skinrestorer.util.FileUtils;
import net.lionarius.skinrestorer.util.PlayerUtils;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.WorldSavePath;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.storage.LevelResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -57,13 +57,13 @@ public final class SkinRestorer {
}
public static void onServerStarted(MinecraftServer server) {
Path worldSkinDirectory = server.getSavePath(WorldSavePath.ROOT).resolve(SkinRestorer.MOD_ID);
Path worldSkinDirectory = server.getWorldPath(LevelResource.ROOT).resolve(SkinRestorer.MOD_ID);
FileUtils.tryMigrateOldSkinDirectory(worldSkinDirectory);
SkinRestorer.skinStorage = new SkinStorage(new SkinIO(worldSkinDirectory));
}
public static CompletableFuture<Pair<Collection<ServerPlayerEntity>, Collection<GameProfile>>> setSkinAsync(MinecraftServer server, Collection<GameProfile> targets, Supplier<SkinResult> skinSupplier) {
public static CompletableFuture<Pair<Collection<ServerPlayer>, Collection<GameProfile>>> setSkinAsync(MinecraftServer server, Collection<GameProfile> targets, Supplier<SkinResult> skinSupplier) {
return CompletableFuture.<Pair<Property, Collection<GameProfile>>>supplyAsync(() -> {
SkinResult result = skinSupplier.get();
if (result.isError()) {
@@ -80,14 +80,14 @@ public final class SkinRestorer {
HashSet<GameProfile> acceptedProfiles = new HashSet<>(targets);
return Pair.of(skin, acceptedProfiles);
}).<Pair<Collection<ServerPlayerEntity>, Collection<GameProfile>>>thenApplyAsync(pair -> {
}).<Pair<Collection<ServerPlayer>, Collection<GameProfile>>>thenApplyAsync(pair -> {
Property skin = pair.left(); // NullPtrException will be caught by 'exceptionally'
Collection<GameProfile> acceptedProfiles = pair.right();
HashSet<ServerPlayerEntity> acceptedPlayers = new HashSet<>();
HashSet<ServerPlayer> acceptedPlayers = new HashSet<>();
for (GameProfile profile : acceptedProfiles) {
ServerPlayerEntity player = server.getPlayerManager().getPlayer(profile.getId());
ServerPlayer player = server.getPlayerList().getPlayer(profile.getId());
if (player == null || PlayerUtils.areSkinPropertiesEquals(skin, PlayerUtils.getPlayerSkin(player)))
continue;

View File

@@ -12,10 +12,10 @@ import net.lionarius.skinrestorer.skin.SkinResult;
import net.lionarius.skinrestorer.skin.SkinVariant;
import net.lionarius.skinrestorer.skin.provider.SkinProvider;
import net.lionarius.skinrestorer.util.TranslationUtils;
import net.minecraft.command.argument.GameProfileArgumentType;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.arguments.GameProfileArgument;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import java.util.Collection;
import java.util.Collections;
@@ -23,19 +23,19 @@ import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import static net.minecraft.server.command.CommandManager.argument;
import static net.minecraft.server.command.CommandManager.literal;
import static net.minecraft.commands.Commands.argument;
import static net.minecraft.commands.Commands.literal;
public final class SkinCommand {
private SkinCommand() {}
public static void register(CommandDispatcher<ServerCommandSource> dispatcher) {
LiteralArgumentBuilder<ServerCommandSource> base =
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
LiteralArgumentBuilder<CommandSourceStack> base =
literal("skin")
.then(buildAction("clear", SkinResult::empty));
LiteralArgumentBuilder<ServerCommandSource> set = literal("set");
LiteralArgumentBuilder<CommandSourceStack> set = literal("set");
for (Map.Entry<String, SkinProvider> entry : SkinRestorer.getProviders()) {
set.then(buildAction(entry.getKey(), entry.getValue()));
@@ -46,8 +46,8 @@ public final class SkinCommand {
dispatcher.register(base);
}
private static LiteralArgumentBuilder<ServerCommandSource> buildAction(String name, SkinProvider provider) {
LiteralArgumentBuilder<ServerCommandSource> action = literal(name);
private static LiteralArgumentBuilder<CommandSourceStack> buildAction(String name, SkinProvider provider) {
LiteralArgumentBuilder<CommandSourceStack> action = literal(name);
if (provider.hasVariantSupport()) {
for (SkinVariant variant : SkinVariant.values()) {
@@ -71,13 +71,13 @@ public final class SkinCommand {
return action;
}
private static ArgumentBuilder<ServerCommandSource, LiteralArgumentBuilder<ServerCommandSource>> buildAction(String name, Supplier<SkinResult> supplier) {
private static ArgumentBuilder<CommandSourceStack, LiteralArgumentBuilder<CommandSourceStack>> buildAction(String name, Supplier<SkinResult> supplier) {
return buildArgument(literal(name), context -> supplier.get());
}
private static <T extends ArgumentBuilder<ServerCommandSource, T>> ArgumentBuilder<ServerCommandSource, T> buildArgument(
ArgumentBuilder<ServerCommandSource, T> argument,
Function<CommandContext<ServerCommandSource>, SkinResult> provider
private static <T extends ArgumentBuilder<CommandSourceStack, T>> ArgumentBuilder<CommandSourceStack, T> buildArgument(
ArgumentBuilder<CommandSourceStack, T> argument,
Function<CommandContext<CommandSourceStack>, SkinResult> provider
) {
return argument
.executes(context -> skinAction(
@@ -87,47 +87,47 @@ public final class SkinCommand {
.then(makeTargetsArgument(provider));
}
private static RequiredArgumentBuilder<ServerCommandSource, GameProfileArgumentType.GameProfileArgument> makeTargetsArgument(
Function<CommandContext<ServerCommandSource>, SkinResult> provider
private static RequiredArgumentBuilder<CommandSourceStack, GameProfileArgument.Result> makeTargetsArgument(
Function<CommandContext<CommandSourceStack>, SkinResult> provider
) {
return argument("targets", GameProfileArgumentType.gameProfile())
.requires(source -> source.hasPermissionLevel(2))
return argument("targets", GameProfileArgument.gameProfile())
.requires(source -> source.hasPermission(2))
.executes(context -> skinAction(
context.getSource(),
GameProfileArgumentType.getProfileArgument(context, "targets"),
GameProfileArgument.getGameProfiles(context, "targets"),
true,
() -> provider.apply(context)
));
}
private static int skinAction(ServerCommandSource src, Collection<GameProfile> targets, boolean setByOperator, Supplier<SkinResult> skinSupplier) {
private static int skinAction(CommandSourceStack src, Collection<GameProfile> targets, boolean setByOperator, Supplier<SkinResult> skinSupplier) {
SkinRestorer.setSkinAsync(src.getServer(), targets, skinSupplier).thenAccept(pair -> {
Collection<GameProfile> profiles = pair.right();
Collection<ServerPlayerEntity> players = pair.left();
Collection<ServerPlayer> players = pair.left();
if (profiles.isEmpty()) {
src.sendError(Text.of(TranslationUtils.getTranslation().skinActionFailed));
src.sendFailure(Component.nullToEmpty(TranslationUtils.getTranslation().skinActionFailed));
return;
}
if (setByOperator) {
src.sendFeedback(() -> Text.of(
src.sendSuccess(() -> Component.nullToEmpty(
String.format(TranslationUtils.getTranslation().skinActionAffectedProfile,
String.join(", ", profiles.stream().map(GameProfile::getName).toList()))), true);
if (!players.isEmpty()) {
src.sendFeedback(() -> Text.of(
src.sendSuccess(() -> Component.nullToEmpty(
String.format(TranslationUtils.getTranslation().skinActionAffectedPlayer,
String.join(", ", players.stream().map(p -> p.getGameProfile().getName()).toList()))), true);
}
} else {
src.sendFeedback(() -> Text.of(TranslationUtils.getTranslation().skinActionOk), true);
src.sendSuccess(() -> Component.nullToEmpty(TranslationUtils.getTranslation().skinActionOk), true);
}
});
return targets.size();
}
private static int skinAction(ServerCommandSource src, Supplier<SkinResult> skinSupplier) {
private static int skinAction(CommandSourceStack src, Supplier<SkinResult> skinSupplier) {
if (src.getPlayer() == null)
return 0;

View File

@@ -0,0 +1,45 @@
package net.lionarius.skinrestorer.mixin;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.skin.SkinResult;
import net.minecraft.network.Connection;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.CommonListenerCookie;
import net.minecraft.server.players.PlayerList;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
@Mixin(PlayerList.class)
public abstract class PlayerListMixin {
@Shadow
public abstract List<ServerPlayer> getPlayers();
@Shadow @Final
private MinecraftServer server;
@Inject(method = "remove", at = @At("TAIL"))
private void remove(ServerPlayer player, CallbackInfo ci) {
SkinRestorer.getSkinStorage().removeSkin(player.getUUID());
}
@Inject(method = "removeAll", at = @At("HEAD"))
private void removeAll(CallbackInfo ci) {
for (ServerPlayer player : getPlayers()) {
SkinRestorer.getSkinStorage().removeSkin(player.getUUID());
}
}
@Inject(method = "placeNewPlayer", at = @At("HEAD"))
private void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie, CallbackInfo ci) {
SkinRestorer.setSkinAsync(server, Collections.singleton(player.getGameProfile()), () -> SkinResult.ofNullable(SkinRestorer.getSkinStorage().getSkin(player.getUUID())));
}
}

View File

@@ -1,45 +0,0 @@
package net.lionarius.skinrestorer.mixin;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.skin.SkinResult;
import net.minecraft.network.ClientConnection;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ConnectedClientData;
import net.minecraft.server.network.ServerPlayerEntity;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
@Mixin(PlayerManager.class)
public abstract class PlayerManagerMixin {
@Shadow
public abstract List<ServerPlayerEntity> getPlayerList();
@Shadow @Final
private MinecraftServer server;
@Inject(method = "remove", at = @At("TAIL"))
private void remove(ServerPlayerEntity player, CallbackInfo ci) {
SkinRestorer.getSkinStorage().removeSkin(player.getUuid());
}
@Inject(method = "disconnectAllPlayers", at = @At("HEAD"))
private void disconnectAllPlayers(CallbackInfo ci) {
for (ServerPlayerEntity player : getPlayerList()) {
SkinRestorer.getSkinStorage().removeSkin(player.getUuid());
}
}
@Inject(method = "onPlayerConnect", at = @At("HEAD"))
private void onPlayerConnected(ClientConnection connection, ServerPlayerEntity player, ConnectedClientData clientData, CallbackInfo ci) {
SkinRestorer.setSkinAsync(server, Collections.singleton(player.getGameProfile()), () -> SkinResult.ofNullable(SkinRestorer.getSkinStorage().getSkin(player.getUuid())));
}
}

View File

@@ -4,7 +4,7 @@ import com.mojang.authlib.GameProfile;
import net.lionarius.skinrestorer.SkinRestorer;
import net.lionarius.skinrestorer.skin.SkinResult;
import net.lionarius.skinrestorer.skin.SkinVariant;
import net.minecraft.server.network.ServerLoginNetworkHandler;
import net.minecraft.server.network.ServerLoginPacketListenerImpl;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -15,34 +15,34 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.concurrent.CompletableFuture;
@Mixin(ServerLoginNetworkHandler.class)
public abstract class ServerLoginNetworkHandlerMixin {
@Mixin(ServerLoginPacketListenerImpl.class)
public abstract class ServerLoginPacketListenerImplMixin {
@Shadow @Nullable
private GameProfile profile;
private GameProfile authenticatedProfile;
@Unique
private CompletableFuture<SkinResult> skinrestorer_pendingSkin;
@Inject(method = "tickVerify", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/PlayerManager;checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;"),
@Inject(method = "verifyLoginAndFinishConnectionSetup", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/players/PlayerList;canPlayerLogin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/network/chat/Component;"),
cancellable = true)
public void waitForSkin(CallbackInfo ci) {
if (skinrestorer_pendingSkin == null) {
skinrestorer_pendingSkin = CompletableFuture.supplyAsync(() -> {
assert profile != null;
SkinRestorer.LOGGER.debug("Fetching {}'s skin", profile.getName());
assert authenticatedProfile != null;
SkinRestorer.LOGGER.debug("Fetching {}'s skin", authenticatedProfile.getName());
if (!SkinRestorer.getSkinStorage().hasSavedSkin(profile.getId())) { // when player joins for the first time fetch Mojang skin by his username
if (!SkinRestorer.getSkinStorage().hasSavedSkin(authenticatedProfile.getId())) { // when player joins for the first time fetch Mojang skin by his username
SkinResult result = SkinRestorer.getProvider("mojang").map(
provider -> provider.getSkin(profile.getName(), SkinVariant.CLASSIC)
provider -> provider.getSkin(authenticatedProfile.getName(), SkinVariant.CLASSIC)
).orElse(SkinResult.empty());
if (!result.isError())
SkinRestorer.getSkinStorage().setSkin(profile.getId(), result.getSkin());
SkinRestorer.getSkinStorage().setSkin(authenticatedProfile.getId(), result.getSkin());
}
return SkinResult.ofNullable(SkinRestorer.getSkinStorage().getSkin(profile.getId()));
return SkinResult.ofNullable(SkinRestorer.getSkinStorage().getSkin(authenticatedProfile.getId()));
});
}

View File

@@ -3,11 +3,11 @@ package net.lionarius.skinrestorer.util;
import com.google.gson.JsonObject;
import com.mojang.authlib.GameProfile;
import com.mojang.authlib.properties.Property;
import net.minecraft.network.packet.s2c.play.*;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerChunkManager;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.network.protocol.game.*;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.players.PlayerList;
import java.util.Collections;
import java.util.List;
@@ -18,47 +18,47 @@ public final class PlayerUtils {
private PlayerUtils() {}
public static boolean isFakePlayer(ServerPlayerEntity player) {
return player.getClass() != ServerPlayerEntity.class; // if the player isn't a server player entity, it must be someone's fake player
public static boolean isFakePlayer(ServerPlayer player) {
return player.getClass() != ServerPlayer.class; // if the player isn't a server player entity, it must be someone's fake player
}
public static void refreshPlayer(ServerPlayerEntity player) {
ServerWorld serverWorld = player.getServerWorld();
PlayerManager playerManager = serverWorld.getServer().getPlayerManager();
ServerChunkManager chunkManager = serverWorld.getChunkManager();
public static void refreshPlayer(ServerPlayer player) {
ServerLevel serverLevel = player.serverLevel();
PlayerList playerList = serverLevel.getServer().getPlayerList();
ServerChunkCache chunkSource = serverLevel.getChunkSource();
playerManager.sendToAll(new BundleS2CPacket(
playerList.broadcastAll(new ClientboundBundlePacket(
List.of(
new PlayerRemoveS2CPacket(List.of(player.getUuid())),
PlayerListS2CPacket.entryFromPlayer(Collections.singleton(player))
new ClientboundPlayerInfoRemovePacket(List.of(player.getUUID())),
ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(Collections.singleton(player))
)
));
if (!player.isDead()) {
chunkManager.unloadEntity(player);
chunkManager.loadEntity(player);
player.networkHandler.send(new BundleS2CPacket(
if (!player.isDeadOrDying()) {
chunkSource.removeEntity(player);
chunkSource.addEntity(player);
player.connection.send(new ClientboundBundlePacket(
List.of(
new PlayerRespawnS2CPacket(player.createCommonPlayerSpawnInfo(serverWorld), PlayerRespawnS2CPacket.KEEP_ALL),
new GameStateChangeS2CPacket(GameStateChangeS2CPacket.INITIAL_CHUNKS_COMING, 0)
new ClientboundRespawnPacket(player.createCommonSpawnInfo(serverLevel), ClientboundRespawnPacket.KEEP_ALL_DATA),
new ClientboundGameEventPacket(ClientboundGameEventPacket.LEVEL_CHUNKS_LOAD_START, 0)
)
), null);
player.networkHandler.requestTeleport(player.getX(), player.getY(), player.getZ(), player.getYaw(), player.getPitch());
player.networkHandler.send(new EntityVelocityUpdateS2CPacket(player), null);
player.sendAbilitiesUpdate();
player.addExperience(0);
playerManager.sendCommandTree(player);
playerManager.sendWorldInfo(player, serverWorld);
playerManager.sendPlayerStatus(player);
playerManager.sendStatusEffects(player);
));
player.connection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot());
player.connection.send(new ClientboundSetEntityMotionPacket(player));
player.onUpdateAbilities();
player.giveExperiencePoints(0);
playerList.sendPlayerPermissionLevel(player);
playerList.sendLevelInfo(player, serverLevel);
playerList.sendAllPlayerInfo(player);
playerList.sendActivePlayerEffects(player);
}
}
public static Property getPlayerSkin(ServerPlayerEntity player) {
public static Property getPlayerSkin(ServerPlayer player) {
return player.getGameProfile().getProperties().get(TEXTURES_KEY).stream().findFirst().orElse(null);
}
public static void applyRestoredSkin(ServerPlayerEntity player, Property skin) {
public static void applyRestoredSkin(ServerPlayer player, Property skin) {
GameProfile profile = player.getGameProfile();
profile.getProperties().removeAll(TEXTURES_KEY);

View File

@@ -4,8 +4,8 @@
"package": "net.lionarius.skinrestorer.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"PlayerManagerMixin",
"ServerLoginNetworkHandlerMixin"
"PlayerListMixin",
"ServerLoginPacketListenerImplMixin"
],
"injectors": {
"defaultRequire": 1

View File

@@ -2,9 +2,9 @@ package net.lionarius.skinrestorer.fabric.mixin;
import com.mojang.brigadier.CommandDispatcher;
import net.lionarius.skinrestorer.command.SkinCommand;
import net.minecraft.command.CommandRegistryAccess;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.command.ServerCommandSource;
import net.minecraft.commands.CommandBuildContext;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -12,15 +12,15 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(CommandManager.class)
public abstract class CommandManagerMixin {
@Mixin(Commands.class)
public abstract class CommandsMixin {
@Final @Shadow
private CommandDispatcher<ServerCommandSource> dispatcher;
private CommandDispatcher<CommandSourceStack> dispatcher;
@Inject(method = "<init>", at = @At(value = "INVOKE",
target = "Lnet/minecraft/server/command/AdvancementCommand;register(Lcom/mojang/brigadier/CommandDispatcher;)V"))
private void init(CommandManager.RegistrationEnvironment environment, CommandRegistryAccess commandRegistryAccess, CallbackInfo ci) {
target = "Lnet/minecraft/server/commands/AdvancementCommands;register(Lcom/mojang/brigadier/CommandDispatcher;)V"))
private void init(Commands.CommandSelection environment, CommandBuildContext commandRegistryAccess, CallbackInfo ci) {
SkinCommand.register(dispatcher);
}
}

View File

@@ -11,7 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
public abstract class MinecraftServerMixin {
@Inject(method = "runServer",
at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Util;getMeasuringTimeNano()J", ordinal = 0))
at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getNanos()J", ordinal = 0))
private void onServerStarted(CallbackInfo ci) {
SkinRestorer.onServerStarted((MinecraftServer) (Object) this);
}

View File

@@ -4,7 +4,7 @@
"package": "net.lionarius.skinrestorer.fabric.mixin",
"compatibilityLevel": "JAVA_8",
"mixins": [
"CommandManagerMixin",
"CommandsMixin",
"MinecraftServerMixin"
],
"injectors": {

View File

@@ -12,8 +12,9 @@ archives_name=skin-restorer
capitalized_name=SkinRestorer
# Mappings
yarn_mappings=1.21+build.4
yarn_mappings_patch_neoforge_version=1.21+build.4
#yarn_mappings=1.21+build.4
#yarn_mappings_patch_neoforge_version=1.21+build.4
parchment_mappings=2024.06.23
# Fabric Properties
# check these on https://fabricmc.net/versions.html