
cn.nukkit.item.RuntimeItemMapping Maven / Gradle / Ivy
package cn.nukkit.item;
import cn.nukkit.Server;
import cn.nukkit.api.DoNotModify;
import cn.nukkit.api.PowerNukkitOnly;
import cn.nukkit.api.PowerNukkitXOnly;
import cn.nukkit.api.Since;
import cn.nukkit.block.customblock.CustomBlock;
import cn.nukkit.item.RuntimeItems.MappingEntry;
import cn.nukkit.item.customitem.CustomItem;
import cn.nukkit.item.customitem.CustomItemDefinition;
import cn.nukkit.utils.BinaryStream;
import com.google.common.base.Preconditions;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.jetbrains.annotations.NotNull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.*;
import java.util.function.Supplier;
/**
* 这个类用于将旧版本物品以id:damage形式转换到新版本物品无damage值(0)的形式,以及提供{@link cn.nukkit.network.protocol.StartGamePacket StartGamePacket}中item Palette的作用
* 例如在1.19.70中,不同颜色的羊毛由wool:color damage变成了minecraft:white_wool,minecraft:orange_wool这类字面量形式。
*/
@Log4j2
public class RuntimeItemMapping {
private final Int2ObjectMap runtime2Legacy = new Int2ObjectOpenHashMap<>();
private final Int2ObjectMap legacy2Runtime = new Int2ObjectOpenHashMap<>();//legacyFullID to Runtime
private final Map identifier2Legacy = new HashMap<>();
@PowerNukkitXOnly
@Since("1.19.70-r2")
private final List itemPaletteEntries = new ArrayList<>();
@PowerNukkitXOnly
@Since("1.19.70-r2")
private final Int2ObjectMap runtimeId2Name = new Int2ObjectOpenHashMap<>();
@PowerNukkitXOnly
@Since("1.19.70-r2")
private final Object2IntMap name2RuntimeId = new Object2IntOpenHashMap<>();
@PowerNukkitXOnly
@Since("1.19.70-r2")
private final Map> namespacedIdItem = new HashMap<>();
@PowerNukkitXOnly
@Since("1.19.80-r1")
private static final BiMap blockMappings = HashBiMap.create();
private byte[] itemPalette;
public RuntimeItemMapping(Map mappings) {
try (InputStream stream = Server.class.getClassLoader().getResourceAsStream("runtime_item_states.json")) {
if (stream == null) {
throw new AssertionError("Unable to load runtime_item_states.json");
}
JsonArray runtimeItems = JsonParser.parseReader(new InputStreamReader(stream)).getAsJsonArray();
for (JsonElement element : runtimeItems) {
if (!element.isJsonObject()) {
throw new IllegalStateException("Invalid entry");
}
JsonObject entry = element.getAsJsonObject();
String identifier = entry.get("name").getAsString();
int runtimeId = entry.get("id").getAsInt();
this.runtimeId2Name.put(runtimeId, identifier);
this.name2RuntimeId.put(identifier, runtimeId);
boolean hasDamage = false;
int damage = 0;
int legacyId;
if (mappings.containsKey(identifier)) {
MappingEntry mapping = mappings.get(identifier);
legacyId = RuntimeItems.getLegacyIdFromLegacyString(mapping.legacyName());
if (legacyId == -1) {
throw new IllegalStateException("Unable to match " + mapping + " with legacyId");
}
damage = mapping.damage();
hasDamage = true;
} else {
legacyId = RuntimeItems.getLegacyIdFromLegacyString(identifier);
if (legacyId == -1) {
log.trace("Unable to find legacyId for " + identifier);
continue;
}
}
int fullId = RuntimeItems.getFullId(legacyId, damage);
LegacyEntry legacyEntry = new LegacyEntry(legacyId, hasDamage, damage);
RuntimeEntry runtimeEntry = new RuntimeEntry(identifier, runtimeId, hasDamage, false);
this.runtime2Legacy.put(runtimeId, legacyEntry);
this.identifier2Legacy.put(identifier, legacyEntry);
if (legacy2Runtime.containsKey(fullId)) {
int old = RuntimeItems.getLegacyIdFromLegacyString(legacy2Runtime.get(fullId).identifier());
int now = RuntimeItems.getLegacyIdFromLegacyString(runtimeEntry.identifier());
if (old != -1 && now == -1) {
legacy2Runtime.put(fullId, runtimeEntry);
}
} else {
this.legacy2Runtime.put(fullId, runtimeEntry);
}
this.itemPaletteEntries.add(runtimeEntry);
}
this.generatePalette();
} catch (IOException e) {
throw new RuntimeException(e);
}
try (InputStream stream = Server.class.getClassLoader().getResourceAsStream("block_mappings.json")) {
if (stream == null) {
throw new AssertionError("Unable to load block_mappings.json");
}
JsonObject itemMapping = JsonParser.parseReader(new InputStreamReader(stream)).getAsJsonObject();
for (String legacyID : itemMapping.keySet()) {
JsonObject convertData = itemMapping.getAsJsonObject(legacyID);
int id = Integer.parseInt(legacyID);
for (String damageStr : convertData.keySet()) {
String identifier = convertData.get(damageStr).getAsString();
int damage = Integer.parseInt(damageStr);
blockMappings.put(id + ":" + damage, identifier);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private void generatePalette() {
if (this.itemPaletteEntries.isEmpty()) {
return;
}
BinaryStream paletteBuffer = new BinaryStream();
paletteBuffer.putUnsignedVarInt(this.itemPaletteEntries.size());
for (RuntimeEntry entry : this.itemPaletteEntries) {
paletteBuffer.putString(entry.identifier());
paletteBuffer.putLShort(entry.runtimeId());
paletteBuffer.putBoolean(entry.isComponent()); // Component item
}
this.itemPalette = paletteBuffer.getBuffer();
}
/**
* From runtimeId to get legacy item entry.
*
* @param runtimeId the runtime id
* @return the legacy entry
*/
public LegacyEntry fromRuntime(int runtimeId) {
LegacyEntry legacyEntry = this.runtime2Legacy.get(runtimeId);
if (legacyEntry == null) {
throw new IllegalArgumentException("Unknown runtime2Legacy mapping: " + runtimeId);
}
return legacyEntry;
}
/**
* from legacy item id and meta value to get runtimeId
*
* @param id the id
* @param meta the meta
* @return the runtime entry
*/
public RuntimeEntry toRuntime(int id, int meta) {
RuntimeEntry runtimeEntry = this.legacy2Runtime.get(RuntimeItems.getFullId(id, meta));
if (runtimeEntry == null) {
runtimeEntry = this.legacy2Runtime.get(RuntimeItems.getFullId(id, 0));
}
if (runtimeEntry == null) {
throw new IllegalArgumentException("Unknown legacy2Runtime mapping: id=" + id + " meta=" + meta);
}
return runtimeEntry;
}
/**
* From identifier to get legacy entry.According to item_mappings.json
*
* @param identifier the identifier
* @return the legacy entry
*/
public LegacyEntry fromIdentifier(String identifier) {
return this.identifier2Legacy.get(identifier);
}
/**
* Get item palette byte [ ].
*
* @return the byte [ ]
*/
public byte[] getItemPalette() {
return this.itemPalette;
}
@PowerNukkitXOnly
@Since("1.6.0.0-PNX")
public void registerCustomItem(CustomItem customItem, Supplier- constructor) {
var runtimeId = CustomItemDefinition.getRuntimeId(customItem.getNamespaceId());
RuntimeEntry entry = new RuntimeEntry(
customItem.getNamespaceId(),
runtimeId,
false,
true
);
this.itemPaletteEntries.add(entry);
this.runtimeId2Name.put(runtimeId, customItem.getNamespaceId());
this.name2RuntimeId.put(customItem.getNamespaceId(), runtimeId);
this.registerNamespacedIdItem(customItem.getNamespaceId(), constructor);
this.generatePalette();
}
@PowerNukkitXOnly
@Since("1.6.0.0-PNX")
public void deleteCustomItem(CustomItem customItem) {
this.runtimeId2Name.remove(customItem.getId());
this.name2RuntimeId.removeInt(customItem.getNamespaceId());
this.itemPaletteEntries.removeIf(next -> next.identifier().equals(customItem.getNamespaceId()));
this.generatePalette();
}
@PowerNukkitXOnly
@Since("1.6.0.0-PNX")
public void registerCustomBlock(List
blocks) {
for (var block : blocks) {
int id = 255 - block.getId();//方块物品id等于 255-方块id(即-750开始递减)
RuntimeEntry entry = new RuntimeEntry(
block.getNamespaceId(),//方块命名空间也是方块物品命名空间
id,
false,
true
);
LegacyEntry legacyEntry = new LegacyEntry(id, false, 0);
this.itemPaletteEntries.add(entry);
this.namespacedIdItem.put(block.getNamespaceId(), block::toItem);
this.identifier2Legacy.put(block.getNamespaceId(), legacyEntry);
this.legacy2Runtime.put(RuntimeItems.getFullId(id, 0), entry);
this.runtime2Legacy.put(id, legacyEntry);
this.runtimeId2Name.put(id, block.getNamespaceId());
this.name2RuntimeId.put(block.getNamespaceId(), id);
}
this.generatePalette();
}
@PowerNukkitXOnly
@Since("1.6.0.0-PNX")
public void deleteCustomBlock(List blocks) {
for (var block : blocks) {
this.runtimeId2Name.remove(block.getId());
this.name2RuntimeId.removeInt(block.getNamespaceId());
this.namespacedIdItem.remove(block.getNamespaceId());
this.identifier2Legacy.remove(block.getNamespaceId());
this.legacy2Runtime.remove(RuntimeItems.getFullId(255 - block.getId(), 0));
this.runtime2Legacy.remove(255 - block.getId());
}
var iter = this.itemPaletteEntries.iterator();
while (iter.hasNext()) {
RuntimeEntry next = iter.next();
for (var block : blocks) {
if (block.getNamespaceId().equals(next.identifier())) {
iter.remove();
break;
}
}
}
this.generatePalette();
}
/**
* Returns the network id based on the full id of the given item.
*
* @param item Given item
* @return The network id
* @throws IllegalArgumentException If the mapping of the full id to the network id is unknown
*/
@PowerNukkitOnly
@Since("1.4.0.0-PN")
public int getNetworkId(Item item) {
if (item instanceof StringItem) {
return name2RuntimeId.getInt(item.getNamespaceId());
}
int fullId = RuntimeItems.getFullId(item.getId(), item.getDamage());
if (!item.hasMeta() && item.getDamage() != 0) { // Fuzzy crafting recipe of a remapped item, like charcoal
fullId = RuntimeItems.getFullId(item.getId(), item.getDamage());
}
RuntimeEntry runtimeEntry = legacy2Runtime.get(fullId);
if (runtimeEntry == null) {
runtimeEntry = legacy2Runtime.get(RuntimeItems.getFullId(item.getId(), 0));
}
if (runtimeEntry == null) {
throw new IllegalArgumentException("Unknown item mapping " + item);
}
return runtimeEntry.runtimeId;
}
/**
* Returns the full id of a given network id.
*
* @param networkId The given network id
* @return The full id
* @throws IllegalArgumentException If the mapping of the full id to the network id is unknown
*/
@PowerNukkitOnly
@Since("1.4.0.0-PN")
public int getLegacyFullId(int networkId) {
LegacyEntry legacyEntry = runtime2Legacy.get(networkId);
if (legacyEntry == null) {
throw new IllegalArgumentException("Unknown network mapping " + networkId);
}
return legacyEntry.fullID();
}
@PowerNukkitOnly
@Since("1.4.0.0-PN")
public byte[] getItemDataPalette() {
return this.itemPalette;
}
/**
* Returns the namespaced id of a given network id.
*
* @param networkId The given network id
* @return The namespace id or {@code null} if it is unknown
*/
@PowerNukkitOnly
@Since("1.4.0.0-PN")
@Nullable
public String getNamespacedIdByNetworkId(int networkId) {
return runtimeId2Name.get(networkId);
}
/**
* Returns the network id of a given namespaced id.
*
* @param namespaceId The given namespaced id
* @return A network id wrapped in {@link OptionalInt} or an empty {@link OptionalInt} if it is unknown
*/
@PowerNukkitOnly
@Since("1.4.0.0-PN")
@NotNull
public OptionalInt getNetworkIdByNamespaceId(@NotNull String namespaceId) {
int id = name2RuntimeId.getOrDefault(namespaceId, -1);
if (id == -1) return OptionalInt.empty();
return OptionalInt.of(id);
}
/**
* Creates a new instance of the respective {@link Item} by the namespaced id.
*
* @param namespaceId The namespaced id
* @param amount How many items will be in the stack.
* @return The correct {@link Item} instance with the write item id and item damage values.
* @throws IllegalArgumentException If there are unknown mappings in the process.
*/
@PowerNukkitOnly
@Since("1.4.0.0-PN")
@NotNull
public Item getItemByNamespaceId(@NotNull String namespaceId, int amount) {
Supplier- constructor = this.namespacedIdItem.get(namespaceId.toLowerCase(Locale.ENGLISH));
if (constructor != null) {
try {
Item item = constructor.get();
item.setCount(amount);
return item;
} catch (Exception e) {
log.warn("Could not create a new instance of {} using the namespaced id {}", constructor, namespaceId, e);
}
}
int legacyFullId;
try {
legacyFullId = getLegacyFullId(
getNetworkIdByNamespaceId(namespaceId)
.orElseThrow(() -> new IllegalArgumentException("The network id of \"" + namespaceId + "\" is unknown"))
);
} catch (IllegalArgumentException e) {
log.debug("Found an unknown item {}", namespaceId, e);
Item item = new StringItemUnknown(namespaceId);
item.setCount(amount);
return item;
}
int id = RuntimeItems.getId(legacyFullId);
int data = 0;
if (RuntimeItems.hasData(legacyFullId)) {
data = RuntimeItems.getData(legacyFullId);
}
return Item.get(id, data, amount);
}
@SneakyThrows
@PowerNukkitXOnly
@Since("1.19.70-r2")
public void registerNamespacedIdItem(@NotNull Class extends StringItem> item) {
Constructor extends StringItem> declaredConstructor = item.getDeclaredConstructor();
var Item = declaredConstructor.newInstance();
registerNamespacedIdItem(Item.getNamespaceId(), stringItemSupplier(declaredConstructor));
}
@PowerNukkitOnly
public void registerNamespacedIdItem(@NotNull String namespacedId, @NotNull Constructor extends Item> constructor) {
Preconditions.checkNotNull(namespacedId, "namespacedId is null");
Preconditions.checkNotNull(constructor, "constructor is null");
this.namespacedIdItem.put(namespacedId.toLowerCase(Locale.ENGLISH), itemSupplier(constructor));
}
@SneakyThrows
@PowerNukkitOnly
public void registerNamespacedIdItem(@NotNull String namespacedId, @NotNull Supplier
- constructor) {
Preconditions.checkNotNull(namespacedId, "namespacedId is null");
Preconditions.checkNotNull(constructor, "constructor is null");
this.namespacedIdItem.put(namespacedId.toLowerCase(Locale.ENGLISH), constructor);
}
@DoNotModify
@Since("1.19.80-r1")
@PowerNukkitXOnly
public static BiMap
getBlockMapping() {
return blockMappings;
}
@NotNull
private static Supplier- itemSupplier(@NotNull Constructor extends Item> constructor) {
return () -> {
try {
return constructor.newInstance();
} catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException(e);
}
};
}
@Since("1.19.60-r1")
@PowerNukkitXOnly
@NotNull
private static Supplier
- stringItemSupplier(@NotNull Constructor extends StringItem> constructor) {
return () -> {
try {
return (Item) constructor.newInstance();
} catch (ReflectiveOperationException e) {
throw new UnsupportedOperationException(e);
}
};
}
public record LegacyEntry(int legacyId, boolean hasDamage, int damage) {
public int getDamage() {
return this.hasDamage ? this.damage : 0;
}
public int fullID() {
return RuntimeItems.getFullId(legacyId, damage);
}
}
/**
* The type Runtime entry.
*
* @param hasDamage 如果为false代表这个runtime物品没有对应的legacy item映射
*/
public record RuntimeEntry(String identifier, int runtimeId, boolean hasDamage, boolean isComponent) {
}
}