diff --git a/jme3-networking/src/main/java/com/jme3/network/base/ConnectorAdapter.java b/jme3-networking/src/main/java/com/jme3/network/base/ConnectorAdapter.java index 8a8ac275ab..d27c1273f9 100644 --- a/jme3-networking/src/main/java/com/jme3/network/base/ConnectorAdapter.java +++ b/jme3-networking/src/main/java/com/jme3/network/base/ConnectorAdapter.java @@ -201,7 +201,7 @@ public void run() Message m = null; while( (m = messageBuffer.pollMessage()) != null ) { - m.setReliable( reliable ); + m = m.setReliable( reliable ); dispatch( m ); } } diff --git a/jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java b/jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java index 3496ea0fbc..1fa403edaa 100644 --- a/jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java +++ b/jme3-networking/src/main/java/com/jme3/network/base/KernelAdapter.java @@ -301,7 +301,7 @@ protected void createAndDispatch( Envelope env ) // Should be complete... and maybe we should check, but we don't. Message m = null; while( (m = protocol.pollMessage()) != null ) { - m.setReliable(reliable); + m = m.setReliable(reliable); dispatch(env.getSource(), m); } } diff --git a/jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java b/jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java index 08d02c02bb..96ccb17360 100644 --- a/jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java +++ b/jme3-networking/src/main/java/com/jme3/network/serializing/Serializer.java @@ -43,10 +43,12 @@ import java.net.URL; import java.nio.ByteBuffer; import java.util.*; +import java.util.function.Predicate; import java.util.jar.Attributes; import java.util.logging.Level; import java.util.logging.Logger; + /** * The main serializer class, which will serialize objects such that * they can be sent across the network. Serializing classes should extend @@ -68,6 +70,17 @@ public abstract class Serializer { private static final Serializer fieldSerializer = new FieldSerializer(); private static final Serializer arraySerializer = new ArraySerializer(); + + // Predicate-based registry for dynamic serializer resolution + private static class PredicateRegistration { + final Predicate> predicate; + final Serializer serializer; + PredicateRegistration(Predicate> p, Serializer s) { + this.predicate = p; + this.serializer = s; + } + } + private static final List predicateSerializers = new ArrayList<>(); private static short nextAvailableId = -2; // historically the first ID was always -2 @@ -95,7 +108,8 @@ public static void initialize() { // Reset all of the indices and tracking variables just in case idRegistrations.clear(); classRegistrations.clear(); - registrations.clear(); + registrations.clear(); + predicateSerializers.clear(); nextAvailableId = -2; // historically the first ID was always -2 @@ -278,17 +292,37 @@ public static SerializerRegistration registerClass(Class cls, boolean failOnMiss short classId = serializable.id(); if (classId == 0) classId = nextId(); - Serializer serializer = getSerializer(serializerClass, false); + Serializer serializer = null; - if (serializer == null) serializer = fieldSerializer; + // ignore both FieldSerializer and Serializer defaults. + if (serializerClass != null && serializerClass != FieldSerializer.class && serializerClass != Serializer.class) { + try { + // Directly instantiate the custom serializer + serializer = (Serializer) serializerClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + log.log(Level.SEVERE, "Could not instantiate explicit custom serializer: " + serializerClass, e); + } + } + + // no explicit custom serializer was requested + if (serializer == null) { + for (PredicateRegistration pr : predicateSerializers) { + if (pr.predicate.test(cls)) { + serializer = pr.serializer; + break; + } + } + } - SerializerRegistration existingReg = getExactSerializerRegistration(cls); + // Fallback to standard resolution if no predicate matched + if (serializer == null) { + serializer = fieldSerializer; + } + SerializerRegistration existingReg = getExactSerializerRegistration(cls); if (existingReg != null) classId = existingReg.getId(); - SerializerRegistration reg = new SerializerRegistration(serializer, cls, classId); - - return registerClassForId( classId, cls, serializer ); + return registerClassForId(classId, cls, serializer); } if (failOnMiss) { throw new IllegalArgumentException( "Class is not marked @Serializable:" + cls ); @@ -465,7 +499,20 @@ public static SerializerRegistration getSerializerRegistration(Class cls, boolea if( failOnMiss ) { throw new IllegalArgumentException( "Class has not been registered:" + cls ); } - return registerClass(cls, fieldSerializer); + + // check predicates for auto-registration fallback + Serializer fallbackSerializer = null; + for (PredicateRegistration pr : predicateSerializers) { + if (pr.predicate.test(cls)) { + fallbackSerializer = pr.serializer; + break; + } + } + if (fallbackSerializer == null) { + fallbackSerializer = fieldSerializer; + } + + return registerClass(cls, fallbackSerializer); } @@ -551,6 +598,24 @@ public static void writeClassAndObject(ByteBuffer buffer, Object object) throws reg.getSerializer().writeObject(buffer, object); } + + /** + * Registers a predicate-based dynamic serializer. + *

+ * Important Ordering Behavior: Predicates are evaluated in registration order + * using a first-match-wins strategy. Furthermore, predicates only affect classes + * registered afterwards; any classes already registered prior to this call will + * not be re-evaluated against the predicate. + * + * @param predicate the condition used to match classes dynamically + * @param serializer the serializer instance to assign when the predicate matches + */ + public static void registerSerializerPredicate(Predicate> predicate, Serializer serializer) { + if (locked) { + throw new RuntimeException("Serializer registry locked trying to register predicate"); + } + predicateSerializers.add(new PredicateRegistration(predicate, serializer)); + } /** * Read an object from the buffer, effectively deserializing it. diff --git a/jme3-networking/src/test/java/com/jme3/network/serializing/PredicateSerializerTest.java b/jme3-networking/src/test/java/com/jme3/network/serializing/PredicateSerializerTest.java new file mode 100644 index 0000000000..6a83f24c41 --- /dev/null +++ b/jme3-networking/src/test/java/com/jme3/network/serializing/PredicateSerializerTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2009-2026 jMonkeyEngine + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * * Neither the name of 'jMonkeyEngine' nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED + * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.jme3.network.serializing; + +import com.jme3.network.serializing.serializers.FieldSerializer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.nio.ByteBuffer; + +import static org.junit.jupiter.api.Assertions.*; + +public class PredicateSerializerTest { + + // A dummy message representing a custom data structure (e.g., mimicking a Record or Kotlin Data Class) + @Serializable + public static class SpecialMessage { + public String data; + public SpecialMessage() {} + } + + // A standard message that should fall back to the default FieldSerializer + @Serializable + public static class StandardMessage { + public int id; + public StandardMessage() {} + } + + @Serializable(serializer = Serializer.class) + public static class SpecialMessageWithAlternateDefault { + public SpecialMessageWithAlternateDefault() {} + } + + // Simulates a class where the user EXPLICITLY requested a custom serializer + @Serializable(serializer = SecondMockSerializer.class) + public static class ExplicitCustomMessage { + public ExplicitCustomMessage() {} + } + + public static class SecondMockSerializer extends Serializer { + @Override + public T readObject(ByteBuffer data, Class c) { return null; } + @Override + public void writeObject(ByteBuffer buffer, Object object) {} + } + + // A mock custom serializer to test the routing mechanism + public static class MockCustomSerializer extends Serializer { + @Override + public T readObject(ByteBuffer data, Class c) { + return null; // Mock implementation + } + + @Override + public void writeObject(ByteBuffer buffer, Object object) { + // Mock implementation + } + } + + @BeforeEach + public void setupRegistry() { + Serializer.initialize(); + + // Register the predicate: any class matching SpecialMessage gets the MockCustomSerializer + Serializer.registerSerializerPredicate( + cls -> cls == SpecialMessage.class, + new MockCustomSerializer() + ); + } + + @Test + public void testPredicateSerializerSelectionOverridesDefault() { + // Register the class + Serializer.registerClass(SpecialMessage.class); + + SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessage.class); + + assertNotNull(registration, "Registration should not be null"); + // Verify the predicate successfully intercepted the registration and applied the mock serializer + assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(), + "SpecialMessage should be routed to MockCustomSerializer via predicate"); + } + + @Test + public void testStandardClassFallsBackToFieldSerializer() { + // Register a normal POJO + Serializer.registerClass(StandardMessage.class); + + SerializerRegistration registration = Serializer.getExactSerializerRegistration(StandardMessage.class); + + assertNotNull(registration, "Registration should not be null"); + // Verify classes that do not match the predicate still receive the default FieldSerializer + assertEquals(FieldSerializer.class, registration.getSerializer().getClass(), + "StandardMessage should fall back to the default FieldSerializer"); + } + + @Test + public void testPredicateAutoRegistrationFallback() { + // Clear registrations + Serializer.initialize(); + Serializer.registerSerializerPredicate(cls -> cls == SpecialMessage.class, new MockCustomSerializer()); + + // Fetch the registration using the auto-registration fallback mechanism (passing failOnMiss=false implicitly or explicitly) + SerializerRegistration registration = Serializer.getSerializerRegistration(SpecialMessage.class, false); + + assertNotNull(registration, "Auto-registration should successfully create a registration"); + assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(), + "Auto-registration should respect the registered predicates"); + } + + @Test + public void testInitializeClearsPredicateRegistrations() { + // @BeforeEach already called initialize() and registered MockCustomSerializer for SpecialMessage. + + // Re-initialize the registry. This MUST clear the predicateSerializers list. + Serializer.initialize(); + + // Register the exact same predicate condition, but with a different serializer (SecondMockSerializer) + Serializer.registerSerializerPredicate( + cls -> cls == SpecialMessage.class, + new SecondMockSerializer() + ); + + // Register the class + Serializer.registerClass(SpecialMessage.class); + + SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessage.class); + + assertNotNull(registration, "Registration should not be null"); + + // If initialize() failed to clear the list, MockCustomSerializer would still be first and win. + // If it correctly cleared the list, SecondMockSerializer should be assigned. + assertEquals(SecondMockSerializer.class, registration.getSerializer().getClass(), + "initialize() failed to clear predicateSerializers, causing a stale serializer to be selected."); + } + + @Test + public void testPredicateEvaluatesForAlternateDefaultSerializer() { + Serializer.initialize(); + Serializer.registerSerializerPredicate( + cls -> cls == SpecialMessageWithAlternateDefault.class, + new MockCustomSerializer() + ); + + Serializer.registerClass(SpecialMessageWithAlternateDefault.class); + SerializerRegistration registration = Serializer.getExactSerializerRegistration(SpecialMessageWithAlternateDefault.class); + + assertNotNull(registration); + + // FAILS HERE in the flawed implementation: + // The registry sees `Serializer.class`, skips the predicate loop, + // and falls back to `FieldSerializer`, ignoring your mock! + assertEquals(MockCustomSerializer.class, registration.getSerializer().getClass(), + "Predicate should intercept the alternate default (Serializer.class)"); + } + + @Test + public void testExplicitCustomSerializerOverridesPredicate() { + Serializer.initialize(); + // Register a predicate that TRIES to intercept this class + Serializer.registerSerializerPredicate( + cls -> cls == ExplicitCustomMessage.class, + new MockCustomSerializer() + ); + + // Because the user explicitly annotated this with `@Serializable(serializer = SecondMockSerializer.class)`, + // the explicit request must win, and the predicate must be ignored. + Serializer.registerClass(ExplicitCustomMessage.class); + SerializerRegistration registration = Serializer.getExactSerializerRegistration(ExplicitCustomMessage.class); + + assertNotNull(registration); + assertEquals(SecondMockSerializer.class, registration.getSerializer().getClass(), + "Explicit custom serializer must override predicate selection"); + } +} \ No newline at end of file