[FEATURE][EXPERIMENTAL] New API: Launcher Backend

This commit is contained in:
Gravita 2024-06-15 16:59:47 +07:00
parent 44e840734c
commit 6b873b5072
No known key found for this signature in database
GPG key ID: 543A8F335C9CD633
18 changed files with 1013 additions and 13 deletions

View file

@ -1,6 +1,6 @@
package pro.gravit.launcher.runtime;
import pro.gravit.launcher.runtime.client.UserSettings;
import pro.gravit.launcher.core.backend.UserSettings;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import java.util.HashMap;

View file

@ -0,0 +1,22 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.backend.UserSettings;
import java.util.Map;
import java.util.UUID;
public class BackendSettings extends UserSettings {
@LauncherNetworkAPI
public AuthorizationData auth;
@LauncherNetworkAPI
public Map<UUID, ProfileSettingsImpl> settings;
public static class AuthorizationData {
@LauncherNetworkAPI
public String accessToken;
@LauncherNetworkAPI
public String refreshToken;
@LauncherNetworkAPI
public long expireIn;
}
}

View file

@ -0,0 +1,253 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.base.Downloader;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.profiles.optional.OptionalView;
import pro.gravit.launcher.base.profiles.optional.actions.OptionalAction;
import pro.gravit.launcher.base.profiles.optional.actions.OptionalActionFile;
import pro.gravit.launcher.core.api.LauncherAPIHolder;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import pro.gravit.launcher.core.hasher.FileNameMatcher;
import pro.gravit.launcher.core.hasher.HashedDir;
import pro.gravit.launcher.core.hasher.HashedEntry;
import pro.gravit.launcher.core.hasher.HashedFile;
import pro.gravit.launcher.runtime.client.DirBridge;
import pro.gravit.launcher.runtime.utils.AssetIndexHelper;
import pro.gravit.utils.helper.LogHelper;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
public class ClientDownloadImpl {
private LauncherBackendImpl backend;
ClientDownloadImpl(LauncherBackendImpl backend) {
this.backend = backend;
}
CompletableFuture<LauncherBackendAPI.ReadyProfile> downloadProfile(ClientProfile profile, ProfileSettingsImpl settings, LauncherBackendAPI.DownloadCallback callback) {
AtomicReference<DownloadedDir> clientRef = new AtomicReference<>();
AtomicReference<DownloadedDir> assetRef = new AtomicReference<>();
AtomicReference<DownloadedDir> javaRef = new AtomicReference<>();
return downloadDir(profile.getDir(), profile.getClientUpdateMatcher(), settings.view, callback).thenCompose((clientDir -> {
clientRef.set(clientDir);
return downloadAsset(profile.getAssetDir(), profile.getAssetUpdateMatcher(), profile.getAssetIndex(), callback);
})).thenCompose(assetDir -> {
assetRef.set(assetDir);
return CompletableFuture.completedFuture((DownloadedDir)null); // TODO Custom Java
}).thenCompose(javaDir -> {
javaRef.set(javaDir);
return CompletableFuture.completedFuture(null);
}).thenApply(v -> {
return new ReadyProfileImpl(backend, profile, settings, clientRef.get(), assetRef.get(), javaRef.get());
});
}
CompletableFuture<DownloadedDir> downloadAsset(String dirName, FileNameMatcher matcher, String assetIndexString, LauncherBackendAPI.DownloadCallback callback) {
Path targetDir = DirBridge.dirUpdates.resolve(dirName);
Path assetIndexPath = targetDir.resolve("indexes").resolve(assetIndexString);
return LauncherAPIHolder.profile().fetchUpdateInfo(dirName).thenComposeAsync((response) -> {
callback.onStage(LauncherBackendAPI.DownloadCallback.STAGE_ASSET_VERIFY);
return verifyAssetIndex(assetIndexString, response, assetIndexPath, targetDir);
}, backend.executorService)
.thenApply(assetData -> {
HashedDir dir = assetData.updateInfo.getHashedDir();
AssetIndexHelper.modifyHashedDir(assetData.index, dir);
return new VirtualUpdateInfo(dir, assetData.updateInfo.getUrl());
})
.thenCompose(response -> downloadDir(targetDir, response, matcher, callback, e -> e));
}
private CompletableFuture<AssetData> verifyAssetIndex(String assetIndexString, ProfileFeatureAPI.UpdateInfo response, Path assetIndexPath, Path targetDir) {
var assetIndexRelPath = String.format("indexes/%s.json", assetIndexString);
var assetIndexHash = response.getHashedDir().findRecursive(assetIndexRelPath);
if(!(assetIndexHash.entry instanceof HashedFile assetIndexHashFile)) {
return CompletableFuture.failedFuture(new FileNotFoundException(String.format("Asset Index %s not found in the server response", assetIndexString)));
}
try {
if(Files.exists(assetIndexPath) && assetIndexHashFile.isSame(assetIndexPath, true)) {
var assetIndex = AssetIndexHelper.parse(assetIndexPath);
return CompletableFuture.completedFuture(new AssetData(response, assetIndex));
} else {
var downloader = Downloader.newDownloader(backend.executorService);
var list = new LinkedList<Downloader.SizedFile>();
list.add(new Downloader.SizedFile(assetIndexRelPath, assetIndexRelPath, assetIndexHashFile.size));
return downloader.downloadFiles(list, response.getUrl(), targetDir, null, backend.executorService, 1).thenComposeAsync(v -> {
try {
var assetIndex = AssetIndexHelper.parse(assetIndexPath);
return CompletableFuture.completedFuture(new AssetData(response, assetIndex));
} catch (IOException e) {
return CompletableFuture.failedFuture(e);
}
}, backend.executorService);
}
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
CompletableFuture<DownloadedDir> downloadDir(String dirName, FileNameMatcher matcher, LauncherBackendAPI.DownloadCallback callback) {
Path targetDir = DirBridge.dirUpdates.resolve(dirName);
return LauncherAPIHolder.profile().fetchUpdateInfo(dirName)
.thenCompose(response -> downloadDir(targetDir, response, matcher, callback, e -> e));
}
CompletableFuture<DownloadedDir> downloadDir(String dirName, FileNameMatcher matcher, OptionalView view, LauncherBackendAPI.DownloadCallback callback) {
Path targetDir = DirBridge.dirUpdates.resolve(dirName);
return LauncherAPIHolder.profile().fetchUpdateInfo(dirName)
.thenCompose(response -> {
var hashedDir = response.getHashedDir();
var remap = applyOptionalMods(view, hashedDir);
return downloadDir(targetDir, new VirtualUpdateInfo(hashedDir, response.getUrl()), matcher, callback, makePathRemapperFunction(remap));
});
}
CompletableFuture<DownloadedDir> downloadDir(Path targetDir, ProfileFeatureAPI.UpdateInfo updateInfo, FileNameMatcher matcher, LauncherBackendAPI.DownloadCallback callback, Function<String, String> remap) {
return CompletableFuture.supplyAsync(() -> {
try {
callback.onStage(LauncherBackendAPI.DownloadCallback.STAGE_HASHING);
HashedDir realFiles = new HashedDir(targetDir, matcher, false, true);
callback.onStage(LauncherBackendAPI.DownloadCallback.STAGE_DIFF);
return realFiles.diff(updateInfo.getHashedDir(), matcher);
} catch (IOException e) {
throw new RuntimeException(e);
}
}, backend.executorService).thenComposeAsync((diff) -> {
return downloadFiles(targetDir, updateInfo, callback, diff, remap);
}, backend.executorService).thenApply(v -> new DownloadedDir(updateInfo.getHashedDir(), targetDir));
}
private CompletableFuture<HashedDir.Diff> downloadFiles(Path targetDir, ProfileFeatureAPI.UpdateInfo updateInfo, LauncherBackendAPI.DownloadCallback callback, HashedDir.Diff diff, Function<String, String> remap) {
Downloader downloader = Downloader.newDownloader(backend.executorService);
try {
var files = collectFilesAndCreateDirectories(targetDir, diff.mismatch, remap);
long total = 0;
for(var e : files) {
total += e.size;
}
callback.onTotalDownload(total);
callback.onCanCancel(downloader::cancel);
return downloader.downloadFiles(files, updateInfo.getUrl(), targetDir, new Downloader.DownloadCallback() {
@Override
public void apply(long fullDiff) {
callback.onCurrentDownloaded(fullDiff);
}
@Override
public void onComplete(Path path) {
}
}, backend.executorService, 4).thenComposeAsync(v -> {
callback.onCanCancel(null);
callback.onStage(LauncherBackendAPI.DownloadCallback.STAGE_DELETE_EXTRA);
try {
deleteExtraDir(targetDir, diff.extra, diff.extra.flag);
} catch (IOException ex) {
return CompletableFuture.failedFuture(ex);
}
callback.onStage(LauncherBackendAPI.DownloadCallback.STAGE_DONE_PART);
return CompletableFuture.completedFuture(diff);
}, backend.executorService);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
private List<Downloader.SizedFile> collectFilesAndCreateDirectories(Path dir, HashedDir mismatch, Function<String, String> pathRemapper) throws IOException {
List<Downloader.SizedFile> files = new ArrayList<>();
mismatch.walk(File.separator, (path, name, entry) -> {
if(entry.getType() == HashedEntry.Type.DIR) {
Files.createDirectory(dir.resolve(path));
return HashedDir.WalkAction.CONTINUE;
}
String pathFixed = path.replace(File.separatorChar, '/');
files.add(new Downloader.SizedFile(pathFixed, pathRemapper.apply(pathFixed), entry.size()));
return HashedDir.WalkAction.CONTINUE;
});
return files;
}
private void deleteExtraDir(Path subDir, HashedDir subHDir, boolean deleteDir) throws IOException {
for (Map.Entry<String, HashedEntry> mapEntry : subHDir.map().entrySet()) {
String name = mapEntry.getKey();
Path path = subDir.resolve(name);
// Delete list and dirs based on type
HashedEntry entry = mapEntry.getValue();
HashedEntry.Type entryType = entry.getType();
switch (entryType) {
case FILE -> Files.delete(path);
case DIR -> deleteExtraDir(path, (HashedDir) entry, deleteDir || entry.flag);
default -> throw new AssertionError("Unsupported hashed entry type: " + entryType.name());
}
}
// Delete!
if (deleteDir) {
Files.delete(subDir);
}
}
private Function<String, String> makePathRemapperFunction(LinkedList<PathRemapperData> map) {
return (path) -> {
for(var e : map) {
if(path.startsWith(e.key)) {
return e.value;
}
}
return path;
};
}
private LinkedList<PathRemapperData> applyOptionalMods(OptionalView view, HashedDir hdir) {
for (OptionalAction action : view.getDisabledActions()) {
if (action instanceof OptionalActionFile optionalActionFile) {
optionalActionFile.disableInHashedDir(hdir);
}
}
LinkedList<PathRemapperData> pathRemapper = new LinkedList<>();
Set<OptionalActionFile> fileActions = view.getActionsByClass(OptionalActionFile.class);
for (OptionalActionFile file : fileActions) {
file.injectToHashedDir(hdir);
file.files.forEach((k, v) -> {
if (v == null || v.isEmpty()) return;
pathRemapper.add(new PathRemapperData(v, k)); //reverse (!)
LogHelper.dev("Remap prepare %s to %s", v, k);
});
}
pathRemapper.sort(Comparator.comparingInt(c -> -c.key.length())); // Support deep remap
return pathRemapper;
}
private record PathRemapperData(String key, String value) {
}
record AssetData(ProfileFeatureAPI.UpdateInfo updateInfo, AssetIndexHelper.AssetIndex index) {
}
record DownloadedDir(HashedDir dir, Path path) {
}
record VirtualUpdateInfo(HashedDir dir, String url) implements ProfileFeatureAPI.UpdateInfo {
@Override
public HashedDir getHashedDir() {
return dir;
}
@Override
public String getUrl() {
return url;
}
}
}

View file

@ -0,0 +1,248 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.base.ClientPermissions;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.core.api.LauncherAPIHolder;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.method.AuthMethod;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
import pro.gravit.launcher.core.api.model.SelfUser;
import pro.gravit.launcher.core.api.model.Texture;
import pro.gravit.launcher.core.api.model.UserPermissions;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import pro.gravit.launcher.core.backend.UserSettings;
import pro.gravit.launcher.core.backend.exceptions.LauncherBackendException;
import pro.gravit.launcher.core.backend.extensions.Extension;
import pro.gravit.launcher.runtime.NewLauncherSettings;
import pro.gravit.launcher.runtime.managers.SettingsManager;
import pro.gravit.launcher.runtime.utils.LauncherUpdater;
import pro.gravit.utils.helper.JavaHelper;
import pro.gravit.utils.helper.LogHelper;
import java.io.IOException;
import java.net.URI;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;
public class LauncherBackendImpl implements LauncherBackendAPI {
private final ClientDownloadImpl clientDownloadImpl = new ClientDownloadImpl(this);
private volatile MainCallback callback;
ExecutorService executorService;
private volatile AuthMethod authMethod;
// Settings
private SettingsManager settingsManager;
private NewLauncherSettings allSettings;
private BackendSettings backendSettings;
// Data
private volatile List<ProfileFeatureAPI.ClientProfile> profiles;
private volatile UserPermissions permissions;
private volatile SelfUser selfUser;
private volatile List<Java> availableJavas;
private volatile CompletableFuture<List<Java>> availableJavasFuture;
@Override
public void setCallback(MainCallback callback) {
this.callback = callback;
}
private void doInit() throws Exception {
executorService = Executors.newScheduledThreadPool(2, (r) -> {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
});
registerUserSettings("backend", BackendSettings.class);
settingsManager = new SettingsManager();
settingsManager.generateConfigIfNotExists();
settingsManager.loadConfig();
allSettings = settingsManager.getConfig();
backendSettings = (BackendSettings) getUserSettings("backend", (k) -> new BackendSettings());
permissions = new ClientPermissions();
}
@Override
public CompletableFuture<LauncherInitData> init() {
try {
doInit();
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
return LauncherAPIHolder.core().checkUpdates().thenCombineAsync(LauncherAPIHolder.core().getAuthMethods(), (updatesInfo, authMethods) -> {
if(updatesInfo.required()) {
try {
LauncherUpdater.prepareUpdate(URI.create(updatesInfo.url()).toURL());
} catch (Exception e) {
throw new RuntimeException(e);
}
callback.onShutdown();
LauncherUpdater.restart();
}
return new LauncherInitData(authMethods);
}, executorService);
}
@Override
public void selectAuthMethod(AuthMethod method) {
this.authMethod = method;
LauncherAPIHolder.changeAuthId(method.getName());
}
@Override
public CompletableFuture<SelfUser> tryAuthorize() {
if(this.authMethod == null) {
return CompletableFuture.failedFuture(new LauncherBackendException("This method call not allowed before select authMethod"));
}
if(backendSettings.auth == null) {
return CompletableFuture.failedFuture(new LauncherBackendException("Auth data not found"));
}
if(backendSettings.auth.expireIn > 0 && LocalDateTime.ofInstant(Instant.ofEpochMilli(backendSettings.auth.expireIn), ZoneOffset.UTC).isBefore(LocalDateTime.now(ZoneOffset.UTC))) {
return LauncherAPIHolder.auth().refreshToken(backendSettings.auth.refreshToken).thenCompose((response) -> {
setAuthToken(response);
return LauncherAPIHolder.auth().restore(backendSettings.auth.accessToken, true);
}).thenApply((user) -> {
onAuthorize(user);
return user;
});
}
return LauncherAPIHolder.auth().restore(backendSettings.auth.accessToken, true).thenApply((user) -> {
onAuthorize(user);
return user;
});
}
private void setAuthToken(AuthFeatureAPI.AuthToken authToken) {
backendSettings.auth = new BackendSettings.AuthorizationData();
backendSettings.auth.accessToken = authToken.getAccessToken();
backendSettings.auth.refreshToken = authToken.getRefreshToken();
if(authToken.getExpire() <= 0) {
backendSettings.auth.expireIn = 0;
}
backendSettings.auth.expireIn = LocalDateTime.now(ZoneOffset.UTC)
.plus(authToken.getExpire(), ChronoUnit.MILLIS).
toEpochSecond(ZoneOffset.UTC);
}
private void onAuthorize(SelfUser selfUser) {
permissions = selfUser.getPermissions();
callback.onAuthorize(selfUser);
}
@Override
public CompletableFuture<SelfUser> authorize(String login, AuthMethodPassword password) {
if(this.authMethod == null) {
return CompletableFuture.failedFuture(new LauncherBackendException("This method call not allowed before select authMethod"));
}
return LauncherAPIHolder.auth().auth(login, password).thenApply((response) -> {
setAuthToken(response.authToken());
onAuthorize(response.user());
return response.user();
});
}
@Override
public CompletableFuture<List<ProfileFeatureAPI.ClientProfile>> fetchProfiles() {
return LauncherAPIHolder.profile().getProfiles().thenApply((profiles) -> {
this.profiles = profiles;
callback.onProfiles(profiles);
return profiles;
});
}
@Override
public ClientProfileSettings makeClientProfileSettings(ProfileFeatureAPI.ClientProfile profile) {
var settings = backendSettings.settings.get(profile.getUUID());
if(settings == null) {
settings = new ProfileSettingsImpl((ClientProfile) profile);
} else {
settings = settings.copy();
settings.initAfterGson((ClientProfile) profile, this);
}
return settings;
}
@Override
public void saveClientProfileSettings(ClientProfileSettings settings) {
var impl = (ProfileSettingsImpl) settings;
impl.updateEnabledMods();
backendSettings.settings.put(impl.profile.getUUID(), impl);
}
@Override
public CompletableFuture<ReadyProfile> downloadProfile(ProfileFeatureAPI.ClientProfile profile, ClientProfileSettings settings, DownloadCallback callback) {
return clientDownloadImpl.downloadProfile((ClientProfile) profile, (ProfileSettingsImpl) settings, callback);
}
@Override
public CompletableFuture<byte[]> fetchTexture(Texture texture) {
return null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public CompletableFuture<List<Java>> getAvailableJava() {
if(availableJavas == null) {
if(availableJavasFuture == null) {
availableJavasFuture = CompletableFuture.supplyAsync(() -> {
return (List) JavaHelper.findJava(); // TODO: Custom Java
}, executorService).thenApply(e -> {
availableJavas = e;
return e;
});
}
}
return CompletableFuture.completedFuture(availableJavas);
}
@Override
public void registerUserSettings(String name, Class<? extends UserSettings> clazz) {
UserSettings.providers.register(name, clazz);
}
@Override
public UserSettings getUserSettings(String name, Function<String, UserSettings> ifNotExist) {
return allSettings.userSettings.computeIfAbsent(name, ifNotExist);
}
@Override
public UserPermissions getPermissions() {
return permissions;
}
@Override
public boolean hasPermission(String permission) {
return permissions.hasPerm(permission);
}
@Override
public String getUsername() {
return selfUser == null ? "Player" : getUsername();
}
@Override
public SelfUser getSelfUser() {
return selfUser;
}
@Override
public <T extends Extension> T getExtension(Class<T> clazz) {
return null;
}
@Override
public void shutdown() {
executorService.shutdownNow();
try {
settingsManager.saveConfig();
} catch (IOException e) {
LogHelper.error("Config not saved", e);
}
}
}

View file

@ -0,0 +1,173 @@
package pro.gravit.launcher.runtime.backend;
import oshi.SystemInfo;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.profiles.optional.OptionalFile;
import pro.gravit.launcher.base.profiles.optional.OptionalView;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import pro.gravit.launcher.runtime.utils.SystemMemory;
import pro.gravit.utils.helper.JVMHelper;
import pro.gravit.utils.helper.JavaHelper;
import java.util.*;
public class ProfileSettingsImpl implements LauncherBackendAPI.ClientProfileSettings {
transient ClientProfile profile;
transient LauncherBackendImpl backend;
@LauncherNetworkAPI
private Map<MemoryClass, Long> ram;
@LauncherNetworkAPI
private Set<Flag> flags;
@LauncherNetworkAPI
private Set<String> enabled;
@LauncherNetworkAPI
private String saveJavaPath;
transient OptionalView view;
transient JavaHelper.JavaVersion selectedJava;
public ProfileSettingsImpl() {
}
public ProfileSettingsImpl(ClientProfile profile) {
this.profile = profile;
var def = profile.getSettings();
this.ram = new HashMap<>();
this.ram.put(MemoryClass.TOTAL, ((long)def.ram) << 20);
this.flags = new HashSet<>();
if(def.autoEnter) {
this.flags.add(Flag.AUTO_ENTER);
}
if(def.fullScreen) {
this.flags.add(Flag.FULLSCREEN);
}
this.view = new OptionalView(profile);
}
@Override
public long getReservedMemoryBytes(MemoryClass memoryClass) {
return ram.getOrDefault(memoryClass, 0L);
}
@Override
public long getMaxMemoryBytes(MemoryClass memoryClass) {
try {
return SystemMemory.getPhysicalMemorySize();
} catch (Throwable e) {
SystemInfo systemInfo = new SystemInfo();
return systemInfo.getHardware().getMemory().getTotal();
}
}
@Override
public void setReservedMemoryBytes(MemoryClass memoryClass, long value) {
this.ram.put(memoryClass, value);
}
@Override
public Set<Flag> getFlags() {
return Collections.unmodifiableSet(flags);
}
@Override
public Set<Flag> getAvailableFlags() {
Set<Flag> set = new HashSet<>();
set.add(Flag.AUTO_ENTER);
set.add(Flag.FULLSCREEN);
if(JVMHelper.OS_TYPE == JVMHelper.OS.LINUX) {
set.add(Flag.LINUX_WAYLAND_SUPPORT);
}
if(backend.hasPermission("launcher.debug.skipfilemonitor")) {
set.add(Flag.DEBUG_SKIP_FILE_MONITOR);
}
return set;
}
@Override
public boolean hasFlag(Flag flag) {
return flags.contains(flag);
}
@Override
public void addFlag(Flag flag) {
flags.add(flag);
}
@Override
public void removeFlag(Flag flag) {
flags.remove(flag);
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Set<ProfileFeatureAPI.OptionalMod> getEnabledOptionals() {
return (Set) view.enabled;
}
@Override
public void enableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback) {
view.enable((OptionalFile) mod, true, callback::onChanged);
}
@Override
public void disableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback) {
view.disable((OptionalFile) mod, callback::onChanged);
}
@Override
public JavaHelper.JavaVersion getSelectedJava() {
return selectedJava;
}
@Override
public void setSelectedJava(LauncherBackendAPI.Java java) {
selectedJava = (JavaHelper.JavaVersion) java;
}
@Override
public boolean isRecommended(LauncherBackendAPI.Java java) {
return java.getMajorVersion() == profile.getRecommendJavaVersion();
}
@Override
public boolean isCompatible(LauncherBackendAPI.Java java) {
return java.getMajorVersion() >= profile.getMinJavaVersion() && java.getMajorVersion() <= profile.getMaxJavaVersion();
}
@Override
public ProfileSettingsImpl copy() {
ProfileSettingsImpl cloned = new ProfileSettingsImpl();
cloned.profile = profile;
cloned.ram = new HashMap<>(ram);
cloned.flags = new HashSet<>(flags);
cloned.enabled = new HashSet<>(enabled);
if(view != null) {
cloned.view = new OptionalView(profile, view);
}
cloned.selectedJava = selectedJava;
cloned.saveJavaPath = saveJavaPath;
return cloned;
}
public void updateEnabledMods() {
enabled = new HashSet<>();
for(var e : view.enabled) {
enabled.add(e.name);
}
saveJavaPath = selectedJava.getPath().toAbsolutePath().toString();
}
public void initAfterGson(ClientProfile profile, LauncherBackendImpl backend) {
this.backend = backend;
this.profile = profile;
this.view = new OptionalView(profile);
for(var e : enabled) {
var opt = profile.getOptionalFile(e);
if(opt == null) {
continue;
}
enableOptional(opt, (var1, var2) -> {});
}
}
}

View file

@ -0,0 +1,128 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.profiles.ClientProfileBuilder;
import pro.gravit.launcher.base.profiles.PlayerProfile;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import pro.gravit.launcher.runtime.client.ClientLauncherProcess;
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.JVMHelper;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.util.ArrayList;
public class ReadyProfileImpl implements LauncherBackendAPI.ReadyProfile {
private LauncherBackendImpl backend;
private ClientProfile profile;
private ProfileSettingsImpl settings;
private ClientDownloadImpl.DownloadedDir clientDir;
private ClientDownloadImpl.DownloadedDir assetDir;
private ClientDownloadImpl.DownloadedDir javaDir;
private volatile Thread writeParamsThread;
private volatile Thread runThread;
private volatile ClientLauncherProcess process;
private volatile Process nativeProcess;
private volatile LauncherBackendAPI.RunCallback callback;
public ReadyProfileImpl(LauncherBackendImpl backend, ClientProfile profile, ProfileSettingsImpl settings, ClientDownloadImpl.DownloadedDir clientDir, ClientDownloadImpl.DownloadedDir assetDir, ClientDownloadImpl.DownloadedDir javaDir) {
this.backend = backend;
this.profile = profile;
this.settings = settings;
this.clientDir = clientDir;
this.assetDir = assetDir;
this.javaDir = javaDir;
}
@Override
public ProfileFeatureAPI.ClientProfile getClientProfile() {
return profile;
}
@Override
public LauncherBackendAPI.ClientProfileSettings getSettings() {
return settings;
}
@Override
public void run(LauncherBackendAPI.RunCallback callback) {
if(isAlive()) {
terminate();
}
this.callback = callback;
if(backend.hasPermission("launcher.debug.skipfilemonitor") && settings.hasFlag(LauncherBackendAPI.ClientProfileSettings.Flag.DEBUG_SKIP_FILE_MONITOR)) {
var builder = new ClientProfileBuilder(profile);
builder.setUpdate(new ArrayList<>());
builder.setUpdateVerify(new ArrayList<>());
builder.setUpdateExclusions(new ArrayList<>());
profile = builder.createClientProfile();
}
process = new ClientLauncherProcess(clientDir.path(), assetDir.path(), settings.getSelectedJava(), clientDir.path().resolve("resourcepacks"),
profile, new PlayerProfile(backend.getSelfUser()), settings.view, backend.getSelfUser().getAccessToken(),
clientDir.dir(), assetDir.dir(), javaDir == null ? null : javaDir.dir());
process.params.ram = (int) (settings.getReservedMemoryBytes(LauncherBackendAPI.ClientProfileSettings.MemoryClass.TOTAL) >> 20);
if (process.params.ram > 0) {
process.jvmArgs.add("-Xms" + process.params.ram + 'M');
process.jvmArgs.add("-Xmx" + process.params.ram + 'M');
}
process.params.fullScreen = settings.hasFlag(LauncherBackendAPI.ClientProfileSettings.Flag.FULLSCREEN);
process.params.autoEnter = settings.hasFlag(LauncherBackendAPI.ClientProfileSettings.Flag.AUTO_ENTER);
if(JVMHelper.OS_TYPE == JVMHelper.OS.LINUX) {
process.params.lwjglGlfwWayland = settings.hasFlag(LauncherBackendAPI.ClientProfileSettings.Flag.LINUX_WAYLAND_SUPPORT);
}
writeParamsThread = new Thread(this::writeParams);
writeParamsThread.setDaemon(true);
writeParamsThread.start();
runThread = new Thread(this::readThread);
runThread.setDaemon(true);
runThread.start();
}
private void readThread() {
try {
process.start(true);
nativeProcess = process.getProcess();
callback.onCanTerminate(this::terminate);
InputStream stream = nativeProcess.getInputStream();
byte[] buf = IOHelper.newBuffer();
try {
for (int length = stream.read(buf); length >= 0; length = stream.read(buf)) {
callback.onNormalOutput(buf, 0, length);
}
} catch (EOFException ignored) {
}
if (nativeProcess.isAlive()) {
int code = nativeProcess.waitFor();
callback.onFinished(code);
}
} catch (Exception e) {
if(e instanceof InterruptedException) {
return;
}
terminate();
}
}
public void terminate() {
if(nativeProcess == null) {
return;
}
nativeProcess.destroyForcibly();
}
public boolean isAlive() {
return nativeProcess != null && nativeProcess.isAlive();
}
private void writeParams() {
try {
process.runWriteParams(new InetSocketAddress("127.0.0.1", Launcher.getConfig().clientPort));
} catch (Throwable e) {
terminate();
}
}
}

View file

@ -1,6 +1,7 @@
package pro.gravit.launcher.runtime.client;
import com.google.gson.GsonBuilder;
import pro.gravit.launcher.core.backend.UserSettings;
import pro.gravit.launcher.start.RuntimeModuleManager;
import pro.gravit.launcher.core.managers.GsonManager;
import pro.gravit.launcher.base.modules.events.PreGsonPhase;

View file

@ -0,0 +1,65 @@
package pro.gravit.launcher.runtime.utils;
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.hasher.HashedDir;
import pro.gravit.launcher.core.hasher.HashedEntry;
import pro.gravit.utils.helper.IOHelper;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Path;
import java.util.*;
public class AssetIndexHelper {
public static AssetIndex parse(Path path) throws IOException {
try (Reader reader = IOHelper.newReader(path)) {
return Launcher.gsonManager.gson.fromJson(reader, AssetIndex.class);
}
}
public static void modifyHashedDir(AssetIndex index, HashedDir original) {
Set<String> hashes = new HashSet<>();
for (AssetIndexObject obj : index.objects.values()) {
hashes.add(obj.hash);
}
HashedDir objects = (HashedDir) original.getEntry("objects");
List<String> toDeleteDirs = new ArrayList<>(16);
for (Map.Entry<String, HashedEntry> entry : objects.map().entrySet()) {
if (entry.getValue().getType() != HashedEntry.Type.DIR) {
continue;
}
HashedDir dir = (HashedDir) entry.getValue();
List<String> toDelete = new ArrayList<>(16);
for (String hash : dir.map().keySet()) {
if (!hashes.contains(hash)) {
toDelete.add(hash);
}
}
for (String s : toDelete) {
dir.remove(s);
}
if (dir.map().isEmpty()) {
toDeleteDirs.add(entry.getKey());
}
}
for (String s : toDeleteDirs) {
objects.remove(s);
}
}
public static class AssetIndex {
@LauncherNetworkAPI
public boolean virtual;
@LauncherNetworkAPI
public Map<String, AssetIndexObject> objects;
}
public static class AssetIndexObject {
@LauncherNetworkAPI
public String hash;
@LauncherNetworkAPI
public long size;
}
}

View file

@ -0,0 +1,10 @@
package pro.gravit.launcher.runtime.utils;
import com.sun.management.OperatingSystemMXBean;
public class SystemMemory {
private static final OperatingSystemMXBean systemMXBean = (OperatingSystemMXBean) java.lang.management.ManagementFactory.getOperatingSystemMXBean();
public static long getPhysicalMemorySize() {
return systemMXBean.getTotalMemorySize();
}
}

View file

@ -235,7 +235,7 @@ protected DownloadTask sendAsync(SizedFile file, URI baseUri, Path targetDir, Do
return task.get();
}
protected HttpRequest makeHttpRequest(URI baseUri, String filePath) throws URISyntaxException {
public static URI makeURI(URI baseUri, String filePath) throws URISyntaxException {
URI uri;
if(baseUri != null) {
String scheme = baseUri.getScheme();
@ -248,6 +248,11 @@ protected HttpRequest makeHttpRequest(URI baseUri, String filePath) throws URISy
} else {
uri = new URI(filePath);
}
return uri;
}
protected HttpRequest makeHttpRequest(URI baseUri, String filePath) throws URISyntaxException {
var uri = makeURI(baseUri, filePath);
return HttpRequest.newBuilder()
.GET()
.uri(uri)

View file

@ -42,6 +42,14 @@ public PlayerProfile(UUID uuid, String username, Map<String, Texture> assets, Ma
this.properties = properties;
}
@SuppressWarnings({"unchecked", "rawtypes"})
public PlayerProfile(User user) {
this.uuid = user.getUUID();
this.username = user.getUsername();
this.assets = new HashMap<>((Map) user.getAssets());
this.properties = user.getProperties();
}
public static PlayerProfile newOfflineProfile(String username) {
return new PlayerProfile(offlineUUID(username), username, new HashMap<>(), new HashMap<>());
}

View file

@ -165,5 +165,15 @@ public CompletableFuture<UpdateInfo> fetchUpdateInfo(String dirName) {
return request.request(new UpdateRequest(dirName)).thenApply(response -> new UpdateInfoData(response.hdir, response.url));
}
public record UpdateInfoData(HashedDir hdir, String url) implements ProfileFeatureAPI.UpdateInfo {}
public record UpdateInfoData(HashedDir hdir, String url) implements ProfileFeatureAPI.UpdateInfo {
@Override
public HashedDir getHashedDir() {
return hdir;
}
@Override
public String getUrl() {
return url;
}
}
}

View file

@ -11,7 +11,10 @@ public interface ProfileFeatureAPI extends FeatureAPI {
CompletableFuture<List<ClientProfile>> getProfiles();
CompletableFuture<UpdateInfo> fetchUpdateInfo(String dirName);
interface UpdateInfo {}
interface UpdateInfo {
HashedDir getHashedDir();
String getUrl();
}
interface ClientProfile {
String getName();

View file

@ -1,5 +1,6 @@
package pro.gravit.launcher.core.backend;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.method.AuthMethod;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
@ -8,8 +9,11 @@
import pro.gravit.launcher.core.api.model.UserPermissions;
import pro.gravit.launcher.core.backend.extensions.Extension;
import java.nio.file.Path;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
public interface LauncherBackendAPI {
void setCallback(MainCallback callback);
@ -19,15 +23,22 @@ public interface LauncherBackendAPI {
CompletableFuture<SelfUser> authorize(String login, AuthMethodPassword password);
CompletableFuture<List<ProfileFeatureAPI.ClientProfile>> fetchProfiles();
ClientProfileSettings makeClientProfileSettings(ProfileFeatureAPI.ClientProfile profile);
void saveClientProfileSettings(ClientProfileSettings settings);
CompletableFuture<ReadyProfile> downloadProfile(ProfileFeatureAPI.ClientProfile profile, ClientProfileSettings settings, DownloadCallback callback);
// Tools
CompletableFuture<byte[]> fetchTexture(Texture texture);
CompletableFuture<List<Java>> getAvailableJava();
// Settings
void registerUserSettings(String name, Class<? extends UserSettings> clazz);
UserSettings getUserSettings(String name, Function<String, UserSettings> ifNotExist);
// Status
UserPermissions getPermissions();
boolean hasPermission(String permission);
String getUsername();
SelfUser getSelfUser();
// Extensions
<T extends Extension> T getExtension(Class<T> clazz);
void shutdown();
record LauncherInitData(List<AuthMethod> methods) {}
@ -38,20 +49,36 @@ interface ReadyProfile {
}
interface ClientProfileSettings {
long getReservedMemoryBytes();
long getMaxMemoryBytes();
void setReservedMemoryBytes(long value);
List<Flag> getFlags();
long getReservedMemoryBytes(MemoryClass memoryClass);
long getMaxMemoryBytes(MemoryClass memoryClass);
void setReservedMemoryBytes(MemoryClass memoryClass, long value);
Set<Flag> getFlags();
Set<Flag> getAvailableFlags();
boolean hasFlag(Flag flag);
void addFlag(Flag flag);
void removeFlag(Flag flag);
List<ProfileFeatureAPI.OptionalMod> getEnabledOptionals();
Set<ProfileFeatureAPI.OptionalMod> getEnabledOptionals();
void enableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback);
void disableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback);
ClientProfileSettings clone();
Java getSelectedJava();
void setSelectedJava(Java java);
boolean isRecommended(Java java);
boolean isCompatible(Java java);
ClientProfileSettings copy();
enum Flag {
@LauncherNetworkAPI
AUTO_ENTER,
@LauncherNetworkAPI
FULLSCREEN,
@LauncherNetworkAPI
LINUX_WAYLAND_SUPPORT,
@LauncherNetworkAPI
DEBUG_SKIP_FILE_MONITOR
}
enum MemoryClass {
TOTAL
}
interface ChangedOptionalStatusCallback {
@ -78,6 +105,10 @@ public void onAuthorize(SelfUser selfUser) {
public void onNotify(String header, String description) {
}
public void onShutdown() {
}
}
class RunCallback {
@ -85,6 +116,10 @@ public void onStarted() {
}
public void onCanTerminate(Runnable terminate) {
}
public void onFinished(int code) {
}
@ -99,6 +134,14 @@ public void onErrorOutput(byte[] buf, int offset, int size) {
}
class DownloadCallback {
public static final String STAGE_ASSET_VERIFY = "assetVerify";
public static final String STAGE_HASHING = "hashing";
public static final String STAGE_DIFF = "diff";
public static final String STAGE_DOWNLOAD = "download";
public static final String STAGE_DELETE_EXTRA = "deleteExtra";
public static final String STAGE_DONE_PART = "done.part";
public static final String STAGE_DONE = "done";
public void onStage(String stage) {
}
@ -115,4 +158,9 @@ public void onCurrentDownloaded(long current) {
}
}
interface Java {
int getMajorVersion();
Path getPath();
}
}

View file

@ -1,4 +1,4 @@
package pro.gravit.launcher.runtime.client;
package pro.gravit.launcher.core.backend;
import pro.gravit.utils.ProviderMap;

View file

@ -0,0 +1,14 @@
package pro.gravit.launcher.core.backend.exceptions;
public class LauncherBackendException extends RuntimeException {
public LauncherBackendException() {
}
public LauncherBackendException(String message) {
super(message);
}
public LauncherBackendException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -1,5 +1,7 @@
package pro.gravit.utils.helper;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
@ -186,7 +188,7 @@ public JavaVersionAndBuild() {
}
}
public static class JavaVersion {
public static class JavaVersion implements LauncherBackendAPI.Java {
public final Path jvmDir;
public final int version;
public final int build;
@ -301,5 +303,15 @@ public static boolean isExistExtJavaLibrary(Path jvmDir, String name) {
Path jdkPathLin = jvmDir.resolve("jre").resolve("lib").resolve(name.concat(".jar"));
return IOHelper.isFile(jrePath) || IOHelper.isFile(jdkPath) || IOHelper.isFile(jdkPathLin) || IOHelper.isFile(jrePathLin);
}
@Override
public int getMajorVersion() {
return version;
}
@Override
public Path getPath() {
return jvmDir;
}
}
}

View file

@ -5,7 +5,7 @@
id 'org.openjfx.javafxplugin' version '0.1.0' apply false
}
group = 'pro.gravit.launcher'
version = '5.6.2'
version = '5.7.0-SNAPSHOT'
apply from: 'props.gradle'