More work on predicates, I'll probably simplify it later

This commit is contained in:
Eclipse 2024-12-01 12:46:21 +00:00
parent 8ea3c973f3
commit 7888956a36
No known key found for this signature in database
GPG key ID: 95E6998F82EC938A
21 changed files with 609 additions and 90 deletions

View file

@ -28,8 +28,11 @@ package org.geysermc.geyser.api.item.custom.v2;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.GeyserApi;
import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import java.util.List;
/**
* This is used to define a custom item and its properties.
*/
@ -66,12 +69,12 @@ public interface CustomItemDefinition {
}
/**
* The predicate that has to match for this item to be used. These predicates are similar to the Java item model predicates.
* The predicates that have to match for this item to be used. These predicates are similar to the Java item model predicates.
*
* <p>If multiple predicates match, then the first registered item with a matching predicate is used. If no predicates match, then the item definition without a predicate
* <p>If all predicates match for multiple definitions, then the first registered item with all matching predicates is used. If no predicates match, then the item definition without any predicates
* is used, if any.</p>
*/
void predicate();
@NonNull List<CustomItemPredicate<?>> predicates();
/**
* The item's Bedrock options. These describe item properties that can't be described in item components, e.g. item texture size and if the item is allowed in the off-hand.
@ -105,6 +108,8 @@ public interface CustomItemDefinition {
Builder displayName(String displayName);
Builder predicate(@NonNull CustomItemPredicate<?> predicate);
Builder bedrockOptions(CustomItemBedrockOptions.@NonNull Builder options);
// TODO do we want another format for this?

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate;
public record CustomItemPredicate<T>(ItemPredicateType<T> type, T data) {
}

View file

@ -0,0 +1,49 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.ConditionPredicateData;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.MatchPredicateData;
import java.util.HashMap;
import java.util.Map;
public class ItemPredicateType<T> {
private static final Map<String, ItemPredicateType<?>> TYPES = new HashMap<>();
public static final ItemPredicateType<ConditionPredicateData> CONDITION = create("condition");
public static final ItemPredicateType<MatchPredicateData<?>> MATCH = create("match");
public static ItemPredicateType<?> getType(String name) {
return TYPES.get(name);
}
private static <T> ItemPredicateType<T> create(String name) {
ItemPredicateType<T> type = new ItemPredicateType<>();
TYPES.put(name, type);
return type;
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate.data;
// TODO maybe type should be a generic class with data, but this works for now
public record ConditionPredicateData(ConditionProperty property, boolean expected, int index) {
public ConditionPredicateData(ConditionProperty property, boolean expected) {
this(property, expected, 0);
}
// TODO maybe we can extend this
public enum ConditionProperty {
BROKEN,
DAMAGED,
CUSTOM_MODEL_DATA
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate.data;
public record CustomModelDataPredicate<T>(T data, int index) {
}

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate.data.match;
public enum ChargeType {
NONE,
ARROW,
ROCKET
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate.data.match;
public record MatchPredicateData<T>(MatchPredicateProperty<T> property, T data) {
}

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.api.item.custom.v2.predicate.data.match;
import net.kyori.adventure.key.Key;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.CustomModelDataPredicate;
import java.util.HashMap;
import java.util.Map;
// TODO can we do more?
public class MatchPredicateProperty<T> {
private static final Map<String, MatchPredicateProperty<?>> PROPERTIES = new HashMap<>();
public static final MatchPredicateProperty<ChargeType> CHARGE_TYPE = create("charge_type");
public static final MatchPredicateProperty<Key> TRIM_MATERIAL = create("trim_material");
public static final MatchPredicateProperty<Key> CONTEXT_DIMENSION = create("context_dimension");
public static final MatchPredicateProperty<CustomModelDataPredicate<String>> CUSTOM_MODEL_DATA = create("custom_model_data");
public static MatchPredicateProperty<?> getProperty(String name) {
return PROPERTIES.get(name);
}
private static <T> MatchPredicateProperty<T> create(String name) {
MatchPredicateProperty<T> property = new MatchPredicateProperty<>();
PROPERTIES.put(name, property);
return property;
}
}

View file

@ -29,16 +29,20 @@ import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions;
import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public record GeyserCustomItemDefinition(@NonNull Key bedrockIdentifier, String displayName, @NonNull Key model,
public record GeyserCustomItemDefinition(@NonNull Key bedrockIdentifier, String displayName, @NonNull Key model, @NonNull List<CustomItemPredicate<?>> predicates,
@NonNull CustomItemBedrockOptions bedrockOptions, @NonNull DataComponents components) implements CustomItemDefinition {
public static class Builder implements CustomItemDefinition.Builder {
private final Key bedrockIdentifier;
private final Key model;
private final List<CustomItemPredicate<?>> predicates = new ArrayList<>();
private String displayName;
private CustomItemBedrockOptions bedrockOptions = CustomItemBedrockOptions.builder().build();
private DataComponents components = new DataComponents(new HashMap<>());
@ -55,6 +59,12 @@ public record GeyserCustomItemDefinition(@NonNull Key bedrockIdentifier, String
return this;
}
@Override
public CustomItemDefinition.Builder predicate(@NonNull CustomItemPredicate<?> predicate) {
predicates.add(predicate);
return this;
}
@Override
public CustomItemDefinition.Builder bedrockOptions(CustomItemBedrockOptions.@NonNull Builder options) {
this.bedrockOptions = options.build();
@ -69,7 +79,7 @@ public record GeyserCustomItemDefinition(@NonNull Key bedrockIdentifier, String
@Override
public CustomItemDefinition build() {
return new GeyserCustomItemDefinition(bedrockIdentifier, displayName, model, bedrockOptions, components);
return new GeyserCustomItemDefinition(bedrockIdentifier, displayName, model, List.copyOf(predicates), bedrockOptions, components);
}
}
}

View file

@ -125,7 +125,7 @@ public class Item {
.damage(mapping.getBedrockData())
.count(count);
ItemTranslator.translateCustomItem(components, builder, mapping);
ItemTranslator.translateCustomItem(session, components, builder, mapping);
return builder;
}

View file

@ -49,7 +49,7 @@ public class PotionItem extends Item {
if (components == null) return super.translateToBedrock(session, count, components, mapping, mappings);
PotionContents potionContents = components.get(DataComponentType.POTION_CONTENTS);
if (potionContents != null) {
ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(components, mapping);
ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(session, components, mapping);
if (customItemDefinition == null) {
Potion potion = Potion.getByJavaId(potionContents.getPotionId());
if (potion != null) {

View file

@ -74,7 +74,7 @@ public class ShulkerBoxItem extends BlockItem {
if (boxComponents != null) {
// Check for custom items
ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(boxComponents, boxMapping);
ItemDefinition customItemDefinition = CustomItemTranslator.getCustomItem(session, boxComponents, boxMapping);
if (customItemDefinition != null) {
bedrockIdentifier = customItemDefinition.getIdentifier();
bedrockData = 0;

View file

@ -29,11 +29,19 @@ import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.kyori.adventure.key.Key;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.geysermc.geyser.Constants;
import org.geysermc.geyser.GeyserImpl;
import org.geysermc.geyser.api.item.custom.v2.BedrockCreativeTab;
import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions;
import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate;
import org.geysermc.geyser.api.item.custom.v2.predicate.ItemPredicateType;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.ConditionPredicateData;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.CustomModelDataPredicate;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.ChargeType;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.MatchPredicateData;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.MatchPredicateProperty;
import org.geysermc.geyser.item.exception.InvalidCustomMappingsFileException;
import org.geysermc.geyser.registry.mappings.components.DataComponentReaders;
import org.geysermc.geyser.registry.mappings.util.CustomBlockMapping;
@ -101,7 +109,7 @@ public class MappingsReader_v2 extends MappingsReader {
builder.displayName(node.get("display_name").asText());
}
// TODO predicate
readPredicates(builder, node.get("predicate"));
builder.bedrockOptions(readBedrockOptions(node.get("bedrock_options")));
@ -166,6 +174,86 @@ public class MappingsReader_v2 extends MappingsReader {
return builder;
}
private void readPredicates(CustomItemDefinition.Builder builder, JsonNode node) throws InvalidCustomMappingsFileException {
if (node == null) {
return;
}
if (node.isObject()) {
readPredicate(builder, node);
} else if (node.isArray()) {
node.forEach(predicate -> {
try {
readPredicate(builder, predicate);
} catch (InvalidCustomMappingsFileException e) {
GeyserImpl.getInstance().getLogger().error("Error in reading predicate", e); // TODO log this better
}
});
} else {
throw new InvalidCustomMappingsFileException("Expected predicate key to be a list of predicates or a predicate");
}
}
private void readPredicate(CustomItemDefinition.Builder builder, @NonNull JsonNode node) throws InvalidCustomMappingsFileException {
if (!node.isObject()) {
throw new InvalidCustomMappingsFileException("Expected predicate to be an object");
}
JsonNode typeNode = node.get("type");
if (typeNode == null || !typeNode.isTextual()) {
throw new InvalidCustomMappingsFileException("Predicate missing type key");
}
ItemPredicateType<?> type = ItemPredicateType.getType(typeNode.asText());
JsonNode propertyNode = node.get("property");
if (propertyNode == null || !propertyNode.isTextual()) {
throw new InvalidCustomMappingsFileException("Predicate missing property key");
}
if (type == ItemPredicateType.CONDITION) {
try {
ConditionPredicateData.ConditionProperty property = ConditionPredicateData.ConditionProperty.valueOf(propertyNode.asText().toUpperCase());
JsonNode expected = node.get("expected");
JsonNode index = node.get("index");
builder.predicate(new CustomItemPredicate<>(ItemPredicateType.CONDITION, new ConditionPredicateData(property,
expected == null || expected.asBoolean(), index == null || !index.isIntegralNumber() ? 0 : index.asInt())));
} catch (IllegalArgumentException exception) {
throw new InvalidCustomMappingsFileException("Unknown property " + propertyNode.asText());
}
} else if (type == ItemPredicateType.MATCH) {
MatchPredicateProperty<?> property = MatchPredicateProperty.getProperty(propertyNode.asText());
if (property == null) {
throw new InvalidCustomMappingsFileException("Unknown property " + propertyNode.asText());
}
JsonNode value = node.get("value");
if (value == null || !value.isTextual()) {
throw new InvalidCustomMappingsFileException("Predicate missing value key");
}
if (property == MatchPredicateProperty.CHARGE_TYPE) {
try {
ChargeType chargeType = ChargeType.valueOf(value.asText().toUpperCase());
builder.predicate(new CustomItemPredicate<>(ItemPredicateType.MATCH,
new MatchPredicateData<>(MatchPredicateProperty.CHARGE_TYPE, chargeType)));
} catch (IllegalArgumentException exception) {
throw new InvalidCustomMappingsFileException("Unknown charge type " + value.asText());
}
} else if (property == MatchPredicateProperty.TRIM_MATERIAL || property == MatchPredicateProperty.CONTEXT_DIMENSION) {
builder.predicate(new CustomItemPredicate<>(ItemPredicateType.MATCH,
new MatchPredicateData<>((MatchPredicateProperty<Key>) property, Key.key(value.asText())))); // TODO
} else if (property == MatchPredicateProperty.CUSTOM_MODEL_DATA) {
JsonNode index = node.get("index");
if (index == null || !index.isIntegralNumber()) {
throw new InvalidCustomMappingsFileException("Predicate missing index key");
}
builder.predicate(new CustomItemPredicate<>(ItemPredicateType.MATCH,
new MatchPredicateData<>(MatchPredicateProperty.CUSTOM_MODEL_DATA, new CustomModelDataPredicate<>(value.asText(), index.asInt()))));
}
}
}
@Override
public CustomBlockMapping readBlockMappingEntry(String identifier, JsonNode node) throws InvalidCustomMappingsFileException {
return null; // TODO

View file

@ -40,6 +40,7 @@ import org.geysermc.geyser.api.item.custom.NonVanillaCustomItemData;
import org.geysermc.geyser.api.item.custom.v2.BedrockCreativeTab;
import org.geysermc.geyser.api.item.custom.v2.CustomItemBedrockOptions;
import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate;
import org.geysermc.geyser.item.GeyserCustomMappingData;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.item.components.WearableSlot;
@ -61,6 +62,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
public class CustomItemRegistryPopulator_v2 {
@ -82,7 +84,7 @@ public class CustomItemRegistryPopulator_v2 {
MappingsConfigReader mappingsConfigReader = new MappingsConfigReader();
// Load custom items from mappings files
mappingsConfigReader.loadItemMappingsFromJson((id, item) -> {
if (initialCheck(item, items)) {
if (initialCheck(id, item, customItems, items)) {
customItems.get(id).add(item);
}
});
@ -103,15 +105,84 @@ public class CustomItemRegistryPopulator_v2 {
return new GeyserCustomMappingData(componentItemData, itemDefinition, customItemName, bedrockId);
}
static boolean initialCheck(CustomItemDefinition item, Map<String, GeyserMappingItem> mappings) {
private static boolean initialCheck(String identifier, CustomItemDefinition item, Multimap<String, CustomItemDefinition> registered, Map<String, GeyserMappingItem> mappings) {
// TODO check if there's already a same model without predicate and this hasn't a predicate either
Key name = item.bedrockIdentifier();
if (name.namespace().equals(Key.MINECRAFT_NAMESPACE)) {
GeyserImpl.getInstance().getLogger().warning("Custom item namespace can't be minecraft");
if (!mappings.containsKey(identifier)) {
GeyserImpl.getInstance().getLogger().error("Could not find the Java item to add custom item properties to for " + item.bedrockIdentifier());
return false;
}
Key bedrockIdentifier = item.bedrockIdentifier();
if (bedrockIdentifier.namespace().equals(Key.MINECRAFT_NAMESPACE)) {
GeyserImpl.getInstance().getLogger().error("Custom item bedrock identifier namespace can't be minecraft");
return false;
} else if (item.model().namespace().equals(Key.MINECRAFT_NAMESPACE) && item.predicates().isEmpty()) {
GeyserImpl.getInstance().getLogger().error("Custom item definition model can't be minecraft without a predicate");
return false;
}
for (Map.Entry<String, CustomItemDefinition> entry : registered.entries()) {
if (entry.getValue().bedrockIdentifier().equals(item.bedrockIdentifier())) {
GeyserImpl.getInstance().getLogger().error("Duplicate custom item definition for Bedrock ID " + item.bedrockIdentifier());
return false;
}
Optional<String> error = checkPredicate(entry, identifier, item);
if (error.isPresent()) {
GeyserImpl.getInstance().getLogger().error("An existing item definition for the Java item " + identifier + " was already registered that conflicts with this one!");
GeyserImpl.getInstance().getLogger().error("First entry: " + entry.getValue().bedrockIdentifier());
GeyserImpl.getInstance().getLogger().error("Second entry: " + item.bedrockIdentifier());
GeyserImpl.getInstance().getLogger().error(error.orElseThrow());
}
}
return true;
}
/**
* Returns an error message if there was a conflict, or an empty optional otherwise
*/
private static Optional<String> checkPredicate(Map.Entry<String, CustomItemDefinition> existing, String identifier, CustomItemDefinition item) {
// TODO this is probably wrong
// If the definitions are for different Java items or models then it doesn't matter
if (!identifier.equals(existing.getKey()) || !item.model().equals(existing.getValue().model())) {
return Optional.empty();
}
// If they both don't have predicates they conflict
if (existing.getValue().predicates().isEmpty() && item.predicates().isEmpty()) {
return Optional.of("Both entries don't have predicates, the first must have a predicate");
}
// If a previously registered entry does have predicates, and this entry doesn't, then they also conflict
// Entries with predicates must always be first
if (existing.getValue().predicates().isEmpty() && !item.predicates().isEmpty()) {
return Optional.of("The first entry has no predicates, meaning that one will always be used");
} else if (item.predicates().isEmpty()) {
return Optional.empty(); // Item definitions are correctly ordered
}
// If all predicates of an existing entry also exist in a new entry, then the new entry is invalid
// This makes it required to order definitions correctly, so that "fallback predicates" are added last:
//
// A && B -> item1
// A -> item2
//
// Is the correct order, not
//
// A -> item2
// A && B -> item1
boolean existingHasAllPredicates = true;
for (CustomItemPredicate<?> predicate : existing.getValue().predicates()) {
if (!item.predicates().contains(predicate)) {
existingHasAllPredicates = false;
break;
}
}
if (existingHasAllPredicates) {
return Optional.of("Reorder your entries so that the one with the least amount of predicates is last");
}
return Optional.empty();
}
private static NbtMapBuilder createComponentNbt(CustomItemDefinition customItemDefinition, Item vanillaJavaItem, GeyserMappingItem vanillaMapping,
String customItemName, int customItemId) {
NbtMapBuilder builder = NbtMap.builder()
@ -325,6 +396,9 @@ public class CustomItemRegistryPopulator_v2 {
itemProperties.putInt("use_duration", (int) (consumable.consumeSeconds() * 20));
itemProperties.putInt("use_animation", BEDROCK_ANIMATIONS.get(consumable.animation()));
componentBuilder.putCompound("minecraft:use_animation", NbtMap.builder()
.putString("value", consumable.animation().toString().toLowerCase())
.build()); // TODO check
// this component is required to allow the eat animation to play
componentBuilder.putCompound("minecraft:food", NbtMap.builder().putBoolean("can_always_eat", canAlwaysEat).build());

View file

@ -51,6 +51,7 @@ import org.geysermc.geyser.session.cache.registry.JavaRegistries;
import org.geysermc.geyser.session.cache.registry.JavaRegistry;
import org.geysermc.geyser.session.cache.registry.JavaRegistryKey;
import org.geysermc.geyser.session.cache.registry.RegistryEntryContext;
import org.geysermc.geyser.session.cache.registry.RegistryEntryData;
import org.geysermc.geyser.session.cache.registry.SimpleJavaRegistry;
import org.geysermc.geyser.text.ChatDecoration;
import org.geysermc.geyser.translator.level.BiomeTranslator;
@ -189,7 +190,7 @@ public final class RegistryCache {
entryIdMap.put(entries.get(i).getId(), i);
}
List<T> builder = new ArrayList<>(entries.size());
List<RegistryEntryData<T>> builder = new ArrayList<>(entries.size());
for (int i = 0; i < entries.size(); i++) {
RegistryEntry entry = entries.get(i);
// If the data is null, that's the server telling us we need to use our default values.
@ -203,7 +204,7 @@ public final class RegistryCache {
RegistryEntryContext context = new RegistryEntryContext(entry, entryIdMap, registryCache.session);
// This is what Geyser wants to keep as a value for this registry.
T cacheEntry = reader.apply(context);
builder.add(i, cacheEntry);
builder.add(i, new RegistryEntryData<>(entry.getId(), cacheEntry));
}
localCache.reset(builder);
});

View file

@ -39,15 +39,25 @@ public interface JavaRegistry<T> {
*/
T byId(@NonNegative int id);
/**
* Looks up a registry entry by its ID, and returns it wrapped in {@link RegistryEntryData} so that its registered key is also known. The object can be null, or not present.
*/
RegistryEntryData<T> entryById(@NonNegative int id);
/**
* Reverse looks-up an object to return its network ID, or -1.
*/
int byValue(T value);
/**
* Reverse looks-up an object to return it wrapped in {@link RegistryEntryData}, or null.
*/
RegistryEntryData<T> entryByValue(T value);
/**
* Resets the objects by these IDs.
*/
void reset(List<T> values);
void reset(List<RegistryEntryData<T>> values);
/**
* All values of this registry, as a list.

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 GeyserMC. http://geysermc.org
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @author GeyserMC
* @link https://github.com/GeyserMC/Geyser
*/
package org.geysermc.geyser.session.cache.registry;
import net.kyori.adventure.key.Key;
public record RegistryEntryData<T>(Key key, T data) {
}

View file

@ -31,10 +31,18 @@ import org.checkerframework.checker.index.qual.NonNegative;
import java.util.List;
public class SimpleJavaRegistry<T> implements JavaRegistry<T> {
protected final ObjectArrayList<T> values = new ObjectArrayList<>();
protected final ObjectArrayList<RegistryEntryData<T>> values = new ObjectArrayList<>();
@Override
public T byId(@NonNegative int id) {
if (id < 0 || id >= this.values.size()) {
return null;
}
return this.values.get(id).data();
}
@Override
public RegistryEntryData<T> entryById(@NonNegative int id) {
if (id < 0 || id >= this.values.size()) {
return null;
}
@ -43,11 +51,26 @@ public class SimpleJavaRegistry<T> implements JavaRegistry<T> {
@Override
public int byValue(T value) {
return this.values.indexOf(value);
for (int i = 0; i < this.values.size(); i++) {
if (values.get(i).data().equals(value)) {
return i;
}
}
return -1;
}
@Override
public void reset(List<T> values) {
public RegistryEntryData<T> entryByValue(T value) {
for (RegistryEntryData<T> entry : this.values) {
if (entry.data().equals(value)) {
return entry;
}
}
return null;
}
@Override
public void reset(List<RegistryEntryData<T>> values) {
this.values.clear();
this.values.addAll(values);
this.values.trim();
@ -55,7 +78,7 @@ public class SimpleJavaRegistry<T> implements JavaRegistry<T> {
@Override
public List<T> values() {
return this.values;
return this.values.stream().map(RegistryEntryData::data).toList();
}
@Override

View file

@ -26,8 +26,21 @@
package org.geysermc.geyser.translator.item;
import net.kyori.adventure.key.Key;
import org.cloudburstmc.protocol.bedrock.data.TrimMaterial;
import org.geysermc.geyser.api.item.custom.v2.CustomItemDefinition;
import org.geysermc.geyser.api.item.custom.v2.predicate.CustomItemPredicate;
import org.geysermc.geyser.api.item.custom.v2.predicate.ItemPredicateType;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.ConditionPredicateData;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.ChargeType;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.MatchPredicateData;
import org.geysermc.geyser.api.item.custom.v2.predicate.data.match.MatchPredicateProperty;
import org.geysermc.geyser.item.Items;
import org.geysermc.geyser.level.JavaDimension;
import org.geysermc.geyser.session.GeyserSession;
import org.geysermc.geyser.session.cache.registry.RegistryEntryData;
import org.geysermc.geyser.util.MinecraftKey;
import org.geysermc.mcprotocollib.protocol.data.game.item.ItemStack;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.ArmorTrim;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponents;
import org.geysermc.mcprotocollib.protocol.data.game.item.component.DataComponentType;
import it.unimi.dsi.fastutil.Pair;
@ -43,7 +56,7 @@ import java.util.List;
public final class CustomItemTranslator {
@Nullable
public static ItemDefinition getCustomItem(DataComponents components, ItemMapping mapping) {
public static ItemDefinition getCustomItem(GeyserSession session, DataComponents components, ItemMapping mapping) {
if (components == null) {
return null;
}
@ -55,79 +68,82 @@ public final class CustomItemTranslator {
Key itemModel = components.getOrDefault(DataComponentType.ITEM_MODEL, MinecraftKey.key("air")); // TODO fallback onto default item model (when thats done by chris)
// TODO check if definitions/predicates are in the correct order
for (Pair<CustomItemDefinition, ItemDefinition> customModel : customItems) { // TODO Predicates
if (customModel.first().model().equals(itemModel)) {
return customModel.second();
boolean allMatch = true;
for (CustomItemPredicate<?> predicate : customModel.first().predicates()) {
if (!predicateMatches(session, predicate, components)) {
allMatch = false;
break;
}
}
if (allMatch) {
return customModel.second();
}
}
}
return null;
/*
List<Pair<CustomItemOptions, ItemDefinition>> customMappings = mapping.getCustomItemOptions();
if (customMappings.isEmpty()) {
return null;
}
int customModelData = components.getOrDefault(DataComponentType.CUSTOM_MODEL_DATA, 0);
boolean checkDamage = mapping.getJavaItem().maxDamage() > 0;
int damage = !checkDamage ? 0 : components.getOrDefault(DataComponentType.DAMAGE, 0);
boolean unbreakable = checkDamage && !isDamaged(components, damage);
for (Pair<CustomItemOptions, ItemDefinition> mappingTypes : customMappings) {
CustomItemOptions options = mappingTypes.key();
// Code note: there may be two or more conditions that a custom item must follow, hence the "continues"
// here with the return at the end.
// Implementation details: Java's predicate system works exclusively on comparing float numbers.
// A value doesn't necessarily have to match 100%; it just has to be the first to meet all predicate conditions.
// This is also why the order of iteration is important as the first to match will be the chosen display item.
// For example, if CustomModelData is set to 2f as the requirement, then the NBT can be any number greater or equal (2, 3, 4...)
// The same behavior exists for Damage (in fraction form instead of whole numbers),
// and Damaged/Unbreakable handles no damage as 0f and damaged as 1f.
if (checkDamage) {
if (unbreakable && options.unbreakable() == TriState.FALSE) {
continue;
}
OptionalInt damagePredicate = options.damagePredicate();
if (damagePredicate.isPresent() && damage < damagePredicate.getAsInt()) {
continue;
}
} else {
if (options.unbreakable() != TriState.NOT_SET || options.damagePredicate().isPresent()) {
// These will never match on this item. 1.19.2 behavior
// Maybe move this to CustomItemRegistryPopulator since it'll be the same for every item? If so, add a test.
continue;
}
}
OptionalInt customModelDataOption = options.customModelData();
if (customModelDataOption.isPresent() && customModelData < customModelDataOption.getAsInt()) {
continue;
}
if (options.defaultItem()) {
return null;
}
return mappingTypes.value();
}
return null;*/
}
/* These two functions are based off their Mojmap equivalents from 1.19.2 */
private static boolean predicateMatches(GeyserSession session, CustomItemPredicate<?> predicate, DataComponents components) {
if (predicate.type() == ItemPredicateType.CONDITION) {
ConditionPredicateData data = (ConditionPredicateData) predicate.data();
return switch (data.property()) {
case BROKEN -> nextDamageWillBreak(components);
case DAMAGED -> isDamaged(components);
case CUSTOM_MODEL_DATA -> false; // TODO 1.21.4
};
} else if (predicate.type() == ItemPredicateType.MATCH) {
MatchPredicateData<?> data = (MatchPredicateData<?>) predicate.data();
private static boolean isDamaged(DataComponents components, int damage) {
return isDamagableItem(components) && damage > 0;
if (data.property() == MatchPredicateProperty.CHARGE_TYPE) {
ChargeType expected = (ChargeType) data.data();
List<ItemStack> charged = components.get(DataComponentType.CHARGED_PROJECTILES);
if (charged == null) {
return expected == ChargeType.NONE;
} else if (expected == ChargeType.ROCKET) {
for (ItemStack projectile : charged) {
if (projectile.getId() == Items.FIREWORK_ROCKET.javaId()) {
return true;
}
}
return false;
}
return true;
} else if (data.property() == MatchPredicateProperty.TRIM_MATERIAL) {
Key material = (Key) data.data();
ArmorTrim trim = components.get(DataComponentType.TRIM);
if (trim == null || trim.material().isCustom()) {
return false;
}
RegistryEntryData<TrimMaterial> registered = session.getRegistryCache().trimMaterials().entryById(trim.material().id());
return registered != null && registered.key().equals(material);
} else if (data.property() == MatchPredicateProperty.CONTEXT_DIMENSION) {
Key dimension = (Key) data.data();
RegistryEntryData<JavaDimension> registered = session.getRegistryCache().dimensions().entryByValue(session.getDimensionType());
return registered != null && dimension.equals(registered.key()); // TODO check if this works
} else if (data.property() == MatchPredicateProperty.CUSTOM_MODEL_DATA) {
// TODO 1.21.4
return false;
}
}
throw new IllegalStateException("Unimplemented predicate type");
}
private static boolean isDamagableItem(DataComponents components) {
// mapping.getMaxDamage > 0 should also be checked (return false if not true) but we already check prior to this function
Boolean unbreakable = components.get(DataComponentType.UNBREAKABLE);
// Tag must either not be present or be set to false
return unbreakable == null || !unbreakable;
/* These three functions are based off their Mojmap equivalents from 1.21.3 */
private static boolean nextDamageWillBreak(DataComponents components) {
return isDamageableItem(components) && components.getOrDefault(DataComponentType.DAMAGE, 0) >= components.getOrDefault(DataComponentType.MAX_DAMAGE, 0) - 1;
}
private static boolean isDamaged(DataComponents components) {
return isDamageableItem(components) && components.getOrDefault(DataComponentType.DAMAGE, 0) > 0;
}
private static boolean isDamageableItem(DataComponents components) {
return components.getOrDefault(DataComponentType.UNBREAKABLE, false) && components.getOrDefault(DataComponentType.MAX_DAMAGE, 0) > 0;
}
private CustomItemTranslator() {

View file

@ -215,7 +215,7 @@ public final class ItemTranslator {
translatePlayerHead(session, components, builder);
}
translateCustomItem(components, builder, bedrockItem);
translateCustomItem(session, components, builder, bedrockItem);
if (components != null) {
// Translate the canDestroy and canPlaceOn Java components
@ -428,7 +428,7 @@ public final class ItemTranslator {
}
}
ItemDefinition definition = CustomItemTranslator.getCustomItem(itemStack.getComponents(), mapping);
ItemDefinition definition = CustomItemTranslator.getCustomItem(session, itemStack.getComponents(), mapping);
if (definition == null) {
// No custom item
return itemDefinition;
@ -469,8 +469,8 @@ public final class ItemTranslator {
/**
* Translates the custom model data of an item
*/
public static void translateCustomItem(DataComponents components, ItemData.Builder builder, ItemMapping mapping) {
ItemDefinition definition = CustomItemTranslator.getCustomItem(components, mapping);
public static void translateCustomItem(GeyserSession session, DataComponents components, ItemData.Builder builder, ItemMapping mapping) {
ItemDefinition definition = CustomItemTranslator.getCustomItem(session, components, mapping);
if (definition != null) {
builder.definition(definition);
builder.blockDefinition(null);

View file

@ -93,7 +93,7 @@ final class BedrockBlockActions {
// If the block is custom or the breaking item is custom, we must keep track of break time ourselves
GeyserItemStack item = session.getPlayerInventory().getItemInHand();
ItemMapping mapping = item.getMapping(session);
ItemDefinition customItem = mapping.isTool() ? CustomItemTranslator.getCustomItem(item.getComponents(), mapping) : null;
ItemDefinition customItem = mapping.isTool() ? CustomItemTranslator.getCustomItem(session, item.getComponents(), mapping) : null;
CustomBlockState blockStateOverride = BlockRegistries.CUSTOM_BLOCK_STATE_OVERRIDES.get(blockState);
SkullCache.Skull skull = session.getSkullCache().getSkulls().get(vector);