Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -222,11 +222,7 @@ private void deserializeValue(Method method, T instance, PropertyDescriptor prop
// Floats need special handling because the database returns them as doubles
Type setType = propertyDescriptor.getWriteMethod().getGenericParameterTypes()[0];
switch (setType.getTypeName()) {
case "float" -> {
double d = (double) setTo;
float f = (float) d;
method.invoke(instance, f);
}
case "float" -> method.invoke(instance, ((Number) setTo).floatValue());
case "org.bukkit.Sound" -> {
Sound s = Registry.SOUNDS
.get(NamespacedKey.fromString(((String) setTo).toLowerCase(Locale.ENGLISH)));
Expand Down Expand Up @@ -686,9 +682,17 @@ private Object deserialize(Object value, Class<?> clazz) {
if (clazz.equals(value.getClass())) {
return value;
}
// Integer to Long promotion
if (clazz.equals(Long.class) && value.getClass().equals(Integer.class)) {
return Long.valueOf((Integer) value);
// Numeric widening. YAML types a number by how it is written, so "20"
// loads as Integer and "20.0" as Double - but a config field declared
// double is perfectly entitled to be written without a decimal point.
// Without this, such a value reaches the setter as an Integer; inside a
// collection, generic erasure means nothing complains until the first
// read throws ClassCastException, a long way from the cause.
if (value instanceof Number number) {
Object widened = widen(number, clazz);
if (widened != null) {
return widened;
}
}
// String-based conversions
if (value instanceof String stringValue) {
Expand All @@ -704,6 +708,44 @@ private Object deserialize(Object value, Class<?> clazz) {
return value;
}

/**
* Widen a YAML-loaded number to the field's declared numeric type, or null if
* the target is not a numeric type this handles.
* <p>
* Widening only. There is deliberately no case for {@code int}, and the
* {@code long} case only accepts integral sources: a value written with a
* decimal point against an integer field is a mistake in the config, and
* narrowing it would silently discard the fraction. Left alone, it fails
* visibly instead. Double to float is the one accepted narrowing, because
* YAML always types decimals as Double and a float field must still load.
*
* @param number the value as YAML typed it
* @param clazz the declared type
* @return the converted value, or null to leave it alone
*/
@Nullable
private Object widen(Number number, Class<?> clazz) {
if ((clazz.equals(Long.class) || clazz.equals(long.class)) && isIntegral(number)) {
return number.longValue();
}
if (clazz.equals(Double.class) || clazz.equals(double.class)) {
return number.doubleValue();
}
if (clazz.equals(Float.class) || clazz.equals(float.class)) {
return number.floatValue();
}
return null;
}

/**
* @param number a YAML-loaded number
* @return true if the value carries no fraction to lose, i.e. YAML typed it as an integer
*/
private boolean isIntegral(Number number) {
return number instanceof Byte || number instanceof Short || number instanceof Integer
|| number instanceof Long;
}

/**
* Deserialize a string value into the target class type.
* Handles numeric types, UUID, Location, and World.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public static class TestDataObject implements DataObject {
private String uniqueId = "test";
private String name = "";
private int count = 0;
private float speed = 0f;
private Map<String, Integer> scores = new HashMap<>();
private Set<String> tags = new HashSet<>();
private List<String> items = new java.util.ArrayList<>();
Expand All @@ -63,6 +64,8 @@ public static class TestDataObject implements DataObject {
public void setName(String name) { this.name = name; }
public int getCount() { return count; }
public void setCount(int count) { this.count = count; }
public float getSpeed() { return speed; }
public void setSpeed(float speed) { this.speed = speed; }
public Map<String, Integer> getScores() { return scores; }
public void setScores(Map<String, Integer> scores) { this.scores = scores; }
public Set<String> getTags() { return tags; }
Expand Down Expand Up @@ -117,6 +120,57 @@ void testDeserializeSameClassReturnsValue() throws Exception {
assertSame(value, deserializeMethod.invoke(handler, value, String.class));
}

@Test
void testDeserializeIntegerToDouble() throws Exception {
// YAML types a number by how it is WRITTEN: "20" loads as Integer even
// where the field is a double. Without widening, that Integer reaches the
// setter, and inside a collection generic erasure hides it until the first
// read throws ClassCastException a long way from the cause.
Object result = deserializeMethod.invoke(handler, 20, Double.class);
assertEquals(20.0, result);
assertEquals(Double.class, result.getClass());
}

@Test
void testDeserializeIntegerToFloat() throws Exception {
Object result = deserializeMethod.invoke(handler, 20, Float.class);
assertEquals(20.0f, result);
assertEquals(Float.class, result.getClass());
}

@Test
void testDeserializeDoubleToIntegerIsNotNarrowed() throws Exception {
// The other direction is deliberately NOT converted. A decimal written
// against an int field is a config mistake, and intValue() would discard
// the fraction silently - 20.9 becoming 20 with nothing said. Left alone,
// it fails visibly instead.
Object result = deserializeMethod.invoke(handler, 20.9, Integer.class);
assertEquals(20.9, result);
assertEquals(Double.class, result.getClass());
}

@Test
void testDeserializeDoubleToLongIsNotNarrowed() throws Exception {
// Same principle for long fields: 5.5 must not silently become 5L
Object result = deserializeMethod.invoke(handler, 5.5, Long.class);
assertEquals(5.5, result);
assertEquals(Double.class, result.getClass());
}

@Test
void testDeserializeLongToDouble() throws Exception {
Object result = deserializeMethod.invoke(handler, 20L, Double.class);
assertEquals(20.0, result);
assertEquals(Double.class, result.getClass());
}

@Test
void testDeserializeNumberToNonNumericTypeIsUntouched() throws Exception {
// Widening must not hijack values whose target is not numeric
Object result = deserializeMethod.invoke(handler, 20, String.class);
assertEquals(20, result);
}

@Test
void testDeserializeIntegerToLong() throws Exception {
Object result = deserializeMethod.invoke(handler, 42, Long.class);
Expand Down Expand Up @@ -402,6 +456,26 @@ void testLoadObjectWithCollections() throws Exception {
assertEquals(3, result.getItems().size());
}

@Test
void testLoadObjectPrimitiveFloatField() throws Exception {
// A primitive float field must load whether the admin wrote the value
// with a decimal point (YAML types it Double) or without (Integer).
// Exercises the "float" arm in deserializeValue, which deserialize()
// now feeds a boxed Float rather than the raw YAML Double.
YamlConfiguration config = new YamlConfiguration();
config.set("uniqueId", "float-test");
config.set("speed", 2.5);
when(connector.loadYamlFile(anyString(), eq("float-test"))).thenReturn(config);

TestDataObject result = handler.loadObject("float-test");
assertNotNull(result);
assertEquals(2.5f, result.getSpeed());

config.set("speed", 3);
result = handler.loadObject("float-test");
assertEquals(3.0f, result.getSpeed());
}

@Test
void testLoadObjectMissingFieldUsesDefault() throws Exception {
YamlConfiguration config = new YamlConfiguration();
Expand Down
Loading