diff --git a/src/main/java/lain/lib/SharedPool.java b/src/main/java/lain/lib/SharedPool.java
new file mode 100644
index 0000000..0c078cb
--- /dev/null
+++ b/src/main/java/lain/lib/SharedPool.java
@@ -0,0 +1,21 @@
+package lain.lib;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ForkJoinPool;
+
+public final class SharedPool
+{
+
+ private static final ExecutorService thePool = Executors.newWorkStealingPool(ForkJoinPool.getCommonPoolParallelism() + 1);
+
+ public static void execute(Runnable command)
+ {
+ thePool.execute(command);
+ }
+
+ private SharedPool()
+ {
+ }
+
+}
diff --git a/src/main/java/lain/mods/skins/api/SkinBundle.java b/src/main/java/lain/mods/skins/api/SkinBundle.java
new file mode 100644
index 0000000..37e5b9d
--- /dev/null
+++ b/src/main/java/lain/mods/skins/api/SkinBundle.java
@@ -0,0 +1,105 @@
+package lain.mods.skins.api;
+
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import lain.mods.skins.api.interfaces.ISkin;
+
+/**
+ * A special ISkin object that will return first ready ISkin object in a collection.
+ * It supports swapping it's collection reference at runtime.
+ *
+ */
+public class SkinBundle implements ISkin
+{
+
+ protected final AtomicReference> ref = new AtomicReference<>(Collections.emptyList());
+ protected final Collection> listeners = new CopyOnWriteArrayList<>();
+ protected final Collection> filters = new CopyOnWriteArrayList<>();
+
+ protected Optional find()
+ {
+ Collection skins;
+ if ((skins = ref.get()).isEmpty())
+ return Optional.empty();
+ return skins.stream().filter(ISkin::isDataReady).findFirst();
+ }
+
+ @Override
+ public ByteBuffer getData()
+ {
+ return find().orElse(SkinProviderAPI.DUMMY).getData();
+ }
+
+ @Override
+ public String getSkinType()
+ {
+ return find().orElse(SkinProviderAPI.DUMMY).getSkinType();
+ }
+
+ @Override
+ public boolean isDataReady()
+ {
+ return find().orElse(SkinProviderAPI.DUMMY).isDataReady();
+ }
+
+ @Override
+ public void onRemoval()
+ {
+ set(Collections.emptyList());
+ }
+
+ public SkinBundle set(Collection c)
+ {
+ Objects.requireNonNull(c);
+ if (!c.isEmpty())
+ {
+ for (ISkin e : c)
+ {
+ listeners.forEach(e::setRemovalListener);
+ filters.forEach(e::setSkinFilter);
+ }
+ }
+ Collection skins;
+ if (!(skins = ref.getAndSet(c)).isEmpty())
+ skins.forEach(ISkin::onRemoval);
+ return this;
+ }
+
+ @Override
+ public boolean setRemovalListener(Consumer listener)
+ {
+ if (listener == null || listeners.contains(listener))
+ return false;
+ if (listeners.add(listener))
+ {
+ Collection skins;
+ if (!(skins = ref.get()).isEmpty())
+ skins.forEach(e -> e.setRemovalListener(listener));
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean setSkinFilter(Function filter)
+ {
+ if (filter == null || filters.contains(filter))
+ return false;
+ if (filters.add(filter))
+ {
+ Collection skins;
+ if (!(skins = ref.get()).isEmpty())
+ skins.forEach(e -> e.setSkinFilter(filter));
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/lain/mods/skins/api/SkinProviderAPI.java b/src/main/java/lain/mods/skins/api/SkinProviderAPI.java
index d0183e1..bdeee20 100644
--- a/src/main/java/lain/mods/skins/api/SkinProviderAPI.java
+++ b/src/main/java/lain/mods/skins/api/SkinProviderAPI.java
@@ -4,17 +4,23 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-import java.util.Optional;
import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ForkJoinPool.ManagedBlocker;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.Supplier;
import java.util.stream.Collectors;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.cache.RemovalListener;
import com.google.common.cache.RemovalNotification;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lain.lib.SharedPool;
import lain.mods.skins.api.interfaces.IPlayerProfile;
import lain.mods.skins.api.interfaces.ISkin;
import lain.mods.skins.api.interfaces.ISkinProvider;
@@ -23,6 +29,46 @@
public class SkinProviderAPI
{
+ public static final ISkin DUMMY = new ISkin()
+ {
+
+ @Override
+ public ByteBuffer getData()
+ {
+ return null;
+ }
+
+ @Override
+ public String getSkinType()
+ {
+ return null;
+ }
+
+ @Override
+ public boolean isDataReady()
+ {
+ return false;
+ }
+
+ @Override
+ public void onRemoval()
+ {
+ }
+
+ @Override
+ public boolean setRemovalListener(Consumer listener)
+ {
+ return false;
+ }
+
+ @Override
+ public boolean setSkinFilter(Function filter)
+ {
+ return false;
+ }
+
+ };
+
/**
* The service for skins.
*/
@@ -33,132 +79,110 @@ public class SkinProviderAPI
public static final ISkinProviderService CAPE = create();
/**
- * @return an empty ISkinProviderService with default implementation, a single ISkin object will be created during runtime with all available ISkin objects bundled in it, if a corresponding profile changes during the lifetime of an ISkin object, the ISkin object will be discarded and a new one will be created.
+ * @return an empty ISkinProviderService with default implementation.
+ * a SkinBundle will be created with all available ISkin objects for that IPlayerProfile.
+ * if the profile got updated during the lifetime of a SkinBundle, new ISkin objects will be gathered and a thread will be used to monitor those objects to wait their isDataReady for up to 10 seconds before updating the SkinBundle.
*/
public static ISkinProviderService create()
{
return new ISkinProviderService()
{
- private final ISkin DUMMY = new ISkin()
+ private final LoadingCache> reloading = CacheBuilder.newBuilder().weakKeys().build(new CacheLoader>()
{
@Override
- public ByteBuffer getData()
+ public AtomicReference