Compare commits

..

2 commits

17 changed files with 383 additions and 161 deletions

2
.gitignore vendored
View file

@ -10,3 +10,5 @@ out/
# Ignore Gradle build output directory
build
run/

View file

@ -0,0 +1,145 @@
package org.adrianvictor.lib;
import org.adrianvictor.lib.versioning.DefaultVersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionMatcher;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionedServiceRegistrar;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
// Define dummy interfaces for testing
interface TestService {}
class TestServiceB1_7_3 implements TestService {}
class TestServiceR1_21 implements TestService {}
// Dummy registrar for testing
class DummyRegistrarB1_7_3 implements VersionedServiceRegistrar {
@Override
public void register(VersionedServiceFactory factory) {
factory.register(TestService.class, "b1_7_3", TestServiceB1_7_3::new);
}
}
class DummyRegistrarR1_21 implements VersionedServiceRegistrar {
@Override
public void register(VersionedServiceFactory factory) {
factory.register(TestService.class, "r1_1", TestServiceR1_21::new); // Registers for r1_1 compat
factory.register(TestService.class, "r1_16_5", TestServiceR1_21::new); // Registers for r1_16_5 compat
factory.register(TestService.class, "r1_21", TestServiceR1_21::new); // Registers for r1_21
}
}
public class DefaultVersionedServiceFactoryTest {
private VersionMatcher mockVersionMatcher;
private DefaultVersionedServiceFactory factory;
@BeforeEach
void setUp() {
mockVersionMatcher = mock(VersionMatcher.class);
factory = new DefaultVersionedServiceFactory(mockVersionMatcher);
}
@Test
void testServiceRegistrationAndRetrievalForB1_7_3() {
// Mock VersionMatcher to return b1_7_3 suffix
when(mockVersionMatcher.getServerVersion()).thenReturn("1.7.3");
when(mockVersionMatcher.getClassSuffix(eq("1.7.3"))).thenReturn("b1_7_3");
// Register the dummy service for b1_7_3
new DummyRegistrarB1_7_3().register(factory);
// Retrieve the service
TestService service = factory.getService(TestService.class);
// Assert that the correct version is returned
assertNotNull(service);
assertTrue(service instanceof TestServiceB1_7_3);
}
@Test
void testServiceRegistrationAndRetrievalForR1_1() {
// Mock VersionMatcher to return r1_1 suffix for a 1.10.2 server
when(mockVersionMatcher.getServerVersion()).thenReturn("1.10.2");
when(mockVersionMatcher.getClassSuffix(eq("1.10.2"))).thenReturn("r1_1");
// Register the dummy service for R1_21 (which covers r1_1 compat)
new DummyRegistrarR1_21().register(factory);
// Retrieve the service
TestService service = factory.getService(TestService.class);
// Assert that the correct compatible version is returned
assertNotNull(service);
assertTrue(service instanceof TestServiceR1_21);
}
@Test
void testServiceRegistrationAndRetrievalForR1_16_5() {
// Mock VersionMatcher to return r1_16_5 suffix for a 1.18.1 server
when(mockVersionMatcher.getServerVersion()).thenReturn("1.18.1");
when(mockVersionMatcher.getClassSuffix(eq("1.18.1"))).thenReturn("r1_16_5");
// Register the dummy service for R1_21 (which covers r1_16_5 compat)
new DummyRegistrarR1_21().register(factory);
// Retrieve the service
TestService service = factory.getService(TestService.class);
// Assert that the correct compatible version is returned
assertNotNull(service);
assertTrue(service instanceof TestServiceR1_21);
}
@Test
void testServiceRegistrationAndRetrievalForR1_21() {
// Mock VersionMatcher to return r1_21 suffix
when(mockVersionMatcher.getServerVersion()).thenReturn("1.21.0");
when(mockVersionMatcher.getClassSuffix(eq("1.21.0"))).thenReturn("r1_21");
// Register the dummy service for r1_21
new DummyRegistrarR1_21().register(factory);
// Retrieve the service
TestService service = factory.getService(TestService.class);
// Assert that the correct version is returned
assertNotNull(service);
assertTrue(service instanceof TestServiceR1_21);
}
@Test
void testNoServiceRegisteredThrowsException() {
// Mock VersionMatcher to return an unknown suffix
when(mockVersionMatcher.getServerVersion()).thenReturn("unknown");
when(mockVersionMatcher.getClassSuffix(anyString())).thenReturn("unknown_version");
// Attempt to retrieve a service without any registration
IllegalStateException exception = assertThrows(IllegalStateException.class, () ->
factory.getService(TestService.class)
);
assertTrue(exception.getMessage().contains("No service registered for type"));
}
@Test
void testSpecificServiceNotRegisteredForVersionThrowsException() {
// Mock VersionMatcher to return b1_7_3 suffix
when(mockVersionMatcher.getServerVersion()).thenReturn("1.7.3");
when(mockVersionMatcher.getClassSuffix(eq("1.7.3"))).thenReturn("b1_7_3");
// Register R1_21 service, but not B1_7_3
new DummyRegistrarR1_21().register(factory); // R1_21 registrar doesn't register b1_7_3
// Attempt to retrieve b1_7_3 service
IllegalStateException exception = assertThrows(IllegalStateException.class, () ->
factory.getService(TestService.class)
);
assertTrue(exception.getMessage().contains("No service registered for type"));
}
}

View file

@ -1,6 +1,6 @@
plugins {
java
id("xyz.jpenilla.run-paper") version "2.3.1"
id("xyz.jpenilla.run-paper") version "3.0.1"
}
group = "org.adrianvictor"
@ -70,7 +70,7 @@ dependencies {
/* ----------------------------------------- */
mcVersions.forEach { ver ->
tasks.register<Jar>("jar${ver.replace(".", "_").replace("-", "_").replace("/", "_").capitalize()}") {
tasks.register<Jar>("jar${ver.replace(".", "_").replace("-", "_").replace("/", "_").replaceFirstChar { it.uppercase() }}") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(sourceSets["main"].output)
from(sourceSets[ver].output)
@ -126,4 +126,13 @@ java {
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
/* ----------------------------------------- */
/* RUN SETTINGS */
/* ----------------------------------------- */
tasks.runServer {
minecraftVersion("1.21.1")
pluginJars.from(tasks.named("jarR1_21"))
}

View file

@ -0,0 +1,20 @@
package org.adrianvictor.lib.impl.b1_7_3;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionedServiceRegistrar;
import org.adrianvictor.lib.configuration.provider.ConfigurationProvider;
import org.adrianvictor.lib.impl.b1_7_3.configuration.Configuration;
import org.adrianvictor.lib.text.provider.TextColorProvider;
import org.adrianvictor.lib.impl.b1_7_3.text.TextColor;
public class B1_7_3Registrar implements VersionedServiceRegistrar {
@Override
public void register(VersionedServiceFactory factory) {
factory.register(ConfigurationProvider.class, "b1_7_3", Configuration::new);
factory.register(TextColorProvider.class, "b1_7_3", TextColor::new);
// Register for b1_8_compat as well, assuming compatibility
factory.register(ConfigurationProvider.class, "b1_8_compat", Configuration::new);
factory.register(TextColorProvider.class, "b1_8_compat", TextColor::new);
}
}

View file

@ -0,0 +1 @@
org.adrianvictor.lib.impl.b1_7_3.B1_7_3Registrar

View file

@ -0,0 +1,42 @@
package org.adrianvictor.lib;
import org.adrianvictor.lib.versioning.DefaultVersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionMatcher;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionedServiceRegistrar;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.ServiceLoader;
public class Main extends JavaPlugin {
private static VersionedServiceFactory versionedServiceFactory;
private static VersionMatcher versionMatcher;
@Override
public void onEnable() {
versionMatcher = new VersionMatcher();
versionedServiceFactory = new DefaultVersionedServiceFactory(versionMatcher);
ServiceLoader<VersionedServiceRegistrar> registrars = ServiceLoader.load(VersionedServiceRegistrar.class);
for (VersionedServiceRegistrar registrar : registrars) {
registrar.register(versionedServiceFactory);
}
getLogger().info("tenkumaLib has been enabled!");
}
@Override
public void onDisable() {
getLogger().info("tenkumaLib has been disabled!");
}
public static VersionedServiceFactory getVersionedServiceFactory() {
return versionedServiceFactory;
}
public static VersionMatcher getVersionMatcher() {
return versionMatcher;
}
}

View file

@ -1,10 +1,10 @@
package org.adrianvictor.lib.configuration;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.configuration.provider.ConfigurationProvider;
import org.adrianvictor.lib.reflection.ImplementationRegistry;
public class Configuration {
public static ConfigurationProvider create(ImplementationRegistry registry) {
return registry.getInstance(ConfigurationProvider.class);
public static ConfigurationProvider create(VersionedServiceFactory factory) {
return factory.getService(ConfigurationProvider.class);
}
}

View file

@ -1,34 +0,0 @@
package org.adrianvictor.lib.reflection;
public class ImplementationRegistry {
private final String classSuffix;
public ImplementationRegistry(String classSuffix) {
this.classSuffix = classSuffix;
}
public <T> T getInstance(Class<T> apiClass, String target, String replacement) {
String base = apiClass.getName();
String implName =
base.replaceFirst(
"org\\.adrianvictor\\.lib",
"org.adrianvictor.lib.impl." + classSuffix
);
try {
Class<?> implClass = Class.forName(implName);
return apiClass.cast(
implClass.getDeclaredConstructor().newInstance()
);
} catch (ReflectiveOperationException e) {
throw new IllegalStateException("Cannot load " + implName, e);
}
}
public <T> T getInstance(Class<T> apiClass) {
return getInstance(apiClass, "org.adrianvictor.lib", "org.adrianvictor.lib.impl.");
}
}

View file

@ -1,116 +0,0 @@
package org.adrianvictor.lib.reflection;
import org.bukkit.Bukkit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class VersionMatcher {
public VersionMatcher() {}
private record Entry(String pattern, String classSuffix) {}
public String getClassSuffix(String type, String serverVersion) {
if (type == null || serverVersion == null) {
return null;
}
Map<String, List<Entry>> map = populateMap();
List<Entry> entries = map.get(type.toLowerCase());
if (entries == null || entries.isEmpty()) {
return null;
}
// Exact match
for (Entry e : entries) {
if (safeMatches(e.pattern, serverVersion)) {
return e.classSuffix;
}
}
// Fallback to closest version
List<Entry> sorted = new ArrayList<>(entries);
Collections.sort(sorted, (a, b) -> compareVersions(a.pattern, b.pattern));
Entry oldest = sorted.get(0);
Entry newest = sorted.get(sorted.size() - 1);
int cmpOldest = compareVersions(serverVersion, oldest.pattern);
int cmpNewest = compareVersions(serverVersion, newest.pattern);
if (cmpOldest < 0) {
return oldest.classSuffix;
} else if (cmpNewest > 0) {
return newest.classSuffix;
}
return newest.classSuffix;
}
public String getServerVersion() {
String rawVersion = null;
try {
rawVersion = Bukkit.getMinecraftVersion();
} catch (NoSuchMethodError ignored) {}
if (rawVersion == null || rawVersion.isEmpty()) {
String v = Bukkit.getVersion();
int start = v.lastIndexOf("MC: ");
if (start != -1) {
rawVersion = v.substring(start + 4, v.length() - 1);
} else {
rawVersion = "unknown";
}
}
return rawVersion;
}
private Map<String, List<Entry>> populateMap() {
Map<String, List<Entry>> map = new HashMap<>();
map.put("release", List.of(
new Entry("^1\\.21\\..*$", "r1_21")
));
map.put("beta", List.of(
new Entry("^1\\.7\\.3$", "b1_7_3")
));
return map;
}
private int compareVersions(String v1, String v2) {
String clean1 = v1.replaceAll("[^0-9.]", "");
String clean2 = v2.replaceAll("[^0-9.]", "");
String[] a1 = clean1.split("\\.");
String[] a2 = clean2.split("\\.");
int len = Math.max(a1.length, a2.length);
for (int i = 0; i < len; i++) {
int n1 = i < a1.length ? parseInt(a1[i]) : 0;
int n2 = i < a2.length ? parseInt(a2[i]) : 0;
if (n1 != n2) {
return n1 - n2;
}
}
return 0;
}
private int parseInt(String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
return 0;
}
}
private static boolean safeMatches(String expression, String against) {
String cleanPattern = expression.trim();
Pattern pattern = Pattern.compile(cleanPattern, Pattern.CASE_INSENSITIVE);
return pattern.matcher(against).matches();
}
}

View file

@ -1,10 +1,10 @@
package org.adrianvictor.lib.text;
import org.adrianvictor.lib.reflection.ImplementationRegistry;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.text.provider.TextColorProvider;
public class TextColor {
public static TextColorProvider create(ImplementationRegistry registry) {
return registry.getInstance(TextColorProvider.class);
public static TextColorProvider create(VersionedServiceFactory factory) {
return factory.getService(TextColorProvider.class);
}
}

View file

@ -1,13 +1,13 @@
package org.adrianvictor.lib.text;
import org.adrianvictor.lib.reflection.ImplementationRegistry;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.text.provider.TextColorProvider;
public class TextColorUtils {
private final TextColorProvider provider;
public TextColorUtils(ImplementationRegistry registry) {
provider = TextColor.create(registry);
public TextColorUtils(VersionedServiceFactory factory) {
provider = TextColor.create(factory);
}
public String formatColors(String message) {

View file

@ -0,0 +1,34 @@
package org.adrianvictor.lib.versioning;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
public class DefaultVersionedServiceFactory implements VersionedServiceFactory {
private final VersionMatcher versionMatcher;
private final Map<Class<?>, Map<String, Supplier<?>>> serviceRegistrations = new HashMap<>();
public DefaultVersionedServiceFactory(VersionMatcher versionMatcher) {
this.versionMatcher = versionMatcher;
}
@Override
public <T> void register(Class<T> serviceType, String versionSuffix, Supplier<T> supplier) {
serviceRegistrations
.computeIfAbsent(serviceType, k -> new HashMap<>())
.put(versionSuffix, supplier);
}
@Override
@SuppressWarnings("unchecked")
public <T> T getService(Class<T> serviceType) {
String serverVersion = versionMatcher.getServerVersion();
String versionSuffix = versionMatcher.getClassSuffix(serverVersion);
Map<String, Supplier<?>> versionSuppliers = serviceRegistrations.get(serviceType);
if (versionSuppliers == null || !versionSuppliers.containsKey(versionSuffix)) {
throw new IllegalStateException("No service registered for type " + serviceType.getName() + " and version " + versionSuffix + ". Current server version: " + serverVersion);
}
return (T) versionSuppliers.get(versionSuffix).get();
}
}

View file

@ -0,0 +1,89 @@
package org.adrianvictor.lib.versioning;
import org.bukkit.Bukkit;
import java.util.Comparator;
import java.util.List;
import java.util.ArrayList;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class VersionMatcher {
public VersionMatcher() {}
private record Entry(String pattern, String classSuffix, int minMajor, int minMinor, int maxMajor, int maxMinor) {}
public String getClassSuffix(String serverVersion) {
if (serverVersion == null) {
return null;
}
List<Entry> entries = populateEntries();
List<Entry> applicableEntries = new ArrayList<>();
int[] parsedServerVersion = parseVersion(serverVersion);
int serverMajor = parsedServerVersion[0];
int serverMinor = parsedServerVersion[1];
for (Entry entry : entries) {
Pattern p = Pattern.compile(entry.pattern());
if (p.matcher(serverVersion).matches()) {
if (serverMajor >= entry.minMajor() && serverMinor >= entry.minMinor() &&
serverMajor <= entry.maxMajor() && serverMinor <= entry.maxMinor()) {
applicableEntries.add(entry);
}
}
}
applicableEntries.sort(Comparator
.comparing(Entry::maxMajor).reversed()
.thenComparing(Entry::maxMinor).reversed()
.thenComparing(Entry::minMajor).reversed()
.thenComparing(Entry::minMinor).reversed());
Optional<Entry> bestMatch = applicableEntries.stream().findFirst();
return bestMatch.map(Entry::classSuffix).orElse(null);
}
public String getServerVersion() {
String rawVersion = null;
try {
rawVersion = Bukkit.getMinecraftVersion();
} catch (NoSuchMethodError ignored) {}
if (rawVersion == null || rawVersion.isEmpty()) {
String v = Bukkit.getVersion();
int start = v.lastIndexOf("MC: ");
if (start != -1) {
rawVersion = v.substring(start + 4, v.length() - 1);
} else {
rawVersion = "unknown";
}
}
return rawVersion;
}
private List<Entry> populateEntries() {
return List.of(
new Entry("^1\\.7\\.3$", "b1_7_3", 1, 7, 1, 7),
// r1_1 covers versions from 1.1.x up to 1.16.4
new Entry("^1\\.(1[0-5]|[1-9])(\\.\\d+)*$", "r1_1", 1, 1, 1, 16),
// r1_16_5 covers versions from 1.16.5 up to 1.20.x
new Entry("^1\\.(16\\.[5-9]|1[7-9]|20)(\\.\\d+)*$", "r1_16_5", 1, 16, 1, 20),
// r1_21 covers 1.21.x and above
new Entry("^1\\.21(\\.\\d+)*$", "r1_21", 1, 21, Integer.MAX_VALUE, Integer.MAX_VALUE)
);
}
private int[] parseVersion(String versionString) {
Matcher matcher = Pattern.compile("^(\\d+)\\.(\\d+).*").matcher(versionString);
if (matcher.find()) {
return new int[]{Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))};
}
return new int[]{0, 0}; // Default for unknown format
}
}

View file

@ -0,0 +1,8 @@
package org.adrianvictor.lib.versioning;
import java.util.function.Supplier;
public interface VersionedServiceFactory {
<T> void register(Class<T> serviceType, String versionSuffix, Supplier<T> supplier);
<T> T getService(Class<T> serviceType);
}

View file

@ -0,0 +1,5 @@
package org.adrianvictor.lib.versioning;
public interface VersionedServiceRegistrar {
void register(VersionedServiceFactory factory);
}

View file

@ -0,0 +1,16 @@
package org.adrianvictor.lib.impl.r1_21;
import org.adrianvictor.lib.versioning.VersionedServiceFactory;
import org.adrianvictor.lib.versioning.VersionedServiceRegistrar;
import org.adrianvictor.lib.configuration.provider.ConfigurationProvider;
import org.adrianvictor.lib.impl.r1_21.configuration.Configuration;
import org.adrianvictor.lib.text.provider.TextColorProvider;
import org.adrianvictor.lib.impl.r1_21.text.TextColor;
public class R1_21Registrar implements VersionedServiceRegistrar {
@Override
public void register(VersionedServiceFactory factory) {
factory.register(ConfigurationProvider.class, "r1_1", Configuration::new);
factory.register(TextColorProvider.class, "r1_16_5", TextColor::new);
}
}

View file

@ -0,0 +1 @@
org.adrianvictor.lib.impl.r1_21.R1_21Registrar