Finished the reworking of the Asset Manager

This commit is contained in:
Speiger 2026-07-09 13:54:58 +02:00
parent 09874d1b8a
commit 641f19dd67
24 changed files with 1147 additions and 79 deletions

View File

@ -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

View File

@ -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'

View File

@ -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;
} }

View File

@ -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());
}
}
} }

View File

@ -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);
}
}
} }
} }

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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()) {
@Override ID id = folder.suffix(start.relativize(path));
public void getAll(AssetLocation folder, BiConsumer<ID, IAsset> assets) { assets.accept(folder, new SimpleAsset(this, id, path));
}
}
catch(Exception e) {
e.printStackTrace();
}
} }
} }

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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() {

View File

@ -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(); }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}
}

View File

@ -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); }
}
}

View File

@ -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;
}
}

View File

@ -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();
});
}
}
}

View File

@ -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();
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}