diff --git a/src/main/java/org/crebs/skpackets/api/util/field/FieldSchema.java b/src/main/java/org/crebs/skpackets/api/util/field/FieldSchema.java index eb55264..fccd56e 100644 --- a/src/main/java/org/crebs/skpackets/api/util/field/FieldSchema.java +++ b/src/main/java/org/crebs/skpackets/api/util/field/FieldSchema.java @@ -66,7 +66,7 @@ public String getReadableFields() { attributes.add(accessor.isOptional() ? "optional" : "required"); if (accessor.getter() == null) attributes.add("creation-only"); String names = accessor.name() + (accessor.aliases().length == 0 ? "" : "|" + String.join("|", accessor.aliases())); - fieldStrings.add("- `" + names + ": " + typeName(accessor.expectedType()) + "` (" + fieldStrings.add("- `" + names + ": " + fieldTypeName(accessor.nativeType(), accessor.expectedType()) + "` (" + String.join("; ", attributes) + ")"); Class valueType = accessor.expectedType().isArray() ? accessor.expectedType().getComponentType() : accessor.expectedType(); @@ -88,6 +88,27 @@ public String getReadableFields() { return String.join("\n", fieldStrings); } + private static String fieldTypeName(java.lang.reflect.Type nativeType, Class expectedType) { + var raw = org.crebs.skpackets.elements.general.field.PacketArguments.raw(nativeType); + if (raw == java.util.BitSet.class) return "bit indices (numbers)"; + if (raw == byte[][].class) return "byte arrays"; + if (raw == java.util.Optional.class) + return fieldTypeName(org.crebs.skpackets.elements.general.field.PacketArguments.argument(nativeType, 0), expectedType); + if (java.util.Map.class.isAssignableFrom(raw)) { + var key = org.crebs.skpackets.elements.general.field.PacketArguments.argument(nativeType, 0); + var value = org.crebs.skpackets.elements.general.field.PacketArguments.argument(nativeType, 1); + return "map of " + fieldTypeName(key, org.crebs.skpackets.elements.general.field.PacketArguments.expressionType(key)) + + " to " + fieldTypeName(value, org.crebs.skpackets.elements.general.field.PacketArguments.expressionType(value)); + } + if (expectedType == org.crebs.skpackets.elements.general.field.PacketValue.class) + return org.crebs.skpackets.elements.general.field.PacketValueTypes.label(raw); + if (expectedType.isArray() && expectedType.getComponentType() == org.crebs.skpackets.elements.general.field.PacketValue.class) + return org.crebs.skpackets.elements.general.field.PacketValueTypes.label( + org.crebs.skpackets.elements.general.field.PacketArguments.raw( + org.crebs.skpackets.elements.general.field.PacketRecords.elementType(nativeType))) + " values"; + return typeName(expectedType); + } + private static String typeName(Class type) { if (type.isArray()) return pluralTypeName(type.getComponentType()); if (type == Boolean.class || type == boolean.class) return "boolean"; diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketArguments.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketArguments.java index 92ce260..9cdee6b 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketArguments.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketArguments.java @@ -15,7 +15,10 @@ private PacketArguments() {} public static Class expressionType(Type type) { Class raw = raw(type); + if (raw == BitSet.class) return Number[].class; + if (raw == byte[][].class) return PacketValue[].class; if (raw == Optional.class) return expressionType(argument(type, 0)); + if (Map.class.isAssignableFrom(raw)) return PacketValue[].class; if (raw.isArray()) return arrayType(expressionType(raw.getComponentType())); if (Collection.class.isAssignableFrom(raw)) return arrayType(expressionType(argument(type, 0))); return PUBLIC_TYPES.computeIfAbsent(raw, PacketArguments::scalarType); @@ -59,6 +62,17 @@ public static Object convert(Object value, Type type, String name) { } if (value instanceof PacketRecord record) value = PacketRecords.construct(record, type, name); if (value == null) throw invalid(name, type, null); + if (target == BitSet.class) { + if (value instanceof BitSet bits) return bits.clone(); + BitSet bits = new BitSet(); + for (Object index : elements(value)) { + if (!(index instanceof Number number)) throw invalid(name, type, index); + int bit = (int) PacketNumbers.convert(number, int.class, name); + if (bit < 0 || bit > 1048575) throw new IllegalArgumentException("Bit index must be between 0 and 1048575: " + name); + bits.set(bit); + } + return bits; + } if (target.isArray()) return convertArray(value, type, name); if (Collection.class.isAssignableFrom(target)) return convertCollection(value, type, name); if (Map.class.isAssignableFrom(target)) return convertMap(value, type, name); @@ -104,7 +118,22 @@ private static Object convertCollection(Object value, Type type, String name) { } private static Object convertMap(Object value, Type type, String name) { - if (!(value instanceof Map input)) throw invalid(name, type, value); + Map input; + if (value instanceof Map map) input = map; + else { + Map entries = new LinkedHashMap<>(); + for (Object entry : elements(value)) { + if (entry instanceof PacketValue wrapped) entry = wrapped.value(); + if (!(entry instanceof PacketRecord record)) throw invalid(name, type, entry); + if (!record.fields().keySet().equals(Set.of("key", "value"))) + throw new IllegalArgumentException("Map entry '" + name + "' requires key and value."); + Object key = convert(record.fields().get("key"), argument(type, 0), name + " key"); + Object item = convert(record.fields().get("value"), argument(type, 1), name + " value"); + if (entries.putIfAbsent(key, item) != null) + throw new IllegalArgumentException("Duplicate map key in '" + name + "': " + key); + } + return entries; + } Map result = new LinkedHashMap<>(); input.forEach((key, entry) -> result.put(convert(key, argument(type, 0), name + " key"), convert(entry, argument(type, 1), name + " value"))); @@ -125,6 +154,11 @@ private static Object convertEnum(String text, Class type, String name) { static Object toSkript(Object value) { if (value == null) return null; if (value instanceof Optional optional) return toSkript(optional.orElse(null)); + if (value instanceof BitSet bits) return bits.stream().mapToObj(Integer::valueOf).toArray(Number[]::new); + if (value instanceof byte[][] rows) return Arrays.stream(rows).map(PacketValue::new).toArray(PacketValue[]::new); + if (value instanceof Map map) return map.entrySet().stream() + .map(entry -> new PacketValue(new PacketRecord(Map.of( + "key", toSkript(entry.getKey()), "value", toSkript(entry.getValue()))))).toArray(PacketValue[]::new); if (value instanceof Collection collection) return collection.stream().map(PacketArguments::toSkript).toArray(); if (value.getClass().isArray()) return Arrays.stream(elements(value)).map(PacketArguments::toSkript).toArray(); var codec = PacketValueCodecs.find(value.getClass()); diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketConstructor.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketConstructor.java index 9555943..6dee9b0 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketConstructor.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketConstructor.java @@ -104,7 +104,7 @@ PacketWrapper create(ConstructionContext values, Map 0))), entry(WrapperPlayClientUpdateSign.class, Map.of("isfronttext", named("sign side").adapting(SIGN_SIDE))), entry(WrapperPlayClientInteractEntity.class, Map.of("sneaking", DEFAULT.adapting(SNEAKING))), diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldSections.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldSections.java index 7a6072f..2c3d97e 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldSections.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketFieldSections.java @@ -80,17 +80,28 @@ private static Expression nested(Type type, SectionNode section, String name) error("Duplicate list entry in '" + name + "': " + label); return null; } Map> record = new LinkedHashMap<>(); - if (!parse(shape.fields(), child, record, name + "." + label)) return null; + if (!parseSelected(shape, child, record, name + "." + label)) return null; records.add(record); } } else { Map> record = new LinkedHashMap<>(); - if (!parse(shape.fields(), section, record, name)) return null; + if (!parseSelected(shape, section, record, name)) return null; records.add(record); } return new RecordsExpression(shape, records, !PacketRecords.isList(type)); } + private static boolean parseSelected(PacketRecords.Shape shape, SectionNode section, + Map> record, String name) { + Set supplied = new HashSet<>(); + for (var node : section) if (!node.isVoid() && node.getKey() != null) supplied.add(node.getKey()); + try { + return parse(shape.select(supplied, name).fields(), section, record, name); + } catch (IllegalArgumentException exception) { + return error(exception.getMessage()); + } + } + private static boolean error(String message) { Skript.error(message); return false; } private static final class RecordsExpression extends SimpleExpression { @@ -103,15 +114,16 @@ private static final class RecordsExpression extends SimpleExpression[] expressions, int pattern, Kleenean delayed, SkriptParser.ParseResult result) { return true; } @Override protected PacketValue[] get(Event event) { List values = new ArrayList<>(); - var schema = new FieldSchema("nested packet value", shape.fields(), context -> { - Map fields = new LinkedHashMap<>(); - for (var field : shape.fields()) { - Object value = context.getOptional(field.name(), Object.class); - if (value != null) fields.put(field.name(), value); - } - return new PacketValue(new PacketRecord(fields)); - }); for (var record : records) { + var selected = shape.select(record.keySet(), "nested packet value"); + var schema = new FieldSchema("nested packet value", selected.fields(), context -> { + Map fields = new LinkedHashMap<>(); + for (var field : selected.fields()) { + Object value = context.getOptional(field.name(), Object.class); + if (value != null) fields.put(field.name(), value); + } + return new PacketValue(new PacketRecord(fields)); + }); Object value = FieldSections.create(schema, "nested packet value", record, event, Map.of()); if (value == null) throw new IllegalArgumentException("A required nested packet entry evaluated to no value."); values.add((PacketValue)value); diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketRecords.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketRecords.java index a614713..355d892 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketRecords.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketRecords.java @@ -13,7 +13,15 @@ public final class PacketRecords { private PacketRecords() {} public record Shape(List> fields, Map defaults, - Function, Object> factory) {} + Function, Object> factory, List alternatives) { + public Shape(List> fields, Map defaults, + Function, Object> factory) { + this(fields, defaults, factory, List.of()); + } + Shape select(Set supplied, String name) { + return alternatives.isEmpty() ? this : PacketValueDiscovery.select(alternatives, supplied, name); + } + } private static final Map, Shape> SHAPES = new HashMap<>(); static { define(Equipment.class, List.of( @@ -45,6 +53,12 @@ public record Shape(List> fields, Map d (int)v.get("uses"), (int)v.get("max uses"), (int)v.get("xp"), (int)v.get("special price"), (float)v.get("price multiplier"), (int)v.get("demand"))); } + private static final ClassValue> DISCOVERED = new ClassValue<>() { + @Override protected Optional computeValue(Class type) { + return Optional.ofNullable(PacketValueDiscovery.discover(type)); + } + }; + private static void define(Class type, List> fields, Map defaults, Function, Object> factory) { SHAPES.put(type, new Shape(List.copyOf(fields), Map.copyOf(defaults), factory)); @@ -63,22 +77,35 @@ public static Type elementType(Type type) { public static boolean isList(Type type) { Class raw = PacketArguments.raw(type); if (raw == Optional.class) return isList(PacketArguments.argument(type, 0)); - return raw.isArray() || Collection.class.isAssignableFrom(raw); + return raw.isArray() || Collection.class.isAssignableFrom(raw) || Map.class.isAssignableFrom(raw); + } + public static Shape shape(Type type) { + if (PacketArguments.raw(type) == Optional.class) return shape(PacketArguments.argument(type, 0)); + if (Map.class.isAssignableFrom(PacketArguments.raw(type))) + return new Shape(List.of(field("key", PacketArguments.argument(type, 0), false), + field("value", PacketArguments.argument(type, 1), false)), Map.of(), v -> v); + Class raw = PacketArguments.raw(elementType(type)); + Shape known = SHAPES.get(raw); + return known != null ? known : DISCOVERED.get(raw).orElse(null); } - public static Shape shape(Type type) { return SHAPES.get(PacketArguments.raw(elementType(type))); } public static Object construct(PacketRecord record, Type type, String name) { - Shape shape = SHAPES.get(PacketArguments.raw(type)); + Shape shape = shape(type); if (shape == null) throw new IllegalArgumentException("Packet field '" + name + "' does not support nested entries."); + return construct(record.fields(), shape.select(record.fields().keySet(), name), name); + } + static Object construct(Map supplied, Shape shape, String name) { Map values = new HashMap<>(shape.defaults()); - for (String key : record.fields().keySet()) { + for (String key : supplied.keySet()) { if (shape.fields().stream().noneMatch(f -> f.name().equals(key))) throw new IllegalArgumentException("Unknown nested packet field '" + name + "." + key + "'."); } - values.putAll(record.fields()); + values.putAll(supplied); Map converted = new HashMap<>(); for (var field : shape.fields()) { Object value = values.get(field.name()); if (value == null && !field.isOptional()) throw new IllegalArgumentException("Missing packet field '" + name + "." + field.name() + "'."); + if (value == null && PacketArguments.raw(field.nativeType()) == Optional.class) + converted.put(field.name(), Optional.empty()); if (value != null) converted.put(field.name(), PacketArguments.convert(value, field.nativeType(), name + "." + field.name())); } return shape.factory().apply(converted); diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueDiscovery.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueDiscovery.java new file mode 100644 index 0000000..7019336 --- /dev/null +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueDiscovery.java @@ -0,0 +1,105 @@ +package org.crebs.skpackets.elements.general.field; + +import org.crebs.skpackets.api.util.field.FieldAccessor; +import java.lang.reflect.*; +import java.util.*; + +final class PacketValueDiscovery { + private PacketValueDiscovery() {} + + static PacketRecords.Shape discover(Class type) { + if (!type.getName().startsWith("com.github.retrooper.packetevents.") + || type.isEnum() || type.isInterface() || Modifier.isAbstract(type.getModifiers())) return null; + List candidates = new ArrayList<>(); + for (Constructor constructor : type.getConstructors()) + if (!constructor.isAnnotationPresent(Deprecated.class)) candidates.add(constructor); + for (Method method : type.getMethods()) + if (Modifier.isStatic(method.getModifiers()) && method.getReturnType() == type + && !method.isAnnotationPresent(Deprecated.class) + && (method.getName().startsWith("create") || method.getName().equals("of"))) candidates.add(method); + candidates.removeIf(value -> value.getParameterCount() == 0 + || Arrays.stream(value.getParameters()).anyMatch(parameter -> !parameter.isNamePresent() + || parameter.getType() == type + || parameter.getType().getSimpleName().endsWith("Event") + || parameter.getType().getSimpleName().equals("ClientVersion") + || parameter.getType().getSimpleName().equals("ServerVersion"))); + candidates.sort(Comparator.comparing(Executable::toGenericString)); + if (candidates.isEmpty()) return null; + List shapes = candidates.stream().map(PacketValueDiscovery::shape).toList(); + Map> fields = new LinkedHashMap<>(); + for (var shape : shapes) for (var field : shape.fields()) + fields.putIfAbsent(field.name(), field(field.name(), field.nativeType(), true)); + return new PacketRecords.Shape(List.copyOf(fields.values()), Map.of(), values -> + PacketRecords.construct(values, select(shapes, values.keySet(), type.getSimpleName()), type.getSimpleName()), shapes); + } + + static PacketRecords.Shape select(List shapes, Set supplied, String name) { + List matches = shapes.stream().filter(shape -> { + Set names = new HashSet<>(); + for (var field : shape.fields()) { + names.add(field.name()); + if (!field.isOptional() && !supplied.contains(field.name())) return false; + } + return names.containsAll(supplied); + }).toList(); + // Prefer the builder that needs the fewest omitted optional inputs. + int size = matches.stream().mapToInt(shape -> shape.fields().size()).min().orElse(-1); + matches = matches.stream().filter(shape -> shape.fields().size() == size).toList(); + if (matches.size() != 1) throw new IllegalArgumentException( + (matches.isEmpty() ? "No matching" : "Ambiguous") + " nested builder for '" + name + + "' with fields " + new TreeSet<>(supplied) + ". Available builders: " + + shapes.stream().map(shape -> shape.fields().stream().map(field -> field.name() + + ": " + field.nativeType().getTypeName() + (field.isOptional() ? " (optional)" : " (required)")).toList()).toList()); + return matches.getFirst(); + } + + private static PacketRecords.Shape shape(Executable selected) { + List> fields = new ArrayList<>(); + for (Parameter parameter : selected.getParameters()) + fields.add(field(PacketReflection.fieldName(parameter.getName()), parameter.getParameterizedType(), + nullable(selected, fields.size()) || parameter.getType() == Optional.class + || parameter.getType() == BitSet.class || parameter.getType().isArray())); + Map defaults = new HashMap<>(); + for (var field : fields) { + Class raw = PacketArguments.raw(field.nativeType()); + if (raw == BitSet.class) defaults.put(field.name(), new Number[0]); + else if (raw.isArray()) defaults.put(field.name(), java.lang.reflect.Array.newInstance( + PacketArguments.expressionType(raw).getComponentType(), 0)); + } + return new PacketRecords.Shape(List.copyOf(fields), defaults, values -> { + Object[] args = fields.stream().map(field -> values.get(field.name())).toArray(); + try { + Object result = selected instanceof Constructor constructor ? constructor.newInstance(args) + : ((Method) selected).invoke(null, args); + return result; + } catch (ReflectiveOperationException exception) { + Throwable cause = exception instanceof InvocationTargetException invocation ? invocation.getCause() : exception; + throw new IllegalArgumentException("Cannot build " + PacketValueTypes.label(selected.getDeclaringClass()) + ": " + cause.getMessage(), cause); + } + }); + } + + private static boolean nullable(Executable executable, int index) { + Class owner = executable.getDeclaringClass(); + try (var stream = owner.getResourceAsStream("/" + owner.getName().replace('.', '/') + ".class")) { + if (stream == null) throw new IllegalStateException("Cannot read " + owner.getName()); + var node = new org.objectweb.asm.tree.ClassNode(); + new org.objectweb.asm.ClassReader(stream).accept(node, org.objectweb.asm.ClassReader.SKIP_CODE + | org.objectweb.asm.ClassReader.SKIP_DEBUG | org.objectweb.asm.ClassReader.SKIP_FRAMES); + String descriptor = executable instanceof Constructor constructor + ? org.objectweb.asm.Type.getConstructorDescriptor(constructor) + : org.objectweb.asm.Type.getMethodDescriptor((Method) executable); + for (var method : node.methods) if (method.desc.equals(descriptor) + && method.name.equals(executable instanceof Constructor ? "" : executable.getName())) + return PacketConstructor.nullable(method, index); + return false; + } catch (java.io.IOException exception) { + throw new IllegalStateException("Cannot read " + owner.getName(), exception); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static FieldAccessor field(String name, Type type, boolean optional) { + return new FieldAccessor(name, new String[0], PacketArguments.expressionType(type), optional, null, null, type); + } +} diff --git a/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueTypes.java b/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueTypes.java index 60e7eb0..47cc0b1 100644 --- a/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueTypes.java +++ b/src/main/java/org/crebs/skpackets/elements/general/field/PacketValueTypes.java @@ -65,7 +65,7 @@ public static String label(Class type) { return "packet team name tag visibility"; String name = type.getName(); - name = name.substring(name.lastIndexOf('.') + 1).replace('$', ' '); + name = name.substring(Math.max(name.lastIndexOf('.'), name.lastIndexOf('$')) + 1); return "packet " + PacketReflection.fieldName(name.replace("Wrapper", "")); } } diff --git a/src/test/java/org/crebs/skpackets/api/util/field/FieldSchemaDocumentationTest.java b/src/test/java/org/crebs/skpackets/api/util/field/FieldSchemaDocumentationTest.java index deb6da9..510cb82 100644 --- a/src/test/java/org/crebs/skpackets/api/util/field/FieldSchemaDocumentationTest.java +++ b/src/test/java/org/crebs/skpackets/api/util/field/FieldSchemaDocumentationTest.java @@ -83,6 +83,18 @@ void fieldsShowEnumValuesWithoutExamples() { assertTrue(docs.contains("field values: `\"update data\"`")); } + @Test + void complexMapFieldsDescribeTheirContentsAndNestedBuilder() throws Exception { + var nativeType = com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkBiomes.class + .getMethod("getChunks").getGenericReturnType(); + var accessor = field("chunks", org.crebs.skpackets.elements.general.field.PacketValue[].class, false, null) + .withNativeType(nativeType); + String docs = new FieldSchema<>("chunk biomes", List.of(accessor), null).getReadableFields(); + assertTrue(docs.contains("chunks: map of vector2 to packet chunk biome data")); + assertTrue(docs.contains("key (required), value (required)")); + assertFalse(docs.contains("packet value")); + } + private static FieldAccessor field(String name, Class type, boolean optional, Function getter) { return new FieldAccessor<>(name, new String[0], type, optional, getter, null); diff --git a/src/test/java/org/crebs/skpackets/elements/general/field/PacketBuilderSelectionTest.java b/src/test/java/org/crebs/skpackets/elements/general/field/PacketBuilderSelectionTest.java new file mode 100644 index 0000000..b1a6b14 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/field/PacketBuilderSelectionTest.java @@ -0,0 +1,62 @@ +package org.crebs.skpackets.elements.general.field; + +import org.crebs.skpackets.api.util.field.FieldAccessor; +import org.junit.jupiter.api.Test; +import java.util.*; +import static org.junit.jupiter.api.Assertions.*; + +class PacketBuilderSelectionTest { + private FieldAccessor field(String name, Class type, boolean optional) { + return new FieldAccessor<>(name, new String[0], Object.class, optional, null, null, type); + } + private PacketRecords.Shape shape(FieldAccessor... fields) { + return new PacketRecords.Shape(List.of(fields), Map.of(), v -> v); + } + @Test void selectsUsingSuppliedFieldsAndPrefersFewerOmissions() { + var shortBuilder = shape(field("name", String.class, false)); + var longBuilder = shape(field("name", String.class, false), field("extra", String.class, true)); + var builders = List.of(shortBuilder, longBuilder); + assertSame(shortBuilder, PacketValueDiscovery.select(builders, Set.of("name"), "value")); + assertSame(longBuilder, PacketValueDiscovery.select(builders, Set.of("name", "extra"), "value")); + assertThrows(IllegalArgumentException.class, () -> PacketValueDiscovery.select(builders, Set.of(), "value")); + assertThrows(IllegalArgumentException.class, () -> PacketValueDiscovery.select(builders, Set.of("unknown"), "value")); + } + @Test void reportsAmbiguityWithTypesAndFieldPath() { + var error = assertThrows(IllegalArgumentException.class, () -> PacketValueDiscovery.select(List.of( + shape(field("value", String.class, false)), shape(field("value", int.class, false))), + Set.of("value"), "entries.first")); + assertTrue(error.getMessage().contains("Ambiguous")); + assertTrue(error.getMessage().contains("entries.first")); + assertTrue(error.getMessage().contains("java.lang.String")); + assertTrue(error.getMessage().contains("int")); + } + @Test void suppliesEmptyOptionalAndRetainsNullableOmission() { + var builder = shape(field("optional", Optional.class, true), field("nullable", String.class, true)); + var values = (Map) PacketRecords.construct(Map.of(), builder, "value"); + assertEquals(Optional.empty(), values.get("optional")); + assertNull(values.get("nullable")); + assertThrows(IllegalArgumentException.class, () -> PacketRecords.construct(Map.of(), + shape(field("required", String.class, false)), "value")); + } + @Test void recognizesOnlyTopLevelNullableParameters() { + var method = new org.objectweb.asm.tree.MethodNode(); + int reference = org.objectweb.asm.TypeReference.newFormalParameterReference(0).getValue(); + method.invisibleTypeAnnotations = new ArrayList<>(); + method.invisibleTypeAnnotations.add(new org.objectweb.asm.tree.TypeAnnotationNode(reference, + null, "Lorg/jetbrains/annotations/Nullable;")); + assertTrue(PacketConstructor.nullable(method, 0)); + assertFalse(PacketConstructor.nullable(method, 1)); + method.invisibleTypeAnnotations.clear(); + method.invisibleTypeAnnotations.add(new org.objectweb.asm.tree.TypeAnnotationNode(reference, + org.objectweb.asm.TypePath.fromString("0;"), "Lorg/jetbrains/annotations/Nullable;")); + assertFalse(PacketConstructor.nullable(method, 0)); + } + @Test void discoversBothUserProfileSignatures() { + var shape = PacketValueDiscovery.discover(com.github.retrooper.packetevents.protocol.player.UserProfile.class); + var uuid = UUID.randomUUID(); + var profile = (com.github.retrooper.packetevents.protocol.player.UserProfile) + shape.factory().apply(Map.of("uuid", uuid, "name", "Builder")); + assertEquals(uuid, profile.getUUID()); + assertEquals("Builder", profile.getName()); + } +} diff --git a/src/test/java/org/crebs/skpackets/elements/general/field/PacketFieldSectionsTest.java b/src/test/java/org/crebs/skpackets/elements/general/field/PacketFieldSectionsTest.java index 4a01a83..1316123 100644 --- a/src/test/java/org/crebs/skpackets/elements/general/field/PacketFieldSectionsTest.java +++ b/src/test/java/org/crebs/skpackets/elements/general/field/PacketFieldSectionsTest.java @@ -75,6 +75,30 @@ class PacketFieldSectionsTest { } } + @Test void openSignEditorAcceptsBothPositionNames() throws Exception { + var schema = PacketFieldRegistry.INSTANCE.getSchema(PacketType.Play.Server.OPEN_SIGN_EDITOR); + assertSame(schema.getAccessor("block position"), schema.getAccessor("position")); + assertTrue(schema.getReadableFields().contains("block position|position")); + for (String name : List.of("block position", "position")) { + Map> expressions = new HashMap<>(); + try (var parsers = mockConstruction(SkriptParser.class, (parser, context) -> { + String input = (String) context.arguments().getFirst(); + Object value = input.equals("position-value") ? new org.bukkit.util.Vector(2, 64, -3) + : org.bukkit.block.sign.Side.FRONT; + doReturn(new SimpleLiteral<>(value, false)).when(parser).parseExpression(any(Class[].class)); + })) { + String text = name + ": position-value\nsign side: front\n"; + var node = new Config(new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)), "test.sk", true, true, ":").getMainNode(); + assertTrue(PacketFieldSections.parse(schema.accessors(), node, expressions, "open sign editor")); + var packet = (com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerOpenSignEditor) + FieldSections.create(schema, PacketType.Play.Server.OPEN_SIGN_EDITOR, expressions, mock(org.bukkit.event.Event.class), Map.of()); + assertNotNull(packet); + assertEquals(new com.github.retrooper.packetevents.util.Vector3i(2, 64, -3), packet.getPosition()); + assertTrue(packet.isFrontText()); + } + } + } + @Test void rejectsBothCanonicalNameAndAliasInsteadOfSilentlyIgnoringOne() throws Exception { var schema = PacketFieldRegistry.INSTANCE.getSchema(PacketType.Play.Server.PLAYER_ABILITIES); String text = "god mode: true\nin god mode: false\n"; diff --git a/src/test/java/org/crebs/skpackets/elements/general/field/PacketMapBuilderTest.java b/src/test/java/org/crebs/skpackets/elements/general/field/PacketMapBuilderTest.java new file mode 100644 index 0000000..e9cff04 --- /dev/null +++ b/src/test/java/org/crebs/skpackets/elements/general/field/PacketMapBuilderTest.java @@ -0,0 +1,49 @@ +package org.crebs.skpackets.elements.general.field; + +import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkBiomes; +import com.github.retrooper.packetevents.util.Vector2i; +import org.crebs.skpackets.api.general.Vector2; +import org.junit.jupiter.api.Test; +import java.lang.reflect.Type; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; + +class PacketMapBuilderTest { + private Type chunks() throws Exception { + return WrapperPlayServerChunkBiomes.class.getMethod("getChunks").getGenericReturnType(); + } + private PacketValue entry(int x, int z, int count) { + return new PacketValue(new PacketRecord(Map.of("key", new Vector2(x, z), "value", + new PacketValue(new PacketRecord(Map.of("biome id", 1, "section count", count)))))); + } + @Test void buildsChunkBiomeMapsAndRoundTripsCopiedValues() throws Exception { + var type = chunks(); + assertEquals(PacketValue[].class, PacketArguments.expressionType(type)); + assertTrue(PacketRecords.isList(type)); + assertNotNull(PacketRecords.shape(type)); + var error = assertThrows(IllegalArgumentException.class, () -> PacketArguments.convert( + new PacketValue[]{entry(2, -3, 24)}, type, "chunks")); + assertTrue(error.getMessage().contains("bitsPerEntry")); + var data = org.mockito.Mockito.mock(WrapperPlayServerChunkBiomes.ChunkBiomeData.class); + var map = Map.of(new Vector2i(2, -3), data); + assertEquals(map, PacketArguments.convert(PacketArguments.toSkript(map), type, "chunks")); + } + @Test void discoversAndInvokesConstructorsWithoutRegistration() { + var shape = PacketValueDiscovery.discover(com.github.retrooper.packetevents.protocol.player.UserProfile.class); + assertNotNull(shape); + var values = new java.util.HashMap(); + var uuid = java.util.UUID.randomUUID(); + values.put("uuid", uuid); + values.put("name", "Builder"); + values.put("texture properties", java.util.List.of()); + var profile = (com.github.retrooper.packetevents.protocol.player.UserProfile) shape.factory().apply(values); + assertEquals(uuid, profile.getUUID()); + assertEquals("Builder", profile.getName()); + } + @Test void rejectsDuplicateChunks() throws Exception { + var type = chunks(); + assertThrows(IllegalArgumentException.class, () -> PacketArguments.convert( + new PacketValue[]{entry(0, 0, 24), entry(0, 0, 24)}, type, "chunks")); + + } +}