v1.6.4: Added support for 1.20.5, 1.20.6 and 1.21 #30
8 changed files with 104 additions and 146 deletions
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
|
@ -29,9 +29,8 @@
|
||||||
<option value="$PROJECT_DIR$/NMS-v1_19_R1/pom.xml" />
|
<option value="$PROJECT_DIR$/NMS-v1_19_R1/pom.xml" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="workspaceImportForciblyTurnedOn" value="true" />
|
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="temurin-11" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
2
.idea/vcs.xml
generated
2
.idea/vcs.xml
generated
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
27
pom.xml
27
pom.xml
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<groupId>tech.sbdevelopment</groupId>
|
<groupId>tech.sbdevelopment</groupId>
|
||||||
<artifactId>MapReflectionAPI</artifactId>
|
<artifactId>MapReflectionAPI</artifactId>
|
||||||
<version>1.6.3</version>
|
<version>1.6.4</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|
||||||
<name>MapReflectionAPI</name>
|
<name>MapReflectionAPI</name>
|
||||||
|
@ -48,14 +48,14 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.10.1</version>
|
<version>3.13.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<release>11</release>
|
<release>11</release>
|
||||||
<annotationProcessorPaths>
|
<annotationProcessorPaths>
|
||||||
<path>
|
<path>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.30</version>
|
<version>1.18.34</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
@ -63,7 +63,7 @@
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
<version>3.3.0</version>
|
<version>3.6.0</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<phase>package</phase>
|
<phase>package</phase>
|
||||||
|
@ -103,12 +103,19 @@
|
||||||
</goals>
|
</goals>
|
||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.34</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-javadoc-plugin</artifactId>
|
<artifactId>maven-javadoc-plugin</artifactId>
|
||||||
<version>3.4.0</version>
|
<version>3.7.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<release>11</release>
|
<release>11</release>
|
||||||
<sourcepath>${maven.lombok.delombok-target}</sourcepath>
|
<sourcepath>${maven.lombok.delombok-target}</sourcepath>
|
||||||
|
@ -161,13 +168,13 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.spigotmc</groupId>
|
<groupId>org.spigotmc</groupId>
|
||||||
<artifactId>spigot-api</artifactId>
|
<artifactId>spigot-api</artifactId>
|
||||||
<version>1.20.4-R0.1-SNAPSHOT</version>
|
<version>1.21-R0.1-SNAPSHOT</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
<version>1.18.30</version>
|
<version>1.18.34</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
@ -178,17 +185,17 @@
|
||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Libraries below are provided by Spigot -->
|
<!-- Libraries below are provided by CraftBukkit -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetbrains</groupId>
|
<groupId>org.jetbrains</groupId>
|
||||||
<artifactId>annotations-java5</artifactId>
|
<artifactId>annotations-java5</artifactId>
|
||||||
<version>23.0.0</version>
|
<version>24.1.0</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.netty</groupId>
|
<groupId>io.netty</groupId>
|
||||||
<artifactId>netty-transport</artifactId>
|
<artifactId>netty-transport</artifactId>
|
||||||
<version>4.1.77.Final</version>
|
<version>4.1.97.Final</version>
|
||||||
<scope>provided</scope>
|
<scope>provided</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
@ -86,6 +86,7 @@ public class MapSender {
|
||||||
|
|
||||||
private static final Class<?> packetPlayOutMapClass = getNMSClass("network.protocol.game", "PacketPlayOutMap");
|
private static final Class<?> packetPlayOutMapClass = getNMSClass("network.protocol.game", "PacketPlayOutMap");
|
||||||
private static final Class<?> worldMapData = supports(17) ? getNMSClass("world.level.saveddata.maps", "WorldMap$b") : null;
|
private static final Class<?> worldMapData = supports(17) ? getNMSClass("world.level.saveddata.maps", "WorldMap$b") : null;
|
||||||
|
private static final Class<?> mapId = supports(21) ? getNMSClass("world.level.saveddata.maps", "MapId") : null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a map to a player
|
* Send a map to a player
|
||||||
|
@ -110,7 +111,6 @@ public class MapSender {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int id = -id0;
|
|
||||||
Object packet;
|
Object packet;
|
||||||
if (supports(17)) { //1.17+
|
if (supports(17)) { //1.17+
|
||||||
Object updateData = ReflectionUtil.callConstructor(worldMapData,
|
Object updateData = ReflectionUtil.callConstructor(worldMapData,
|
||||||
|
@ -121,16 +121,26 @@ public class MapSender {
|
||||||
content.array //Data
|
content.array //Data
|
||||||
);
|
);
|
||||||
|
|
||||||
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
if (supports(21)) { //1.21+
|
||||||
id, //ID
|
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
||||||
(byte) 0, //Scale, 0 = 1 block per pixel
|
ReflectionUtil.callConstructor(mapId, -id0), //ID
|
||||||
false, //Show icons
|
(byte) 0, //Scale, 0 = 1 block per pixel
|
||||||
new ReflectionUtil.CollectionParam<>(), //Icons
|
false, //Show icons
|
||||||
updateData
|
new ReflectionUtil.CollectionParam<>(), //Icons
|
||||||
);
|
updateData
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
||||||
|
-id0, //ID
|
||||||
|
(byte) 0, //Scale, 0 = 1 block per pixel
|
||||||
|
false, //Show icons
|
||||||
|
new ReflectionUtil.CollectionParam<>(), //Icons
|
||||||
|
updateData
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (supports(14)) { //1.16-1.14
|
} else if (supports(14)) { //1.16-1.14
|
||||||
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
||||||
id, //ID
|
-id0, //ID
|
||||||
(byte) 0, //Scale, 0 = 1 block per pixel
|
(byte) 0, //Scale, 0 = 1 block per pixel
|
||||||
false, //Tracking position
|
false, //Tracking position
|
||||||
false, //Locked
|
false, //Locked
|
||||||
|
@ -143,7 +153,7 @@ public class MapSender {
|
||||||
);
|
);
|
||||||
} else { //1.13-
|
} else { //1.13-
|
||||||
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
packet = ReflectionUtil.callConstructor(packetPlayOutMapClass,
|
||||||
id, //ID
|
-id0, //ID
|
||||||
(byte) 0, //Scale, 0 = 1 block per pixel
|
(byte) 0, //Scale, 0 = 1 block per pixel
|
||||||
false, //???
|
false, //???
|
||||||
new ReflectionUtil.CollectionParam<>(), //Icons
|
new ReflectionUtil.CollectionParam<>(), //Icons
|
||||||
|
|
|
@ -67,6 +67,8 @@ public class MapWrapper extends AbstractMapWrapper {
|
||||||
private static final Class<?> entityMetadataPacketClass = getNMSClass("network.protocol.game", "PacketPlayOutEntityMetadata");
|
private static final Class<?> entityMetadataPacketClass = getNMSClass("network.protocol.game", "PacketPlayOutEntityMetadata");
|
||||||
private static final Class<?> entityItemFrameClass = getNMSClass("world.entity.decoration", "EntityItemFrame");
|
private static final Class<?> entityItemFrameClass = getNMSClass("world.entity.decoration", "EntityItemFrame");
|
||||||
private static final Class<?> dataWatcherItemClass = getNMSClass("network.syncher", "DataWatcher$Item");
|
private static final Class<?> dataWatcherItemClass = getNMSClass("network.syncher", "DataWatcher$Item");
|
||||||
|
private static final Class<?> minecraftKeyClass = getNMSClass("resources", "MinecraftKey");
|
||||||
|
private static final Class<?> builtInRegistriesClass = getNMSClass("core.registries", "BuiltInRegistries");
|
||||||
|
|
||||||
protected MapController controller = new MapController() {
|
protected MapController controller = new MapController() {
|
||||||
private final Map<UUID, Integer> viewers = new HashMap<>();
|
private final Map<UUID, Integer> viewers = new HashMap<>();
|
||||||
|
@ -164,7 +166,10 @@ public class MapWrapper extends AbstractMapWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
String inventoryMenuName;
|
String inventoryMenuName;
|
||||||
if (supports(20)) {
|
if (supports(21)) {
|
||||||
|
//1.21 = cc
|
||||||
|
inventoryMenuName = "cc";
|
||||||
|
} else if (supports(20)) {
|
||||||
//>= 1.20.2 = bR, 1.20(.1) = bQ
|
//>= 1.20.2 = bR, 1.20(.1) = bQ
|
||||||
inventoryMenuName = supports(20, 2) ? "bR" : "bQ";
|
inventoryMenuName = supports(20, 2) ? "bR" : "bQ";
|
||||||
} else if (supports(19)) {
|
} else if (supports(19)) {
|
||||||
|
@ -286,7 +291,14 @@ public class MapWrapper extends AbstractMapWrapper {
|
||||||
|
|
||||||
Object nmsStack = ReflectionUtil.callMethod(craftStackClass, "asNMSCopy", stack);
|
Object nmsStack = ReflectionUtil.callMethod(craftStackClass, "asNMSCopy", stack);
|
||||||
|
|
||||||
if (supports(13)) {
|
if (supports(21)) { //1.21 method
|
||||||
|
Object minecraftKey = ReflectionUtil.callDeclaredMethod(minecraftKeyClass, "a", "minecraft:map_id");
|
||||||
|
Object dataComponentTypeRegistery = ReflectionUtil.getDeclaredField(builtInRegistriesClass, "aq");
|
||||||
|
Object dataComponentType = ReflectionUtil.callMethod(dataComponentTypeRegistery, "a", minecraftKey);
|
||||||
|
|
||||||
|
Object dataComponentMap = ReflectionUtil.callMethod(nmsStack, "a");
|
||||||
|
ReflectionUtil.callMethod(dataComponentMap, "a", dataComponentType, mapId);
|
||||||
|
} else if (supports(13)) { //1.13 - 1.20 method
|
||||||
String nbtObjectName;
|
String nbtObjectName;
|
||||||
if (supports(20)) { //1.20
|
if (supports(20)) { //1.20
|
||||||
nbtObjectName = "w";
|
nbtObjectName = "w";
|
||||||
|
@ -307,7 +319,9 @@ public class MapWrapper extends AbstractMapWrapper {
|
||||||
Object nmsStack = createCraftItemStack(stack, mapId);
|
Object nmsStack = createCraftItemStack(stack, mapId);
|
||||||
|
|
||||||
String dataWatcherObjectName;
|
String dataWatcherObjectName;
|
||||||
if (supports(19, 3)) { //1.19.3 and 1.20(.1)
|
if (supports(21)) { //1.21
|
||||||
|
dataWatcherObjectName = "f";
|
||||||
|
} else if (supports(19, 3)) { //1.19.3 and 1.20(.1)
|
||||||
dataWatcherObjectName = "g";
|
dataWatcherObjectName = "g";
|
||||||
} else if (supports(19)) { //1.19-1.19.2
|
} else if (supports(19)) { //1.19-1.19.2
|
||||||
dataWatcherObjectName = "ao";
|
dataWatcherObjectName = "ao";
|
||||||
|
@ -328,7 +342,7 @@ public class MapWrapper extends AbstractMapWrapper {
|
||||||
|
|
||||||
Object packet;
|
Object packet;
|
||||||
if (supports(19, 3)) { //1.19.3
|
if (supports(19, 3)) { //1.19.3
|
||||||
Class<?> dataWatcherRecordClass = getNMSClass("network.syncher", "DataWatcher$b");
|
Class<?> dataWatcherRecordClass = getNMSClass("network.syncher", "DataWatcher$" + (supports(21) ? "c" : "b"));
|
||||||
// Sadly not possible to use ReflectionUtil (in its current state), because of the Object parameter
|
// Sadly not possible to use ReflectionUtil (in its current state), because of the Object parameter
|
||||||
Object dataWatcherItem;
|
Object dataWatcherItem;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -140,8 +140,8 @@ public class PacketListener implements Listener {
|
||||||
} else if (packet.getClass().isAssignableFrom(packetPlayInSetCreativeSlotClass)) {
|
} else if (packet.getClass().isAssignableFrom(packetPlayInSetCreativeSlotClass)) {
|
||||||
Object packetPlayInSetCreativeSlot = packetPlayInSetCreativeSlotClass.cast(packet);
|
Object packetPlayInSetCreativeSlot = packetPlayInSetCreativeSlotClass.cast(packet);
|
||||||
|
|
||||||
int slot = (int) ReflectionUtil.callDeclaredMethod(packetPlayInSetCreativeSlot, supports(19, 4) ? "a" : supports(13) ? "b" : "a"); //1.19.4 = a, 1.19.3 - 1.13 = b, 1.12 = a
|
int slot = (int) ReflectionUtil.callDeclaredMethod(packetPlayInSetCreativeSlot, supports(21) ? "b" : supports(19, 4) ? "a" : supports(13) ? "b" : "a"); //1.21 = b, 1.19.4 = a, 1.19.3 - 1.13 = b, 1.12 = a
|
||||||
Object nmsStack = ReflectionUtil.callDeclaredMethod(packetPlayInSetCreativeSlot, supports(20, 2) ? "d" : supports(18) ? "c" : "getItemStack"); //1.20.2 = d, >= 1.18 = c, 1.17 = getItemStack
|
Object nmsStack = ReflectionUtil.callDeclaredMethod(packetPlayInSetCreativeSlot, supports(21) ? "e" : supports(20, 2) ? "d" : supports(18) ? "c" : "getItemStack"); //1.21 = e, 1.20.2 = d, >= 1.18 = c, 1.17 = getItemStack
|
||||||
ItemStack craftStack = (ItemStack) ReflectionUtil.callMethod(craftStackClass, "asBukkitCopy", nmsStack);
|
ItemStack craftStack = (ItemStack) ReflectionUtil.callMethod(craftStackClass, "asBukkitCopy", nmsStack);
|
||||||
|
|
||||||
boolean async = !MapReflectionAPI.getInstance().getServer().isPrimaryThread();
|
boolean async = !MapReflectionAPI.getInstance().getServer().isPrimaryThread();
|
||||||
|
|
|
@ -18,6 +18,9 @@
|
||||||
|
|
||||||
package tech.sbdevelopment.mapreflectionapi.utils;
|
package tech.sbdevelopment.mapreflectionapi.utils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
public class MainUtil {
|
public class MainUtil {
|
||||||
private MainUtil() {
|
private MainUtil() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* The MIT License (MIT)
|
* The MIT License (MIT)
|
||||||
*
|
*
|
||||||
* Copyright (c) 2018 Hex_27
|
* Copyright (c) 2018 Hex_27
|
||||||
* Copyright (c) 2023 Crypto Morin
|
* Copyright (c) 2024 Crypto Morin
|
||||||
*
|
*
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
@ -30,7 +30,7 @@ import org.bukkit.Material;
|
||||||
import org.bukkit.inventory.ItemStack;
|
import org.bukkit.inventory.ItemStack;
|
||||||
import org.bukkit.inventory.meta.ItemMeta;
|
import org.bukkit.inventory.meta.ItemMeta;
|
||||||
import org.bukkit.inventory.meta.SpawnEggMeta;
|
import org.bukkit.inventory.meta.SpawnEggMeta;
|
||||||
import org.bukkit.potion.Potion;
|
import org.jetbrains.annotations.ApiStatus;
|
||||||
|
|
||||||
import javax.annotation.Nonnull;
|
import javax.annotation.Nonnull;
|
||||||
import javax.annotation.Nullable;
|
import javax.annotation.Nullable;
|
||||||
|
@ -38,7 +38,6 @@ import java.util.*;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.regex.PatternSyntaxException;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -61,11 +60,11 @@ import java.util.stream.Collectors;
|
||||||
* <b>/give @p minecraft:dirt 1 10</b> where 1 is the item amount, and 10 is the data value. The material {@link #DIRT} with a data value of {@code 10} doesn't exist.
|
* <b>/give @p minecraft:dirt 1 10</b> where 1 is the item amount, and 10 is the data value. The material {@link #DIRT} with a data value of {@code 10} doesn't exist.
|
||||||
*
|
*
|
||||||
* @author Crypto Morin
|
* @author Crypto Morin
|
||||||
* @version 11.5.0
|
* @version 12.0.0
|
||||||
* @see Material
|
* @see Material
|
||||||
* @see ItemStack
|
* @see ItemStack
|
||||||
*/
|
*/
|
||||||
public enum XMaterial {
|
public enum XMaterial /* implements com.cryptomorin.xseries.abstractions.Material*/ {
|
||||||
ACACIA_BOAT("BOAT_ACACIA"),
|
ACACIA_BOAT("BOAT_ACACIA"),
|
||||||
ACACIA_BUTTON("WOOD_BUTTON"),
|
ACACIA_BUTTON("WOOD_BUTTON"),
|
||||||
ACACIA_CHEST_BOAT,
|
ACACIA_CHEST_BOAT,
|
||||||
|
@ -108,6 +107,8 @@ public enum XMaterial {
|
||||||
ANVIL,
|
ANVIL,
|
||||||
APPLE,
|
APPLE,
|
||||||
ARCHER_POTTERY_SHERD,
|
ARCHER_POTTERY_SHERD,
|
||||||
|
ARMADILLO_SCUTE,
|
||||||
|
ARMADILLO_SPAWN_EGG,
|
||||||
ARMOR_STAND,
|
ARMOR_STAND,
|
||||||
ARMS_UP_POTTERY_SHERD,
|
ARMS_UP_POTTERY_SHERD,
|
||||||
ARROW,
|
ARROW,
|
||||||
|
@ -223,6 +224,8 @@ public enum XMaterial {
|
||||||
BLUE_TERRACOTTA(11, "STAINED_CLAY"),
|
BLUE_TERRACOTTA(11, "STAINED_CLAY"),
|
||||||
BLUE_WALL_BANNER(4, "WALL_BANNER"),
|
BLUE_WALL_BANNER(4, "WALL_BANNER"),
|
||||||
BLUE_WOOL(11, "WOOL"),
|
BLUE_WOOL(11, "WOOL"),
|
||||||
|
BOGGED_SPAWN_EGG,
|
||||||
|
BOLT_ARMOR_TRIM_SMITHING_TEMPLATE,
|
||||||
BONE,
|
BONE,
|
||||||
BONE_BLOCK,
|
BONE_BLOCK,
|
||||||
BONE_MEAL(15, "INK_SACK"),
|
BONE_MEAL(15, "INK_SACK"),
|
||||||
|
@ -235,6 +238,7 @@ public enum XMaterial {
|
||||||
BRAIN_CORAL_FAN,
|
BRAIN_CORAL_FAN,
|
||||||
BRAIN_CORAL_WALL_FAN,
|
BRAIN_CORAL_WALL_FAN,
|
||||||
BREAD,
|
BREAD,
|
||||||
|
BREEZE_ROD,
|
||||||
BREEZE_SPAWN_EGG,
|
BREEZE_SPAWN_EGG,
|
||||||
BREWER_POTTERY_SHERD,
|
BREWER_POTTERY_SHERD,
|
||||||
BREWING_STAND("BREWING_STAND", "BREWING_STAND_ITEM"),
|
BREWING_STAND("BREWING_STAND", "BREWING_STAND_ITEM"),
|
||||||
|
@ -591,7 +595,7 @@ public enum XMaterial {
|
||||||
FERMENTED_SPIDER_EYE,
|
FERMENTED_SPIDER_EYE,
|
||||||
FERN(2, "LONG_GRASS"),
|
FERN(2, "LONG_GRASS"),
|
||||||
/**
|
/**
|
||||||
* For some reasons filled map items are really special.
|
* For some reason, filled map items are really special.
|
||||||
* Their data value starts from 0 and every time a player
|
* Their data value starts from 0 and every time a player
|
||||||
* creates a new map that maps data value increases.
|
* creates a new map that maps data value increases.
|
||||||
* <a href="https://github.com/CryptoMorin/XSeries/issues/91">GitHub Issue</a>
|
* <a href="https://github.com/CryptoMorin/XSeries/issues/91">GitHub Issue</a>
|
||||||
|
@ -613,6 +617,9 @@ public enum XMaterial {
|
||||||
FLOWERING_AZALEA_LEAVES,
|
FLOWERING_AZALEA_LEAVES,
|
||||||
FLOWER_BANNER_PATTERN,
|
FLOWER_BANNER_PATTERN,
|
||||||
FLOWER_POT("FLOWER_POT", "FLOWER_POT_ITEM"),
|
FLOWER_POT("FLOWER_POT", "FLOWER_POT_ITEM"),
|
||||||
|
FLOW_ARMOR_TRIM_SMITHING_TEMPLATE,
|
||||||
|
FLOW_BANNER_PATTERN,
|
||||||
|
FLOW_POTTERY_SHERD,
|
||||||
FOX_SPAWN_EGG,
|
FOX_SPAWN_EGG,
|
||||||
FRIEND_POTTERY_SHERD,
|
FRIEND_POTTERY_SHERD,
|
||||||
FROGSPAWN,
|
FROGSPAWN,
|
||||||
|
@ -699,11 +706,14 @@ public enum XMaterial {
|
||||||
GRINDSTONE,
|
GRINDSTONE,
|
||||||
GUARDIAN_SPAWN_EGG(68, "MONSTER_EGG"),
|
GUARDIAN_SPAWN_EGG(68, "MONSTER_EGG"),
|
||||||
GUNPOWDER("SULPHUR"),
|
GUNPOWDER("SULPHUR"),
|
||||||
|
GUSTER_BANNER_PATTERN,
|
||||||
|
GUSTER_POTTERY_SHERD,
|
||||||
HANGING_ROOTS,
|
HANGING_ROOTS,
|
||||||
HAY_BLOCK,
|
HAY_BLOCK,
|
||||||
HEARTBREAK_POTTERY_SHERD,
|
HEARTBREAK_POTTERY_SHERD,
|
||||||
HEART_OF_THE_SEA,
|
HEART_OF_THE_SEA,
|
||||||
HEART_POTTERY_SHERD,
|
HEART_POTTERY_SHERD,
|
||||||
|
HEAVY_CORE,
|
||||||
HEAVY_WEIGHTED_PRESSURE_PLATE("IRON_PLATE"),
|
HEAVY_WEIGHTED_PRESSURE_PLATE("IRON_PLATE"),
|
||||||
HOGLIN_SPAWN_EGG("MONSTER_EGG"),
|
HOGLIN_SPAWN_EGG("MONSTER_EGG"),
|
||||||
HONEYCOMB,
|
HONEYCOMB,
|
||||||
|
@ -856,6 +866,7 @@ public enum XMaterial {
|
||||||
LLAMA_SPAWN_EGG(103, "MONSTER_EGG"),
|
LLAMA_SPAWN_EGG(103, "MONSTER_EGG"),
|
||||||
LODESTONE,
|
LODESTONE,
|
||||||
LOOM,
|
LOOM,
|
||||||
|
MACE,
|
||||||
MAGENTA_BANNER(13, "STANDING_BANNER", "BANNER"),
|
MAGENTA_BANNER(13, "STANDING_BANNER", "BANNER"),
|
||||||
MAGENTA_BED(supports(12) ? 2 : 0, "BED_BLOCK", "BED"),
|
MAGENTA_BED(supports(12) ? 2 : 0, "BED_BLOCK", "BED"),
|
||||||
MAGENTA_CANDLE,
|
MAGENTA_CANDLE,
|
||||||
|
@ -939,11 +950,14 @@ public enum XMaterial {
|
||||||
MUSIC_DISC_BLOCKS("RECORD_3"),
|
MUSIC_DISC_BLOCKS("RECORD_3"),
|
||||||
MUSIC_DISC_CAT("GREEN_RECORD"),
|
MUSIC_DISC_CAT("GREEN_RECORD"),
|
||||||
MUSIC_DISC_CHIRP("RECORD_4"),
|
MUSIC_DISC_CHIRP("RECORD_4"),
|
||||||
|
MUSIC_DISC_CREATOR,
|
||||||
|
MUSIC_DISC_CREATOR_MUSIC_BOX,
|
||||||
MUSIC_DISC_FAR("RECORD_5"),
|
MUSIC_DISC_FAR("RECORD_5"),
|
||||||
MUSIC_DISC_MALL("RECORD_6"),
|
MUSIC_DISC_MALL("RECORD_6"),
|
||||||
MUSIC_DISC_MELLOHI("RECORD_7"),
|
MUSIC_DISC_MELLOHI("RECORD_7"),
|
||||||
MUSIC_DISC_OTHERSIDE,
|
MUSIC_DISC_OTHERSIDE,
|
||||||
MUSIC_DISC_PIGSTEP,
|
MUSIC_DISC_PIGSTEP,
|
||||||
|
MUSIC_DISC_PRECIPICE,
|
||||||
MUSIC_DISC_RELIC,
|
MUSIC_DISC_RELIC,
|
||||||
MUSIC_DISC_STAL("RECORD_8"),
|
MUSIC_DISC_STAL("RECORD_8"),
|
||||||
MUSIC_DISC_STRAD("RECORD_9"),
|
MUSIC_DISC_STRAD("RECORD_9"),
|
||||||
|
@ -1009,6 +1023,8 @@ public enum XMaterial {
|
||||||
OBSIDIAN,
|
OBSIDIAN,
|
||||||
OCELOT_SPAWN_EGG(98, "MONSTER_EGG"),
|
OCELOT_SPAWN_EGG(98, "MONSTER_EGG"),
|
||||||
OCHRE_FROGLIGHT,
|
OCHRE_FROGLIGHT,
|
||||||
|
OMINOUS_BOTTLE,
|
||||||
|
OMINOUS_TRIAL_KEY,
|
||||||
ORANGE_BANNER(14, "STANDING_BANNER", "BANNER"),
|
ORANGE_BANNER(14, "STANDING_BANNER", "BANNER"),
|
||||||
ORANGE_BED(supports(12) ? 1 : 0, "BED_BLOCK", "BED"),
|
ORANGE_BED(supports(12) ? 1 : 0, "BED_BLOCK", "BED"),
|
||||||
ORANGE_CANDLE,
|
ORANGE_CANDLE,
|
||||||
|
@ -1285,6 +1301,7 @@ public enum XMaterial {
|
||||||
SANDSTONE_STAIRS,
|
SANDSTONE_STAIRS,
|
||||||
SANDSTONE_WALL,
|
SANDSTONE_WALL,
|
||||||
SCAFFOLDING,
|
SCAFFOLDING,
|
||||||
|
SCRAPE_POTTERY_SHERD,
|
||||||
SCULK,
|
SCULK,
|
||||||
SCULK_CATALYST,
|
SCULK_CATALYST,
|
||||||
SCULK_SENSOR,
|
SCULK_SENSOR,
|
||||||
|
@ -1479,9 +1496,11 @@ public enum XMaterial {
|
||||||
TUFF_WALL,
|
TUFF_WALL,
|
||||||
TURTLE_EGG,
|
TURTLE_EGG,
|
||||||
TURTLE_HELMET,
|
TURTLE_HELMET,
|
||||||
|
TURTLE_SCUTE,
|
||||||
TURTLE_SPAWN_EGG,
|
TURTLE_SPAWN_EGG,
|
||||||
TWISTING_VINES,
|
TWISTING_VINES,
|
||||||
TWISTING_VINES_PLANT,
|
TWISTING_VINES_PLANT,
|
||||||
|
VAULT,
|
||||||
VERDANT_FROGLIGHT,
|
VERDANT_FROGLIGHT,
|
||||||
VEX_ARMOR_TRIM_SMITHING_TEMPLATE,
|
VEX_ARMOR_TRIM_SMITHING_TEMPLATE,
|
||||||
VEX_SPAWN_EGG(35, "MONSTER_EGG"),
|
VEX_SPAWN_EGG(35, "MONSTER_EGG"),
|
||||||
|
@ -1598,12 +1617,14 @@ public enum XMaterial {
|
||||||
WHITE_WALL_BANNER(15, "WALL_BANNER"),
|
WHITE_WALL_BANNER(15, "WALL_BANNER"),
|
||||||
WHITE_WOOL("WOOL"),
|
WHITE_WOOL("WOOL"),
|
||||||
WILD_ARMOR_TRIM_SMITHING_TEMPLATE,
|
WILD_ARMOR_TRIM_SMITHING_TEMPLATE,
|
||||||
|
WIND_CHARGE,
|
||||||
WITCH_SPAWN_EGG(66, "MONSTER_EGG"),
|
WITCH_SPAWN_EGG(66, "MONSTER_EGG"),
|
||||||
WITHER_ROSE,
|
WITHER_ROSE,
|
||||||
WITHER_SKELETON_SKULL(1, "SKULL", "SKULL_ITEM"),
|
WITHER_SKELETON_SKULL(1, "SKULL", "SKULL_ITEM"),
|
||||||
WITHER_SKELETON_SPAWN_EGG(5, "MONSTER_EGG"),
|
WITHER_SKELETON_SPAWN_EGG(5, "MONSTER_EGG"),
|
||||||
WITHER_SKELETON_WALL_SKULL(1, "SKULL", "SKULL_ITEM"),
|
WITHER_SKELETON_WALL_SKULL(1, "SKULL", "SKULL_ITEM"),
|
||||||
WITHER_SPAWN_EGG,
|
WITHER_SPAWN_EGG,
|
||||||
|
WOLF_ARMOR,
|
||||||
WOLF_SPAWN_EGG(95, "MONSTER_EGG"),
|
WOLF_SPAWN_EGG(95, "MONSTER_EGG"),
|
||||||
WOODEN_AXE("WOOD_AXE"),
|
WOODEN_AXE("WOOD_AXE"),
|
||||||
WOODEN_HOE("WOOD_HOE"),
|
WOODEN_HOE("WOOD_HOE"),
|
||||||
|
@ -1664,14 +1685,6 @@ public enum XMaterial {
|
||||||
private static final Cache<String, XMaterial> NAME_CACHE = CacheBuilder.newBuilder()
|
private static final Cache<String, XMaterial> NAME_CACHE = CacheBuilder.newBuilder()
|
||||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||||
.build();
|
.build();
|
||||||
/**
|
|
||||||
* This is used for {@link #isOneOf(Collection)}
|
|
||||||
*
|
|
||||||
* @since 3.4.0
|
|
||||||
*/
|
|
||||||
private static final Cache<String, Pattern> CACHED_REGEX = CacheBuilder.newBuilder()
|
|
||||||
.expireAfterAccess(3, TimeUnit.HOURS)
|
|
||||||
.build();
|
|
||||||
/**
|
/**
|
||||||
* The maximum data value in the pre-flattening update which belongs to {@link #VILLAGER_SPAWN_EGG}<br>
|
* The maximum data value in the pre-flattening update which belongs to {@link #VILLAGER_SPAWN_EGG}<br>
|
||||||
* <a href="https://minecraftitemids.com/types/spawn-egg">Spawn Eggs</a>
|
* <a href="https://minecraftitemids.com/types/spawn-egg">Spawn Eggs</a>
|
||||||
|
@ -1910,9 +1923,10 @@ public enum XMaterial {
|
||||||
|
|
||||||
// Potions used the items data value to store
|
// Potions used the items data value to store
|
||||||
// information about the type of potion in 1.8
|
// information about the type of potion in 1.8
|
||||||
if (!supports(9) && material.endsWith("ION")) {
|
if (!supports(9) && material.equals("POTION")) {
|
||||||
// There's also 16000+ data value technique, but this is more reliable.
|
// Source: v1.8.8 org.bukkit.potion.Potion.fromDamage(int damage)
|
||||||
return Potion.fromItemStack(item).isSplash() ? SPLASH_POTION : POTION;
|
int damage = item.getDurability();
|
||||||
|
return ((damage & 16384) > 0) ? SPLASH_POTION : POTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refer to the enum for info.
|
// Refer to the enum for info.
|
||||||
|
@ -1921,9 +1935,14 @@ public enum XMaterial {
|
||||||
// If this happens to more materials in the future, I might have to change the system.
|
// If this happens to more materials in the future, I might have to change the system.
|
||||||
if (supports(13) && !supports(14)) {
|
if (supports(13) && !supports(14)) {
|
||||||
// https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/diff/src/main/java/org/bukkit/Material.java?until=67d908a9830c71267ee740f5bddd728ce9c64cc7
|
// https://hub.spigotmc.org/stash/projects/SPIGOT/repos/bukkit/diff/src/main/java/org/bukkit/Material.java?until=67d908a9830c71267ee740f5bddd728ce9c64cc7
|
||||||
if (material.equals("CACTUS_GREEN")) return GREEN_DYE;
|
switch (material) {
|
||||||
if (material.equals("ROSE_RED")) return RED_DYE;
|
case "CACTUS_GREEN":
|
||||||
if (material.equals("DANDELION_YELLOW")) return YELLOW_DYE;
|
return GREEN_DYE;
|
||||||
|
case "ROSE_RED":
|
||||||
|
return RED_DYE;
|
||||||
|
case "DANDELION_YELLOW":
|
||||||
|
return YELLOW_DYE;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check FILLED_MAP enum for more info.
|
// Check FILLED_MAP enum for more info.
|
||||||
|
@ -1935,29 +1954,6 @@ public enum XMaterial {
|
||||||
throw new IllegalArgumentException("Unsupported material from item: " + material + " (" + data + ')');
|
throw new IllegalArgumentException("Unsupported material from item: " + material + " (" + data + ')');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the XMaterial based on the material's ID (Magic Value) and data value.<br>
|
|
||||||
* You should avoid using this for performance issues.
|
|
||||||
*
|
|
||||||
* @param id the ID (Magic value) of the material.
|
|
||||||
* @param data the data value of the material.
|
|
||||||
* @return a parsed XMaterial with the same ID and data value.
|
|
||||||
* @see #matchXMaterial(ItemStack)
|
|
||||||
* @since 2.0.0
|
|
||||||
* @deprecated this method loops through all the available materials and matches their ID using {@link #getId()}
|
|
||||||
* which takes a really long time. Plugins should no longer support IDs. If you want, you can make a {@link Map} cache yourself.
|
|
||||||
* This method obviously doesn't work for 1.13+ and will not be supported. This is only here for debugging purposes.
|
|
||||||
*/
|
|
||||||
@Nonnull
|
|
||||||
@Deprecated
|
|
||||||
public static Optional<XMaterial> matchXMaterial(int id, byte data) {
|
|
||||||
if (id < 0 || id > MAX_ID || data < 0) return Optional.empty();
|
|
||||||
for (XMaterial materials : VALUES) {
|
|
||||||
if (materials.data == data && materials.getId() == id) return Optional.of(materials);
|
|
||||||
}
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The main method that parses the given material name and data value as an XMaterial.
|
* The main method that parses the given material name and data value as an XMaterial.
|
||||||
* All the values passed to this method will not be null or empty and are formatted correctly.
|
* All the values passed to this method will not be null or empty and are formatted correctly.
|
||||||
|
@ -1966,7 +1962,6 @@ public enum XMaterial {
|
||||||
* @param data the data value of the material. Is always 0 or {@link #UNKNOWN_DATA_VALUE} when {@link Data#ISFLAT}
|
* @param data the data value of the material. Is always 0 or {@link #UNKNOWN_DATA_VALUE} when {@link Data#ISFLAT}
|
||||||
* @return an XMaterial (with the same data value if specified)
|
* @return an XMaterial (with the same data value if specified)
|
||||||
* @see #matchXMaterial(Material)
|
* @see #matchXMaterial(Material)
|
||||||
* @see #matchXMaterial(int, byte)
|
|
||||||
* @see #matchXMaterial(ItemStack)
|
* @see #matchXMaterial(ItemStack)
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
|
@ -2053,12 +2048,14 @@ public enum XMaterial {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* This is an internal API. Use {@link com.cryptomorin.xseries.reflection.XReflection#supports(int)} instead.
|
||||||
* Checks if the specified version is the same version or higher than the current server version.
|
* Checks if the specified version is the same version or higher than the current server version.
|
||||||
*
|
*
|
||||||
* @param version the major version to be checked. "1." is ignored. E.g. 1.12 = 12 | 1.9 = 9
|
* @param version the major version to be checked. "1." is ignored. E.g. 1.12 = 12 | 1.9 = 9
|
||||||
* @return true of the version is equal or higher than the current version.
|
* @return true of the version is equal or higher than the current version.
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
public static boolean supports(int version) {
|
public static boolean supports(int version) {
|
||||||
return Data.VERSION >= version;
|
return Data.VERSION >= version;
|
||||||
}
|
}
|
||||||
|
@ -2067,78 +2064,6 @@ public enum XMaterial {
|
||||||
return this.legacy;
|
return this.legacy;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the list of given material names matches the given base material.
|
|
||||||
* Mostly used for configs.
|
|
||||||
* <p>
|
|
||||||
* Supports {@link String#contains} {@code CONTAINS:NAME} and Regular Expression {@code REGEX:PATTERN} formats.
|
|
||||||
* <p>
|
|
||||||
* <b>Example:</b>
|
|
||||||
* <blockquote><pre>
|
|
||||||
* XMaterial material = {@link #matchXMaterial(ItemStack)};
|
|
||||||
* if (material.isOneOf(plugin.getConfig().getStringList("disabled-items")) return;
|
|
||||||
* </pre></blockquote>
|
|
||||||
* <br>
|
|
||||||
* <b>{@code CONTAINS} Examples:</b>
|
|
||||||
* <pre>
|
|
||||||
* {@code "CONTAINS:CHEST" -> CHEST, ENDERCHEST, TRAPPED_CHEST -> true}
|
|
||||||
* {@code "cOnTaINS:dYe" -> GREEN_DYE, YELLOW_DYE, BLUE_DYE, INK_SACK -> true}
|
|
||||||
* </pre>
|
|
||||||
* <p>
|
|
||||||
* <b>{@code REGEX} Examples</b>
|
|
||||||
* <pre>
|
|
||||||
* {@code "REGEX:^.+_.+_.+$" -> Every Material with 3 underlines or more: SHULKER_SPAWN_EGG, SILVERFISH_SPAWN_EGG, SKELETON_HORSE_SPAWN_EGG}
|
|
||||||
* {@code "REGEX:^.{1,3}$" -> Material names that have 3 letters only: BED, MAP, AIR}
|
|
||||||
* </pre>
|
|
||||||
* <p>
|
|
||||||
* The reason that there are tags for {@code CONTAINS} and {@code REGEX} is for the performance.
|
|
||||||
* Although RegEx patterns are cached in this method,
|
|
||||||
* please avoid using the {@code REGEX} tag if you can use the {@code CONTAINS} tag instead.
|
|
||||||
* It'll have a huge impact on performance.
|
|
||||||
* Please avoid using {@code (capturing groups)} there's no use for them in this case.
|
|
||||||
* If you want to use groups, use {@code (?: non-capturing groups)}. It's faster.
|
|
||||||
* <p>
|
|
||||||
* Want to learn RegEx? You can mess around in <a href="https://regexr.com/">RegExr</a> website.
|
|
||||||
*
|
|
||||||
* @param materials the material names to check base material on.
|
|
||||||
* @return true if one of the given material names is similar to the base material.
|
|
||||||
* @since 3.1.1
|
|
||||||
* @deprecated Use XTag.stringMatcher() instead.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
public boolean isOneOf(@Nullable Collection<String> materials) {
|
|
||||||
if (materials == null || materials.isEmpty()) return false;
|
|
||||||
String name = this.name();
|
|
||||||
|
|
||||||
for (String comp : materials) {
|
|
||||||
String checker = comp.toUpperCase(Locale.ENGLISH);
|
|
||||||
if (checker.startsWith("CONTAINS:")) {
|
|
||||||
comp = format(checker.substring(9));
|
|
||||||
if (name.contains(comp)) return true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (checker.startsWith("REGEX:")) {
|
|
||||||
comp = comp.substring(6);
|
|
||||||
Pattern pattern = CACHED_REGEX.getIfPresent(comp);
|
|
||||||
if (pattern == null) {
|
|
||||||
try {
|
|
||||||
pattern = Pattern.compile(comp);
|
|
||||||
CACHED_REGEX.put(comp, pattern);
|
|
||||||
} catch (PatternSyntaxException ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pattern != null && pattern.matcher(name).matches()) return true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Direct Object Equals
|
|
||||||
Optional<XMaterial> xMat = matchXMaterial(comp);
|
|
||||||
if (xMat.isPresent() && xMat.get() == this) return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the {@link Material} (and data value on older versions) of an item.
|
* Sets the {@link Material} (and data value on older versions) of an item.
|
||||||
* Damageable materials will not have their durability changed.
|
* Damageable materials will not have their durability changed.
|
||||||
|
@ -2209,7 +2134,6 @@ public enum XMaterial {
|
||||||
* Spigot added material ID support back in 1.16+
|
* Spigot added material ID support back in 1.16+
|
||||||
*
|
*
|
||||||
* @return the ID of the material or <b>-1</b> if it's not a legacy material or the server doesn't support the material.
|
* @return the ID of the material or <b>-1</b> if it's not a legacy material or the server doesn't support the material.
|
||||||
* @see #matchXMaterial(int, byte)
|
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
|
@ -2379,6 +2303,7 @@ public enum XMaterial {
|
||||||
*
|
*
|
||||||
* @since 9.0.0
|
* @since 9.0.0
|
||||||
*/
|
*/
|
||||||
|
@ApiStatus.Internal
|
||||||
private static final class Data {
|
private static final class Data {
|
||||||
/**
|
/**
|
||||||
* The current version of the server in the form of a major version.
|
* The current version of the server in the form of a major version.
|
||||||
|
|
Loading…
Add table
Reference in a new issue