Finished the reworking of the Asset Manager
This commit is contained in:
parent
09874d1b8a
commit
641f19dd67
@ -5,7 +5,7 @@ connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
|
|||||||
connection.project.dir=
|
connection.project.dir=
|
||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
gradle.user.home=
|
gradle.user.home=
|
||||||
java.home=C\:/Program Files/Eclipse Adoptium/jdk25
|
java.home=C\:/Program Files/Java/jdk25
|
||||||
jvm.arguments=
|
jvm.arguments=
|
||||||
offline.mode=false
|
offline.mode=false
|
||||||
override.workspace.settings=true
|
override.workspace.settings=true
|
||||||
|
|||||||
@ -77,6 +77,7 @@ dependencies {
|
|||||||
implementation "org.lwjgl:lwjgl-freetype::$lwjglNatives"
|
implementation "org.lwjgl:lwjgl-freetype::$lwjglNatives"
|
||||||
implementation "org.lwjgl:lwjgl-harfbuzz::$lwjglNatives"
|
implementation "org.lwjgl:lwjgl-harfbuzz::$lwjglNatives"
|
||||||
implementation "org.lwjgl:lwjgl-nanovg::$lwjglNatives"
|
implementation "org.lwjgl:lwjgl-nanovg::$lwjglNatives"
|
||||||
|
implementation "org.jspecify:jspecify:1.0.0"
|
||||||
|
|
||||||
//Gson
|
//Gson
|
||||||
implementation 'com.google.code.gson:gson:2.8.6'
|
implementation 'com.google.code.gson:gson:2.8.6'
|
||||||
|
|||||||
@ -1,14 +1,30 @@
|
|||||||
package speiger.src.coreengine.assets.api;
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.channels.SeekableByteChannel;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.impl.parsers.Buffers;
|
||||||
|
import speiger.src.coreengine.assets.impl.parsers.Closables;
|
||||||
|
import speiger.src.coreengine.assets.impl.parsers.Serialized;
|
||||||
|
|
||||||
public interface IAsset {
|
public interface IAsset {
|
||||||
public IAssetPackage owner();
|
public IAssetPackage owner();
|
||||||
public ID location();
|
public ID location();
|
||||||
|
|
||||||
public String hash();
|
|
||||||
public InputStream stream() throws IOException;
|
public InputStream stream() throws IOException;
|
||||||
|
public default ByteBuffer bytes() throws IOException { return parse(Buffers.HEAP); }
|
||||||
|
public default BufferedReader reader() throws IOException { return parse(Closables.READER); }
|
||||||
|
public default SeekableByteChannel channel() throws IOException { return parse(Closables.BYTE_CHANNEL); }
|
||||||
|
public default List<String> lines() throws IOException { return parse(Serialized.LINES); }
|
||||||
|
public default JsonObject jsonObj() throws IOException { return parse(Serialized.JSON_OBJ); }
|
||||||
|
public default JsonArray jsonArray() throws IOException { return parse(Serialized.JSON_ARRAY); }
|
||||||
|
|
||||||
public <T> T parse(IAssetParser<T> parser) throws IOException;
|
public <T> T parse(IAssetParser<T> parser) throws IOException;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,38 @@
|
|||||||
package speiger.src.coreengine.assets.api;
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.WatchService;
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import speiger.src.coreengine.assets.AssetLocation;
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.impl.FolderWatcher.ChangeType;
|
||||||
|
|
||||||
|
@NullMarked
|
||||||
public interface IAssetPackage extends AutoCloseable {
|
public interface IAssetPackage extends AutoCloseable {
|
||||||
void watchService(WatchService listener) throws IOException;
|
void registerWatchers(IFileWatcher watcher) throws IOException;
|
||||||
|
boolean handleChange(Path file, ChangeType type, Consumer<ID> changed) throws IOException;
|
||||||
List<String> domains();
|
List<String> domains();
|
||||||
Map<ID, String> fileHashes();
|
@Nullable
|
||||||
Optional<IAsset> get(AssetLocation location);
|
IAsset get(ID location);
|
||||||
void getAll(AssetLocation folder, BiConsumer<ID, IAsset> assets);
|
void getAll(ID folder, BiConsumer<ID, IAsset> assets);
|
||||||
|
|
||||||
|
public record AssetIdentity(long size, FileTime lastModified) {
|
||||||
|
|
||||||
|
public AssetIdentity(Path path) throws IOException {
|
||||||
|
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
|
||||||
|
this(attributes.size(), attributes.lastModifiedTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean matches(Path path) throws IOException {
|
||||||
|
BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);
|
||||||
|
return size() == attributes.size() && lastModified().equals(attributes.lastModifiedTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,16 +1,64 @@
|
|||||||
package speiger.src.coreengine.assets.api;
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
public interface IAssetProvider {
|
import org.jspecify.annotations.NullMarked;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.lists.ObjectList;
|
||||||
|
import speiger.src.collections.objects.utils.ObjectLists;
|
||||||
|
|
||||||
|
@NullMarked
|
||||||
|
public interface IAssetProvider extends AutoCloseable {
|
||||||
|
@Nullable
|
||||||
IAsset get(ID id);
|
IAsset get(ID id);
|
||||||
|
@Nullable
|
||||||
MultiAsset getAll(ID id);
|
MultiAsset getAll(ID id);
|
||||||
String hash(ID id);
|
|
||||||
Map<ID, IAsset> list(String folder, Predicate<ID> filter);
|
Map<ID, IAsset> list(String folder, Predicate<ID> filter);
|
||||||
Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter);
|
Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter);
|
||||||
|
|
||||||
public interface IReloadableAssetProvider extends IAssetProvider, AutoCloseable {
|
public static class ReloadTracker {
|
||||||
void addReloadListener(Runnable run);
|
ObjectList<Listener> listeners = new ObjectArrayList<>();
|
||||||
|
|
||||||
|
public void register(IResourceListener listener) {
|
||||||
|
listeners.add(new Listener(listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(Predicate<ID> filter, IResourceListener listener) {
|
||||||
|
listeners.add(new Listener(filter, listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IResourceListener> findByFilter(Set<ID> changed) {
|
||||||
|
if(changed.isEmpty()) return ObjectLists.empty();
|
||||||
|
List<IResourceListener> result = new ObjectArrayList<>();
|
||||||
|
for(int i = 0,m=listeners.size();i<m;i++) {
|
||||||
|
Listener listener = listeners.get(i);
|
||||||
|
for(ID id : changed) {
|
||||||
|
if(listener.contains(id)) {
|
||||||
|
result.add(listener.listener());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IResourceListener> listeners() {
|
||||||
|
return listeners.map(Listener::listener).pourAsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Listener(Predicate<ID> filter, IResourceListener listener) {
|
||||||
|
public Listener(IResourceListener listener) {
|
||||||
|
this(_ -> true, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean contains(ID id) {
|
||||||
|
return filter.test(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
package speiger.src.coreengine.assets.api;
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public record ID(String domain, String path) implements Comparable<ID> {
|
public record ID(String domain, String path) implements Comparable<ID> {
|
||||||
|
|
||||||
public ID {
|
public ID {
|
||||||
Objects.requireNonNull(domain);
|
if(!isValidDomain(Objects.requireNonNull(domain))) throw new IllegalArgumentException("Non [a-zA-Z0-9_.-] Character found in domain of location ["+domain+":"+path+"]");
|
||||||
Objects.requireNonNull(path);
|
if(!isValidPath(Objects.requireNonNull(path))) throw new IllegalArgumentException("Non [a-zA-Z0-9/_.-] Character found in path of location ["+domain+":"+path+"]");
|
||||||
if(!isValidDomain(domain)) throw new IllegalArgumentException("Non [a-zA-Z0-9_.-] Character found in domain of location ["+domain+":"+path+"]");
|
|
||||||
if(!isValidPath(path)) throw new IllegalArgumentException("Non [a-zA-Z0-9/_.-] Character found in path of location ["+domain+":"+path+"]");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ID prefix(String prefix) { return of(domain, prefix+"/"+path); }
|
public ID prefix(String prefix) { return of(domain, prefix+"/"+path); }
|
||||||
public ID suffix(String suffix) { return of(domain, path+"/"+suffix); }
|
public ID suffix(String suffix) { return of(domain, path+"/"+suffix); }
|
||||||
|
public ID suffix(Path path) { return suffix(path.toString().replace("\\", "/")); }
|
||||||
public ID alternate(String alterversion) { return of(domain, path+"."+alterversion); }
|
public ID alternate(String alterversion) { return of(domain, path+"."+alterversion); }
|
||||||
|
|
||||||
public String fileLocation() { return "assets/"+domain+"/"+path; }
|
public String fileLocation() { return domain+"/"+path; }
|
||||||
public boolean isRoot() { return !path.contains("/"); }
|
public boolean isRoot() { return !path.contains("/"); }
|
||||||
public boolean isRootFolder(String folder) { return path.indexOf('/', folder.length()+1) < 0; }
|
public boolean isRootFolder(String folder) { return path.indexOf('/', folder.length()+1) < 0; }
|
||||||
public boolean endsWith(String suffix) { return path.endsWith(suffix); }
|
public boolean endsWith(String suffix) { return path.endsWith(suffix); }
|
||||||
@ -35,11 +35,11 @@ public record ID(String domain, String path) implements Comparable<ID> {
|
|||||||
return id.domain.equals(domain) && id.path.equals(path);
|
return id.domain.equals(domain) && id.path.equals(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ID of(String domain, String path) {
|
public static final ID of(String domain, String path) {
|
||||||
return new ID((domain == null || domain.isBlank() ? "base" : domain), path);
|
return new ID((domain == null || domain.isBlank() ? "base" : domain), path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ID of(String location) {
|
public static final ID of(String location) {
|
||||||
Objects.requireNonNull(location);
|
Objects.requireNonNull(location);
|
||||||
int index = location.indexOf(":");
|
int index = location.indexOf(":");
|
||||||
String domain = location.substring(0, index);
|
String domain = location.substring(0, index);
|
||||||
@ -49,10 +49,13 @@ public record ID(String domain, String path) implements Comparable<ID> {
|
|||||||
return new ID(domain, path);
|
return new ID(domain, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final ID of(Path path) {
|
||||||
|
return of(path.getName(0).toString(), path.subpath(1, path.getNameCount()).toString().replace("\\", "/"));
|
||||||
|
}
|
||||||
|
|
||||||
public static final ID tryOf(String location) {
|
public static final ID tryOf(String location) {
|
||||||
try { return of(location); }
|
try { return of(location); }
|
||||||
catch(Exception e) {}
|
catch(Exception e) { return null; }
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isValidDomain(String s) {
|
private static boolean isValidDomain(String s) {
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
public interface IFileWatcher {
|
||||||
|
void addDirectory(Path basePath) throws IOException;
|
||||||
|
void addFile(Path file) throws IOException;
|
||||||
|
|
||||||
|
public static record FileWatcher(IOConsumer<Path> folder, IOConsumer<Path> file) implements IFileWatcher {
|
||||||
|
@Override
|
||||||
|
public void addDirectory(Path basePath) throws IOException { folder().accept(basePath); }
|
||||||
|
@Override
|
||||||
|
public void addFile(Path file) throws IOException { file().accept(file); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static interface IOConsumer<T> {
|
||||||
|
void accept(T t) throws IOException;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
public interface IResourceListener {
|
||||||
|
void name();
|
||||||
|
CompletableFuture<Void> reload(ReloadContext context, ISyncer sync);
|
||||||
|
void release();
|
||||||
|
|
||||||
|
public static interface ISyncer {
|
||||||
|
<T> CompletableFuture<T> sync(T value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
package speiger.src.coreengine.assets.api;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
public record ReloadContext(IAssetProvider provider, Set<ID> filter, Executor main, Executor background) {
|
||||||
|
|
||||||
|
public boolean hasChanged(ID id) {
|
||||||
|
return filter.isEmpty() || filter.contains(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,29 +4,67 @@ import java.io.IOException;
|
|||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.WatchService;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import speiger.src.collections.objects.lists.ObjectArrayList;
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
import speiger.src.collections.objects.utils.ObjectLists;
|
import speiger.src.collections.objects.utils.ObjectLists;
|
||||||
import speiger.src.coreengine.assets.AssetLocation;
|
|
||||||
import speiger.src.coreengine.assets.api.IAsset;
|
import speiger.src.coreengine.assets.api.IAsset;
|
||||||
import speiger.src.coreengine.assets.api.IAssetPackage;
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
import speiger.src.coreengine.assets.api.ID;
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.IFileWatcher;
|
||||||
|
import speiger.src.coreengine.assets.impl.FolderWatcher.ChangeType;
|
||||||
|
|
||||||
public class FolderAssetPackage implements IAssetPackage {
|
public class FolderAssetPackage implements IAssetPackage {
|
||||||
|
Map<ID, AssetIdentity> knownIdentities = new HashMap<>();
|
||||||
Path basePath;
|
Path basePath;
|
||||||
ID id;
|
|
||||||
|
|
||||||
public FolderAssetPackage(Path basePath) {
|
public FolderAssetPackage(Path basePath) {
|
||||||
this.basePath = basePath;
|
this.basePath = basePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void watchService(WatchService listener) throws IOException {
|
public void registerWatchers(IFileWatcher watcher) throws IOException {
|
||||||
|
initCache();
|
||||||
|
watcher.addDirectory(basePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCache() throws IOException {
|
||||||
|
for(Path path : Files.walk(basePath).filter(Files::isRegularFile).toList()) {
|
||||||
|
knownIdentities.put(ID.of(basePath.relativize(path)), new AssetIdentity(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean handleChange(Path path, ChangeType type, Consumer<ID> changedIds) throws IOException {
|
||||||
|
if(Files.notExists(basePath)) return false;
|
||||||
|
Path target = basePath.relativize(path);
|
||||||
|
ID id = ID.of(target);
|
||||||
|
switch(type) {
|
||||||
|
case ADD:
|
||||||
|
if(Files.isRegularFile(path)) {
|
||||||
|
knownIdentities.put(id, new AssetIdentity(path));
|
||||||
|
changedIds.accept(id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MODIFIED:
|
||||||
|
AssetIdentity original = knownIdentities.get(id);
|
||||||
|
if(original != null && !original.matches(path)) {
|
||||||
|
knownIdentities.put(id, new AssetIdentity(path));
|
||||||
|
changedIds.accept(id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case REMOVE: {
|
||||||
|
if(knownIdentities.remove(id) != null) {
|
||||||
|
changedIds.accept(id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -34,7 +72,7 @@ public class FolderAssetPackage implements IAssetPackage {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> domains() {
|
public List<String> domains() {
|
||||||
try(DirectoryStream<Path> dirs = Files.newDirectoryStream(basePath.resolve("assets"))) {
|
try(DirectoryStream<Path> dirs = Files.newDirectoryStream(basePath)) {
|
||||||
List<String> domains = new ObjectArrayList<>();
|
List<String> domains = new ObjectArrayList<>();
|
||||||
dirs.forEach(T -> domains.add(T.getFileName().toString()));
|
dirs.forEach(T -> domains.add(T.getFileName().toString()));
|
||||||
return domains;
|
return domains;
|
||||||
@ -46,19 +84,24 @@ public class FolderAssetPackage implements IAssetPackage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<ID, String> fileHashes() {
|
public IAsset get(ID location) {
|
||||||
return null;
|
Path path = basePath.resolve(location.fileLocation());
|
||||||
|
return Files.notExists(path) ? null : new SimpleAsset(this, location, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<IAsset> get(AssetLocation location) {
|
public void getAll(ID folder, BiConsumer<ID, IAsset> assets) {
|
||||||
Path path = basePath.resolve(location.actualLocation());
|
Path start = basePath.resolve(folder.fileLocation());
|
||||||
return Files.notExists(path) ? Optional.empty() : Optional.of(new SimpleAsset(this, id, path, null));
|
if(Files.notExists(start)) return;
|
||||||
|
try(Stream<Path> stream = Files.walk(start).filter(Files::isRegularFile)) {
|
||||||
|
for(Path path : (Iterable<Path>)() -> stream.iterator()) {
|
||||||
|
ID id = folder.suffix(start.relativize(path));
|
||||||
|
assets.accept(folder, new SimpleAsset(this, id, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void getAll(AssetLocation folder, BiConsumer<ID, IAsset> assets) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,110 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.FileVisitResult;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.SimpleFileVisitor;
|
||||||
|
import java.nio.file.StandardWatchEventKinds;
|
||||||
|
import java.nio.file.WatchEvent;
|
||||||
|
import java.nio.file.WatchEvent.Kind;
|
||||||
|
import java.nio.file.WatchKey;
|
||||||
|
import java.nio.file.WatchService;
|
||||||
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.maps.impl.hash.Object2ObjectOpenHashMap;
|
||||||
|
|
||||||
|
public class FolderWatcher<T> {
|
||||||
|
WatchService watcher;
|
||||||
|
Map<WatchKey, PathFlags<T>> keys = new Object2ObjectOpenHashMap<>();
|
||||||
|
|
||||||
|
public FolderWatcher() throws IOException {
|
||||||
|
watcher = FileSystems.getDefault().newWatchService();
|
||||||
|
}
|
||||||
|
|
||||||
|
public FolderWatcher(WatchService service) {
|
||||||
|
this.watcher = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
keys.keySet().forEach(WatchKey::cancel);
|
||||||
|
keys.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
watcher.close();
|
||||||
|
keys.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(Path basePath, T owner, boolean recursive) throws IOException {
|
||||||
|
if(recursive) {
|
||||||
|
Files.walkFileTree(basePath, new SimpleFileVisitor<Path>() {
|
||||||
|
@Override
|
||||||
|
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
|
||||||
|
keys.put(register(dir), new PathFlags<>(dir, owner, true));
|
||||||
|
return FileVisitResult.CONTINUE;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
keys.put(register(basePath), new PathFlags<>(basePath, owner, recursive));
|
||||||
|
}
|
||||||
|
|
||||||
|
private WatchKey register(Path path) throws IOException {
|
||||||
|
return path.register(watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pollEvents(FolderListener<T> output) {
|
||||||
|
WatchKey key = null;
|
||||||
|
boolean checkInvalidation = false;
|
||||||
|
while((key = watcher.poll()) != null) {
|
||||||
|
PathFlags<T> flags = keys.get(key);
|
||||||
|
if(flags == null) continue;
|
||||||
|
for(WatchEvent<?> event : key.pollEvents()) {
|
||||||
|
if(event.kind() == StandardWatchEventKinds.OVERFLOW) continue;
|
||||||
|
Path child = flags.resolve((Path)event.context());
|
||||||
|
ChangeType type = toType(event.kind());
|
||||||
|
if(type == ChangeType.ADD && flags.recursion() && Files.isDirectory(child, LinkOption.NOFOLLOW_LINKS)) {
|
||||||
|
try { register(child, flags.owner(), true); }
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
else if(type == ChangeType.REMOVE && !checkInvalidation) {
|
||||||
|
checkInvalidation = true;
|
||||||
|
}
|
||||||
|
output.onChanged(child, type, flags.owner());
|
||||||
|
}
|
||||||
|
if(!key.reset()) {
|
||||||
|
keys.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(checkInvalidation) {
|
||||||
|
keys.keySet().removeIf(((Predicate<WatchKey>)WatchKey::isValid).negate());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ChangeType toType(Kind<?> kind) {
|
||||||
|
if(kind == StandardWatchEventKinds.ENTRY_CREATE) return ChangeType.ADD;
|
||||||
|
else if(kind == StandardWatchEventKinds.ENTRY_MODIFY) return ChangeType.MODIFIED;
|
||||||
|
return ChangeType.REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface FolderListener<T> {
|
||||||
|
public void onChanged(Path path, ChangeType type, T owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record PathFlags<T>(Path folder, T owner, boolean recursion) {
|
||||||
|
public Path resolve(Path subFolder) {
|
||||||
|
return folder.resolve(subFolder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static enum ChangeType {
|
||||||
|
ADD,
|
||||||
|
MODIFIED,
|
||||||
|
REMOVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
|
||||||
|
import speiger.src.collections.objects.sets.ObjectOpenHashSet;
|
||||||
|
import speiger.src.collections.objects.utils.ObjectLists;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider.ReloadTracker;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.IFileWatcher.FileWatcher;
|
||||||
|
import speiger.src.coreengine.assets.api.IResourceListener;
|
||||||
|
import speiger.src.coreengine.assets.impl.FolderWatcher.ChangeType;
|
||||||
|
|
||||||
|
public class ReloadManager {
|
||||||
|
private static final ScopedValue<IAssetPackage> CURRENT_PACKAGE = ScopedValue.newInstance();
|
||||||
|
private static final ScopedValue<Set<ID>> TRACKED = ScopedValue.newInstance();
|
||||||
|
Map<Path, Map<Path, IAssetPackage>> trackers;
|
||||||
|
FolderWatcher<IAssetPackage> watcher;
|
||||||
|
Runnable onStart;
|
||||||
|
Consumer<Path> onNewFound;
|
||||||
|
Consumer<IAssetPackage> onRemoved;
|
||||||
|
|
||||||
|
public ReloadManager(Runnable onStart, Consumer<Path> onNewFound, Consumer<IAssetPackage> onRemoved) {
|
||||||
|
this.onStart = onStart;
|
||||||
|
this.onNewFound = onNewFound;
|
||||||
|
this.onRemoved = onRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enableReload() {
|
||||||
|
if(trackers != null) return;
|
||||||
|
try {
|
||||||
|
watcher = new FolderWatcher<>();
|
||||||
|
trackers = Object2ObjectMap.builder().map();
|
||||||
|
if(onStart != null) onStart.run();
|
||||||
|
}
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void disableReload() {
|
||||||
|
if(trackers == null) return;
|
||||||
|
try { watcher.close(); }
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
watcher = null;
|
||||||
|
trackers = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reset() {
|
||||||
|
if(trackers == null) return;
|
||||||
|
trackers.clear();
|
||||||
|
watcher.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPackage(IAssetPackage pack) {
|
||||||
|
try { ScopedValue.where(CURRENT_PACKAGE, pack).call(this::registerWatchers); }
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<IResourceListener> handleFileEvents(ReloadTracker listeners, @Nullable Set<ID> changedIds) {
|
||||||
|
if(watcher == null) return ObjectLists.empty();
|
||||||
|
if(changedIds == null) changedIds = new ObjectOpenHashSet<>();
|
||||||
|
ScopedValue.where(TRACKED, changedIds).run(() -> watcher.pollEvents(this::handleEvent));
|
||||||
|
return changedIds.isEmpty() ? ObjectLists.empty() : listeners.findByFilter(changedIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void handleEvent(Path path, ChangeType type, IAssetPackage owner) {
|
||||||
|
if(owner == null) {
|
||||||
|
owner = findOwner(path);
|
||||||
|
if(owner == null) {
|
||||||
|
if(type == ChangeType.ADD && onNewFound != null) onNewFound.accept(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<ID> changes = new ObjectArrayList<>();
|
||||||
|
if(owner.handleChange(path, type, changes::add)) {
|
||||||
|
TRACKED.get().addAll(changes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
if(onRemoved != null) onRemoved.accept(owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected IAssetPackage findOwner(Path path) {
|
||||||
|
Map<Path, IAssetPackage> packages = trackers.get(path.getParent());
|
||||||
|
return packages == null ? null : packages.get(path.getFileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Void registerWatchers() throws IOException {
|
||||||
|
if(!CURRENT_PACKAGE.isBound()) throw new IllegalStateException("Current Package is required!");
|
||||||
|
CURRENT_PACKAGE.get().registerWatchers(new FileWatcher(this::onFolderChanged, this::onFileChange));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFileChange(Path file) throws IOException {
|
||||||
|
if(!CURRENT_PACKAGE.isBound()) throw new IllegalStateException("Current Package is required!");
|
||||||
|
Map<Path, IAssetPackage> packages = trackers.get(file.getParent());
|
||||||
|
if(packages == null) {
|
||||||
|
packages = Object2ObjectMap.builder().map();
|
||||||
|
trackers.put(file.getParent(), packages);
|
||||||
|
watcher.register(file, null, false);
|
||||||
|
}
|
||||||
|
packages.put(file.getFileName(), CURRENT_PACKAGE.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFolderChanged(Path folder) throws IOException {
|
||||||
|
if(!CURRENT_PACKAGE.isBound()) throw new IllegalStateException("Current Package is required!");
|
||||||
|
watcher.register(folder, CURRENT_PACKAGE.get(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ import speiger.src.coreengine.assets.api.IAssetPackage;
|
|||||||
import speiger.src.coreengine.assets.api.IAssetParser;
|
import speiger.src.coreengine.assets.api.IAssetParser;
|
||||||
import speiger.src.coreengine.assets.api.ID;
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
|
||||||
public record SimpleAsset(IAssetPackage owner, ID location, Path path, String hash) implements IAsset {
|
public record SimpleAsset(IAssetPackage owner, ID location, Path path) implements IAsset {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IAssetPackage owner() {
|
public IAssetPackage owner() {
|
||||||
|
|||||||
@ -0,0 +1,129 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.DirectoryStream;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.maps.impl.hash.Object2LongOpenHashMap;
|
||||||
|
import speiger.src.collections.objects.maps.interfaces.Object2LongMap;
|
||||||
|
import speiger.src.collections.objects.utils.ObjectLists;
|
||||||
|
import speiger.src.coreengine.assets.api.IAsset;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.IFileWatcher;
|
||||||
|
import speiger.src.coreengine.assets.impl.FolderWatcher.ChangeType;
|
||||||
|
|
||||||
|
public class ZipAssetPackage implements IAssetPackage {
|
||||||
|
Object2LongMap<ID> crcCache = new Object2LongOpenHashMap<>();
|
||||||
|
Path baseFile;
|
||||||
|
String startFolder;
|
||||||
|
FileSystem cache;
|
||||||
|
|
||||||
|
public ZipAssetPackage(Path baseFile, String startFolder) {
|
||||||
|
this.baseFile = baseFile;
|
||||||
|
this.startFolder = startFolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
if(cache == null) return;
|
||||||
|
FileSystem cache = this.cache;
|
||||||
|
this.cache = null;
|
||||||
|
cache.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected FileSystem getReference() throws IOException {
|
||||||
|
if(cache == null) cache = FileSystems.newFileSystem(baseFile);
|
||||||
|
return cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWatchers(IFileWatcher watcher) throws IOException {
|
||||||
|
initCache();
|
||||||
|
watcher.addFile(baseFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initCache() {
|
||||||
|
try(AutoCloseable _ = this) {
|
||||||
|
Path root = getReference().getPath(startFolder);
|
||||||
|
for(Path path : Files.walk(root).filter(Files::isRegularFile).toList()) {
|
||||||
|
crcCache.put(ID.of(root.relativize(path)), getCrc(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getCrc(Path path) throws IOException {
|
||||||
|
return (Long)Files.readAttributes(path, "crc").get("crc");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean handleChange(Path file, ChangeType type, Consumer<ID> changed) throws IOException {
|
||||||
|
if(!baseFile.equals(file)) return true;
|
||||||
|
if(Files.notExists(baseFile)) return false;
|
||||||
|
if(type == ChangeType.MODIFIED) {
|
||||||
|
try {
|
||||||
|
Path root = getReference().getPath(startFolder);
|
||||||
|
for(Path path : Files.walk(root).filter(Files::isRegularFile).toList()) {
|
||||||
|
ID id = ID.of(root.relativize(path));
|
||||||
|
long fileCRC = getCrc(path);
|
||||||
|
if(crcCache.getOrDefault(id, -1L) != fileCRC) {
|
||||||
|
crcCache.put(id, fileCRC);
|
||||||
|
changed.accept(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> domains() {
|
||||||
|
try(AutoCloseable _ = this; DirectoryStream<Path> dirs = Files.newDirectoryStream(getReference().getPath(startFolder))) {
|
||||||
|
List<String> domains = new ObjectArrayList<>();
|
||||||
|
dirs.forEach(T -> domains.add(T.getFileName().toString()));
|
||||||
|
return domains;
|
||||||
|
}
|
||||||
|
catch(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return ObjectLists.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IAsset get(ID location) {
|
||||||
|
try {
|
||||||
|
Path path = getReference().getPath(startFolder, location.fileLocation());
|
||||||
|
return Files.notExists(path) ? null : new SimpleAsset(this, location, path);
|
||||||
|
}
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getAll(ID folder, BiConsumer<ID, IAsset> assets) {
|
||||||
|
try {
|
||||||
|
Path root = getReference().getPath(startFolder, folder.fileLocation());
|
||||||
|
if(Files.notExists(root)) return;
|
||||||
|
for(Path path : Files.walk(root).filter(Files::isRegularFile).toList()) {
|
||||||
|
ID id = ID.of(root.relativize(path));
|
||||||
|
assets.accept(id, new SimpleAsset(this, id, path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl.parsers;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.lwjgl.system.MemoryUtil;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetParser;
|
||||||
|
|
||||||
|
public class Buffers {
|
||||||
|
public static final IAssetParser<ByteBuffer> HEAP = new Heap();
|
||||||
|
public static final IAssetParser<ByteBuffer> DIRECT = new Direct();
|
||||||
|
|
||||||
|
public static class Heap implements IAssetParser<ByteBuffer> {
|
||||||
|
@Override
|
||||||
|
public ByteBuffer parse(Path path) throws IOException {
|
||||||
|
return ByteBuffer.wrap(Files.readAllBytes(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Direct implements IAssetParser<ByteBuffer> {
|
||||||
|
@Override
|
||||||
|
public ByteBuffer parse(Path path) throws IOException {
|
||||||
|
byte[] data = Files.readAllBytes(path);
|
||||||
|
ByteBuffer buffer = MemoryUtil.memAlloc(data.length);
|
||||||
|
buffer.put(data);
|
||||||
|
buffer.flip();
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl.parsers;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.channels.SeekableByteChannel;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardOpenOption;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetParser;
|
||||||
|
|
||||||
|
public class Closables {
|
||||||
|
public static final IAssetParser<BufferedReader> READER = new Reader();
|
||||||
|
public static final IAssetParser<SeekableByteChannel> BYTE_CHANNEL = new ByteChannel();
|
||||||
|
|
||||||
|
public static class Reader implements IAssetParser<BufferedReader> {
|
||||||
|
@Override
|
||||||
|
public BufferedReader parse(Path path) throws IOException {
|
||||||
|
return Files.newBufferedReader(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ByteChannel implements IAssetParser<SeekableByteChannel> {
|
||||||
|
@Override
|
||||||
|
public SeekableByteChannel parse(Path path) throws IOException {
|
||||||
|
return Files.newByteChannel(path, StandardOpenOption.READ);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
package speiger.src.coreengine.assets.impl.parsers;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetParser;
|
||||||
|
|
||||||
|
public class Serialized {
|
||||||
|
public static final IAssetParser<List<String>> LINES = new Lines();
|
||||||
|
public static final IAssetParser<JsonObject> JSON_OBJ = new JsonObj();
|
||||||
|
public static final IAssetParser<JsonArray> JSON_ARRAY = new JsonList();
|
||||||
|
|
||||||
|
public static class Lines implements IAssetParser<List<String>> {
|
||||||
|
@Override
|
||||||
|
public List<String> parse(Path path) throws IOException {
|
||||||
|
return Files.readAllLines(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JsonObj implements IAssetParser<JsonObject> {
|
||||||
|
@Override
|
||||||
|
public JsonObject parse(Path path) throws IOException {
|
||||||
|
try(BufferedReader reader = Files.newBufferedReader(path)) {
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class JsonList implements IAssetParser<JsonArray> {
|
||||||
|
@Override
|
||||||
|
public JsonArray parse(Path path) throws IOException {
|
||||||
|
try(BufferedReader reader = Files.newBufferedReader(path)) {
|
||||||
|
return JsonParser.parseReader(reader).getAsJsonArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package speiger.src.coreengine.assets.manager;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.sets.ObjectOpenHashSet;
|
||||||
|
import speiger.src.coreengine.assets.api.IAsset;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider.ReloadTracker;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.IResourceListener;
|
||||||
|
import speiger.src.coreengine.assets.api.MultiAsset;
|
||||||
|
import speiger.src.coreengine.assets.impl.ReloadManager;
|
||||||
|
import speiger.src.coreengine.assets.providers.BaseProvider;
|
||||||
|
import speiger.src.coreengine.assets.providers.LayeredProvider;
|
||||||
|
import speiger.src.coreengine.assets.providers.SingleProvider;
|
||||||
|
|
||||||
|
public class AssetManager {
|
||||||
|
BaseProvider provider;
|
||||||
|
List<IAssetPackage> packages;
|
||||||
|
ReloadTracker tracker = new ReloadTracker();
|
||||||
|
ReloadManager manager;
|
||||||
|
|
||||||
|
public AssetManager(BaseProvider provider) {
|
||||||
|
setPackages(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssetManager single(IAssetPackage pack) {
|
||||||
|
return new AssetManager(new SingleProvider(pack));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AssetManager mult(List<IAssetPackage> packages) {
|
||||||
|
return new AssetManager(new LayeredProvider(packages));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPackages(BaseProvider provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
this.packages = provider.packages();
|
||||||
|
if(manager != null) {
|
||||||
|
manager.reset();
|
||||||
|
packages.forEach(manager::registerPackage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public AssetManager withReload(@Nullable Consumer<Path> onNewFound, @Nullable Consumer<IAssetPackage> onRemoval) {
|
||||||
|
if(manager != null) throw new IllegalStateException("Can't replace the reload system");
|
||||||
|
manager = new ReloadManager(null, onNewFound, onRemoval);
|
||||||
|
manager.enableReload();
|
||||||
|
packages.forEach(manager::registerPackage);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(IResourceListener listener) {
|
||||||
|
tracker.register(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(Predicate<ID> filter, IResourceListener listener) {
|
||||||
|
tracker.register(filter, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadTracker listeners() {
|
||||||
|
return tracker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadTask reload(ReloadBuilder builder) {
|
||||||
|
if(builder.packages.isPresent()) setPackages(builder.packages.get());
|
||||||
|
if(builder.listeners.isEmpty()) builder.withListener(tracker.listeners());
|
||||||
|
return new ReloadTask(get(), builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReloadable() {
|
||||||
|
return manager != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public ReloadTask handleChanges(ReloadBuilder builder) {
|
||||||
|
if(manager == null) return null;
|
||||||
|
Set<ID> changed = new ObjectOpenHashSet<>();
|
||||||
|
List<IResourceListener> reloading = manager.handleFileEvents(tracker, changed);
|
||||||
|
if(reloading.isEmpty()) return null;
|
||||||
|
return reload(builder.withFilter(changed).withPackages((BaseProvider)null).withListener(reloading));
|
||||||
|
}
|
||||||
|
|
||||||
|
public IAssetProvider get() {
|
||||||
|
return new WrappedProvider(provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class WrappedProvider implements IAssetProvider {
|
||||||
|
BaseProvider provider;
|
||||||
|
|
||||||
|
public WrappedProvider(BaseProvider provider) {
|
||||||
|
this.provider = provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
BaseProvider provider = this.provider;
|
||||||
|
this.provider = null;
|
||||||
|
provider.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable IAsset get(ID id) { return provider.get(id); }
|
||||||
|
@Override
|
||||||
|
public @Nullable MultiAsset getAll(ID id) { return provider.getAll(id); }
|
||||||
|
@Override
|
||||||
|
public Map<ID, IAsset> list(String folder, Predicate<ID> filter) { return provider.list(folder, filter); }
|
||||||
|
@Override
|
||||||
|
public Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter) { return provider.listAll(folder, filter); }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package speiger.src.coreengine.assets.manager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.utils.ObjectSets;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.IResourceListener;
|
||||||
|
import speiger.src.coreengine.assets.providers.BaseProvider;
|
||||||
|
import speiger.src.coreengine.assets.providers.LayeredProvider;
|
||||||
|
import speiger.src.coreengine.assets.providers.SingleProvider;
|
||||||
|
|
||||||
|
public class ReloadBuilder {
|
||||||
|
Executor mainRunner = Runnable::run;
|
||||||
|
Executor syncRunner = Runnable::run;
|
||||||
|
Set<ID> filter = ObjectSets.empty();
|
||||||
|
Optional<BaseProvider> packages = Optional.empty();
|
||||||
|
Optional<List<IResourceListener>> listeners = Optional.empty();
|
||||||
|
|
||||||
|
public ReloadBuilder withFilter(Set<ID> filter) {
|
||||||
|
this.filter = Objects.requireNonNull(filter);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withMain(Executor main) {
|
||||||
|
mainRunner = Objects.requireNonNull(main);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withSync(Executor sync) {
|
||||||
|
syncRunner = Objects.requireNonNull(sync);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withPackages(List<IAssetPackage> packages) {
|
||||||
|
return withPackages(new LayeredProvider(packages));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withPackages(IAssetPackage pack) {
|
||||||
|
return withPackages(new SingleProvider(pack));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withPackages(BaseProvider packages) {
|
||||||
|
this.packages = Optional.ofNullable(packages);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReloadBuilder withListener(List<IResourceListener> listeners) {
|
||||||
|
this.listeners = Optional.ofNullable(listeners);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package speiger.src.coreengine.assets.manager;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.sets.ObjectOpenHashSet;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider;
|
||||||
|
import speiger.src.coreengine.assets.api.IResourceListener;
|
||||||
|
import speiger.src.coreengine.assets.api.IResourceListener.ISyncer;
|
||||||
|
import speiger.src.coreengine.assets.api.ReloadContext;
|
||||||
|
|
||||||
|
public class ReloadTask {
|
||||||
|
AtomicInteger started = new AtomicInteger();
|
||||||
|
AtomicInteger finished = new AtomicInteger();
|
||||||
|
CompletableFuture<?> future;
|
||||||
|
|
||||||
|
public ReloadTask(IAssetProvider provider, ReloadBuilder builder) {
|
||||||
|
future = reload(new ReloadContext(provider, builder.filter, new CountingExecutor(builder.mainRunner, started, finished), new CountingExecutor(builder.syncRunner, started, finished)), new ObjectArrayList<>(builder.listeners.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int started() { return started.get(); }
|
||||||
|
public int stopped() { return finished.get(); }
|
||||||
|
public float progress() { return finished.floatValue() / started.floatValue(); }
|
||||||
|
public CompletableFuture<?> future() { return future; }
|
||||||
|
public void join() { future.join(); }
|
||||||
|
|
||||||
|
private CompletableFuture<?> reload(ReloadContext context, List<IResourceListener> listeners) {
|
||||||
|
started.getAndIncrement();
|
||||||
|
Set<IResourceListener> todo = new ObjectOpenHashSet<>(listeners);
|
||||||
|
CompletableFuture<Void> syncFuture = new CompletableFuture<>();
|
||||||
|
CompletableFuture<Void> chainedTask = CompletableFuture.completedFuture(null);
|
||||||
|
List<CompletableFuture<Void>> tasks = new ObjectArrayList<>();
|
||||||
|
for(IResourceListener listener : listeners) {
|
||||||
|
CompletableFuture<Void> current = chainedTask;
|
||||||
|
chainedTask = listener.reload(context, new Syncer(context.main(), current, syncFuture, listener, todo));
|
||||||
|
tasks.add(chainedTask);
|
||||||
|
}
|
||||||
|
return CompletableFuture.allOf(tasks.toArray(CompletableFuture[]::new)).thenRunAsync(() -> {
|
||||||
|
finished.getAndIncrement();
|
||||||
|
try { context.provider().close(); }
|
||||||
|
catch(Exception e) { e.printStackTrace(); }
|
||||||
|
}, context.main());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private record Syncer(Executor main, CompletableFuture<?> currentStep, CompletableFuture<?> syncStep, IResourceListener listener, Set<IResourceListener> toComplete) implements ISyncer {
|
||||||
|
@Override
|
||||||
|
public <T> CompletableFuture<T> sync(T value) {
|
||||||
|
main.execute(() -> {
|
||||||
|
toComplete.remove(listener);
|
||||||
|
if(toComplete.isEmpty()) syncStep.complete(null);
|
||||||
|
});
|
||||||
|
return syncStep.thenCombine(currentStep, (_, _) -> value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private record CountingExecutor(Executor wrapper, AtomicInteger started, AtomicInteger finished) implements Executor {
|
||||||
|
@Override
|
||||||
|
public void execute(Runnable command) {
|
||||||
|
started.getAndIncrement();
|
||||||
|
wrapper.execute(() -> {
|
||||||
|
command.run();
|
||||||
|
finished.getAndIncrement();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
package speiger.src.coreengine.assets.providers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider;
|
||||||
|
|
||||||
|
public interface BaseProvider extends IAssetProvider, AutoCloseable {
|
||||||
|
List<IAssetPackage> packages();
|
||||||
|
}
|
||||||
@ -0,0 +1,137 @@
|
|||||||
|
package speiger.src.coreengine.assets.providers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.lists.ObjectList;
|
||||||
|
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
|
||||||
|
import speiger.src.collections.objects.utils.maps.Object2ObjectMaps;
|
||||||
|
import speiger.src.coreengine.assets.api.IAsset;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetProvider;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.MultiAsset;
|
||||||
|
|
||||||
|
public class LayeredProvider implements BaseProvider {
|
||||||
|
Map<String, DomainProvider> domains = Object2ObjectMap.builder().linkedMap();
|
||||||
|
List<IAssetPackage> packages;
|
||||||
|
|
||||||
|
public LayeredProvider(List<IAssetPackage> packages) {
|
||||||
|
this.packages = packages;
|
||||||
|
for(IAssetPackage pack : packages) {
|
||||||
|
for(String domain : pack.domains()) {
|
||||||
|
domains.computeIfAbsent(domain, DomainProvider::new).add(pack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IAssetPackage> packages() {
|
||||||
|
return packages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
for(IAssetPackage pack : packages) {
|
||||||
|
pack.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable IAsset get(ID id) {
|
||||||
|
DomainProvider provider = domains.get(id.domain());
|
||||||
|
return provider == null ? null : provider.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MultiAsset getAll(ID id) {
|
||||||
|
DomainProvider provider = domains.get(id.domain());
|
||||||
|
return provider == null ? null : provider.getAll(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, IAsset> list(String folder, Predicate<ID> filter) {
|
||||||
|
Map<ID, IAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(DomainProvider provider : domains.values()) {
|
||||||
|
result.putAll(provider.list(folder, filter));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter) {
|
||||||
|
Map<ID, MultiAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(DomainProvider provider : domains.values()) {
|
||||||
|
result.putAll(provider.listAll(folder, filter));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DomainProvider implements IAssetProvider {
|
||||||
|
final String domain;
|
||||||
|
ObjectList<IAssetPackage> packages = new ObjectArrayList<>();
|
||||||
|
|
||||||
|
public DomainProvider(String domain) {
|
||||||
|
this.domain = domain;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(IAssetPackage pack) {
|
||||||
|
packages.add(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable IAsset get(ID id) {
|
||||||
|
for(int i = packages.size()-1;i>=0;i--) {
|
||||||
|
IAsset asset = packages.get(i).get(id);
|
||||||
|
if(asset != null) return asset;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MultiAsset getAll(ID id) {
|
||||||
|
ObjectList<IAsset> result = new ObjectArrayList<>();
|
||||||
|
for(int i = packages.size()-1;i>=0;i--) {
|
||||||
|
IAsset asset = packages.get(i).get(id);
|
||||||
|
if(asset != null) result.add(asset);
|
||||||
|
}
|
||||||
|
return new MultiAsset(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, IAsset> list(String folder, Predicate<ID> filter) {
|
||||||
|
ID base = ID.of(domain, folder);
|
||||||
|
Map<ID, IAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(int i = packages.size()-1;i>=0;i--) {
|
||||||
|
packages.get(i).getAll(base, (K, V) -> {
|
||||||
|
if(filter.test(K)) result.putIfAbsent(K, V);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter) {
|
||||||
|
ID base = ID.of(domain, folder);
|
||||||
|
Object2ObjectMap<ID, ObjectList<IAsset>> assets = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(int i = packages.size()-1;i>=0;i--) {
|
||||||
|
packages.get(i).getAll(base, (K, V) -> {
|
||||||
|
if(filter.test(K)) assets.supplyIfAbsent(K, ObjectArrayList::new).add(V);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Map<ID, MultiAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(Object2ObjectMap.Entry<ID, ObjectList<IAsset>> entry : Object2ObjectMaps.fastIterable(assets)) {
|
||||||
|
result.put(entry.getKey(), new MultiAsset(entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,74 @@
|
|||||||
|
package speiger.src.coreengine.assets.providers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
|
import speiger.src.collections.objects.lists.ObjectArrayList;
|
||||||
|
import speiger.src.collections.objects.lists.ObjectList;
|
||||||
|
import speiger.src.collections.objects.maps.interfaces.Object2ObjectMap;
|
||||||
|
import speiger.src.collections.objects.utils.ObjectLists;
|
||||||
|
import speiger.src.collections.objects.utils.maps.Object2ObjectMaps;
|
||||||
|
import speiger.src.coreengine.assets.api.IAsset;
|
||||||
|
import speiger.src.coreengine.assets.api.IAssetPackage;
|
||||||
|
import speiger.src.coreengine.assets.api.ID;
|
||||||
|
import speiger.src.coreengine.assets.api.MultiAsset;
|
||||||
|
|
||||||
|
public class SingleProvider implements BaseProvider {
|
||||||
|
IAssetPackage pack;
|
||||||
|
List<String> domains;
|
||||||
|
|
||||||
|
public SingleProvider(IAssetPackage pack) {
|
||||||
|
this.pack = pack;
|
||||||
|
this.domains = pack.domains();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
pack.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<IAssetPackage> packages() {
|
||||||
|
return ObjectLists.singleton(pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable IAsset get(ID id) {
|
||||||
|
return pack.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable MultiAsset getAll(ID id) {
|
||||||
|
IAsset asset = pack.get(id);
|
||||||
|
return asset == null ? null : new MultiAsset(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, IAsset> list(String folder, Predicate<ID> filter) {
|
||||||
|
Map<ID, IAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(String domain : domains) {
|
||||||
|
pack.getAll(ID.of(domain, folder), (K, V) -> {
|
||||||
|
if(filter.test(K)) result.putIfAbsent(K, V);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter) {
|
||||||
|
Object2ObjectMap<ID, ObjectList<IAsset>> assets = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(String domain : domains) {
|
||||||
|
pack.getAll(ID.of(domain, folder), (K, V) -> {
|
||||||
|
if(filter.test(K)) assets.supplyIfAbsent(K, ObjectArrayList::new).add(V);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Map<ID, MultiAsset> result = Object2ObjectMap.builder().linkedMap();
|
||||||
|
for(Object2ObjectMap.Entry<ID, ObjectList<IAsset>> entry : Object2ObjectMaps.fastIterable(assets)) {
|
||||||
|
result.put(entry.getKey(), new MultiAsset(entry.getValue()));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,38 +0,0 @@
|
|||||||
package speiger.src.coreengine.assets.simple;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Predicate;
|
|
||||||
|
|
||||||
import speiger.src.coreengine.assets.api.IAsset;
|
|
||||||
import speiger.src.coreengine.assets.api.IAssetProvider;
|
|
||||||
import speiger.src.coreengine.assets.api.ID;
|
|
||||||
import speiger.src.coreengine.assets.api.MultiAsset;
|
|
||||||
|
|
||||||
public class SimpleAssetProvider implements IAssetProvider {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public IAsset get(ID id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public MultiAsset getAll(ID id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String hash(ID id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<ID, IAsset> list(String folder, Predicate<ID> filter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Map<ID, MultiAsset> listAll(String folder, Predicate<ID> filter) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user