[FEATURE][EXPERIMENTAL] Merge experimental/5.7.x

This commit is contained in:
Gravita 2025-06-11 21:34:33 +07:00
commit 6110a29f5c
73 changed files with 2161 additions and 68 deletions

View file

@ -152,12 +152,8 @@ public static ClientProfile makeProfile(ClientProfile.Version version, String ti
}
}
}
builder.setMinJavaVersion(17);
builder.setRecommendJavaVersion(17);
if(version.compareTo(ClientProfileVersions.MINECRAFT_1_20_3) >= 0) {
builder.setMinJavaVersion(21);
builder.setRecommendJavaVersion(21);
}
jvmArgs.add("-Dfml.ignorePatchDiscrepancies=true");
jvmArgs.add("-Dfml.ignoreInvalidMinecraftCertificates=true");
builder.setJvmArgs(jvmArgs);

View file

@ -1,4 +1,4 @@
{
"features": [],
"features": ["new-api"],
"info": []
}

View file

@ -1,4 +1,4 @@
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.gradleup.shadow'
String mainClassName = "pro.gravit.launcher.start.ClientLauncherWrapper"
@ -7,8 +7,8 @@
url "https://repo.spring.io/plugins-release/"
}
}
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
configurations {
bundle

View file

@ -2,7 +2,15 @@
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.base.LauncherConfig;
import pro.gravit.launcher.base.request.*;
import pro.gravit.launcher.client.*;
import pro.gravit.launcher.core.api.LauncherAPI;
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.features.UserFeatureAPI;
import pro.gravit.launcher.core.backend.LauncherBackendAPIHolder;
import pro.gravit.launcher.runtime.backend.LauncherBackendImpl;
import pro.gravit.launcher.runtime.client.*;
import pro.gravit.launcher.runtime.client.events.ClientEngineInitPhase;
import pro.gravit.launcher.client.events.ClientExitPhase;
@ -19,9 +27,6 @@
import pro.gravit.launcher.base.modules.events.PreConfigPhase;
import pro.gravit.launcher.base.profiles.optional.actions.OptionalAction;
import pro.gravit.launcher.base.profiles.optional.triggers.OptionalTrigger;
import pro.gravit.launcher.base.request.Request;
import pro.gravit.launcher.base.request.RequestException;
import pro.gravit.launcher.base.request.RequestService;
import pro.gravit.launcher.base.request.auth.*;
import pro.gravit.launcher.base.request.websockets.OfflineRequestService;
import pro.gravit.launcher.base.request.websockets.StdWebSocketService;
@ -38,6 +43,7 @@
import java.security.interfaces.ECPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
@ -243,6 +249,17 @@ public void start(String... args) throws Throwable {
}
Request.startAutoRefresh();
Request.getRequestService().registerEventHandler(new BasicLauncherEventHandler());
// Init New API
LauncherAPIHolder.setCoreAPI(new RequestCoreFeatureAPIImpl(Request.getRequestService()));
LauncherAPIHolder.setCreateApiFactory((authId) -> {
var impl = new RequestFeatureAPIImpl(Request.getRequestService(), authId);
return new LauncherAPI(Map.of(
AuthFeatureAPI.class, impl,
UserFeatureAPI.class, impl,
ProfileFeatureAPI.class, impl));
});
LauncherBackendAPIHolder.setApi(new LauncherBackendImpl());
//
Objects.requireNonNull(args, "args");
if (started.getAndSet(true))
throw new IllegalStateException("Launcher has been already started");

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,43 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.backend.UserSettings;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class BackendSettings extends UserSettings {
@LauncherNetworkAPI
public AuthorizationData auth;
@LauncherNetworkAPI
public Map<UUID, ProfileSettingsImpl> settings = new HashMap<>();
public static class AuthorizationData {
@LauncherNetworkAPI
public String accessToken;
@LauncherNetworkAPI
public String refreshToken;
@LauncherNetworkAPI
public long expireIn;
public AuthFeatureAPI.AuthToken toToken() {
return new AuthFeatureAPI.AuthToken() {
@Override
public String getAccessToken() {
return accessToken;
}
@Override
public String getRefreshToken() {
return refreshToken;
}
@Override
public long getExpire() {
return expireIn;
}
};
}
}
}

View file

@ -0,0 +1,263 @@
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 LauncherAPIHolder.profile().changeCurrentProfile(profile)
.thenCompose(vv -> 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+".json");
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 updateInfo.getHashedDir().diff(realFiles, 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) {
var dirPath = dir.resolve(path);
try {
if(!Files.exists(dirPath)) {
Files.createDirectory(dirPath);
} else if (!Files.isDirectory(dirPath)) {
throw new IOException(String.format("%s is not a directory", path));
}
} catch (IOException e) {
throw new RuntimeException(e);
}
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,304 @@
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.CoreFeatureAPI;
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.client.DirBridge;
import pro.gravit.launcher.runtime.client.ServerPinger;
import pro.gravit.launcher.runtime.debug.DebugMain;
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.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
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;
private final Map<UUID, CompletableFuture<ServerPingInfo>> pingFutures = new ConcurrentHashMap<>();
@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();
DirBridge.dirUpdates = DirBridge.defaultUpdatesDir;
}
@Override
public CompletableFuture<LauncherInitData> init() {
try {
doInit();
} catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
CompletableFuture<CoreFeatureAPI.LauncherUpdateInfo> feature;
if(isTestMode()) {
feature = CompletableFuture.completedFuture(new CoreFeatureAPI.LauncherUpdateInfo(null, "Unknown", false, false));
} else {
feature = LauncherAPIHolder.core().checkUpdates();
}
return feature.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);
}
public AuthFeatureAPI.AuthToken getAuthToken() {
return backendSettings.auth.toToken();
}
public AuthMethod getAuthMethod() {
return authMethod;
}
@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) {
this.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);
settings.backend = this;
settings.updateEnabledMods();
} 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 CompletableFuture.failedFuture(new UnsupportedOperationException());
}
@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 availableJavasFuture;
}
return CompletableFuture.completedFuture(availableJavas);
}
@Override
public CompletableFuture<ServerPingInfo> pingServer(ProfileFeatureAPI.ClientProfile profile) {
return pingFutures.computeIfAbsent(profile.getUUID(), (k) -> {
CompletableFuture<ServerPingInfo> future = new CompletableFuture<>();
executorService.submit(() -> {
try {
ServerPinger pinger = new ServerPinger((ClientProfile) profile);
future.complete(pinger.ping());
} catch (Throwable e) {
future.completeExceptionally(e);
}
});
return future;
});
}
@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 boolean isTestMode() {
try {
return DebugMain.IS_DEBUG.get();
} catch (Throwable ex) {
return false;
}
}
@Override
public <T extends Extension> T getExtension(Class<T> clazz) {
return null;
}
@Override
public void shutdown() {
if(executorService != null) {
executorService.shutdownNow();
}
if(settingsManager != null) {
try {
settingsManager.saveConfig();
} catch (IOException e) {
LogHelper.error("Config not saved", e);
}
}
}
}

View file

@ -0,0 +1,201 @@
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.*;
import java.util.concurrent.ExecutionException;
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 JavaHelper.JavaVersion getRecommendedJava() {
JavaHelper.JavaVersion result = null;
try {
for(var java : backend.getAvailableJava().get()) {
if(isRecommended(java)) {
return (JavaHelper.JavaVersion) java;
}
if(isCompatible(java)) {
if(result == null) {
result = (JavaHelper.JavaVersion) java;
continue;
}
if(result.getMajorVersion() < java.getMajorVersion()) {
result = (JavaHelper.JavaVersion) java;
}
}
}
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
return result;
}
@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.backend = backend;
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);
}
if(selectedJava != null) {
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,135 @@
package pro.gravit.launcher.runtime.backend;
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.base.events.request.AuthRequestEvent;
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.LauncherAPIHolder;
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();
}
var java = settings.getSelectedJava();
if(java == null) {
java = settings.getRecommendedJava();
}
process = new ClientLauncherProcess(clientDir.path(), assetDir.path(), java, clientDir.path().resolve("resourcepacks"),
profile, new PlayerProfile(backend.getSelfUser()), settings.view, backend.getSelfUser().getAccessToken(),
clientDir.dir(), assetDir.dir(), javaDir == null ? null : javaDir.dir(),
new AuthRequestEvent.OAuthRequestEvent(backend.getAuthToken()), backend.getAuthMethod().getName());
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

@ -2,6 +2,7 @@
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.base.LauncherConfig;
import pro.gravit.launcher.base.events.request.AuthRequestEvent;
import pro.gravit.launcher.client.ClientLauncherEntryPoint;
import pro.gravit.launcher.client.ClientParams;
import pro.gravit.launcher.runtime.LauncherEngine;
@ -69,6 +70,12 @@ public ClientLauncherProcess(Path clientDir, Path assetDir,
public ClientLauncherProcess(Path clientDir, Path assetDir, JavaHelper.JavaVersion javaVersion, Path resourcePackDir,
ClientProfile profile, PlayerProfile playerProfile, OptionalView view, String accessToken,
HashedDir clientHDir, HashedDir assetHDir, HashedDir jvmHDir) {
this(clientDir, assetDir, javaVersion, resourcePackDir, profile, playerProfile, view, accessToken, clientHDir, assetHDir, jvmHDir, null, null);
}
public ClientLauncherProcess(Path clientDir, Path assetDir, JavaHelper.JavaVersion javaVersion, Path resourcePackDir,
ClientProfile profile, PlayerProfile playerProfile, OptionalView view, String accessToken,
HashedDir clientHDir, HashedDir assetHDir, HashedDir jvmHDir, AuthRequestEvent.OAuthRequestEvent oAuthRequestEvent, String authId) {
this.javaVersion = javaVersion;
this.workDir = clientDir.toAbsolutePath();
this.executeFile = IOHelper.resolveJavaBin(this.javaVersion.jvmDir);
@ -77,6 +84,8 @@ public ClientLauncherProcess(Path clientDir, Path assetDir, JavaHelper.JavaVersi
this.params.resourcePackDir = resourcePackDir.toAbsolutePath().toString();
this.params.assetDir = assetDir.toAbsolutePath().toString();
this.params.timestamp = System.currentTimeMillis();
this.params.oauth = oAuthRequestEvent;
this.params.authId = authId;
Path nativesPath;
if(profile.hasFlag(ClientProfile.CompatibilityFlags.LEGACY_NATIVES_DIR)) {
nativesPath = workDir.resolve("natives");
@ -119,10 +128,8 @@ private void applyClientProfile() {
if (params.ram > 0) {
this.jvmArgs.add("-Xmx" + params.ram + 'M');
}
this.params.oauth = Request.getOAuth();
if (this.params.oauth == null) {
throw new UnsupportedOperationException("Legacy session not supported");
} else {
this.params.oauth = Request.getOAuth();
this.params.authId = Request.getAuthId();
this.params.oauthExpiredTime = Request.getTokenExpiredTime();
this.params.extendedTokens = Request.getExtendedTokens();

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

@ -5,6 +5,7 @@
import com.google.gson.JsonParser;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.profiles.ClientProfileVersions;
import pro.gravit.launcher.core.backend.LauncherBackendAPI;
import pro.gravit.launcher.core.serialize.HInput;
import pro.gravit.launcher.core.serialize.HOutput;
import pro.gravit.utils.helper.IOHelper;
@ -18,6 +19,7 @@
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.regex.Pattern;
@ -208,7 +210,7 @@ public Result ping() throws IOException {
}
}
public static final class Result {
public static final class Result implements LauncherBackendAPI.ServerPingInfo {
public final int onlinePlayers;
@ -228,5 +230,20 @@ public Result(int onlinePlayers, int maxPlayers, String raw) {
public boolean isOverfilled() {
return onlinePlayers >= maxPlayers;
}
@Override
public int getMaxOnline() {
return maxPlayers;
}
@Override
public int getOnline() {
return onlinePlayers;
}
@Override
public List<String> getPlayerNames() {
return List.of();
}
}
}

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

@ -1,5 +1,5 @@
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
dependencies {
api project(':LauncherCore')

View file

@ -1,10 +1,11 @@
package pro.gravit.launcher.base;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.core.api.model.UserPermissions;
import java.util.*;
public class ClientPermissions {
public class ClientPermissions implements UserPermissions {
public static final ClientPermissions DEFAULT = new ClientPermissions();
@LauncherNetworkAPI
private List<String> roles;
@ -28,6 +29,7 @@ public static ClientPermissions getSuperuserAccount() {
return perm;
}
@Override
public boolean hasRole(String role) {
return roles != null && roles.contains(role);
}
@ -46,6 +48,7 @@ public synchronized void compile() {
}
}
@Override
public boolean hasPerm(String action) {
if (available == null) {
compile();

View file

@ -234,7 +234,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();
@ -247,6 +247,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

@ -1,6 +1,7 @@
package pro.gravit.launcher.base.events.request;
import pro.gravit.launcher.base.ClientPermissions;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.base.events.RequestEvent;
import pro.gravit.launcher.base.profiles.PlayerProfile;
@ -67,7 +68,15 @@ public String getType() {
return "auth";
}
public static class OAuthRequestEvent {
public CurrentUserRequestEvent.UserInfo makeUserInfo() {
var userInfo = new CurrentUserRequestEvent.UserInfo();
userInfo.accessToken = accessToken;
userInfo.permissions = permissions;
userInfo.playerProfile = playerProfile;
return userInfo;
}
public static class OAuthRequestEvent implements AuthFeatureAPI.AuthToken {
public final String accessToken;
public final String refreshToken;
public final long expire;
@ -77,5 +86,26 @@ public OAuthRequestEvent(String accessToken, String refreshToken, long expire) {
this.refreshToken = refreshToken;
this.expire = expire;
}
public OAuthRequestEvent(AuthFeatureAPI.AuthToken token) {
this.accessToken = token.getAccessToken();
this.refreshToken = token.getRefreshToken();
this.expire = token.getExpire();
}
@Override
public String getAccessToken() {
return accessToken;
}
@Override
public String getRefreshToken() {
return refreshToken;
}
@Override
public long getExpire() {
return expire;
}
}
}

View file

@ -3,6 +3,11 @@
import pro.gravit.launcher.base.ClientPermissions;
import pro.gravit.launcher.base.events.RequestEvent;
import pro.gravit.launcher.base.profiles.PlayerProfile;
import pro.gravit.launcher.core.api.model.SelfUser;
import pro.gravit.launcher.core.api.model.Texture;
import java.util.Map;
import java.util.UUID;
public class CurrentUserRequestEvent extends RequestEvent {
public final UserInfo userInfo;
@ -16,7 +21,7 @@ public String getType() {
return "currentUser";
}
public static class UserInfo {
public static class UserInfo implements SelfUser {
public ClientPermissions permissions;
public String accessToken;
public PlayerProfile playerProfile;
@ -29,5 +34,35 @@ public UserInfo(ClientPermissions permissions, String accessToken, PlayerProfile
this.accessToken = accessToken;
this.playerProfile = playerProfile;
}
@Override
public String getAccessToken() {
return accessToken;
}
@Override
public ClientPermissions getPermissions() {
return permissions;
}
@Override
public String getUsername() {
return playerProfile.getUsername();
}
@Override
public UUID getUUID() {
return playerProfile.getUUID();
}
@Override
public Map<String, Texture> getAssets() {
return playerProfile.getAssets();
}
@Override
public Map<String, String> getProperties() {
return playerProfile.getProperties();
}
}
}

View file

@ -1,9 +1,16 @@
package pro.gravit.launcher.base.events.request;
import pro.gravit.launcher.base.request.auth.details.AuthLoginOnlyDetails;
import pro.gravit.launcher.base.request.auth.details.AuthWebViewDetails;
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.base.events.RequestEvent;
import pro.gravit.launcher.core.api.method.AuthMethod;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
import pro.gravit.launcher.core.api.method.details.AuthPasswordDetails;
import pro.gravit.launcher.core.api.method.details.AuthWebDetails;
import pro.gravit.utils.TypeSerializeInterface;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@ -37,10 +44,11 @@ public enum ServerFeature {
}
}
public interface AuthAvailabilityDetails extends TypeSerializeInterface {
public interface AuthAvailabilityDetails extends AuthMethodDetails, TypeSerializeInterface {
AuthMethodDetails toAuthMethodDetails();
}
public static class AuthAvailability {
public static class AuthAvailability implements AuthMethod {
public final List<AuthAvailabilityDetails> details;
@LauncherNetworkAPI
public String name;
@ -59,5 +67,34 @@ public AuthAvailability(List<AuthAvailabilityDetails> details, String name, Stri
this.visible = visible;
this.features = features;
}
@Override
public List<AuthMethodDetails> getDetails() {
List<AuthMethodDetails> convert = new ArrayList<>();
for(var e : details) {
convert.add(e.toAuthMethodDetails());
}
return convert;
}
@Override
public String getName() {
return name;
}
@Override
public String getDisplayName() {
return displayName;
}
@Override
public boolean isVisible() {
return visible;
}
@Override
public Set<String> getFeatures() {
return features;
}
}
}

View file

@ -6,6 +6,7 @@
import pro.gravit.launcher.base.profiles.optional.OptionalDepend;
import pro.gravit.launcher.base.profiles.optional.OptionalFile;
import pro.gravit.launcher.base.profiles.optional.triggers.OptionalTrigger;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.utils.helper.IOHelper;
import pro.gravit.utils.helper.VerifyHelper;
import pro.gravit.utils.launch.LaunchOptions;
@ -13,8 +14,9 @@
import java.lang.reflect.Type;
import java.net.InetSocketAddress;
import java.util.*;
import java.util.stream.Collectors;
public final class ClientProfile implements Comparable<ClientProfile> {
public final class ClientProfile implements Comparable<ClientProfile>, ProfileFeatureAPI.ClientProfile {
private static final FileNameMatcher ASSET_MATCHER = new FileNameMatcher(
new String[0], new String[]{"indexes", "objects"}, new String[0]);
@LauncherNetworkAPI
@ -284,10 +286,25 @@ public String toString() {
return String.format("%s (%s)", title, uuid);
}
@Override
public String getName() {
return title;
}
public UUID getUUID() {
return uuid;
}
@Override
public String getDescription() {
return info;
}
@Override
public List<ProfileFeatureAPI.OptionalMod> getOptionalMods() {
return updateOptional.stream().collect(Collectors.toUnmodifiableList());
}
public boolean hasFlag(CompatibilityFlags flag) {
return flags.contains(flag);
}
@ -361,6 +378,11 @@ public Map<String, String> getProperties() {
return Collections.unmodifiableMap(properties);
}
@Override
public ServerInfo getServer() {
return getDefaultServerProfile();
}
public List<String> getCompatClasses() {
return Collections.unmodifiableList(compatClasses);
}
@ -498,7 +520,7 @@ public JsonElement serialize(Version src, Type typeOfSrc, JsonSerializationConte
}
}
public static class ServerProfile {
public static class ServerProfile implements ServerInfo {
public String name;
public String serverAddress;
public int serverPort;
@ -525,6 +547,16 @@ public ServerProfile(String name, String serverAddress, int serverPort, boolean
public InetSocketAddress toSocketAddress() {
return InetSocketAddress.createUnresolved(serverAddress, serverPort);
}
@Override
public String getAddress() {
return serverAddress;
}
@Override
public int getPort() {
return serverPort;
}
}
public static class ProfileDefaultSettings {

View file

@ -1,5 +1,6 @@
package pro.gravit.launcher.base.profiles;
import pro.gravit.launcher.core.api.model.User;
import pro.gravit.utils.helper.IOHelper;
import java.util.HashMap;
@ -7,7 +8,7 @@
import java.util.Objects;
import java.util.UUID;
public final class PlayerProfile {
public final class PlayerProfile implements User {
public final UUID uuid;
public final String username;
@ -41,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<>());
}
@ -49,4 +58,24 @@ public static UUID offlineUUID(String username) {
return UUID.nameUUIDFromBytes(IOHelper.encodeASCII("OfflinePlayer:" + username));
}
@Override
public String getUsername() {
return username;
}
@Override
public UUID getUUID() {
return uuid;
}
@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Map<String, pro.gravit.launcher.core.api.model.Texture> getAssets() {
return (Map) assets;
}
@Override
public Map<String, String> getProperties() {
return properties;
}
}

View file

@ -14,7 +14,7 @@
import java.util.Arrays;
import java.util.Map;
public final class Texture extends StreamObject {
public final class Texture extends StreamObject implements pro.gravit.launcher.core.api.model.Texture {
private static final SecurityHelper.DigestAlgorithm DIGEST_ALGO = SecurityHelper.DigestAlgorithm.SHA256;
// Instance
@ -85,4 +85,19 @@ public String toString() {
", metadata=" + metadata +
'}';
}
@Override
public String getUrl() {
return url;
}
@Override
public String getHash() {
return SecurityHelper.toHex(digest);
}
@Override
public Map<String, String> getMetadata() {
return metadata;
}
}

View file

@ -2,11 +2,13 @@
import pro.gravit.launcher.core.LauncherNetworkAPI;
import pro.gravit.launcher.base.profiles.optional.actions.OptionalAction;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import java.util.List;
import java.util.Objects;
import java.util.Set;
public class OptionalFile {
public class OptionalFile implements ProfileFeatureAPI.OptionalMod {
@LauncherNetworkAPI
public List<OptionalAction> actions;
@LauncherNetworkAPI
@ -57,10 +59,25 @@ public String getName() {
return name;
}
@Override
public String getDescription() {
return info;
}
@Override
public String getCategory() {
return category;
}
public boolean isVisible() {
return visible;
}
@Override
public Set<ProfileFeatureAPI.OptionalMod> getDependencies() {
return Set.of(dependencies);
}
public boolean isMark() {
return mark;
}

View file

@ -0,0 +1,31 @@
package pro.gravit.launcher.base.request;
import pro.gravit.launcher.base.request.auth.GetAvailabilityAuthRequest;
import pro.gravit.launcher.base.request.update.LauncherRequest;
import pro.gravit.launcher.core.api.features.CoreFeatureAPI;
import pro.gravit.launcher.core.api.method.AuthMethod;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class RequestCoreFeatureAPIImpl implements CoreFeatureAPI {
private final RequestService request;
public RequestCoreFeatureAPIImpl(RequestService request) {
this.request = request;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public CompletableFuture<List<AuthMethod>> getAuthMethods() {
return request.request(new GetAvailabilityAuthRequest()).thenApply(response -> (List) response.list);
}
@Override
public CompletableFuture<LauncherUpdateInfo> checkUpdates() {
return request.request(new LauncherRequest()).thenApply(response -> new LauncherUpdateInfo(response.url,
"Unknown", response.needUpdate, response.needUpdate));
}
}

View file

@ -2,7 +2,7 @@
import java.io.IOException;
public final class RequestException extends IOException {
public final class RequestException extends RuntimeException {
public RequestException(String message) {

View file

@ -0,0 +1,191 @@
package pro.gravit.launcher.base.request;
import pro.gravit.launcher.base.Launcher;
import pro.gravit.launcher.base.profiles.ClientProfile;
import pro.gravit.launcher.base.request.auth.*;
import pro.gravit.launcher.base.request.auth.password.*;
import pro.gravit.launcher.base.request.update.ProfilesRequest;
import pro.gravit.launcher.base.request.update.UpdateRequest;
import pro.gravit.launcher.base.request.uuid.ProfileByUUIDRequest;
import pro.gravit.launcher.base.request.uuid.ProfileByUsernameRequest;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.UserFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
import pro.gravit.launcher.core.api.method.password.AuthChainPassword;
import pro.gravit.launcher.core.api.method.password.AuthOAuthPassword;
import pro.gravit.launcher.core.api.method.password.AuthPlainPassword;
import pro.gravit.launcher.core.api.method.password.AuthTotpPassword;
import pro.gravit.launcher.core.api.model.SelfUser;
import pro.gravit.launcher.core.api.model.User;
import pro.gravit.launcher.core.hasher.HashedDir;
import pro.gravit.utils.helper.SecurityHelper;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class RequestFeatureAPIImpl implements AuthFeatureAPI, UserFeatureAPI, ProfileFeatureAPI {
private final RequestService request;
private final String authId;
public RequestFeatureAPIImpl(RequestService request, String authId) {
this.request = request;
this.authId = authId;
}
@Override
public CompletableFuture<SelfUser> getCurrentUser() {
return request.request(new CurrentUserRequest()).thenApply(response -> response.userInfo);
}
@Override
public CompletableFuture<AuthResponse> auth(String login, AuthMethodPassword password) {
AuthRequest.ConnectTypes connectType = AuthRequest.ConnectTypes.API;
if(Request.getExtendedTokens() != null && Request.getExtendedTokens().get("launcher") != null) {
connectType = AuthRequest.ConnectTypes.CLIENT;
}
return request.request(new AuthRequest(login, convertAuthPasswordAll(password), authId, false, connectType))
.thenApply(response -> new AuthResponse(response.makeUserInfo(), response.oauth));
}
private AuthRequest.AuthPasswordInterface convertAuthPasswordAll(AuthMethodPassword password) {
AuthRequest.AuthPasswordInterface requestPassword;
if(password instanceof AuthChainPassword chain) {
if(chain.list().size() == 1) {
requestPassword = convertAuthPassword(chain.list().get(0));
} else if(chain.list().size() == 2) {
requestPassword = new Auth2FAPassword(convertAuthPassword(chain.list().get(0)),
convertAuthPassword(chain.list().get(1)));
} else {
var multi = new AuthMultiPassword();
for(var e : chain.list()) {
multi.list.add(convertAuthPassword(e));
}
requestPassword = multi;
}
} else {
requestPassword = convertAuthPassword(password);
}
return requestPassword;
}
private AuthRequest.AuthPasswordInterface convertAuthPassword(AuthMethodPassword password) {
if(password instanceof AuthPlainPassword plain) {
String encryptKey = Launcher.getConfig().passwordEncryptKey;
if(encryptKey != null) {
try {
return new AuthAESPassword(SecurityHelper.encrypt(encryptKey, plain.value()));
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
return new pro.gravit.launcher.base.request.auth.password.AuthPlainPassword(plain.value());
}
} else if(password instanceof AuthTotpPassword totp) {
return new AuthTOTPPassword(totp.value());
} else if(password instanceof AuthOAuthPassword oauth) {
return new AuthCodePassword(oauth.redirectUrl());
} else if(password instanceof AuthRequest.AuthPasswordInterface custom) {
return custom;
} else if(password == null) {
return null;
}
else {
throw new UnsupportedOperationException();
}
}
@Override
public CompletableFuture<User> getUserByUsername(String username) {
return request.request(new ProfileByUsernameRequest(username)).thenApply(response -> response.playerProfile);
}
@Override
public CompletableFuture<User> getUserByUUID(UUID uuid) {
return request.request(new ProfileByUUIDRequest(uuid)).thenApply(response -> response.playerProfile);
}
@Override
public CompletableFuture<Void> joinServer(String username, String accessToken, String serverID) {
return request.request(new JoinServerRequest(username, accessToken, serverID)).thenCompose(response -> {
if(response.allow) {
return CompletableFuture.completedFuture(null);
} else {
return CompletableFuture.failedFuture(new RequestException("Not allowed"));
}
});
}
@Override
public CompletableFuture<Void> joinServer(UUID uuid, String accessToken, String serverID) {
return request.request(new JoinServerRequest(uuid, accessToken, serverID)).thenCompose(response -> {
if(response.allow) {
return CompletableFuture.completedFuture(null);
} else {
return CompletableFuture.failedFuture(new RequestException("Not allowed"));
}
});
}
@Override
public CompletableFuture<CheckServerResponse> checkServer(String username, String serverID, boolean extended) {
return request.request(new CheckServerRequest(username, serverID, extended, extended))
.thenApply(response -> new CheckServerResponse(response.playerProfile, response.hardwareId,
response.sessionId, response.sessionProperties));
}
@Override
public CompletableFuture<AuthToken> refreshToken(String refreshToken) {
return request.request(new RefreshTokenRequest(authId, refreshToken)).thenApply(response -> response.oauth);
}
@Override
public CompletableFuture<SelfUser> restore(String accessToken, boolean fetchUser) {
Map<String, String> extended = new HashMap<>();
if(Request.getExtendedTokens() != null) { // TODO: Control extended token
for(var e : Request.getExtendedTokens().entrySet()) {
extended.put(e.getKey(), e.getValue().token);
}
}
return request.request(new RestoreRequest(authId, accessToken, extended, fetchUser)).thenApply(e -> {
// TODO: invalidToken process
return e.userInfo;
});
}
@Override
public CompletableFuture<Void> exit() {
return request.request(new ExitRequest()).thenApply(response -> null);
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public CompletableFuture<List<ProfileFeatureAPI.ClientProfile>> getProfiles() {
return request.request(new ProfilesRequest()).thenApply(response -> (List) response.profiles);
}
@Override
public CompletableFuture<Void> changeCurrentProfile(ClientProfile profile) {
return request.request(new SetProfileRequest((pro.gravit.launcher.base.profiles.ClientProfile) profile)).thenApply(response -> null);
}
@Override
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 {
@Override
public HashedDir getHashedDir() {
return hdir;
}
@Override
public String getUrl() {
return url;
}
}
}

View file

@ -5,27 +5,23 @@
import java.util.concurrent.ExecutionException;
public interface RequestService {
<T extends WebSocketEvent> CompletableFuture<T> request(Request<T> request) throws IOException;
<T extends WebSocketEvent> CompletableFuture<T> request(Request<T> request);
void connect() throws Exception;
void registerEventHandler(EventHandler handler);
void unregisterEventHandler(EventHandler handler);
default <T extends WebSocketEvent> T requestSync(Request<T> request) throws IOException {
default <T extends WebSocketEvent> T requestSync(Request<T> request) {
try {
return request(request).get();
} catch (InterruptedException e) {
throw new RequestException("Request interrupted");
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException)
throw (IOException) e.getCause();
else {
throw new RequestException(cause);
}
}
}
boolean isClosed();

View file

@ -5,6 +5,7 @@
import pro.gravit.launcher.base.request.Request;
import pro.gravit.launcher.base.request.auth.password.*;
import pro.gravit.launcher.base.request.websockets.WebSocketRequest;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
import pro.gravit.utils.ProviderMap;
public final class AuthRequest extends Request<AuthRequestEvent> implements WebSocketRequest {
@ -64,7 +65,7 @@ public enum ConnectTypes {
API
}
public interface AuthPasswordInterface {
public interface AuthPasswordInterface extends AuthMethodPassword {
boolean check();
default boolean isAllowSave() {

View file

@ -1,10 +1,16 @@
package pro.gravit.launcher.base.request.auth.details;
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public class AuthLoginOnlyDetails implements GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails {
@Override
public String getType() {
return "loginonly";
}
@Override
public AuthMethodDetails toAuthMethodDetails() {
return new pro.gravit.launcher.core.api.method.details.AuthLoginOnlyDetails();
}
}

View file

@ -1,6 +1,7 @@
package pro.gravit.launcher.base.request.auth.details;
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public class AuthPasswordDetails implements GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails {
@Override
@ -9,4 +10,8 @@ public String getType() {
}
@Override
public AuthMethodDetails toAuthMethodDetails() {
return new pro.gravit.launcher.core.api.method.details.AuthPasswordDetails();
}
}

View file

@ -1,6 +1,7 @@
package pro.gravit.launcher.base.request.auth.details;
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public class AuthTotpDetails implements GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails {
public final String alg;
@ -20,4 +21,9 @@ public AuthTotpDetails(String alg) {
public String getType() {
return "totp";
}
@Override
public AuthMethodDetails toAuthMethodDetails() {
return new pro.gravit.launcher.core.api.method.details.AuthTotpDetails(maxKeyLength);
}
}

View file

@ -1,6 +1,8 @@
package pro.gravit.launcher.base.request.auth.details;
import pro.gravit.launcher.base.events.request.GetAvailabilityAuthRequestEvent;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
import pro.gravit.launcher.core.api.method.details.AuthWebDetails;
public class AuthWebViewDetails implements GetAvailabilityAuthRequestEvent.AuthAvailabilityDetails {
public final String url;
@ -26,4 +28,9 @@ public AuthWebViewDetails(String url, String redirectUrl) {
public String getType() {
return "webview";
}
@Override
public AuthMethodDetails toAuthMethodDetails() {
return new AuthWebDetails(url, redirectUrl, canBrowser);
}
}

View file

@ -6,6 +6,14 @@ public class Auth2FAPassword implements AuthRequest.AuthPasswordInterface {
public AuthRequest.AuthPasswordInterface firstPassword;
public AuthRequest.AuthPasswordInterface secondPassword;
public Auth2FAPassword() {
}
public Auth2FAPassword(AuthRequest.AuthPasswordInterface firstPassword, AuthRequest.AuthPasswordInterface secondPassword) {
this.firstPassword = firstPassword;
this.secondPassword = secondPassword;
}
@Override
public boolean check() {
return firstPassword != null && firstPassword.check() && secondPassword != null && secondPassword.check();

View file

@ -5,6 +5,13 @@
public class AuthTOTPPassword implements AuthRequest.AuthPasswordInterface {
public String totp;
public AuthTOTPPassword() {
}
public AuthTOTPPassword(String totp) {
this.totp = totp;
}
@Override
public boolean check() {
return true;

View file

@ -97,10 +97,14 @@ public <T extends WebSocketEvent> void eventHandle(T webSocketEvent) {
processEventHandlers(webSocketEvent);
}
public <T extends WebSocketEvent> CompletableFuture<T> request(Request<T> request) throws IOException {
public <T extends WebSocketEvent> CompletableFuture<T> request(Request<T> request) {
CompletableFuture<T> result = new CompletableFuture<>();
futureMap.put(request.requestUUID, result);
try {
sendObject(request, WebSocketRequest.class);
} catch (IOException e) {
return CompletableFuture.failedFuture(e);
}
return result;
}
@ -114,20 +118,16 @@ public void unregisterEventHandler(RequestService.EventHandler handler) {
eventHandlers.remove(handler);
}
public <T extends WebSocketEvent> T requestSync(Request<T> request) throws IOException {
public <T extends WebSocketEvent> T requestSync(Request<T> request) {
try {
return request(request).get();
} catch (InterruptedException e) {
throw new RequestException("Request interrupted");
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof IOException)
throw (IOException) e.getCause();
else {
throw new RequestException(cause);
}
}
}
@Override
public boolean isClosed() {

View file

@ -7,8 +7,8 @@
url "https://repo.spring.io/plugins-release/"
}
}
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
jar {
archiveClassifier.set('clean')

View file

@ -1,5 +1,5 @@
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
dependencies {
compileOnly group: 'org.fusesource.jansi', name: 'jansi', version: rootProject['verJansi']

View file

@ -0,0 +1,34 @@
package pro.gravit.launcher.core.api;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.FeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.features.UserFeatureAPI;
import java.util.HashMap;
import java.util.Map;
public class LauncherAPI {
private final Map<Class<? extends FeatureAPI>, FeatureAPI> map;
public LauncherAPI(Map<Class<? extends FeatureAPI>, FeatureAPI> map) {
this.map = new HashMap<>(map);
}
public AuthFeatureAPI auth() {
return get(AuthFeatureAPI.class);
}
public UserFeatureAPI user() {
return get(UserFeatureAPI.class);
}
public ProfileFeatureAPI profile() {
return get(ProfileFeatureAPI.class);
}
@SuppressWarnings("unchecked")
public<T extends FeatureAPI> T get(Class<T> clazz) {
return (T) map.get(clazz);
}
}

View file

@ -0,0 +1,70 @@
package pro.gravit.launcher.core.api;
import pro.gravit.launcher.core.api.features.AuthFeatureAPI;
import pro.gravit.launcher.core.api.features.CoreFeatureAPI;
import pro.gravit.launcher.core.api.features.ProfileFeatureAPI;
import pro.gravit.launcher.core.api.features.UserFeatureAPI;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
public final class LauncherAPIHolder {
private static volatile CoreFeatureAPI coreAPI;
private static volatile LauncherAPI api;
private static volatile Function<String, LauncherAPI> createApiFactory;
private static final Map<String, LauncherAPI> map = new ConcurrentHashMap<>();
public static void setCoreAPI(CoreFeatureAPI coreAPI) {
LauncherAPIHolder.coreAPI = coreAPI;
}
public static void setApi(LauncherAPI api) {
LauncherAPIHolder.api = api;
}
public static void setCreateApiFactory(Function<String, LauncherAPI> createApiFactory) {
LauncherAPIHolder.createApiFactory = createApiFactory;
}
public static void changeAuthId(String authId) {
LauncherAPIHolder.api = map.computeIfAbsent(authId, createApiFactory);
}
public static LauncherAPI get() {
return api;
}
public static LauncherAPI get(String authId) {
return map.computeIfAbsent(authId, createApiFactory);
}
public static CoreFeatureAPI core() {
return coreAPI;
}
public static AuthFeatureAPI auth() {
if(api == null) {
throw new UnsupportedOperationException();
}
return api.auth();
}
public static UserFeatureAPI user() {
if(api == null) {
throw new UnsupportedOperationException();
}
return api.user();
}
public static ProfileFeatureAPI profile() {
if(api == null) {
throw new UnsupportedOperationException();
}
return api.profile();
}
public static void set(LauncherAPI api) {
LauncherAPIHolder.api = api;
}
}

View file

@ -0,0 +1,24 @@
package pro.gravit.launcher.core.api.features;
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 java.util.List;
import java.util.concurrent.CompletableFuture;
public interface AuthFeatureAPI extends FeatureAPI {
CompletableFuture<SelfUser> getCurrentUser();
CompletableFuture<AuthResponse> auth(String login, AuthMethodPassword password);
CompletableFuture<AuthToken> refreshToken(String refreshToken);
CompletableFuture<SelfUser> restore(String accessToken, boolean fetchUser);
CompletableFuture<Void> exit();
record AuthResponse(SelfUser user, AuthToken authToken) {}
interface AuthToken {
String getAccessToken();
String getRefreshToken();
long getExpire();
}
}

View file

@ -0,0 +1,13 @@
package pro.gravit.launcher.core.api.features;
import pro.gravit.launcher.core.api.method.AuthMethod;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public interface CoreFeatureAPI {
CompletableFuture<List<AuthMethod>> getAuthMethods();
CompletableFuture<LauncherUpdateInfo> checkUpdates();
record LauncherUpdateInfo(String url, String version, boolean available, boolean required) {}
}

View file

@ -0,0 +1,4 @@
package pro.gravit.launcher.core.api.features;
public interface FeatureAPI {
}

View file

@ -0,0 +1,43 @@
package pro.gravit.launcher.core.api.features;
import pro.gravit.launcher.core.hasher.HashedDir;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public interface ProfileFeatureAPI extends FeatureAPI {
CompletableFuture<List<ClientProfile>> getProfiles();
CompletableFuture<Void> changeCurrentProfile(ClientProfile profile);
CompletableFuture<UpdateInfo> fetchUpdateInfo(String dirName);
interface UpdateInfo {
HashedDir getHashedDir();
String getUrl();
}
interface ClientProfile {
String getName();
UUID getUUID();
String getDescription();
List<OptionalMod> getOptionalMods();
String getProperty(String name);
Map<String, String> getProperties();
ServerInfo getServer();
interface ServerInfo {
String getAddress();
int getPort();
}
}
interface OptionalMod {
String getName();
String getDescription();
String getCategory();
boolean isVisible();
Set<OptionalMod> getDependencies();
}
}

View file

@ -0,0 +1,20 @@
package pro.gravit.launcher.core.api.features;
import pro.gravit.launcher.core.api.model.Texture;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
public interface TextureUploadFeatureAPI {
CompletableFuture<TextureUploadInfo> fetchInfo();
CompletableFuture<Texture> upload(String name, byte[] bytes, UploadSettings settings);
interface TextureUploadInfo {
Set<String> getAvailable();
boolean isRequireManualSlimSkinSelect();
}
record UploadSettings(boolean slim) {
}
}

View file

@ -0,0 +1,37 @@
package pro.gravit.launcher.core.api.features;
import pro.gravit.launcher.core.api.model.User;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public interface UserFeatureAPI extends FeatureAPI {
CompletableFuture<User> getUserByUsername(String username);
CompletableFuture<User> getUserByUUID(UUID uuid);
CompletableFuture<Void> joinServer(String username, String accessToken, String serverID);
CompletableFuture<Void> joinServer(UUID uuid, String accessToken, String serverID);
CompletableFuture<CheckServerResponse> checkServer(String username, String serverID, boolean extended);
default CompletableFuture<List<User>> getUsersByUsernames(List<String> usernames) {
List<CompletableFuture<User>> list = new ArrayList<>();
for(var username : usernames) {
list.add(getUserByUsername(username));
}
return CompletableFuture.allOf(list.toArray(CompletableFuture[]::new)).thenApply(x -> {
List<User> r = new ArrayList<>();
for(var e : list) {
try {
r.add(e.get());
} catch (InterruptedException | ExecutionException ex) {
r.add(null);
}
}
return r;
});
}
record CheckServerResponse(User user, String hardwareId, String sessionId, Map<String, String> sessionProperties) {}
}

View file

@ -0,0 +1,12 @@
package pro.gravit.launcher.core.api.method;
import java.util.List;
import java.util.Set;
public interface AuthMethod {
List<AuthMethodDetails> getDetails();
String getName();
String getDisplayName();
boolean isVisible();
Set<String> getFeatures();
}

View file

@ -0,0 +1,4 @@
package pro.gravit.launcher.core.api.method;
public interface AuthMethodDetails {
}

View file

@ -0,0 +1,4 @@
package pro.gravit.launcher.core.api.method;
public interface AuthMethodPassword {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.details;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public class AuthLoginOnlyDetails implements AuthMethodDetails {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.details;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public class AuthPasswordDetails implements AuthMethodDetails {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.details;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public record AuthTotpDetails(int length) implements AuthMethodDetails {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.details;
import pro.gravit.launcher.core.api.method.AuthMethodDetails;
public record AuthWebDetails(String url, String redirectUrl, boolean externalBrowserSupport) implements AuthMethodDetails {
}

View file

@ -0,0 +1,8 @@
package pro.gravit.launcher.core.api.method.password;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
import java.util.List;
public record AuthChainPassword(List<AuthMethodPassword> list) implements AuthMethodPassword {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.password;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
public record AuthOAuthPassword(String redirectUrl) implements AuthMethodPassword {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.password;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
public record AuthPlainPassword(String value) implements AuthMethodPassword {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.method.password;
import pro.gravit.launcher.core.api.method.AuthMethodPassword;
public record AuthTotpPassword(String value) implements AuthMethodPassword {
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.model;
public interface SelfUser extends User {
String getAccessToken();
UserPermissions getPermissions();
}

View file

@ -0,0 +1,9 @@
package pro.gravit.launcher.core.api.model;
import java.util.Map;
public interface Texture {
String getUrl();
String getHash();
Map<String, String> getMetadata();
}

View file

@ -0,0 +1,11 @@
package pro.gravit.launcher.core.api.model;
import java.util.Map;
import java.util.UUID;
public interface User {
String getUsername();
UUID getUUID();
Map<String, Texture> getAssets();
Map<String, String> getProperties();
}

View file

@ -0,0 +1,6 @@
package pro.gravit.launcher.core.api.model;
public interface UserPermissions {
boolean hasRole(String role);
boolean hasPerm(String action);
}

View file

@ -0,0 +1,175 @@
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;
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.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);
CompletableFuture<LauncherInitData> init();
void selectAuthMethod(AuthMethod method);
CompletableFuture<SelfUser> tryAuthorize();
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();
CompletableFuture<ServerPingInfo> pingServer(ProfileFeatureAPI.ClientProfile profile);
// 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();
boolean isTestMode();
// Extensions
<T extends Extension> T getExtension(Class<T> clazz);
void shutdown();
record LauncherInitData(List<AuthMethod> methods) {}
interface ReadyProfile {
ProfileFeatureAPI.ClientProfile getClientProfile();
ClientProfileSettings getSettings();
void run(RunCallback callback) throws Exception;
}
interface ClientProfileSettings {
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);
Set<ProfileFeatureAPI.OptionalMod> getEnabledOptionals();
void enableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback);
void disableOptional(ProfileFeatureAPI.OptionalMod mod, ChangedOptionalStatusCallback callback);
Java getSelectedJava();
Java getRecommendedJava();
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 {
void onChanged(ProfileFeatureAPI.OptionalMod mod, boolean enabled);
}
}
// Callbacks
class MainCallback {
// On any request
public void onChangeStatus(String status) {
}
public void onProfiles(List<ProfileFeatureAPI.ClientProfile> profiles) {
}
public void onAuthorize(SelfUser selfUser) {
}
public void onNotify(String header, String description) {
}
public void onShutdown() {
}
}
class RunCallback {
public void onStarted() {
}
public void onCanTerminate(Runnable terminate) {
}
public void onFinished(int code) {
}
public void onNormalOutput(byte[] buf, int offset, int size) {
}
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) {
}
public void onCanCancel(Runnable cancel) {
}
public void onTotalDownload(long total) {
}
public void onCurrentDownloaded(long current) {
}
}
interface Java {
int getMajorVersion();
Path getPath();
}
interface ServerPingInfo {
int getMaxOnline();
int getOnline();
List<String> getPlayerNames();
}
}

View file

@ -0,0 +1,13 @@
package pro.gravit.launcher.core.backend;
public class LauncherBackendAPIHolder {
private static volatile LauncherBackendAPI api;
public static LauncherBackendAPI getApi() {
return api;
}
public static void setApi(LauncherBackendAPI api) {
LauncherBackendAPIHolder.api = api;
}
}

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

@ -0,0 +1,4 @@
package pro.gravit.launcher.core.backend.extensions;
public interface Extension {
}

View file

@ -0,0 +1,11 @@
package pro.gravit.launcher.core.backend.extensions;
import pro.gravit.launcher.core.api.features.TextureUploadFeatureAPI;
import pro.gravit.launcher.core.api.model.Texture;
import java.util.concurrent.CompletableFuture;
public interface TextureUploadExtension extends Extension {
CompletableFuture<TextureUploadFeatureAPI.TextureUploadInfo> fetchTextureUploadInfo();
CompletableFuture<Texture> uploadTexture(String name, byte[] bytes, TextureUploadFeatureAPI.UploadSettings settings);
}

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

@ -8,8 +8,8 @@
url "https://repo.spring.io/plugins-release/"
}
}
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
jar {
archiveClassifier.set('clean')

View file

@ -1,4 +1,4 @@
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.gradleup.shadow'
String mainClassName = "pro.gravit.launcher.server.ServerWrapper"
String mainAgentName = "pro.gravit.launcher.server.ServerAgent"
@ -14,8 +14,8 @@
}
}
sourceCompatibility = '17'
targetCompatibility = '17'
sourceCompatibility = '21'
targetCompatibility = '21'
jar {
archiveClassifier.set('clean')

View file

@ -1,5 +1,5 @@
plugins {
id 'com.github.johnrengelman.shadow' version '8.1.1' apply false
id 'com.gradleup.shadow' version '9.0.0-beta15' apply false
id 'maven-publish'
id 'signing'
id 'org.openjfx.javafxplugin' version '0.1.0' apply false
@ -30,13 +30,6 @@
maven {
url "https://jitpack.io/"
}
maven {
url 'https://maven.gravit-support.ru/repository/jitpack'
credentials {
username = 'gravitlauncher'
password = 'gravitlauncher'
}
}
}
jar {
@ -71,6 +64,10 @@
repositories {
maven {
url = version.endsWith('SNAPSHOT') ? getProperty('mavenSnapshotRepository') : getProperty('mavenReleaseRepository')
credentials {
username getProperty('mavenUsername')
password getProperty('mavenPassword')
}
}
}
}

@ -1 +1 @@
Subproject commit 6f699fae50f98cec19279092d9d5fac1ac451914
Subproject commit acab14e403bb12649883017b14f2cf66cb1f4e68