diff --git a/src/main/java/amidst/Application.java b/src/main/java/amidst/Application.java index fc9141308..577edc903 100644 --- a/src/main/java/amidst/Application.java +++ b/src/main/java/amidst/Application.java @@ -1,5 +1,7 @@ package amidst; +import java.util.Optional; + import amidst.dependency.injection.Factory0; import amidst.dependency.injection.Factory1; import amidst.documentation.AmidstThread; @@ -10,14 +12,18 @@ import amidst.gui.main.MainWindowDialogs; import amidst.gui.main.UpdatePrompt; import amidst.gui.profileselect.ProfileSelectWindow; -import amidst.mojangapi.MojangApi; +import amidst.mojangapi.LauncherProfileRunner; +import amidst.mojangapi.RunningLauncherProfile; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; @NotThreadSafe public class Application { - private final MojangApi mojangApi; + private final Optional preferredLauncherProfile; + private final LauncherProfileRunner launcherProfileRunner; private final Factory1 noisyUpdatePromptFactory; private final Factory0 silentUpdatePromptFactory; - private final Factory0 mainWindowFactory; + private final Factory1 mainWindowFactory; private final Factory0 profileSelectWindowFactory; private final Factory0 licenseWindowFactory; @@ -26,13 +32,15 @@ public class Application { @CalledOnlyBy(AmidstThread.EDT) public Application( - MojangApi mojangApi, + Optional preferredLauncherProfile, + LauncherProfileRunner launcherProfileRunner, Factory1 noisyUpdatePromptFactory, Factory0 silentUpdatePromptFactory, - Factory0 mainWindowFactory, + Factory1 mainWindowFactory, Factory0 profileSelectWindowFactory, Factory0 licenseWindowFactory) { - this.mojangApi = mojangApi; + this.preferredLauncherProfile = preferredLauncherProfile; + this.launcherProfileRunner = launcherProfileRunner; this.noisyUpdatePromptFactory = noisyUpdatePromptFactory; this.silentUpdatePromptFactory = silentUpdatePromptFactory; this.mainWindowFactory = mainWindowFactory; @@ -41,10 +49,10 @@ public Application( } @CalledOnlyBy(AmidstThread.EDT) - public void run() { + public void run() throws LocalMinecraftInterfaceCreationException { checkForUpdatesSilently(); - if (mojangApi.canCreateWorld()) { - displayMainWindow(); + if (preferredLauncherProfile.isPresent()) { + displayMainWindow(launcherProfileRunner.run(preferredLauncherProfile.get())); } else { displayProfileSelectWindow(); } @@ -61,8 +69,8 @@ public void checkForUpdatesSilently() { } @CalledOnlyBy(AmidstThread.EDT) - public void displayMainWindow() { - setMainWindow(mainWindowFactory.create()); + public void displayMainWindow(RunningLauncherProfile runningLauncherProfile) { + setMainWindow(mainWindowFactory.create(runningLauncherProfile)); setProfileSelectWindow(null); } diff --git a/src/main/java/amidst/CommandLineParameters.java b/src/main/java/amidst/CommandLineParameters.java index 559c9e32d..f889052cd 100644 --- a/src/main/java/amidst/CommandLineParameters.java +++ b/src/main/java/amidst/CommandLineParameters.java @@ -14,9 +14,6 @@ public class CommandLineParameters { @Option(name = "-mcpath", usage = "location of the '.minecraft' directory.", metaVar = "") public volatile String dotMinecraftDirectory; - @Option(name = "-mclibs", usage = "location of the '.minecraft/libraries' directory", metaVar = "") - public volatile String minecraftLibrariesDirectory; - @Option(name = "-mcjar", usage = "location of the minecraft jar file", metaVar = "", depends = { "-mcjson" }) public volatile String minecraftJarFile; diff --git a/src/main/java/amidst/PerApplicationInjector.java b/src/main/java/amidst/PerApplicationInjector.java index ee16c0012..e40278be7 100644 --- a/src/main/java/amidst/PerApplicationInjector.java +++ b/src/main/java/amidst/PerApplicationInjector.java @@ -1,5 +1,8 @@ package amidst; +import java.io.IOException; +import java.util.Optional; + import amidst.documentation.AmidstThread; import amidst.documentation.CalledOnlyBy; import amidst.documentation.NotThreadSafe; @@ -16,15 +19,18 @@ import amidst.gui.main.viewer.ViewerFacade; import amidst.gui.main.viewer.Zoom; import amidst.gui.profileselect.ProfileSelectWindow; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.MojangApiBuilder; +import amidst.mojangapi.LauncherProfileRunner; +import amidst.mojangapi.RunningLauncherProfile; import amidst.mojangapi.file.DotMinecraftDirectoryNotFoundException; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.mojangapi.file.PlayerInformationCache; +import amidst.mojangapi.file.PlayerInformationProvider; +import amidst.mojangapi.file.VersionListProvider; import amidst.mojangapi.world.SeedHistoryLogger; import amidst.mojangapi.world.World; import amidst.mojangapi.world.WorldBuilder; -import amidst.mojangapi.world.player.PlayerInformationCache; -import amidst.mojangapi.world.player.PlayerInformationCacheImpl; +import amidst.parsing.FormatException; import amidst.settings.biomeprofile.BiomeProfileDirectory; import amidst.threading.ThreadMaster; @@ -32,12 +38,15 @@ public class PerApplicationInjector { private final AmidstMetaData metadata; private final AmidstSettings settings; - private final PlayerInformationCache playerInformationCache; + private final PlayerInformationProvider playerInformationProvider; private final SeedHistoryLogger seedHistoryLogger; + private final MinecraftInstallation minecraftInstallation; + private final Optional preferredLauncherProfile; private final WorldBuilder worldBuilder; - private final MojangApi mojangApi; + private final LauncherProfileRunner launcherProfileRunner; private final BiomeProfileDirectory biomeProfileDirectory; private final ThreadMaster threadMaster; + private final VersionListProvider versionListProvider; private final LayerBuilder layerBuilder; private final Zoom zoom; private final FragmentManager fragmentManager; @@ -47,21 +56,29 @@ public class PerApplicationInjector { @CalledOnlyBy(AmidstThread.EDT) public PerApplicationInjector(CommandLineParameters parameters, AmidstMetaData metadata, AmidstSettings settings) throws DotMinecraftDirectoryNotFoundException, - LocalMinecraftInterfaceCreationException { + FormatException, + IOException { this.metadata = metadata; this.settings = settings; - this.playerInformationCache = new PlayerInformationCacheImpl(); + this.playerInformationProvider = new PlayerInformationCache(); this.seedHistoryLogger = SeedHistoryLogger.from(parameters.seedHistoryFile); - this.worldBuilder = new WorldBuilder(playerInformationCache, seedHistoryLogger); - this.mojangApi = new MojangApiBuilder(worldBuilder, parameters).construct(); + this.minecraftInstallation = MinecraftInstallation + .newLocalMinecraftInstallation(parameters.dotMinecraftDirectory); + this.preferredLauncherProfile = minecraftInstallation + .tryReadLauncherProfile(parameters.minecraftJarFile, parameters.minecraftJsonFile); + this.worldBuilder = new WorldBuilder(playerInformationProvider, seedHistoryLogger); + this.launcherProfileRunner = new LauncherProfileRunner(worldBuilder); this.biomeProfileDirectory = BiomeProfileDirectory.create(parameters.biomeProfilesDirectory); this.threadMaster = new ThreadMaster(); + this.versionListProvider = VersionListProvider + .createLocalAndStartDownloadingRemote(threadMaster.getWorkerExecutor()); this.layerBuilder = new LayerBuilder(); this.zoom = new Zoom(settings.maxZoom); this.fragmentManager = new FragmentManager(layerBuilder.getConstructors(), layerBuilder.getNumberOfLayers()); this.biomeSelection = new BiomeSelection(); this.application = new Application( - mojangApi, + preferredLauncherProfile, + launcherProfileRunner, this::createNoisyUpdatePrompt, this::createSilentUpdatePrompt, this::createMainWindow, @@ -80,12 +97,13 @@ private UpdatePrompt createSilentUpdatePrompt() { } @CalledOnlyBy(AmidstThread.EDT) - private MainWindow createMainWindow() { + private MainWindow createMainWindow(RunningLauncherProfile runningLauncherProfile) { return new PerMainWindowInjector( application, metadata, settings, - mojangApi, + minecraftInstallation, + runningLauncherProfile, biomeProfileDirectory, this::createViewerFacade, threadMaster).getMainWindow(); @@ -93,7 +111,14 @@ private MainWindow createMainWindow() { @CalledOnlyBy(AmidstThread.EDT) private ProfileSelectWindow createProfileSelectWindow() { - return new ProfileSelectWindow(application, metadata, threadMaster.getWorkerExecutor(), mojangApi, settings); + return new ProfileSelectWindow( + application, + metadata, + threadMaster.getWorkerExecutor(), + versionListProvider, + minecraftInstallation, + launcherProfileRunner, + settings); } @CalledOnlyBy(AmidstThread.EDT) diff --git a/src/main/java/amidst/gui/main/MainWindowDialogs.java b/src/main/java/amidst/gui/main/MainWindowDialogs.java index 9b4754c3c..c521057d5 100644 --- a/src/main/java/amidst/gui/main/MainWindowDialogs.java +++ b/src/main/java/amidst/gui/main/MainWindowDialogs.java @@ -12,7 +12,7 @@ import amidst.documentation.CalledOnlyBy; import amidst.documentation.NotThreadSafe; import amidst.logging.AmidstMessageBox; -import amidst.mojangapi.MojangApi; +import amidst.mojangapi.RunningLauncherProfile; import amidst.mojangapi.world.WorldSeed; import amidst.mojangapi.world.WorldType; import amidst.mojangapi.world.export.WorldExporterConfiguration; @@ -21,13 +21,13 @@ @NotThreadSafe public class MainWindowDialogs { private final AmidstSettings settings; - private final MojangApi mojangApi; + private final RunningLauncherProfile runningLauncherProfile; private final JFrame frame; @CalledOnlyBy(AmidstThread.EDT) - public MainWindowDialogs(AmidstSettings settings, MojangApi mojangApi, JFrame frame) { + public MainWindowDialogs(AmidstSettings settings, RunningLauncherProfile runningLauncherProfile, JFrame frame) { this.settings = settings; - this.mojangApi = mojangApi; + this.runningLauncherProfile = runningLauncherProfile; this.frame = frame; } @@ -52,7 +52,7 @@ public File askForSaveGame() { @CalledOnlyBy(AmidstThread.EDT) private JFileChooser createSaveGameFileChooser() { - JFileChooser result = new JFileChooser(mojangApi.getSaves()); + JFileChooser result = new JFileChooser(runningLauncherProfile.getLauncherProfile().getSaves()); result.setFileFilter(new LevelFileFilter()); result.setAcceptAllFileFilterUsed(false); result.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES); diff --git a/src/main/java/amidst/gui/main/PerMainWindowInjector.java b/src/main/java/amidst/gui/main/PerMainWindowInjector.java index ac22edc62..1774e3ec9 100644 --- a/src/main/java/amidst/gui/main/PerMainWindowInjector.java +++ b/src/main/java/amidst/gui/main/PerMainWindowInjector.java @@ -18,7 +18,8 @@ import amidst.gui.main.viewer.ViewerFacade; import amidst.gui.seedsearcher.SeedSearcher; import amidst.gui.seedsearcher.SeedSearcherWindow; -import amidst.mojangapi.MojangApi; +import amidst.mojangapi.RunningLauncherProfile; +import amidst.mojangapi.file.MinecraftInstallation; import amidst.mojangapi.world.World; import amidst.settings.biomeprofile.BiomeProfileDirectory; import amidst.threading.ThreadMaster; @@ -26,10 +27,17 @@ @NotThreadSafe public class PerMainWindowInjector { @CalledOnlyBy(AmidstThread.EDT) - private static String createVersionString(AmidstMetaData metadata, MojangApi mojangApi) { - return metadata.getVersion().createLongVersionString() + " - Selected Profile: " + mojangApi.getProfileName() - + " - Minecraft Version " + mojangApi.getVersionId() + " (recognised: " - + mojangApi.getRecognisedVersionName() + ")"; + private static String createVersionString(AmidstMetaData metadata, RunningLauncherProfile runningLauncherProfile) { + return new StringBuilder() + .append(metadata.getVersion().createLongVersionString()) + .append(" - Selected Profile: ") + .append(runningLauncherProfile.getLauncherProfile().getProfileName()) + .append(" - Minecraft Version ") + .append(runningLauncherProfile.getLauncherProfile().getVersionId()) + .append(" (recognised: ") + .append(runningLauncherProfile.getRecognisedVersion().getName()) + .append(")") + .toString(); } private final Factory2 viewerFacadeFactory; @@ -50,18 +58,20 @@ public PerMainWindowInjector( Application application, AmidstMetaData metadata, AmidstSettings settings, - MojangApi mojangApi, + MinecraftInstallation minecraftInstallation, + RunningLauncherProfile runningLauncherProfile, BiomeProfileDirectory biomeProfileDirectory, Factory2 viewerFacadeFactory, ThreadMaster threadMaster) { this.viewerFacadeFactory = viewerFacadeFactory; - this.versionString = createVersionString(metadata, mojangApi); + this.versionString = createVersionString(metadata, runningLauncherProfile); this.frame = new JFrame(); this.contentPane = frame.getContentPane(); this.viewerFacadeReference = new AtomicReference<>(); - this.dialogs = new MainWindowDialogs(settings, mojangApi, frame); + this.dialogs = new MainWindowDialogs(settings, runningLauncherProfile, frame); this.worldSwitcher = new WorldSwitcher( - mojangApi, + minecraftInstallation, + runningLauncherProfile, this::createViewerFacade, threadMaster, frame, @@ -70,7 +80,10 @@ public PerMainWindowInjector( dialogs, this::getMenuBar); if (FeatureToggles.SEED_SEARCH) { - this.seedSearcher = new SeedSearcher(dialogs, mojangApi, threadMaster.getWorkerExecutor()); + this.seedSearcher = new SeedSearcher( + dialogs, + runningLauncherProfile.createSilentPlayerlessCopy(), + threadMaster.getWorkerExecutor()); this.seedSearcherWindow = new SeedSearcherWindow(metadata, dialogs, worldSwitcher, seedSearcher); } else { this.seedSearcher = null; diff --git a/src/main/java/amidst/gui/main/UpdateInformationRetriever.java b/src/main/java/amidst/gui/main/UpdateInformationRetriever.java index 11bea5170..a8c5daca0 100644 --- a/src/main/java/amidst/gui/main/UpdateInformationRetriever.java +++ b/src/main/java/amidst/gui/main/UpdateInformationRetriever.java @@ -1,34 +1,20 @@ package amidst.gui.main; import java.io.IOException; -import java.io.Reader; - -import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; import amidst.documentation.Immutable; import amidst.documentation.NotNull; -import amidst.mojangapi.file.URIUtils; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; @Immutable public enum UpdateInformationRetriever { ; private static final String UPDATE_INFORMATION_JSON_URL = "https://toolbox4minecraft.github.io/amidst/api/update-information.json"; - private static final Gson GSON = new Gson(); @NotNull - public static UpdateInformationJson retrieve() throws IOException { - try (Reader theReader = URIUtils.newReader(UPDATE_INFORMATION_JSON_URL)) { - UpdateInformationJson result = GSON.fromJson(theReader, UpdateInformationJson.class); - if (result != null) { - return result; - } else { - throw new IOException("result was null"); - } - } catch (JsonSyntaxException | JsonIOException e) { - throw new IOException(e); - } + public static UpdateInformationJson retrieve() throws FormatException, IOException { + return JsonReader.readLocation(UPDATE_INFORMATION_JSON_URL, UpdateInformationJson.class); } } diff --git a/src/main/java/amidst/gui/main/WorldSwitcher.java b/src/main/java/amidst/gui/main/WorldSwitcher.java index 570c93314..485db0a0f 100644 --- a/src/main/java/amidst/gui/main/WorldSwitcher.java +++ b/src/main/java/amidst/gui/main/WorldSwitcher.java @@ -16,19 +16,21 @@ import amidst.gui.main.menu.AmidstMenu; import amidst.gui.main.viewer.ViewerFacade; import amidst.logging.AmidstLogger; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.MojangApiParsingException; +import amidst.mojangapi.RunningLauncherProfile; +import amidst.mojangapi.file.MinecraftInstallation; import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.world.World; import amidst.mojangapi.world.WorldSeed; import amidst.mojangapi.world.WorldType; import amidst.mojangapi.world.player.MovablePlayerList; import amidst.mojangapi.world.player.WorldPlayerType; +import amidst.parsing.FormatException; import amidst.threading.ThreadMaster; @NotThreadSafe public class WorldSwitcher { - private final MojangApi mojangApi; + private final MinecraftInstallation minecraftInstallation; + private final RunningLauncherProfile runningLauncherProfile; private final Factory1 viewerFacadeFactory; private final ThreadMaster threadMaster; private final JFrame frame; @@ -39,7 +41,8 @@ public class WorldSwitcher { @CalledOnlyBy(AmidstThread.EDT) public WorldSwitcher( - MojangApi mojangApi, + MinecraftInstallation minecraftInstallation, + RunningLauncherProfile runningLauncherProfile, Factory1 viewerFacadeFactory, ThreadMaster threadMaster, JFrame frame, @@ -47,7 +50,8 @@ public WorldSwitcher( AtomicReference viewerFacadeReference, MainWindowDialogs dialogs, Supplier menuBarSupplier) { - this.mojangApi = mojangApi; + this.minecraftInstallation = minecraftInstallation; + this.runningLauncherProfile = runningLauncherProfile; this.viewerFacadeFactory = viewerFacadeFactory; this.threadMaster = threadMaster; this.frame = frame; @@ -60,7 +64,8 @@ public WorldSwitcher( @CalledOnlyBy(AmidstThread.EDT) public void displayWorld(WorldSeed worldSeed, WorldType worldType) { try { - setWorld(mojangApi.createWorldFromSeed(worldSeed, worldType)); + clearViewerFacade(); + setWorld(runningLauncherProfile.createWorldFromSeed(worldSeed, worldType)); } catch (IllegalStateException | MinecraftInterfaceException e) { AmidstLogger.warn(e); dialogs.displayError(e); @@ -70,16 +75,28 @@ public void displayWorld(WorldSeed worldSeed, WorldType worldType) { @CalledOnlyBy(AmidstThread.EDT) public void displayWorld(File file) { try { - setWorld(mojangApi.createWorldFromSaveGame(file)); - } catch (IllegalStateException | MinecraftInterfaceException | IOException | MojangApiParsingException e) { + clearViewerFacade(); + setWorld(runningLauncherProfile.createWorldFromSaveGame(minecraftInstallation.newSaveGame(file))); + } catch (IllegalStateException | MinecraftInterfaceException | IOException | FormatException e) { AmidstLogger.warn(e); dialogs.displayError(e); } } + @CalledOnlyBy(AmidstThread.EDT) + private void clearViewerFacade() { + threadMaster.clearOnRepaintTick(); + threadMaster.clearOnFragmentLoadTick(); + ViewerFacade viewerFacade = viewerFacadeReference.getAndSet(null); + if (viewerFacade != null) { + contentPane.remove(viewerFacade.getComponent()); + viewerFacade.dispose(); + } + menuBarSupplier.get().clear(); + } + @CalledOnlyBy(AmidstThread.EDT) private void setWorld(World world) { - clearViewerFacade(); if (decideWorldPlayerType(world.getMovablePlayerList())) { setViewerFacade(viewerFacadeFactory.create(world)); } else { @@ -114,18 +131,6 @@ private void setViewerFacade(ViewerFacade viewerFacade) { viewerFacadeReference.set(viewerFacade); } - @CalledOnlyBy(AmidstThread.EDT) - private void clearViewerFacade() { - threadMaster.clearOnRepaintTick(); - threadMaster.clearOnFragmentLoadTick(); - ViewerFacade viewerFacade = viewerFacadeReference.getAndSet(null); - if (viewerFacade != null) { - contentPane.remove(viewerFacade.getComponent()); - viewerFacade.dispose(); - } - menuBarSupplier.get().clear(); - } - @CalledOnlyBy(AmidstThread.EDT) public void clearWorld() { clearViewerFacade(); diff --git a/src/main/java/amidst/gui/main/viewer/ViewerFacade.java b/src/main/java/amidst/gui/main/viewer/ViewerFacade.java index 7f0e7c6a1..fceab036e 100644 --- a/src/main/java/amidst/gui/main/viewer/ViewerFacade.java +++ b/src/main/java/amidst/gui/main/viewer/ViewerFacade.java @@ -94,6 +94,7 @@ public void dispose() { graph.dispose(); zoom.skipFading(); zoom.reset(); + world.dispose(); } @CalledOnlyBy(AmidstThread.EDT) diff --git a/src/main/java/amidst/gui/profileselect/LocalProfileComponent.java b/src/main/java/amidst/gui/profileselect/LocalProfileComponent.java index eb412c021..426a8f014 100644 --- a/src/main/java/amidst/gui/profileselect/LocalProfileComponent.java +++ b/src/main/java/amidst/gui/profileselect/LocalProfileComponent.java @@ -1,7 +1,8 @@ package amidst.gui.profileselect; -import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Locale; +import java.util.Optional; import java.util.regex.Pattern; import amidst.Application; @@ -10,64 +11,68 @@ import amidst.documentation.NotThreadSafe; import amidst.logging.AmidstLogger; import amidst.logging.AmidstMessageBox; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.directory.ProfileDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.file.json.launcherprofiles.LauncherProfileJson; +import amidst.mojangapi.LauncherProfileRunner; +import amidst.mojangapi.RunningLauncherProfile; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.file.UnresolvedLauncherProfile; +import amidst.mojangapi.file.VersionListProvider; import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; +import amidst.parsing.FormatException; import amidst.threading.WorkerExecutor; @NotThreadSafe public class LocalProfileComponent extends ProfileComponent { private final Application application; private final WorkerExecutor workerExecutor; - private final MojangApi mojangApi; - private final LauncherProfileJson profile; + private final VersionListProvider versionListProvider; + private final LauncherProfileRunner launcherProfileRunner; + private final UnresolvedLauncherProfile unresolvedProfile; - private volatile boolean isSearching = false; - private volatile boolean failedSearching = false; + private volatile boolean isResolving = false; + private volatile boolean failedResolving = false; private volatile boolean isLoading = false; private volatile boolean failedLoading = false; - private volatile VersionDirectory versionDirectory; - private volatile ProfileDirectory profileDirectory; + private volatile LauncherProfile resolvedProfile; @CalledOnlyBy(AmidstThread.EDT) public LocalProfileComponent( Application application, WorkerExecutor workerExecutor, - MojangApi mojangApi, - LauncherProfileJson profile) { + VersionListProvider versionListProvider, + LauncherProfileRunner launcherProfileRunner, + UnresolvedLauncherProfile unresolvedProfile) { this.application = application; - this.mojangApi = mojangApi; this.workerExecutor = workerExecutor; - this.profile = profile; + this.versionListProvider = versionListProvider; + this.launcherProfileRunner = launcherProfileRunner; + this.unresolvedProfile = unresolvedProfile; initComponent(); - initDirectoriesLater(); } @CalledOnlyBy(AmidstThread.EDT) - private void initDirectoriesLater() { - isSearching = true; + @Override + public void resolveLater() { + resolvedProfile = null; + isResolving = true; repaintComponent(); - workerExecutor.run(this::tryFind, this::findFinished); + workerExecutor.run(this::tryResolve, this::resolveFinished); } @CalledOnlyBy(AmidstThread.WORKER) - private boolean tryFind() { + private Optional tryResolve() { try { - profileDirectory = profile.createValidProfileDirectory(mojangApi); - versionDirectory = profile.createValidVersionDirectory(mojangApi); - return true; - } catch (FileNotFoundException e) { + return Optional.of(unresolvedProfile.resolve(versionListProvider.getRemoteOrElseLocal())); + } catch (FormatException | IOException e) { AmidstLogger.warn(e); - return false; + return Optional.empty(); } } @CalledOnlyBy(AmidstThread.EDT) - private void findFinished(boolean isSuccessful) { - isSearching = false; - failedSearching = !isSuccessful; + private void resolveFinished(Optional launcherProfile) { + isResolving = false; + failedResolving = !launcherProfile.isPresent(); + resolvedProfile = launcherProfile.orElse(null); repaintComponent(); } @@ -80,12 +85,12 @@ public void load() { } @CalledOnlyBy(AmidstThread.WORKER) - private boolean tryLoad() { + private Optional tryLoad() { // TODO: Replace with proper handling for modded profiles. try { AmidstLogger.info( - "using minecraft launcher profile '" + getProfileName() + "' with versionId '" + getVersionName() - + "'"); + "using minecraft launcher profile '" + resolvedProfile.getProfileName() + "' with versionId '" + + resolvedProfile.getVersionId() + "'"); String possibleModProfiles = ".*(optifine|forge).*"; if (Pattern.matches(possibleModProfiles, getVersionName().toLowerCase(Locale.ENGLISH))) { @@ -94,38 +99,37 @@ private boolean tryLoad() { AmidstMessageBox.displayError( "Error", "Amidst does not support modded Minecraft profiles! Please select or create an unmodded Minecraft profile via the Minecraft Launcher."); - return false; + return Optional.empty(); } - mojangApi.set(getProfileName(), profileDirectory, versionDirectory); - return true; + return Optional.of(launcherProfileRunner.run(resolvedProfile)); } catch (LocalMinecraftInterfaceCreationException e) { AmidstLogger.error(e); AmidstMessageBox.displayError("Error", e); - return false; + return Optional.empty(); } } @CalledOnlyBy(AmidstThread.EDT) - private void loadFinished(boolean isSuccessful) { + private void loadFinished(Optional runningLauncherProfile) { isLoading = false; - failedLoading = !isSuccessful; + failedLoading = !runningLauncherProfile.isPresent(); repaintComponent(); - if (isSuccessful) { - application.displayMainWindow(); + if (runningLauncherProfile.isPresent()) { + application.displayMainWindow(runningLauncherProfile.get()); } } @CalledOnlyBy(AmidstThread.EDT) @Override - protected boolean isSearching() { - return isSearching; + protected boolean isResolving() { + return isResolving; } @CalledOnlyBy(AmidstThread.EDT) @Override - protected boolean failedSearching() { - return failedSearching; + protected boolean failedResolving() { + return failedResolving; } @CalledOnlyBy(AmidstThread.EDT) @@ -143,20 +147,20 @@ protected boolean failedLoading() { @CalledOnlyBy(AmidstThread.EDT) @Override protected boolean isReadyToLoad() { - return !isSearching && !failedSearching; + return resolvedProfile != null; } @CalledOnlyBy(AmidstThread.EDT) @Override protected String getProfileName() { - return profile.getName(); + return unresolvedProfile.getName(); } @CalledOnlyBy(AmidstThread.EDT) @Override protected String getVersionName() { - if (isReadyToLoad()) { - return versionDirectory.getVersionId(); + if (resolvedProfile != null) { + return resolvedProfile.getVersionId(); } else { return ""; } diff --git a/src/main/java/amidst/gui/profileselect/ProfileComponent.java b/src/main/java/amidst/gui/profileselect/ProfileComponent.java index 1e3df1998..3b0d03337 100644 --- a/src/main/java/amidst/gui/profileselect/ProfileComponent.java +++ b/src/main/java/amidst/gui/profileselect/ProfileComponent.java @@ -167,10 +167,10 @@ private String getLoadingStatus() { return "failed loading"; } else if (isLoading()) { return "loading"; - } else if (failedSearching()) { + } else if (failedResolving()) { return "not found"; - } else if (isSearching()) { - return "searching"; + } else if (isResolving()) { + return "resolving"; } else { return "found"; } @@ -182,9 +182,9 @@ private BufferedImage getIcon() { return INACTIVE_ICON; } else if (isLoading()) { return LOADING_ICON; - } else if (failedSearching()) { + } else if (failedResolving()) { return INACTIVE_ICON; - } else if (isSearching()) { + } else if (isResolving()) { return INACTIVE_ICON; } else { return ACTIVE_ICON; @@ -197,7 +197,7 @@ private Color getBackgroundColor() { return FAILED_BG_COLOR; } else if (isLoading()) { return LOADING_BG_COLOR; - } else if (failedSearching()) { + } else if (failedResolving()) { return FAILED_BG_COLOR; } else if (isSelected) { return SELECTED_BG_COLOR; @@ -210,10 +210,10 @@ private Color getBackgroundColor() { protected abstract void load(); @CalledOnlyBy(AmidstThread.EDT) - protected abstract boolean isSearching(); + protected abstract boolean isResolving(); @CalledOnlyBy(AmidstThread.EDT) - protected abstract boolean failedSearching(); + protected abstract boolean failedResolving(); @CalledOnlyBy(AmidstThread.EDT) protected abstract boolean isLoading(); @@ -229,4 +229,7 @@ private Color getBackgroundColor() { @CalledOnlyBy(AmidstThread.EDT) protected abstract String getVersionName(); + + @CalledOnlyBy(AmidstThread.EDT) + public abstract void resolveLater(); } diff --git a/src/main/java/amidst/gui/profileselect/ProfileSelectPanel.java b/src/main/java/amidst/gui/profileselect/ProfileSelectPanel.java index 1e3a70e48..cbe50626b 100644 --- a/src/main/java/amidst/gui/profileselect/ProfileSelectPanel.java +++ b/src/main/java/amidst/gui/profileselect/ProfileSelectPanel.java @@ -261,4 +261,11 @@ private boolean isLoading() { } return false; } + + @CalledOnlyBy(AmidstThread.EDT) + public void resolveAllLater() { + if (!isLoading()) { + profileComponents.forEach(ProfileComponent::resolveLater); + } + } } diff --git a/src/main/java/amidst/gui/profileselect/ProfileSelectWindow.java b/src/main/java/amidst/gui/profileselect/ProfileSelectWindow.java index ee44112e2..61417f6ee 100644 --- a/src/main/java/amidst/gui/profileselect/ProfileSelectWindow.java +++ b/src/main/java/amidst/gui/profileselect/ProfileSelectWindow.java @@ -4,6 +4,7 @@ import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.IOException; +import java.util.List; import javax.swing.JFrame; import javax.swing.JLabel; @@ -19,10 +20,11 @@ import amidst.documentation.NotThreadSafe; import amidst.logging.AmidstLogger; import amidst.logging.AmidstMessageBox; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.launcherprofiles.LauncherProfileJson; -import amidst.mojangapi.file.json.launcherprofiles.LauncherProfilesJson; +import amidst.mojangapi.LauncherProfileRunner; +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.mojangapi.file.UnresolvedLauncherProfile; +import amidst.mojangapi.file.VersionListProvider; +import amidst.parsing.FormatException; import amidst.threading.WorkerExecutor; import net.miginfocom.swing.MigLayout; @@ -33,23 +35,31 @@ public class ProfileSelectWindow { private final Application application; private final AmidstMetaData metadata; private final WorkerExecutor workerExecutor; - private final MojangApi mojangApi; + private final VersionListProvider versionListProvider; + private final MinecraftInstallation minecraftInstallation; + private final LauncherProfileRunner launcherProfileRunner; private final AmidstSettings settings; private final JFrame frame; private final ProfileSelectPanel profileSelectPanel; + private volatile boolean isDisposed = false; + @CalledOnlyBy(AmidstThread.EDT) public ProfileSelectWindow( Application application, AmidstMetaData metadata, WorkerExecutor workerExecutor, - MojangApi mojangApi, + VersionListProvider versionListProvider, + MinecraftInstallation minecraftInstallation, + LauncherProfileRunner launcherProfileRunner, AmidstSettings settings) { this.application = application; this.metadata = metadata; this.workerExecutor = workerExecutor; - this.mojangApi = mojangApi; + this.versionListProvider = versionListProvider; + this.minecraftInstallation = minecraftInstallation; + this.launcherProfileRunner = launcherProfileRunner; this.settings = settings; this.profileSelectPanel = new ProfileSelectPanel(settings.lastProfile, "Scanning..."); this.frame = createFrame(); @@ -112,34 +122,48 @@ private void scanAndLoadProfilesLater() { } @CalledOnlyBy(AmidstThread.WORKER) - private LauncherProfilesJson scanAndLoadProfiles() throws MojangApiParsingException, IOException { + private List scanAndLoadProfiles() throws FormatException, IOException { AmidstLogger.info("Scanning for profiles."); - LauncherProfilesJson launcherProfile = mojangApi.getDotMinecraftDirectory().readLauncherProfilesJson(); + List launcherProfiles = minecraftInstallation.readLauncherProfiles(); AmidstLogger.info("Successfully loaded profile list."); - return launcherProfile; + return launcherProfiles; } @CalledOnlyBy(AmidstThread.EDT) - private void displayProfiles(LauncherProfilesJson launcherProfile) { - createProfileComponentsIfNecessary(launcherProfile); + private void displayProfiles(List launcherProfiles) { + createProfileComponentsIfNecessary(launcherProfiles); restoreSelection(); frame.pack(); } @CalledOnlyBy(AmidstThread.EDT) - private void createProfileComponentsIfNecessary(LauncherProfilesJson launcherProfile) { - if (launcherProfile.getProfiles().isEmpty()) { + private void createProfileComponentsIfNecessary(List launcherProfiles) { + if (launcherProfiles.isEmpty()) { AmidstLogger.warn("No profiles found in launcher_profiles.json"); profileSelectPanel.setEmptyMessage("No profiles found"); } else { - createProfileComponents(launcherProfile); + createProfileComponents(launcherProfiles); + versionListProvider.onDownloadRemoteFinished(this::resolveAllLater); + profileSelectPanel.resolveAllLater(); + } + } + + private void resolveAllLater() { + if (!isDisposed) { + profileSelectPanel.resolveAllLater(); } } @CalledOnlyBy(AmidstThread.EDT) - private void createProfileComponents(LauncherProfilesJson launcherProfile) { - for (LauncherProfileJson profile : launcherProfile.getProfiles()) { - profileSelectPanel.addProfile(new LocalProfileComponent(application, workerExecutor, mojangApi, profile)); + private void createProfileComponents(List launcherProfiles) { + for (UnresolvedLauncherProfile profile : launcherProfiles) { + profileSelectPanel.addProfile( + new LocalProfileComponent( + application, + workerExecutor, + versionListProvider, + launcherProfileRunner, + profile)); } } @@ -162,6 +186,7 @@ private void scanAndLoadProfilesFailed(Exception e) { @CalledOnlyBy(AmidstThread.EDT) public void dispose() { + isDisposed = true; frame.dispose(); } } diff --git a/src/main/java/amidst/gui/seedsearcher/SeedSearcher.java b/src/main/java/amidst/gui/seedsearcher/SeedSearcher.java index 917ebddb6..f1e4fa858 100644 --- a/src/main/java/amidst/gui/seedsearcher/SeedSearcher.java +++ b/src/main/java/amidst/gui/seedsearcher/SeedSearcher.java @@ -7,7 +7,7 @@ import amidst.documentation.NotThreadSafe; import amidst.gui.main.MainWindowDialogs; import amidst.logging.AmidstLogger; -import amidst.mojangapi.MojangApi; +import amidst.mojangapi.RunningLauncherProfile; import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.world.World; import amidst.mojangapi.world.WorldSeed; @@ -18,16 +18,19 @@ @NotThreadSafe public class SeedSearcher { private final MainWindowDialogs dialogs; - private final MojangApi mojangApi; + private final RunningLauncherProfile runningLauncherProfile; private final WorkerExecutor workerExecutor; private volatile boolean isSearching = false; private volatile boolean isStopRequested = false; @CalledOnlyBy(AmidstThread.EDT) - public SeedSearcher(MainWindowDialogs dialogs, MojangApi mojangApi, WorkerExecutor workerExecutor) { + public SeedSearcher( + MainWindowDialogs dialogs, + RunningLauncherProfile runningLauncherProfile, + WorkerExecutor workerExecutor) { this.dialogs = dialogs; - this.mojangApi = mojangApi.createSilentPlayerlessCopy(); + this.runningLauncherProfile = runningLauncherProfile; this.workerExecutor = workerExecutor; } @@ -87,13 +90,16 @@ private void doSearch(ProgressReporter reporter, SeedSearcherConfigur @CalledOnlyBy(AmidstThread.WORKER) private void doSearchOne(ProgressReporter reporter, SeedSearcherConfiguration configuration) - throws MinecraftInterfaceException { + throws IllegalStateException, + MinecraftInterfaceException { while (!isStopRequested) { - World world = mojangApi.createWorldFromSeed(WorldSeed.random(), configuration.getWorldType()); + World world = runningLauncherProfile.createWorldFromSeed(WorldSeed.random(), configuration.getWorldType()); if (configuration.getWorldFilter().isValid(world)) { reporter.report(world.getWorldSeed()); + world.dispose(); break; } + world.dispose(); } } } diff --git a/src/main/java/amidst/mojangapi/LauncherProfileRunner.java b/src/main/java/amidst/mojangapi/LauncherProfileRunner.java new file mode 100644 index 000000000..43d6039bb --- /dev/null +++ b/src/main/java/amidst/mojangapi/LauncherProfileRunner.java @@ -0,0 +1,19 @@ +package amidst.mojangapi; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; +import amidst.mojangapi.world.WorldBuilder; + +@Immutable +public class LauncherProfileRunner { + private final WorldBuilder worldBuilder; + + public LauncherProfileRunner(WorldBuilder worldBuilder) { + this.worldBuilder = worldBuilder; + } + + public RunningLauncherProfile run(LauncherProfile launcherProfile) throws LocalMinecraftInterfaceCreationException { + return RunningLauncherProfile.from(worldBuilder, launcherProfile); + } +} diff --git a/src/main/java/amidst/mojangapi/MojangApi.java b/src/main/java/amidst/mojangapi/MojangApi.java deleted file mode 100644 index 786a1f8ad..000000000 --- a/src/main/java/amidst/mojangapi/MojangApi.java +++ /dev/null @@ -1,182 +0,0 @@ -package amidst.mojangapi; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; - -import amidst.documentation.NotNull; -import amidst.documentation.ThreadSafe; -import amidst.mojangapi.file.FilenameFactory; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.directory.DotMinecraftDirectory; -import amidst.mojangapi.file.directory.ProfileDirectory; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.file.json.JsonReader; -import amidst.mojangapi.file.json.versionlist.VersionListJson; -import amidst.mojangapi.minecraftinterface.MinecraftInterface; -import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; -import amidst.mojangapi.minecraftinterface.RecognisedVersion; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; -import amidst.mojangapi.world.World; -import amidst.mojangapi.world.WorldBuilder; -import amidst.mojangapi.world.WorldSeed; -import amidst.mojangapi.world.WorldType; - -@ThreadSafe -public class MojangApi { - private static final String UNKNOWN_VERSION_ID = "unknown"; - private static final String UNKNOWN_PROFILE_NAME = "unknown"; - - private final WorldBuilder worldBuilder; - private final DotMinecraftDirectory dotMinecraftDirectory; - - private volatile VersionListJson versionList; - private volatile ProfileDirectory profileDirectory; - private volatile VersionDirectory versionDirectory; - private volatile MinecraftInterface minecraftInterface; - private volatile String profileName; - - public MojangApi(WorldBuilder worldBuilder, DotMinecraftDirectory dotMinecraftDirectory) { - this.worldBuilder = worldBuilder; - this.dotMinecraftDirectory = dotMinecraftDirectory; - } - - public DotMinecraftDirectory getDotMinecraftDirectory() { - return dotMinecraftDirectory; - } - - @NotNull - public VersionListJson getVersionList() throws FileNotFoundException { - VersionListJson versionList = this.versionList; - if (versionList == null) { - synchronized (this) { - versionList = this.versionList; - if (versionList == null) { - versionList = JsonReader.readRemoteOrLocalVersionList(); - this.versionList = versionList; - } - } - } - return versionList; - } - - public void set(String profileName, ProfileDirectory profileDirectory, VersionDirectory versionDirectory) - throws LocalMinecraftInterfaceCreationException { - this.profileName = profileName; - this.profileDirectory = profileDirectory; - this.versionDirectory = versionDirectory; - if (versionDirectory != null) { - try { - this.minecraftInterface = versionDirectory.createLocalMinecraftInterface(); - } catch (LocalMinecraftInterfaceCreationException e) { - this.minecraftInterface = null; - throw e; - } - } else { - this.minecraftInterface = null; - } - } - - public VersionDirectory createVersionDirectory(String versionId) { - File versions = dotMinecraftDirectory.getVersions(); - File jar = FilenameFactory.getClientJarFile(versions, versionId); - File json = FilenameFactory.getClientJsonFile(versions, versionId); - return doCreateVersionDirectory(versionId, jar, json); - } - - public VersionDirectory createVersionDirectory(File jar, File json) { - return doCreateVersionDirectory(UNKNOWN_VERSION_ID, jar, json); - } - - private VersionDirectory doCreateVersionDirectory(String versionId, File jar, File json) { - return new VersionDirectory(dotMinecraftDirectory, versionId, jar, json); - } - - public MojangApi createSilentPlayerlessCopy() { - MojangApi result = new MojangApi(WorldBuilder.createSilentPlayerless(), dotMinecraftDirectory); - try { - result.set(profileName, profileDirectory, versionDirectory); - } catch (LocalMinecraftInterfaceCreationException e) { - // This will not happen normally, because we already successfully - // created the same LocalMinecraftInterface once before. - throw new RuntimeException("exception while duplicating the MojangApi", e); - } - return result; - } - - public File getSaves() { - ProfileDirectory profileDirectory = this.profileDirectory; - if (profileDirectory != null) { - return profileDirectory.getSaves(); - } else { - return dotMinecraftDirectory.getSaves(); - } - } - - public boolean canCreateWorld() { - return minecraftInterface != null; - } - - /** - * Due to the limitation of the minecraft interface, you can only work with - * one world at a time. Creating a new world will break all previously - * created world objects. - */ - public World createWorldFromSeed(WorldSeed worldSeed, WorldType worldType) - throws IllegalStateException, - MinecraftInterfaceException { - MinecraftInterface minecraftInterface = this.minecraftInterface; - if (minecraftInterface != null) { - return worldBuilder.fromSeed(minecraftInterface, worldSeed, worldType); - } else { - throw new IllegalStateException("cannot create a world without a minecraft interface"); - } - } - - /** - * Due to the limitation of the minecraft interface, you can only work with - * one world at a time. Creating a new world will break all previously - * created world objects. - */ - public World createWorldFromSaveGame(File file) - throws FileNotFoundException, - IOException, - IllegalStateException, - MinecraftInterfaceException, - MojangApiParsingException { - MinecraftInterface minecraftInterface = this.minecraftInterface; - if (minecraftInterface != null) { - return worldBuilder.fromSaveGame(minecraftInterface, SaveDirectory.from(file)); - } else { - throw new IllegalStateException("cannot create a world without a minecraft interface"); - } - } - - public String getVersionId() { - VersionDirectory versionDirectory = this.versionDirectory; - if (versionDirectory != null) { - return versionDirectory.getVersionId(); - } else { - return UNKNOWN_VERSION_ID; - } - } - - public String getRecognisedVersionName() { - MinecraftInterface minecraftInterface = this.minecraftInterface; - if (minecraftInterface != null) { - return minecraftInterface.getRecognisedVersion().getName(); - } else { - return RecognisedVersion.UNKNOWN.getName(); - } - } - - public String getProfileName() { - String profileName = this.profileName; - if (profileName != null) { - return profileName; - } else { - return UNKNOWN_PROFILE_NAME; - } - } -} diff --git a/src/main/java/amidst/mojangapi/MojangApiBuilder.java b/src/main/java/amidst/mojangapi/MojangApiBuilder.java deleted file mode 100644 index 4d0454293..000000000 --- a/src/main/java/amidst/mojangapi/MojangApiBuilder.java +++ /dev/null @@ -1,73 +0,0 @@ -package amidst.mojangapi; - -import java.io.File; - -import amidst.CommandLineParameters; -import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.DotMinecraftDirectoryFinder; -import amidst.mojangapi.file.DotMinecraftDirectoryNotFoundException; -import amidst.mojangapi.file.directory.DotMinecraftDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; -import amidst.mojangapi.world.WorldBuilder; - -@Immutable -public class MojangApiBuilder { - private final WorldBuilder worldBuilder; - private final CommandLineParameters parameters; - - public MojangApiBuilder(WorldBuilder worldBuilder, CommandLineParameters parameters) { - this.worldBuilder = worldBuilder; - this.parameters = parameters; - } - - @NotNull - public MojangApi construct() - throws DotMinecraftDirectoryNotFoundException, - LocalMinecraftInterfaceCreationException { - DotMinecraftDirectory dotMinecraftDirectory = createDotMinecraftDirectory(); - if (dotMinecraftDirectory.isValid()) { - AmidstLogger.info( - "using '.minecraft' directory at: '" + dotMinecraftDirectory.getRoot() + "', libraries: '" - + dotMinecraftDirectory.getLibraries() + "'"); - } else { - throw new DotMinecraftDirectoryNotFoundException( - "invalid '.minecraft' directory at: '" + dotMinecraftDirectory.getRoot() + "', libraries: '" - + dotMinecraftDirectory.getLibraries() + "'"); - } - MojangApi result = new MojangApi(worldBuilder, dotMinecraftDirectory); - result.set(null, null, createVersionDirectory(result)); - return result; - } - - @NotNull - private DotMinecraftDirectory createDotMinecraftDirectory() { - File dotMinecraftDirectory = DotMinecraftDirectoryFinder.find(parameters.dotMinecraftDirectory); - if (parameters.minecraftLibrariesDirectory != null) { - return new DotMinecraftDirectory(dotMinecraftDirectory, new File(parameters.minecraftLibrariesDirectory)); - } else { - return new DotMinecraftDirectory(dotMinecraftDirectory); - } - } - - private VersionDirectory createVersionDirectory(MojangApi mojangApi) { - if (parameters.minecraftJarFile != null && parameters.minecraftJsonFile != null) { - File jar = new File(parameters.minecraftJarFile); - File json = new File(parameters.minecraftJsonFile); - VersionDirectory result = mojangApi.createVersionDirectory(jar, json); - if (result.isValid()) { - AmidstLogger.info( - "using minecraft version directory. versionId: '" + result.getVersionId() + "', jar file: '" - + result.getJar() + "', json file: '" + result.getJson() + "'"); - return result; - } else { - AmidstLogger.warn( - "invalid minecraft version directory. versionId: '" + result.getVersionId() + "', jar file: '" - + result.getJar() + "', json file: '" + result.getJson() + "'"); - } - } - return null; - } -} diff --git a/src/main/java/amidst/mojangapi/RunningLauncherProfile.java b/src/main/java/amidst/mojangapi/RunningLauncherProfile.java new file mode 100644 index 000000000..58819ff98 --- /dev/null +++ b/src/main/java/amidst/mojangapi/RunningLauncherProfile.java @@ -0,0 +1,103 @@ +package amidst.mojangapi; + +import java.io.IOException; + +import amidst.documentation.ThreadSafe; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.file.SaveGame; +import amidst.mojangapi.minecraftinterface.MinecraftInterface; +import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; +import amidst.mojangapi.minecraftinterface.RecognisedVersion; +import amidst.mojangapi.minecraftinterface.local.DefaultClassTranslator; +import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterface; +import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; +import amidst.mojangapi.world.World; +import amidst.mojangapi.world.WorldBuilder; +import amidst.mojangapi.world.WorldSeed; +import amidst.mojangapi.world.WorldType; + +@ThreadSafe +public class RunningLauncherProfile { + public static RunningLauncherProfile from(WorldBuilder worldBuilder, LauncherProfile launcherProfile) + throws LocalMinecraftInterfaceCreationException { + return new RunningLauncherProfile( + worldBuilder, + launcherProfile, + LocalMinecraftInterface.create(DefaultClassTranslator.INSTANCE.get(), launcherProfile)); + } + + private final WorldBuilder worldBuilder; + private final LauncherProfile launcherProfile; + private final MinecraftInterface minecraftInterface; + private volatile World currentWorld = null; + + public RunningLauncherProfile( + WorldBuilder worldBuilder, + LauncherProfile launcherProfile, + MinecraftInterface minecraftInterface) { + this.worldBuilder = worldBuilder; + this.launcherProfile = launcherProfile; + this.minecraftInterface = minecraftInterface; + } + + public LauncherProfile getLauncherProfile() { + return launcherProfile; + } + + public RecognisedVersion getRecognisedVersion() { + return minecraftInterface.getRecognisedVersion(); + } + + public RunningLauncherProfile createSilentPlayerlessCopy() { + try { + return RunningLauncherProfile.from(WorldBuilder.createSilentPlayerless(), launcherProfile); + } catch (LocalMinecraftInterfaceCreationException e) { + // This will not happen normally, because we already successfully + // created the same LocalMinecraftInterface once before. + throw new RuntimeException("exception while duplicating the RunningLauncherProfile", e); + } + } + + /** + * Due to the limitation of the minecraft interface, you can only work with + * one world at a time. Creating a new world will break all previously + * created world objects. + */ + public synchronized World createWorldFromSeed(WorldSeed worldSeed, WorldType worldType) + throws IllegalStateException, + MinecraftInterfaceException { + if (currentWorld == null) { + currentWorld = worldBuilder.fromSeed(minecraftInterface, this::unlock, worldSeed, worldType); + return currentWorld; + } else { + throw new IllegalStateException( + "Each minecraft interface can only handle one world at a time. Dispose the previous world before creating a new one."); + } + } + + /** + * Due to the limitation of the minecraft interface, you can only work with + * one world at a time. Creating a new world will break all previously + * created world objects. + */ + public synchronized World createWorldFromSaveGame(SaveGame saveGame) + throws IllegalStateException, + IOException, + MinecraftInterfaceException { + if (currentWorld == null) { + currentWorld = worldBuilder.fromSaveGame(minecraftInterface, this::unlock, saveGame); + return currentWorld; + } else { + throw new IllegalStateException( + "Each minecraft interface can only handle one world at a time. Dispose the previous world before creating a new one."); + } + } + + private synchronized void unlock(World world) throws IllegalStateException { + if (currentWorld == world) { + currentWorld = null; + } else { + throw new IllegalStateException("The requested world is no longer the currentWorld."); + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/DotMinecraftDirectoryFinder.java b/src/main/java/amidst/mojangapi/file/DotMinecraftDirectoryFinder.java deleted file mode 100644 index 60c584df0..000000000 --- a/src/main/java/amidst/mojangapi/file/DotMinecraftDirectoryFinder.java +++ /dev/null @@ -1,42 +0,0 @@ -package amidst.mojangapi.file; - -import java.io.File; - -import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.util.OperatingSystemDetector; - -@Immutable -public enum DotMinecraftDirectoryFinder { - ; - - @NotNull - public static File find(String dotMinecraftCMDParameter) { - if (dotMinecraftCMDParameter != null) { - File result = new File(dotMinecraftCMDParameter); - if (result.isDirectory()) { - return result; - } else { - AmidstLogger.warn( - "Unable to set Minecraft directory to: " + result - + " as that location does not exist or is not a folder."); - } - } - return getMinecraftDirectory(); - } - - @NotNull - private static File getMinecraftDirectory() { - File home = new File(System.getProperty("user.home", ".")); - if (OperatingSystemDetector.isWindows()) { - File appData = new File(System.getenv("APPDATA")); - if (appData.isDirectory()) { - return new File(appData, ".minecraft"); - } - } else if (OperatingSystemDetector.isMac()) { - return new File(home, "Library/Application Support/minecraft"); - } - return new File(home, ".minecraft"); - } -} diff --git a/src/main/java/amidst/mojangapi/file/ImmutablePlayerInformationProvider.java b/src/main/java/amidst/mojangapi/file/ImmutablePlayerInformationProvider.java new file mode 100644 index 000000000..efd17833e --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/ImmutablePlayerInformationProvider.java @@ -0,0 +1,26 @@ +package amidst.mojangapi.file; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.mojangapi.world.player.PlayerInformation; + +@Immutable +public class ImmutablePlayerInformationProvider implements PlayerInformationProvider { + private final PlayerInformation playerInformation; + + public ImmutablePlayerInformationProvider(PlayerInformation playerInformation) { + this.playerInformation = playerInformation; + } + + @NotNull + @Override + public PlayerInformation getByPlayerUUID(String playerUUID) { + return playerInformation; + } + + @NotNull + @Override + public PlayerInformation getByPlayerName(String playerName) { + return playerInformation; + } +} diff --git a/src/main/java/amidst/mojangapi/file/LauncherProfile.java b/src/main/java/amidst/mojangapi/file/LauncherProfile.java new file mode 100644 index 000000000..188e9b761 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/LauncherProfile.java @@ -0,0 +1,58 @@ +package amidst.mojangapi.file; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URLClassLoader; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.directory.DotMinecraftDirectory; +import amidst.mojangapi.file.directory.ProfileDirectory; +import amidst.mojangapi.file.directory.VersionDirectory; +import amidst.mojangapi.file.json.version.VersionJson; +import amidst.mojangapi.file.service.ClassLoaderService; + +@Immutable +public class LauncherProfile { + private final ClassLoaderService classLoaderService = new ClassLoaderService(); + private final DotMinecraftDirectory dotMinecraftDirectory; + private final ProfileDirectory profileDirectory; + private final VersionDirectory versionDirectory; + private final VersionJson versionJson; + private final String profileName; + + public LauncherProfile( + DotMinecraftDirectory dotMinecraftDirectory, + ProfileDirectory profileDirectory, + VersionDirectory versionDirectory, + VersionJson versionJson, + String profileName) { + this.dotMinecraftDirectory = dotMinecraftDirectory; + this.profileDirectory = profileDirectory; + this.versionDirectory = versionDirectory; + this.versionJson = versionJson; + this.profileName = profileName; + } + + public String getVersionId() { + return versionJson.getId(); + } + + public String getProfileName() { + return profileName; + } + + public File getJar() { + return versionDirectory.getJar(); + } + + public File getSaves() { + return profileDirectory.getSaves(); + } + + public URLClassLoader newClassLoader() throws MalformedURLException { + return classLoaderService.createClassLoader( + dotMinecraftDirectory.getLibraries(), + versionJson.getLibraries(), + versionDirectory.getJar()); + } +} diff --git a/src/main/java/amidst/mojangapi/file/LibraryFinder.java b/src/main/java/amidst/mojangapi/file/LibraryFinder.java deleted file mode 100644 index 531fc0861..000000000 --- a/src/main/java/amidst/mojangapi/file/LibraryFinder.java +++ /dev/null @@ -1,103 +0,0 @@ -package amidst.mojangapi.file; - -import java.io.File; -import java.net.MalformedURLException; -import java.net.URL; -import java.util.ArrayList; -import java.util.List; - -import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.json.version.LibraryJson; -import amidst.util.OperatingSystemDetector; - -@Immutable -public enum LibraryFinder { - ; - - @NotNull - public static List getLibraryUrls(File librariesDirectory, List libraries) { - List result = new ArrayList<>(); - for (LibraryJson library : libraries) { - File libraryFile = getLibraryFile(librariesDirectory, library); - if (libraryFile != null) { - try { - result.add(libraryFile.toURI().toURL()); - AmidstLogger.info("Found library: " + libraryFile); - } catch (MalformedURLException e) { - AmidstLogger.warn(e, "Unable to convert library file to URL: " + libraryFile); - } - } else { - AmidstLogger.info("Skipping library: " + library.getName()); - } - } - return result; - } - - private static File getLibraryFile(File librariesDirectory, LibraryJson library) { - try { - if (library.isActive(getOs(), OperatingSystemDetector.getVersion())) { - return getLibraryFile(getLibrarySearchPath(librariesDirectory, library.getName())); - } else { - return null; - } - } catch (NullPointerException e) { - return null; - } - } - - private static String getOs() { - if (OperatingSystemDetector.isWindows()) { - return "windows"; - } else if (OperatingSystemDetector.isMac()) { - return "osx"; - } else { - return "linux"; - } - } - - private static File getLibrarySearchPath(File librariesDirectory, String libraryName) { - String result = librariesDirectory.getAbsolutePath() + "/"; - String[] split = libraryName.split(":"); - split[0] = split[0].replace('.', '/'); - for (String element : split) { - result += element + "/"; - } - return new File(result); - } - - private static File getLibraryFile(File librarySearchPath) { - if (librarySearchPath.exists()) { - File result = getFirstFileWithExtension(librarySearchPath.listFiles(), "jar"); - if (result != null && result.exists()) { - return result; - } else { - AmidstLogger.warn( - "Attempted to search for file at path: " + librarySearchPath + " but found nothing. Skipping."); - return null; - } - } else { - AmidstLogger.warn("Failed attempt to load library at: " + librarySearchPath); - return null; - } - } - - private static File getFirstFileWithExtension(File[] files, String extension) { - for (File libraryFile : files) { - if (getFileExtension(libraryFile.getName()).equals(extension)) { - return libraryFile; - } - } - return null; - } - - private static String getFileExtension(String fileName) { - String extension = ""; - int q = fileName.lastIndexOf('.'); - if (q > 0) { - extension = fileName.substring(q + 1); - } - return extension; - } -} diff --git a/src/main/java/amidst/mojangapi/file/MinecraftInstallation.java b/src/main/java/amidst/mojangapi/file/MinecraftInstallation.java new file mode 100644 index 000000000..363db4df2 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/MinecraftInstallation.java @@ -0,0 +1,115 @@ +package amidst.mojangapi.file; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import amidst.documentation.Immutable; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.directory.DotMinecraftDirectory; +import amidst.mojangapi.file.directory.SaveDirectory; +import amidst.mojangapi.file.directory.VersionDirectory; +import amidst.mojangapi.file.json.version.VersionJson; +import amidst.mojangapi.file.service.DotMinecraftDirectoryService; +import amidst.mojangapi.file.service.SaveDirectoryService; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; + +@Immutable +public class MinecraftInstallation { + public static MinecraftInstallation newCustomMinecraftInstallation( + File libraries, + File saves, + File versions, + File launcherProfilesJson) throws DotMinecraftDirectoryNotFoundException { + DotMinecraftDirectory dotMinecraftDirectory = new DotMinecraftDirectoryService() + .createCustomDotMinecraftDirectory(libraries, saves, versions, launcherProfilesJson); + AmidstLogger.info("using '.minecraft' directory at: '" + dotMinecraftDirectory.getRoot() + "'"); + return new MinecraftInstallation(dotMinecraftDirectory); + } + + public static MinecraftInstallation newLocalMinecraftInstallation() throws DotMinecraftDirectoryNotFoundException { + return newLocalMinecraftInstallation(null); + } + + public static MinecraftInstallation newLocalMinecraftInstallation(String preferredDotMinecraftDirectory) + throws DotMinecraftDirectoryNotFoundException { + DotMinecraftDirectory dotMinecraftDirectory = new DotMinecraftDirectoryService() + .createDotMinecraftDirectory(preferredDotMinecraftDirectory); + AmidstLogger.info("using '.minecraft' directory at: '" + dotMinecraftDirectory.getRoot() + "'"); + return new MinecraftInstallation(dotMinecraftDirectory); + } + + private final SaveDirectoryService saveDirectoryService = new SaveDirectoryService(); + private final DotMinecraftDirectoryService dotMinecraftDirectoryService = new DotMinecraftDirectoryService(); + private final DotMinecraftDirectory dotMinecraftDirectory; + + public MinecraftInstallation(DotMinecraftDirectory dotMinecraftDirectory) { + this.dotMinecraftDirectory = dotMinecraftDirectory; + } + + public List readInstalledVersionsAsLauncherProfiles() throws FormatException, IOException { + List result = new LinkedList<>(); + for (VersionDirectory versionDirectory : dotMinecraftDirectoryService + .findInstalledValidVersionDirectories(dotMinecraftDirectory)) { + result.add(newLauncherProfile(versionDirectory)); + } + return result; + } + + public List readLauncherProfiles() throws FormatException, IOException { + return dotMinecraftDirectoryService + .readLauncherProfilesFrom(dotMinecraftDirectory) + .getProfiles() + .values() + .stream() + .map(p -> new UnresolvedLauncherProfile(dotMinecraftDirectory, p)) + .collect(Collectors.toList()); + } + + public LauncherProfile newLauncherProfile(String versionId) throws FormatException, IOException { + return newLauncherProfile( + dotMinecraftDirectoryService.createValidVersionDirectory(dotMinecraftDirectory, versionId)); + } + + public LauncherProfile newLauncherProfile(File jar, File json) throws FormatException, IOException { + return newLauncherProfile(dotMinecraftDirectoryService.createValidVersionDirectory(jar, json)); + } + + private LauncherProfile newLauncherProfile(VersionDirectory versionDirectory) throws FormatException, IOException { + VersionJson versionJson = JsonReader.readLocation(versionDirectory.getJson(), VersionJson.class); + return new LauncherProfile( + dotMinecraftDirectory, + dotMinecraftDirectory.asProfileDirectory(), + versionDirectory, + versionJson, + versionJson.getId()); + } + + public SaveGame newSaveGame(File location) throws IOException, FormatException { + SaveDirectory saveDirectory = saveDirectoryService.newSaveDirectory(location); + return new SaveGame(saveDirectory, saveDirectoryService.readLevelDat(saveDirectory)); + } + + public Optional tryReadLauncherProfile( + String preferredMinecraftJarFile, + String preferredMinecraftJsonFile) { + if (preferredMinecraftJarFile != null && preferredMinecraftJsonFile != null) { + try { + return Optional.of( + newLauncherProfile(new File(preferredMinecraftJarFile), new File(preferredMinecraftJsonFile))); + } catch (FormatException | IOException e) { + AmidstLogger.error( + e, + "cannot read launcher profile. preferredMinecraftJarFile: '" + preferredMinecraftJarFile + + "', preferredMinecraftJsonFile: '" + "'"); + return Optional.empty(); + } + } else { + return Optional.empty(); + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/MojangApiParsingException.java b/src/main/java/amidst/mojangapi/file/MojangApiParsingException.java deleted file mode 100644 index f5b3df5f4..000000000 --- a/src/main/java/amidst/mojangapi/file/MojangApiParsingException.java +++ /dev/null @@ -1,16 +0,0 @@ -package amidst.mojangapi.file; - -@SuppressWarnings("serial") -public class MojangApiParsingException extends Exception { - public MojangApiParsingException(String message) { - super(message); - } - - public MojangApiParsingException(Throwable cause) { - super(cause); - } - - public MojangApiParsingException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/src/main/java/amidst/mojangapi/world/player/PlayerInformationCacheImpl.java b/src/main/java/amidst/mojangapi/file/PlayerInformationCache.java similarity index 68% rename from src/main/java/amidst/mojangapi/world/player/PlayerInformationCacheImpl.java rename to src/main/java/amidst/mojangapi/file/PlayerInformationCache.java index bf833b865..0a0706df8 100644 --- a/src/main/java/amidst/mojangapi/world/player/PlayerInformationCacheImpl.java +++ b/src/main/java/amidst/mojangapi/file/PlayerInformationCache.java @@ -1,4 +1,4 @@ -package amidst.mojangapi.world.player; +package amidst.mojangapi.file; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -6,6 +6,8 @@ import amidst.documentation.NotNull; import amidst.documentation.ThreadSafe; import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.service.PlayerInformationService; +import amidst.mojangapi.world.player.PlayerInformation; /** * Even though this class is thread-safe, it is possible that the same player @@ -15,20 +17,21 @@ * problem. */ @ThreadSafe -public class PlayerInformationCacheImpl implements PlayerInformationCache { +public class PlayerInformationCache implements PlayerInformationProvider { private final Map byUUID = new ConcurrentHashMap<>(); private final Map byName = new ConcurrentHashMap<>(); + private final PlayerInformationService playerInformationService = new PlayerInformationService(); @NotNull @Override - public PlayerInformation getByUUID(String uuid) { - String cleanUUID = getCleanUUID(uuid); + public PlayerInformation getByPlayerUUID(String playerUUID) { + String cleanUUID = getCleanUUID(playerUUID); PlayerInformation result = byUUID.get(cleanUUID); if (result != null) { return result; } else { AmidstLogger.info("requesting player information for uuid: " + cleanUUID); - result = PlayerInformation.fromUUID(cleanUUID); + result = playerInformationService.fromUUID(cleanUUID); put(result); return result; } @@ -36,13 +39,13 @@ public PlayerInformation getByUUID(String uuid) { @NotNull @Override - public PlayerInformation getByName(String name) { - PlayerInformation result = byName.get(name); + public PlayerInformation getByPlayerName(String playerName) { + PlayerInformation result = byName.get(playerName); if (result != null) { return result; } else { - AmidstLogger.info("requesting player information for name: " + name); - result = PlayerInformation.fromName(name); + AmidstLogger.info("requesting player information for name: " + playerName); + result = playerInformationService.fromName(playerName); put(result); return result; } diff --git a/src/main/java/amidst/mojangapi/file/PlayerInformationProvider.java b/src/main/java/amidst/mojangapi/file/PlayerInformationProvider.java new file mode 100644 index 000000000..797bf1451 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/PlayerInformationProvider.java @@ -0,0 +1,14 @@ +package amidst.mojangapi.file; + +import amidst.documentation.NotNull; +import amidst.documentation.ThreadSafe; +import amidst.mojangapi.world.player.PlayerInformation; + +@ThreadSafe +public interface PlayerInformationProvider { + @NotNull + PlayerInformation getByPlayerUUID(String playerUUID); + + @NotNull + PlayerInformation getByPlayerName(String playerName); +} diff --git a/src/main/java/amidst/mojangapi/file/SaveGame.java b/src/main/java/amidst/mojangapi/file/SaveGame.java new file mode 100644 index 000000000..919338303 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/SaveGame.java @@ -0,0 +1,71 @@ +package amidst.mojangapi.file; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.directory.SaveDirectory; +import amidst.mojangapi.file.nbt.LevelDatNbt; +import amidst.mojangapi.file.service.SaveDirectoryService; +import amidst.mojangapi.world.WorldType; +import amidst.mojangapi.world.coordinates.CoordinatesInWorld; + +@Immutable +public class SaveGame { + private final SaveDirectoryService saveDirectoryService = new SaveDirectoryService(); + private final SaveDirectory saveDirectory; + private final LevelDatNbt levelDatNbt; + + public SaveGame(SaveDirectory saveDirectory, LevelDatNbt levelDatNbt) { + this.saveDirectory = saveDirectory; + this.levelDatNbt = levelDatNbt; + } + + public long getSeed() { + return levelDatNbt.getSeed(); + } + + public CoordinatesInWorld getWorldSpawn() { + return levelDatNbt.getWorldSpawn(); + } + + public WorldType getWorldType() { + return levelDatNbt.getWorldType(); + } + + public String getGeneratorOptions() { + return levelDatNbt.getGeneratorOptions(); + } + + public boolean hasSingleplayerPlayer() { + return levelDatNbt.hasPlayer(); + } + + public boolean hasMultiplayerPlayers() { + return saveDirectory.hasMultiplayerPlayers(); + } + + public Optional tryReadSingleplayerPlayer() { + return saveDirectoryService + .tryReadSingleplayerPlayerNbt(saveDirectory) + .map(p -> new SaveGamePlayer(saveDirectory, p)); + } + + public List tryReadMultiplayerPlayers() { + return saveDirectoryService + .tryReadMultiplayerPlayerNbts(saveDirectory) + .stream() + .map(p -> new SaveGamePlayer(saveDirectory, p)) + .collect(Collectors.toList()); + } + + public List tryReadAllPlayers() { + List result = new LinkedList<>(); + result.addAll(tryReadSingleplayerPlayer().map(Collections::singletonList).orElseGet(Collections::emptyList)); + result.addAll(tryReadMultiplayerPlayers()); + return result; + } +} diff --git a/src/main/java/amidst/mojangapi/file/SaveGamePlayer.java b/src/main/java/amidst/mojangapi/file/SaveGamePlayer.java new file mode 100644 index 000000000..f1950d1f4 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/SaveGamePlayer.java @@ -0,0 +1,36 @@ +package amidst.mojangapi.file; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.directory.SaveDirectory; +import amidst.mojangapi.file.nbt.player.PlayerNbt; +import amidst.mojangapi.file.service.SaveDirectoryService; +import amidst.mojangapi.world.player.PlayerCoordinates; +import amidst.mojangapi.world.player.PlayerInformation; + +@Immutable +public class SaveGamePlayer { + private final SaveDirectoryService saveDirectoryService = new SaveDirectoryService(); + private final SaveDirectory saveDirectory; + private final PlayerNbt playerNbt; + + public SaveGamePlayer(SaveDirectory saveDirectory, PlayerNbt playerNbt) { + this.saveDirectory = saveDirectory; + this.playerNbt = playerNbt; + } + + public PlayerCoordinates getPlayerCoordinates() { + return playerNbt.getPlayerCoordinates(); + } + + public boolean tryBackupAndWritePlayerCoordinates(PlayerCoordinates coordinates) { + return saveDirectoryService.tryBackup(saveDirectory, playerNbt) + && saveDirectoryService.tryWriteCoordinates(saveDirectory, playerNbt, coordinates); + } + + public PlayerInformation getPlayerInformation(PlayerInformationProvider playerInformationProvider) { + return playerNbt.map( + () -> PlayerInformation.theSingleplayerPlayer(), + playerUUID -> playerInformationProvider.getByPlayerUUID(playerUUID), + playerName -> playerInformationProvider.getByPlayerName(playerName)); + } +} diff --git a/src/main/java/amidst/mojangapi/file/URIUtils.java b/src/main/java/amidst/mojangapi/file/URIUtils.java deleted file mode 100644 index 9ceac4341..000000000 --- a/src/main/java/amidst/mojangapi/file/URIUtils.java +++ /dev/null @@ -1,86 +0,0 @@ -package amidst.mojangapi.file; - -import java.io.BufferedInputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; - -import amidst.documentation.Immutable; - -@Immutable -public enum URIUtils { - ; - - public static URI newURI(String location) throws IOException { - try { - return URI.create(location); - } catch (IllegalArgumentException e) { - throw new IOException("malformed uri: " + location, e); - } - } - - public static URL newURL(String location) throws IOException { - return newURI(location).toURL(); - } - - public static Reader newReader(String location) throws IOException { - return newReader(newURL(location)); - } - - public static Reader newReader(URL url) throws IOException { - return new InputStreamReader(newInputStream(url)); - } - - public static Reader newReader(File file) throws FileNotFoundException { - return new BufferedReader(new FileReader(file)); - } - - public static boolean exists(String location) { - try { - return exists(newURL(location)); - } catch (IOException e) { - return false; - } - } - - public static boolean exists(URL url) { - try { - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("HEAD"); - return connection.getResponseCode() == HttpURLConnection.HTTP_OK; - } catch (IOException e) { - return false; - } - } - - public static void download(String from, String to) throws IOException { - download(newURL(from), Paths.get(to)); - } - - private static void download(URL from, Path to) throws IOException { - to.getParent().toFile().mkdirs(); - if (to.toFile().exists()) { - return; - } - Path part = Paths.get(to.toString() + ".part"); - InputStream in = newInputStream(from); - Files.copy(in, part, StandardCopyOption.REPLACE_EXISTING); - Files.move(part, to, StandardCopyOption.REPLACE_EXISTING); - } - - private static BufferedInputStream newInputStream(URL url) throws IOException { - return new BufferedInputStream(url.openStream()); - } -} diff --git a/src/main/java/amidst/mojangapi/file/UnresolvedLauncherProfile.java b/src/main/java/amidst/mojangapi/file/UnresolvedLauncherProfile.java new file mode 100644 index 000000000..ccaa876da --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/UnresolvedLauncherProfile.java @@ -0,0 +1,45 @@ +package amidst.mojangapi.file; + +import java.io.IOException; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.directory.DotMinecraftDirectory; +import amidst.mojangapi.file.directory.ProfileDirectory; +import amidst.mojangapi.file.directory.VersionDirectory; +import amidst.mojangapi.file.json.launcherprofiles.LauncherProfileJson; +import amidst.mojangapi.file.json.version.VersionJson; +import amidst.mojangapi.file.service.DotMinecraftDirectoryService; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; + +@Immutable +public class UnresolvedLauncherProfile { + private final DotMinecraftDirectoryService dotMinecraftDirectoryService = new DotMinecraftDirectoryService(); + private final DotMinecraftDirectory dotMinecraftDirectory; + private final LauncherProfileJson launcherProfileJson; + + public UnresolvedLauncherProfile( + DotMinecraftDirectory dotMinecraftDirectory, + LauncherProfileJson launcherProfileJson) { + this.dotMinecraftDirectory = dotMinecraftDirectory; + this.launcherProfileJson = launcherProfileJson; + } + + public String getName() { + return launcherProfileJson.getName(); + } + + public LauncherProfile resolve(VersionList versionList) throws FormatException, IOException { + ProfileDirectory profileDirectory = dotMinecraftDirectoryService + .createValidProfileDirectory(launcherProfileJson, dotMinecraftDirectory); + VersionDirectory versionDirectory = dotMinecraftDirectoryService + .createValidVersionDirectory(launcherProfileJson, versionList, dotMinecraftDirectory); + VersionJson versionJson = JsonReader.readLocation(versionDirectory.getJson(), VersionJson.class); + return new LauncherProfile( + dotMinecraftDirectory, + profileDirectory, + versionDirectory, + versionJson, + launcherProfileJson.getName()); + } +} diff --git a/src/main/java/amidst/mojangapi/file/Version.java b/src/main/java/amidst/mojangapi/file/Version.java new file mode 100644 index 000000000..87c26af41 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/Version.java @@ -0,0 +1,61 @@ +package amidst.mojangapi.file; + +import java.io.File; +import java.io.IOException; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.json.ReleaseType; +import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; +import amidst.mojangapi.file.service.DownloadService; +import amidst.mojangapi.file.service.FilenameService; + +@Immutable +public class Version { + private final FilenameService filenameService = new FilenameService(); + private final DownloadService downloadService = new DownloadService(); + private final VersionListEntryJson versionListEntryJson; + + public Version(VersionListEntryJson versionListEntryJson) { + this.versionListEntryJson = versionListEntryJson; + } + + public String getId() { + return versionListEntryJson.getId(); + } + + public ReleaseType getType() { + return versionListEntryJson.getType(); + } + + public boolean hasServer() { + return downloadService.hasServer(versionListEntryJson.getId()); + } + + public boolean hasClient() { + return downloadService.hasClient(versionListEntryJson.getId()); + } + + public boolean tryDownloadServer(String prefix) { + return downloadService.tryDownloadServer(prefix, versionListEntryJson.getId()); + } + + public boolean tryDownloadClient(String prefix) { + return downloadService.tryDownloadClient(prefix, versionListEntryJson.getId()); + } + + public void downloadServer(String prefix) throws IOException { + downloadService.downloadServer(prefix, versionListEntryJson.getId()); + } + + public void downloadClient(String prefix) throws IOException { + downloadService.downloadClient(prefix, versionListEntryJson.getId()); + } + + public File getClientJarFile(File prefix) { + return filenameService.getClientJarFile(prefix, versionListEntryJson.getId()); + } + + public File getClientJsonFile(File prefix) { + return filenameService.getClientJsonFile(prefix, versionListEntryJson.getId()); + } +} diff --git a/src/main/java/amidst/mojangapi/file/VersionList.java b/src/main/java/amidst/mojangapi/file/VersionList.java new file mode 100644 index 000000000..3bc48bf80 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/VersionList.java @@ -0,0 +1,42 @@ +package amidst.mojangapi.file; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.service.VersionListService; +import amidst.parsing.FormatException; + +@Immutable +public class VersionList { + public static VersionList newRemoteVersionList() throws FormatException, IOException { + return new VersionList( + new VersionListService() + .readRemoteVersionList() + .getVersions() + .stream() + .map(v -> new Version(v)) + .collect(Collectors.toList())); + } + + public static VersionList newLocalVersionList() throws FormatException, IOException { + return new VersionList( + new VersionListService() + .readLocalVersionListFromResource() + .getVersions() + .stream() + .map(v -> new Version(v)) + .collect(Collectors.toList())); + } + + private final List versions; + + public VersionList(List versions) { + this.versions = versions; + } + + public List getVersions() { + return versions; + } +} diff --git a/src/main/java/amidst/mojangapi/file/VersionListProvider.java b/src/main/java/amidst/mojangapi/file/VersionListProvider.java new file mode 100644 index 000000000..fa5ceca9d --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/VersionListProvider.java @@ -0,0 +1,80 @@ +package amidst.mojangapi.file; + +import java.io.IOException; +import java.util.concurrent.ConcurrentLinkedQueue; + +import amidst.documentation.AmidstThread; +import amidst.documentation.CalledByAny; +import amidst.documentation.CalledOnlyBy; +import amidst.documentation.NotThreadSafe; +import amidst.logging.AmidstLogger; +import amidst.parsing.FormatException; +import amidst.threading.WorkerExecutor; + +/** + * This class is responsible to provide the version list. It will instantly + * provide the local version list, but try to download the remote version list + * in the background. Listeners can be registered to be informed about a + * successful download. + */ +@NotThreadSafe +public class VersionListProvider { + @CalledOnlyBy(AmidstThread.EDT) + public static VersionListProvider createLocalAndStartDownloadingRemote(WorkerExecutor workerExecutor) + throws FormatException, + IOException { + VersionListProvider versionListProvider = new VersionListProvider(VersionList.newLocalVersionList()); + versionListProvider.startDownloading(workerExecutor); + return versionListProvider; + } + + private final ConcurrentLinkedQueue listeners = new ConcurrentLinkedQueue<>(); + private final VersionList local; + private volatile VersionList remote; + + @CalledOnlyBy(AmidstThread.EDT) + public VersionListProvider(VersionList local) { + this.local = local; + } + + @CalledOnlyBy(AmidstThread.EDT) + public void onDownloadRemoteFinished(Runnable listener) { + if (remote == null) { + listeners.offer(listener); + } + } + + @CalledOnlyBy(AmidstThread.EDT) + private void startDownloading(WorkerExecutor workerExecutor) { + workerExecutor.run(this::doDownload, this::finishedDownload, this::downloadFailed); + } + + @CalledOnlyBy(AmidstThread.WORKER) + private VersionList doDownload() throws FormatException, IOException { + AmidstLogger.info("Starting to download remote version list."); + return VersionList.newRemoteVersionList(); + } + + @CalledOnlyBy(AmidstThread.EDT) + private void finishedDownload(VersionList remote) { + AmidstLogger.info("Successfully loaded remote version list."); + this.remote = remote; + listeners.forEach(Runnable::run); + listeners.clear(); + } + + @CalledOnlyBy(AmidstThread.EDT) + private void downloadFailed(Exception e) { + AmidstLogger.warn(e, "Error while downloading remote version list."); + } + + @CalledByAny + public VersionList getRemoteOrElseLocal() { + VersionList remote = this.remote; + if (remote != null) { + return remote; + } else { + return local; + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/directory/DotMinecraftDirectory.java b/src/main/java/amidst/mojangapi/file/directory/DotMinecraftDirectory.java index 057e57731..ee46fa5ce 100644 --- a/src/main/java/amidst/mojangapi/file/directory/DotMinecraftDirectory.java +++ b/src/main/java/amidst/mojangapi/file/directory/DotMinecraftDirectory.java @@ -1,16 +1,31 @@ package amidst.mojangapi.file.directory; import java.io.File; -import java.io.IOException; +import java.util.Objects; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.JsonReader; -import amidst.mojangapi.file.json.launcherprofiles.LauncherProfilesJson; @Immutable public class DotMinecraftDirectory { + /** + * Allows to customize all parts of the .minecraft directory, mainly for + * testing and the dev tools. Pass null to use default values. + */ + public static DotMinecraftDirectory newCustom( + File root, + File libraries, + File saves, + File versions, + File launcherProfilesJson) { + Objects.requireNonNull(root); + return new DotMinecraftDirectory( + root, + libraries != null ? libraries : new File(root, "libraries"), + saves != null ? saves : new File(root, "saves"), + versions != null ? versions : new File(root, "versions"), + launcherProfilesJson != null ? launcherProfilesJson : new File(root, "launcher_profiles.json")); + } + private final File root; private final File libraries; private final File saves; @@ -25,16 +40,20 @@ public DotMinecraftDirectory(File root) { this.launcherProfilesJson = new File(root, "launcher_profiles.json"); } - public DotMinecraftDirectory(File root, File libraries) { + private DotMinecraftDirectory(File root, File libraries, File saves, File versions, File launcherProfilesJson) { this.root = root; this.libraries = libraries; - this.saves = new File(root, "saves"); - this.versions = new File(root, "versions"); - this.launcherProfilesJson = new File(root, "launcher_profiles.json"); + this.saves = saves; + this.versions = versions; + this.launcherProfilesJson = launcherProfilesJson; } public boolean isValid() { - return root.isDirectory() && libraries.isDirectory() && launcherProfilesJson.isFile(); + return root.isDirectory() && libraries.isDirectory() && versions.isDirectory() && launcherProfilesJson.isFile(); + } + + public ProfileDirectory asProfileDirectory() { + return new ProfileDirectory(root); } public File getRoot() { @@ -56,9 +75,4 @@ public File getVersions() { public File getLauncherProfilesJson() { return launcherProfilesJson; } - - @NotNull - public LauncherProfilesJson readLauncherProfilesJson() throws MojangApiParsingException, IOException { - return JsonReader.readLauncherProfilesFrom(launcherProfilesJson); - } } diff --git a/src/main/java/amidst/mojangapi/file/directory/SaveAmidstBackupDirectory.java b/src/main/java/amidst/mojangapi/file/directory/SaveAmidstBackupDirectory.java index cb1c6d25d..d638411be 100644 --- a/src/main/java/amidst/mojangapi/file/directory/SaveAmidstBackupDirectory.java +++ b/src/main/java/amidst/mojangapi/file/directory/SaveAmidstBackupDirectory.java @@ -1,9 +1,6 @@ package amidst.mojangapi.file.directory; import java.io.File; -import java.io.IOException; -import java.nio.file.Files; -import java.sql.Timestamp; public class SaveAmidstBackupDirectory { private final File root; @@ -27,56 +24,4 @@ public File getPlayers() { public File getPlayerdata() { return playerdata; } - - public File getPlayersFile(String playerName) { - return new File(players, playerName + ".dat" + "_" + millis()); - } - - public File getPlayerdataFile(String playerUUID) { - return new File(playerdata, playerUUID + ".dat" + "_" + millis()); - } - - public File getLevelDat() { - return new File(root, "level.dat" + "_" + millis()); - } - - private String millis() { - return new Timestamp(System.currentTimeMillis()) - .toString() - .replace(" ", "_") - .replace(":", "-") - .replace(".", "_"); - } - - public boolean tryBackupPlayersFile(File from, String playerName) { - return tryBackup(players, from, getPlayersFile(playerName)); - } - - public boolean tryBackupPlayerdataFile(File from, String playerUUID) { - return tryBackup(playerdata, from, getPlayerdataFile(playerUUID)); - } - - public boolean tryBackupLevelDat(File from) { - return tryBackup(root, from, getLevelDat()); - } - - private boolean tryBackup(File toDirectory, File from, File to) { - return ensureDirectoryExists(toDirectory) && tryCopy(from, to) && to.isFile(); - } - - private boolean ensureDirectoryExists(File directory) { - if (!directory.exists()) { - directory.mkdirs(); - } - return directory.isDirectory(); - } - - private boolean tryCopy(File from, File to) { - try { - Files.copy(from.toPath(), to.toPath()); - return true; - } catch (IOException e) { - return false; - } - } } diff --git a/src/main/java/amidst/mojangapi/file/directory/SaveDirectory.java b/src/main/java/amidst/mojangapi/file/directory/SaveDirectory.java index 3c64f853a..e3c684617 100644 --- a/src/main/java/amidst/mojangapi/file/directory/SaveDirectory.java +++ b/src/main/java/amidst/mojangapi/file/directory/SaveDirectory.java @@ -1,67 +1,11 @@ package amidst.mojangapi.file.directory; import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.jnbt.CompoundTag; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.nbt.LevelDatNbt; -import amidst.mojangapi.file.nbt.NBTUtils; -import amidst.mojangapi.file.nbt.player.LevelDatPlayerNbt; -import amidst.mojangapi.file.nbt.player.PlayerNbt; -import amidst.mojangapi.file.nbt.player.PlayerdataPlayerNbt; -import amidst.mojangapi.file.nbt.player.PlayersPlayerNbt; @Immutable public class SaveDirectory { - /** - * Returns a new valid instance of the class SaveDirectory. It tries to use - * the given file. If that is not valid it tires to use its parent file. If - * that is also not valid it will throw a FileNotFoundException. - */ - @NotNull - public static SaveDirectory from(File file) throws FileNotFoundException { - File currentFile = file; - SaveDirectory result = null; - if (currentFile == null) { - // error - } else { - result = createValidSaveDirectory(currentFile); - currentFile = currentFile.getParentFile(); - if (result != null) { - return result; - } else if (currentFile == null) { - // error - } else { - result = createValidSaveDirectory(currentFile); - currentFile = currentFile.getParentFile(); - if (result != null) { - return result; - } else { - // error - } - } - } - throw new FileNotFoundException("unable to load save directory: " + file); - } - - private static SaveDirectory createValidSaveDirectory(File currentFile) { - SaveDirectory result = new SaveDirectory(currentFile); - if (result.isValid()) { - return result; - } else { - return null; - } - } - private final File root; private final SaveAmidstDirectory amidst; private final File players; @@ -111,109 +55,4 @@ public File getPlayersFile(String playerName) { public File getPlayerdataFile(String playerUUID) { return new File(playerdata, playerUUID + ".dat"); } - - public File[] getPlayersFiles() { - File[] files = players.listFiles(); - if (files == null) { - return new File[0]; - } else { - return files; - } - } - - public File[] getPlayerdataFiles() { - File[] files = playerdata.listFiles(); - if (files == null) { - return new File[0]; - } else { - return files; - } - } - - public CompoundTag readLevelDat() throws IOException { - return NBTUtils.readTagFromFile(levelDat); - } - - public LevelDatNbt createLevelDat() throws IOException, MojangApiParsingException { - return new LevelDatNbt(readLevelDat()); - } - - public boolean tryBackupPlayersFile(String playerName) { - return amidst.getBackup().tryBackupPlayersFile(getPlayersFile(playerName), playerName); - } - - public boolean tryBackupPlayerdataFile(String playerUUID) { - return amidst.getBackup().tryBackupPlayerdataFile(getPlayerdataFile(playerUUID), playerUUID); - } - - public boolean tryBackupLevelDat() { - return amidst.getBackup().tryBackupLevelDat(getLevelDat()); - } - - /** - * Since version 1.7.6, minecraft stores players in the playerdata directory - * and uses the player uuid as filename. - */ - @NotNull - public List createMultiplayerPlayerNbts() { - List result = new ArrayList<>(); - for (File playerdataFile : getPlayerdataFiles()) { - if (playerdataFile.isFile()) { - result.add(createPlayerdataPlayerNbt(getPlayerUUIDFromPlayerdataFile(playerdataFile))); - } - } - if (!result.isEmpty()) { - AmidstLogger.info("using players from the playerdata directory"); - return result; - } - for (File playersFile : getPlayersFiles()) { - if (playersFile.isFile()) { - result.add(createPlayersPlayerNbt(getPlayerNameFromPlayersFile(playersFile))); - } - } - if (!result.isEmpty()) { - AmidstLogger.info("using players from the players directory"); - return result; - } - AmidstLogger.info("no multiplayer players found"); - return result; - } - - /** - * We need to let the user decide if he wants to load the singleplayer - * player from the level.dat file or if he wants to load the multiplayer - * players. That is, because a singleplayer map will have the playerdata - * directory. It contains information about each player that ever played on - * the map. However, if the map is loaded as singleplayer map, minecraft - * will use the information that is stored in the level.dat file. It will - * also overwrite the file in the playerdata directory that belongs to the - * player that loaded the map in singleplayer mode. So, if we change the - * player location in the playerdata directory it will just be ignored if - * the map is used as singleplayer map. - */ - @NotNull - public List createSingleplayerPlayerNbts() { - AmidstLogger.info("using player from level.dat"); - return Arrays.asList(createLevelDatPlayerNbt()); - } - - private PlayerNbt createLevelDatPlayerNbt() { - return new LevelDatPlayerNbt(this); - } - - private PlayerNbt createPlayerdataPlayerNbt(String playerUUID) { - return new PlayerdataPlayerNbt(this, playerUUID); - } - - private PlayerNbt createPlayersPlayerNbt(String playerName) { - return new PlayersPlayerNbt(this, playerName); - } - - private String getPlayerUUIDFromPlayerdataFile(File playerdataFile) { - return playerdataFile.getName().split("\\.")[0]; - } - - private String getPlayerNameFromPlayersFile(File playersFile) { - return playersFile.getName().split("\\.")[0]; - } } diff --git a/src/main/java/amidst/mojangapi/file/directory/VersionDirectory.java b/src/main/java/amidst/mojangapi/file/directory/VersionDirectory.java index 578b8766d..851f99040 100644 --- a/src/main/java/amidst/mojangapi/file/directory/VersionDirectory.java +++ b/src/main/java/amidst/mojangapi/file/directory/VersionDirectory.java @@ -1,37 +1,15 @@ package amidst.mojangapi.file.directory; import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.List; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.JsonReader; -import amidst.mojangapi.file.json.version.VersionJson; -import amidst.mojangapi.minecraftinterface.MinecraftInterface; -import amidst.mojangapi.minecraftinterface.local.DefaultClassTranslator; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceBuilder; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; @Immutable public class VersionDirectory { - private static final LocalMinecraftInterfaceBuilder LOCAL_MINECRAFT_INTERFACE_BUILDER = new LocalMinecraftInterfaceBuilder( - DefaultClassTranslator.INSTANCE.get()); - - private final DotMinecraftDirectory dotMinecraftDirectory; - private final String versionId; private final File jar; private final File json; - public VersionDirectory(DotMinecraftDirectory dotMinecraftDirectory, String versionId, File jar, File json) { - this.dotMinecraftDirectory = dotMinecraftDirectory; - this.versionId = versionId; + public VersionDirectory(File jar, File json) { this.jar = jar; this.json = json; } @@ -40,10 +18,6 @@ public boolean isValid() { return jar.isFile() && json.isFile(); } - public String getVersionId() { - return versionId; - } - public File getJar() { return jar; } @@ -51,51 +25,4 @@ public File getJar() { public File getJson() { return json; } - - @NotNull - public VersionJson readVersionJson() throws MojangApiParsingException, IOException { - return JsonReader.readVersionFrom(json); - } - - @NotNull - public URLClassLoader createClassLoader() throws MalformedURLException { - if (json.isFile()) { - AmidstLogger.info("Loading libraries."); - return doCreateClassLoader(getJarFileUrl(), getAllLibraryUrls()); - } else { - AmidstLogger.info("Unable to find Minecraft library JSON at: " + json + ". Skipping."); - return doCreateClassLoader(getJarFileUrl()); - } - } - - @NotNull - private URL getJarFileUrl() throws MalformedURLException { - return jar.toURI().toURL(); - } - - @NotNull - private List getAllLibraryUrls() { - try { - return readVersionJson().getLibraryUrls(dotMinecraftDirectory.getLibraries()); - } catch (IOException | MojangApiParsingException e) { - AmidstLogger.warn("Invalid jar profile loaded. Library loading will be skipped. (Path: " + json + ")"); - return new ArrayList<>(); - } - } - - @NotNull - private URLClassLoader doCreateClassLoader(URL jarFileUrl, List libraries) { - libraries.add(jarFileUrl); - return new URLClassLoader(libraries.toArray(new URL[libraries.size()])); - } - - @NotNull - private URLClassLoader doCreateClassLoader(URL jarFileUrl) { - return new URLClassLoader(new URL[] { jarFileUrl }); - } - - @NotNull - public MinecraftInterface createLocalMinecraftInterface() throws LocalMinecraftInterfaceCreationException { - return LOCAL_MINECRAFT_INTERFACE_BUILDER.create(this); - } } diff --git a/src/main/java/amidst/mojangapi/file/json/JsonReader.java b/src/main/java/amidst/mojangapi/file/json/JsonReader.java deleted file mode 100644 index 146978a88..000000000 --- a/src/main/java/amidst/mojangapi/file/json/JsonReader.java +++ /dev/null @@ -1,139 +0,0 @@ -package amidst.mojangapi.file.json; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.Reader; -import java.net.URL; - -import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; - -import amidst.ResourceLoader; -import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.URIUtils; -import amidst.mojangapi.file.json.launcherprofiles.LauncherProfilesJson; -import amidst.mojangapi.file.json.player.PlayerJson; -import amidst.mojangapi.file.json.player.SimplePlayerJson; -import amidst.mojangapi.file.json.version.VersionJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; - -/** - * This is a utility class used to read JSON data. Please use this class only to - * read JSON data provided by Mojang, because it throws a - * MojangApiParsingException when an error occurs. - */ -@Immutable -public enum JsonReader { - ; - - private static final Gson GSON = new Gson(); - - private static final String REMOTE_VERSION_LIST = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; - private static final URL LOCAL_VERSION_LIST = ResourceLoader - .getResourceURL("/amidst/mojangapi/version_manifest.json"); - - private static final String UUID_TO_PROFILE = "https://sessionserver.mojang.com/session/minecraft/profile/"; - private static final String PLAYERNAME_TO_UUID = "https://api.mojang.com/users/profiles/minecraft/"; - - @NotNull - public static VersionListJson readRemoteOrLocalVersionList() throws FileNotFoundException { - AmidstLogger.info("Beginning latest version list load."); - AmidstLogger.info("Attempting to download remote version list..."); - VersionListJson remote = null; - try { - remote = readRemoteVersionList(); - } catch (IOException | MojangApiParsingException e) { - AmidstLogger.warn("Unable to read remote version list."); - AmidstLogger.warn(e); - AmidstLogger.warn("Aborting version list load. URL: " + REMOTE_VERSION_LIST); - } - if (remote != null) { - AmidstLogger.info("Successfully loaded version list. URL: " + REMOTE_VERSION_LIST); - return remote; - } - AmidstLogger.info("Attempting to load local version list..."); - VersionListJson local = null; - try { - local = readLocalVersionListFromResource(); - } catch (IOException | MojangApiParsingException e) { - AmidstLogger.warn("Unable to read local version list."); - AmidstLogger.warn(e); - AmidstLogger.warn("Aborting version list load. URL: " + LOCAL_VERSION_LIST); - } - if (local != null) { - AmidstLogger.info("Successfully loaded version list. URL: " + LOCAL_VERSION_LIST); - return local; - } - AmidstLogger.warn("Failed to load both remote and local version list."); - throw new FileNotFoundException("unable to read version list"); - } - - @NotNull - public static VersionListJson readRemoteVersionList() throws IOException, MojangApiParsingException { - return read(URIUtils.newReader(REMOTE_VERSION_LIST), VersionListJson.class); - } - - @NotNull - public static VersionListJson readLocalVersionListFromResource() throws IOException, MojangApiParsingException { - return read(URIUtils.newReader(LOCAL_VERSION_LIST), VersionListJson.class); - } - - @NotNull - public static VersionJson readVersionFrom(File file) throws MojangApiParsingException, IOException { - return read(URIUtils.newReader(file), VersionJson.class); - } - - @NotNull - public static LauncherProfilesJson readLauncherProfilesFrom(File file) - throws MojangApiParsingException, - IOException { - return read(URIUtils.newReader(file), LauncherProfilesJson.class); - } - - @NotNull - public static T read(Reader reader, Class clazz) throws MojangApiParsingException, IOException { - try (Reader theReader = reader) { - T result = GSON.fromJson(theReader, clazz); - if (result != null) { - return result; - } else { - throw new MojangApiParsingException("result was null"); - } - } catch (JsonSyntaxException e) { - throw new MojangApiParsingException(e); - } catch (JsonIOException e) { - throw new IOException(e); - } - } - - @NotNull - public static PlayerJson readPlayerFromUUID(String uuid) throws IOException, MojangApiParsingException { - return read(URIUtils.newReader(UUID_TO_PROFILE + uuid), PlayerJson.class); - } - - @NotNull - public static SimplePlayerJson readSimplePlayerFromPlayerName(String playerName) - throws IOException, - MojangApiParsingException { - return read(URIUtils.newReader(PLAYERNAME_TO_UUID + playerName), SimplePlayerJson.class); - } - - @NotNull - public static T read(String string, Class clazz) throws MojangApiParsingException { - try { - T result = GSON.fromJson(string, clazz); - if (result != null) { - return result; - } else { - throw new MojangApiParsingException("result was null"); - } - } catch (JsonSyntaxException e) { - throw new MojangApiParsingException(e); - } - } -} diff --git a/src/main/java/amidst/mojangapi/file/json/PlayerInformationRetriever.java b/src/main/java/amidst/mojangapi/file/json/PlayerInformationRetriever.java deleted file mode 100644 index 352bb91be..000000000 --- a/src/main/java/amidst/mojangapi/file/json/PlayerInformationRetriever.java +++ /dev/null @@ -1,88 +0,0 @@ -package amidst.mojangapi.file.json; - -import java.awt.Color; -import java.awt.Graphics2D; -import java.awt.image.BufferedImage; -import java.io.IOException; -import java.net.URL; - -import javax.imageio.ImageIO; - -import amidst.documentation.Immutable; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.player.PlayerJson; - -@Immutable -public enum PlayerInformationRetriever { - ; - - private static final String SIMPLE_PLAYER_SKIN_URL = "http://s3.amazonaws.com/MinecraftSkins/"; - - public static PlayerJson tryGetPlayerJsonByName(String name) { - try { - return getPlayerJsonByName(name); - } catch (IOException | MojangApiParsingException | NullPointerException e) { - AmidstLogger.warn("unable to load player information by name: " + name); - return null; - } - } - - public static PlayerJson tryGetPlayerJsonByUUID(String uuid) { - try { - return getPlayerJsonByUUID(uuid); - } catch (IOException | MojangApiParsingException | NullPointerException e) { - AmidstLogger.warn("unable to load player information by uuid: " + uuid); - return null; - } - } - - public static BufferedImage tryGetPlayerHeadByName(String name) { - try { - return getPlayerHeadByName(name); - } catch (IOException | NullPointerException e) { - AmidstLogger.warn("unable to load player head by name: " + name); - return null; - } - } - - public static BufferedImage tryGetPlayerHeadBySkinUrl(String skinUrl) { - try { - return getPlayerHeadBySkinUrl(skinUrl); - } catch (IOException | NullPointerException e) { - AmidstLogger.warn("unable to load player head by skin url: " + skinUrl); - return null; - } - } - - private static PlayerJson getPlayerJsonByName(String name) throws IOException, MojangApiParsingException { - return JsonReader.readPlayerFromUUID(JsonReader.readSimplePlayerFromPlayerName(name).getId()); - } - - private static PlayerJson getPlayerJsonByUUID(String uuid) throws IOException, MojangApiParsingException { - return JsonReader.readPlayerFromUUID(uuid); - } - - private static BufferedImage getPlayerHeadByName(String name) throws IOException { - return extractPlayerHead(new URL(SIMPLE_PLAYER_SKIN_URL + name + ".png")); - } - - private static BufferedImage getPlayerHeadBySkinUrl(String skinUrl) throws IOException { - return extractPlayerHead(new URL(skinUrl)); - } - - private static BufferedImage extractPlayerHead(URL url) throws IOException { - return extractPlayerHead(ImageIO.read(url)); - } - - private static BufferedImage extractPlayerHead(BufferedImage skin) { - BufferedImage head = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = head.createGraphics(); - g2d.setColor(Color.black); - g2d.fillRect(0, 0, 20, 20); - g2d.drawImage(skin, 2, 2, 18, 18, 8, 8, 16, 16, null); - g2d.dispose(); - skin.flush(); - return head; - } -} diff --git a/src/main/java/amidst/mojangapi/file/json/filter/WorldFilterJson_MatchAll.java b/src/main/java/amidst/mojangapi/file/json/filter/WorldFilterJson_MatchAll.java index 6eda53128..5704944d5 100644 --- a/src/main/java/amidst/mojangapi/file/json/filter/WorldFilterJson_MatchAll.java +++ b/src/main/java/amidst/mojangapi/file/json/filter/WorldFilterJson_MatchAll.java @@ -6,27 +6,24 @@ import java.util.List; import java.util.Optional; -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; import amidst.logging.AmidstLogger; import amidst.mojangapi.world.filter.WorldFilter; import amidst.mojangapi.world.filter.WorldFilter_MatchAll; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; @Immutable public class WorldFilterJson_MatchAll { public static Optional from(String queryString) { try { - return Optional.ofNullable(GSON.fromJson(queryString, WorldFilterJson_MatchAll.class)); - } catch (JsonSyntaxException e) { + return Optional.of(JsonReader.readString(queryString, WorldFilterJson_MatchAll.class)); + } catch (FormatException e) { return Optional.empty(); } } - private static final Gson GSON = new Gson(); - private volatile List biomeFilters = Collections.emptyList(); private volatile List structureFilters = Collections.emptyList(); diff --git a/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfileJson.java b/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfileJson.java index 299a49802..e1ad22318 100644 --- a/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfileJson.java +++ b/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfileJson.java @@ -1,18 +1,11 @@ package amidst.mojangapi.file.json.launcherprofiles; -import java.io.File; -import java.io.FileNotFoundException; import java.util.Arrays; import java.util.List; import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.directory.ProfileDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; import amidst.mojangapi.file.json.ReleaseType; -import amidst.mojangapi.file.json.versionlist.VersionListJson; @Immutable public class LauncherProfileJson { @@ -48,35 +41,7 @@ public String getGameDir() { return gameDir; } - @NotNull - public ProfileDirectory createValidProfileDirectory(MojangApi mojangApi) throws FileNotFoundException { - if (gameDir != null) { - ProfileDirectory result = new ProfileDirectory(new File(gameDir)); - if (result.isValid()) { - return result; - } else { - throw new FileNotFoundException( - "cannot find valid profile directory for launcher profile '" + name + "': " + gameDir); - } - } else { - return new ProfileDirectory(mojangApi.getDotMinecraftDirectory().getRoot()); - } - } - - @NotNull - public VersionDirectory createValidVersionDirectory(MojangApi mojangApi) throws FileNotFoundException { - VersionListJson versionList = mojangApi.getVersionList(); - if (lastVersionId != null) { - VersionDirectory result = mojangApi.createVersionDirectory(lastVersionId); - if (result.isValid()) { - return result; - } - } else { - VersionDirectory result = versionList.tryFindFirstValidVersionDirectory(allowedReleaseTypes, mojangApi); - if (result != null) { - return result; - } - } - throw new FileNotFoundException("cannot find valid version directory for launcher profile '" + name + "'"); + public List getAllowedReleaseTypes() { + return allowedReleaseTypes; } } diff --git a/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfilesJson.java b/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfilesJson.java index acfa402f8..0d9877245 100644 --- a/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfilesJson.java +++ b/src/main/java/amidst/mojangapi/file/json/launcherprofiles/LauncherProfilesJson.java @@ -1,6 +1,5 @@ package amidst.mojangapi.file.json.launcherprofiles; -import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -15,7 +14,7 @@ public class LauncherProfilesJson { public LauncherProfilesJson() { } - public Collection getProfiles() { - return profiles.values(); + public Map getProfiles() { + return profiles; } } diff --git a/src/main/java/amidst/mojangapi/file/json/player/PlayerJson.java b/src/main/java/amidst/mojangapi/file/json/player/PlayerJson.java index a657e2f46..b3b3e4c93 100644 --- a/src/main/java/amidst/mojangapi/file/json/player/PlayerJson.java +++ b/src/main/java/amidst/mojangapi/file/json/player/PlayerJson.java @@ -5,9 +5,6 @@ import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.JsonReader; @Immutable public class PlayerJson { @@ -30,28 +27,4 @@ public String getName() { public List getProperties() { return properties; } - - @NotNull - public TexturesPropertyJson readTexturesProperty() throws MojangApiParsingException { - for (PropertyJson property : properties) { - if (property.isTexturesProperty()) { - return JsonReader.read(property.getDecodedValue(), TexturesPropertyJson.class); - } - } - throw new MojangApiParsingException("player json does not contain the textures property"); - } - - @NotNull - public String getSkinUrl() throws MojangApiParsingException { - try { - String result = readTexturesProperty().getTextures().getSKIN().getUrl(); - if (result != null) { - return result; - } else { - throw new MojangApiParsingException("unable to get skin url"); - } - } catch (NullPointerException e) { - throw new MojangApiParsingException("unable to get skin url", e); - } - } } diff --git a/src/main/java/amidst/mojangapi/file/json/player/PropertyJson.java b/src/main/java/amidst/mojangapi/file/json/player/PropertyJson.java index 406b478e5..5ed0058e4 100644 --- a/src/main/java/amidst/mojangapi/file/json/player/PropertyJson.java +++ b/src/main/java/amidst/mojangapi/file/json/player/PropertyJson.java @@ -1,12 +1,7 @@ package amidst.mojangapi.file.json.player; -import java.nio.charset.StandardCharsets; -import java.util.Base64; - import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.mojangapi.file.MojangApiParsingException; @Immutable public class PropertyJson { @@ -24,23 +19,4 @@ public String getName() { public String getValue() { return value; } - - @NotNull - public String getDecodedValue() throws MojangApiParsingException { - if (value == null) { - throw new MojangApiParsingException("unable to decode property value"); - } else { - return new String( - Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)), - StandardCharsets.UTF_8); - } - } - - public boolean isTexturesProperty() throws MojangApiParsingException { - if (name == null) { - throw new MojangApiParsingException("property has no name"); - } else { - return name.equals("textures"); - } - } } diff --git a/src/main/java/amidst/mojangapi/file/json/player/SKINJson.java b/src/main/java/amidst/mojangapi/file/json/player/SKINJson.java index af4332e2b..bf46646f9 100644 --- a/src/main/java/amidst/mojangapi/file/json/player/SKINJson.java +++ b/src/main/java/amidst/mojangapi/file/json/player/SKINJson.java @@ -19,8 +19,4 @@ public String getUrl() { public MetadataJson getMetadata() { return metadata; } - - public boolean isSlimModel() { - return metadata != null && metadata.getModel().equals("slim"); - } } diff --git a/src/main/java/amidst/mojangapi/file/json/version/LibraryJson.java b/src/main/java/amidst/mojangapi/file/json/version/LibraryJson.java index 05b7864d0..698698fd2 100644 --- a/src/main/java/amidst/mojangapi/file/json/version/LibraryJson.java +++ b/src/main/java/amidst/mojangapi/file/json/version/LibraryJson.java @@ -19,21 +19,7 @@ public String getName() { return name; } - /** - * Note, that multiple rules might be applicable. We take the last - * applicable rule. However, this might be wrong so we need to take the most - * specific rule? For now this works fine. - */ - public boolean isActive(String os, String version) { - if (rules.isEmpty()) { - return true; - } - boolean result = false; - for (LibraryRuleJson rule : rules) { - if (rule.isApplicable(os, version)) { - result = rule.isAllowed(); - } - } - return result; + public List getRules() { + return rules; } } diff --git a/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleJson.java b/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleJson.java index 7a8803d14..537b74109 100644 --- a/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleJson.java +++ b/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleJson.java @@ -5,8 +5,6 @@ @Immutable public class LibraryRuleJson { - private static final String ACTION_ALLOW = "allow"; - private volatile String action; private volatile LibraryRuleOsJson os; @@ -14,11 +12,11 @@ public class LibraryRuleJson { public LibraryRuleJson() { } - public boolean isApplicable(String os, String version) { - return this.os == null || this.os.isApplicable(os, version); + public String getAction() { + return action; } - public boolean isAllowed() { - return action.equals(ACTION_ALLOW); + public LibraryRuleOsJson getOs() { + return os; } } diff --git a/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleOsJson.java b/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleOsJson.java index b2ef1b438..f2c0e57dd 100644 --- a/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleOsJson.java +++ b/src/main/java/amidst/mojangapi/file/json/version/LibraryRuleOsJson.java @@ -1,7 +1,5 @@ package amidst.mojangapi.file.json.version; -import java.util.regex.Pattern; - import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; @@ -14,7 +12,11 @@ public class LibraryRuleOsJson { public LibraryRuleOsJson() { } - public boolean isApplicable(String os, String version) { - return this.name.equals(os) && (this.version == null || Pattern.matches(this.version, version)); + public String getName() { + return name; + } + + public String getVersion() { + return version; } } diff --git a/src/main/java/amidst/mojangapi/file/json/version/VersionJson.java b/src/main/java/amidst/mojangapi/file/json/version/VersionJson.java index 43f793ef3..56ad5cb75 100644 --- a/src/main/java/amidst/mojangapi/file/json/version/VersionJson.java +++ b/src/main/java/amidst/mojangapi/file/json/version/VersionJson.java @@ -1,29 +1,30 @@ package amidst.mojangapi.file.json.version; -import java.io.File; -import java.net.URL; import java.util.Collections; import java.util.List; import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.documentation.NotNull; -import amidst.mojangapi.file.LibraryFinder; @Immutable public class VersionJson { + private volatile String id; + private volatile String inheritsFrom; private volatile List libraries = Collections.emptyList(); @GsonConstructor public VersionJson() { } - public List getLibraries() { - return libraries; + public String getId() { + return id; + } + + public String getInheritsFrom() { + return inheritsFrom; } - @NotNull - public List getLibraryUrls(File librariesDirectory) { - return LibraryFinder.getLibraryUrls(librariesDirectory, libraries); + public List getLibraries() { + return libraries; } } diff --git a/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListEntryJson.java b/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListEntryJson.java index 5b4b9ba41..44f495319 100644 --- a/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListEntryJson.java +++ b/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListEntryJson.java @@ -1,14 +1,7 @@ package amidst.mojangapi.file.json.versionlist; -import java.io.IOException; - import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.logging.AmidstLogger; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.FilenameFactory; -import amidst.mojangapi.file.URIUtils; -import amidst.mojangapi.file.directory.VersionDirectory; import amidst.mojangapi.file.json.ReleaseType; @Immutable @@ -27,69 +20,4 @@ public String getId() { public ReleaseType getType() { return type; } - - public VersionDirectory createVersionDirectory(MojangApi mojangApi) { - return mojangApi.createVersionDirectory(id); - } - - public String getClientJar(String prefix) { - return FilenameFactory.getClientJar(prefix, id); - } - - public String getClientJson(String prefix) { - return FilenameFactory.getClientJson(prefix, id); - } - - public String getServerJar(String prefix) { - return FilenameFactory.getServerJar(prefix, id); - } - - public String getRemoteClientJar() { - return FilenameFactory.getRemoteClientJar(id); - } - - public String getRemoteClientJson() { - return FilenameFactory.getRemoteClientJson(id); - } - - public String getRemoteServerJar() { - return FilenameFactory.getRemoteServerJar(id); - } - - public boolean hasServer() { - return URIUtils.exists(getRemoteServerJar()); - } - - public boolean hasClient() { - return URIUtils.exists(getRemoteClientJar()); - } - - public void downloadServer(String prefix) throws IOException { - URIUtils.download(getRemoteServerJar(), getServerJar(prefix)); - } - - public void downloadClient(String prefix) throws IOException { - URIUtils.download(getRemoteClientJar(), getClientJar(prefix)); - URIUtils.download(getRemoteClientJson(), getClientJson(prefix)); - } - - public boolean tryDownloadServer(String prefix) { - try { - downloadServer(prefix); - return true; - } catch (IOException e) { - AmidstLogger.warn(e, "unable to download server: " + id); - } - return false; - } - - public boolean tryDownloadClient(String prefix) { - try { - downloadClient(prefix); - return true; - } catch (IOException e) { - AmidstLogger.warn(e, "unable to download client: " + id); - } - return false; - } } diff --git a/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListJson.java b/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListJson.java index aa7456dec..cd95c6ff6 100644 --- a/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListJson.java +++ b/src/main/java/amidst/mojangapi/file/json/versionlist/VersionListJson.java @@ -5,9 +5,6 @@ import amidst.documentation.GsonConstructor; import amidst.documentation.Immutable; -import amidst.mojangapi.MojangApi; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.file.json.ReleaseType; @Immutable public class VersionListJson { @@ -20,18 +17,4 @@ public VersionListJson() { public List getVersions() { return versions; } - - public VersionDirectory tryFindFirstValidVersionDirectory( - List allowedReleaseTypes, - MojangApi mojangApi) { - for (VersionListEntryJson version : versions) { - if (allowedReleaseTypes.contains(version.getType())) { - VersionDirectory versionDirectory = version.createVersionDirectory(mojangApi); - if (versionDirectory.isValid()) { - return versionDirectory; - } - } - } - return null; - } } diff --git a/src/main/java/amidst/mojangapi/file/nbt/LevelDatNbt.java b/src/main/java/amidst/mojangapi/file/nbt/LevelDatNbt.java index 650d5564d..1483f2bd9 100644 --- a/src/main/java/amidst/mojangapi/file/nbt/LevelDatNbt.java +++ b/src/main/java/amidst/mojangapi/file/nbt/LevelDatNbt.java @@ -1,74 +1,102 @@ package amidst.mojangapi.file.nbt; +import java.io.File; +import java.io.IOException; + import org.jnbt.CompoundTag; import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; import amidst.mojangapi.world.WorldType; import amidst.mojangapi.world.coordinates.CoordinatesInWorld; +import amidst.parsing.FormatException; @Immutable public class LevelDatNbt { - private final long seed; - private final CoordinatesInWorld worldSpawn; - private final WorldType worldType; - private final String generatorOptions; - private final boolean hasPlayer; - - public LevelDatNbt(CompoundTag root) throws MojangApiParsingException { + public static LevelDatNbt from(File file) throws IOException, FormatException { try { - CompoundTag dataTag = readDataTag(root); - this.seed = readRandomSeed(dataTag); - this.worldSpawn = CoordinatesInWorld.from(readSpawnX(dataTag), readSpawnZ(dataTag)); - if (hasGeneratorName(dataTag)) { - this.worldType = WorldType.from(readGeneratorName(dataTag)); - if (worldType == WorldType.CUSTOMIZED) { - this.generatorOptions = readGeneratorOptions(dataTag); - } else { - this.generatorOptions = ""; - } - } else { - this.worldType = WorldType.DEFAULT; - this.generatorOptions = ""; - } - this.hasPlayer = hasPlayerTag(dataTag); + CompoundTag dataTag = readDataTag(NBTUtils.readTagFromFile(file)); + long seed = readRandomSeed(dataTag); + CoordinatesInWorld worldSpawn = readWorldSpawn(dataTag); + WorldType worldType = readWorldType(dataTag); + String generatorOptions = readGeneratorOptions(dataTag, worldType); + boolean hasPlayer = hasPlayerTag(dataTag); + return new LevelDatNbt(seed, worldSpawn, worldType, generatorOptions, hasPlayer); } catch (NullPointerException e) { - throw new MojangApiParsingException("cannot read leve.dat", e); + throw new FormatException("cannot read level.dat: " + file); } } - private CompoundTag readDataTag(CompoundTag root) { + private static CompoundTag readDataTag(CompoundTag root) { return (CompoundTag) root.getValue().get(NBTTagKeys.TAG_KEY_DATA); } - private long readRandomSeed(CompoundTag dataTag) { + private static long readRandomSeed(CompoundTag dataTag) { return NBTUtils.getLongValue(dataTag.getValue().get(NBTTagKeys.TAG_KEY_RANDOM_SEED)); } - private long readSpawnX(CompoundTag dataTag) { + private static CoordinatesInWorld readWorldSpawn(CompoundTag dataTag) { + return CoordinatesInWorld.from(readSpawnX(dataTag), readSpawnZ(dataTag)); + } + + private static long readSpawnX(CompoundTag dataTag) { return NBTUtils.getLongValue(dataTag.getValue().get(NBTTagKeys.TAG_KEY_SPAWN_X)); } - private long readSpawnZ(CompoundTag dataTag) { + private static long readSpawnZ(CompoundTag dataTag) { return NBTUtils.getLongValue(dataTag.getValue().get(NBTTagKeys.TAG_KEY_SPAWN_Z)); } - private boolean hasGeneratorName(CompoundTag dataTag) { + private static WorldType readWorldType(CompoundTag dataTag) { + if (hasGeneratorName(dataTag)) { + return WorldType.from(readGeneratorName(dataTag)); + } else { + return WorldType.DEFAULT; + } + } + + private static boolean hasGeneratorName(CompoundTag dataTag) { return dataTag.getValue().get(NBTTagKeys.TAG_KEY_GENERATOR_NAME) != null; } - private String readGeneratorName(CompoundTag dataTag) { + private static String readGeneratorOptions(CompoundTag dataTag, WorldType worldType) { + if (worldType == WorldType.CUSTOMIZED) { + return readGeneratorOptions(dataTag); + } else { + return ""; + } + } + + private static String readGeneratorName(CompoundTag dataTag) { return (String) dataTag.getValue().get(NBTTagKeys.TAG_KEY_GENERATOR_NAME).getValue(); } - private String readGeneratorOptions(CompoundTag dataTag) { + private static String readGeneratorOptions(CompoundTag dataTag) { return (String) dataTag.getValue().get(NBTTagKeys.TAG_KEY_GENERATOR_OPTIONS).getValue(); } - private boolean hasPlayerTag(CompoundTag dataTag) { + private static boolean hasPlayerTag(CompoundTag dataTag) { return dataTag.getValue().containsKey(NBTTagKeys.TAG_KEY_PLAYER); } + private final long seed; + private final CoordinatesInWorld worldSpawn; + private final WorldType worldType; + private final String generatorOptions; + private final boolean hasPlayer; + + public LevelDatNbt( + long seed, + CoordinatesInWorld worldSpawn, + WorldType worldType, + String generatorOptions, + boolean hasPlayer) { + this.seed = seed; + this.worldSpawn = worldSpawn; + this.worldType = worldType; + this.generatorOptions = generatorOptions; + this.hasPlayer = hasPlayer; + } + public long getSeed() { return seed; } diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/LevelDatPlayerNbt.java b/src/main/java/amidst/mojangapi/file/nbt/player/LevelDatPlayerNbt.java deleted file mode 100644 index a743958a6..000000000 --- a/src/main/java/amidst/mojangapi/file/nbt/player/LevelDatPlayerNbt.java +++ /dev/null @@ -1,40 +0,0 @@ -package amidst.mojangapi.file.nbt.player; - -import java.io.IOException; - -import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.world.player.Player; -import amidst.mojangapi.world.player.PlayerCoordinates; -import amidst.mojangapi.world.player.PlayerInformation; -import amidst.mojangapi.world.player.PlayerInformationCache; - -@Immutable -public class LevelDatPlayerNbt extends PlayerNbt { - private final SaveDirectory saveDirectory; - - public LevelDatPlayerNbt(SaveDirectory saveDirectory) { - this.saveDirectory = saveDirectory; - } - - @Override - protected boolean tryBackup() { - return saveDirectory.tryBackupLevelDat(); - } - - @Override - protected void doWriteCoordinates(PlayerCoordinates coordinates) throws MojangApiParsingException { - PlayerLocationSaver.writeToLevelDat(coordinates, saveDirectory.getLevelDat()); - } - - @Override - public PlayerCoordinates readCoordinates() throws IOException, MojangApiParsingException { - return PlayerLocationLoader.readFromLevelDat(saveDirectory.readLevelDat()); - } - - @Override - public Player createPlayer(PlayerInformationCache cache) { - return new Player(PlayerInformation.theSingleplayerPlayer(), this); - } -} diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationLoader.java b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationLoader.java index f2514d11a..8e64471a3 100644 --- a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationLoader.java +++ b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationLoader.java @@ -1,6 +1,9 @@ package amidst.mojangapi.file.nbt.player; +import java.io.File; +import java.io.IOException; import java.util.List; +import java.util.Optional; import org.jnbt.CompoundTag; import org.jnbt.IntTag; @@ -8,27 +11,31 @@ import org.jnbt.Tag; import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; +import amidst.logging.AmidstLogger; import amidst.mojangapi.file.nbt.NBTTagKeys; +import amidst.mojangapi.file.nbt.NBTUtils; import amidst.mojangapi.world.player.PlayerCoordinates; @Immutable public enum PlayerLocationLoader { ; - public static PlayerCoordinates readFromPlayerFile(CompoundTag file) throws MojangApiParsingException { + public static Optional tryReadFromPlayerFile(File file) throws IOException { try { - return readPlayerCoordinates(file); + return Optional.of(readPlayerCoordinates(NBTUtils.readTagFromFile(file))); } catch (NullPointerException e) { - throw new MojangApiParsingException("cannot read player coordinates", e); + AmidstLogger.warn(e, "cannot read player from file: " + file); + return Optional.empty(); } } - public static PlayerCoordinates readFromLevelDat(CompoundTag file) throws MojangApiParsingException { + public static Optional tryReadFromLevelDat(File file) throws IOException { try { - return readPlayerCoordinates(getSinglePlayerPlayerTag(getTagRootTag(file))); + return Optional + .of(readPlayerCoordinates(getSinglePlayerPlayerTag(getTagRootTag(NBTUtils.readTagFromFile(file))))); } catch (NullPointerException e) { - throw new MojangApiParsingException("cannot read player coordinates", e); + AmidstLogger.warn(e, "cannot read player from level.dat: " + file); + return Optional.empty(); } } diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationSaver.java b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationSaver.java index 2dd30c5d5..ab00d0752 100644 --- a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationSaver.java +++ b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerLocationSaver.java @@ -13,7 +13,7 @@ import org.jnbt.Tag; import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; +import amidst.logging.AmidstLogger; import amidst.mojangapi.file.nbt.NBTTagKeys; import amidst.mojangapi.file.nbt.NBTUtils; import amidst.mojangapi.world.player.PlayerCoordinates; @@ -22,23 +22,27 @@ public enum PlayerLocationSaver { ; - public static void writeToPlayerFile(PlayerCoordinates coordinates, File file) throws MojangApiParsingException { + public static boolean tryWriteToPlayerFile(PlayerCoordinates coordinates, File file) throws IOException { try { CompoundTag dataTag = NBTUtils.readTagFromFile(file); CompoundTag modifiedDataTag = modifyPositionInDataTagMultiPlayer(dataTag, coordinates); NBTUtils.writeTagToFile(file, modifiedDataTag); - } catch (IOException | NullPointerException e) { - throw new MojangApiParsingException("cannot write player coordinates", e); + return true; + } catch (NullPointerException e) { + AmidstLogger.warn(e, "cannot write player to file: " + file); + return false; } } - public static void writeToLevelDat(PlayerCoordinates coordinates, File file) throws MojangApiParsingException { + public static boolean tryWriteToLevelDat(PlayerCoordinates coordinates, File file) throws IOException { try { CompoundTag baseTag = NBTUtils.readTagFromFile(file); CompoundTag modifiedBaseTag = modifyPositionInBaseTagSinglePlayer(baseTag, coordinates); NBTUtils.writeTagToFile(file, modifiedBaseTag); - } catch (IOException | NullPointerException e) { - throw new MojangApiParsingException("cannot write player coordinates", e); + return true; + } catch (NullPointerException e) { + AmidstLogger.warn(e, "cannot write player to level.dat: " + file); + return false; } } diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerNbt.java b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerNbt.java index 354141fb2..9b62690ee 100644 --- a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerNbt.java +++ b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerNbt.java @@ -1,29 +1,25 @@ package amidst.mojangapi.file.nbt.player; -import java.io.IOException; +import java.util.function.Function; +import java.util.function.Supplier; import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.world.player.Player; import amidst.mojangapi.world.player.PlayerCoordinates; -import amidst.mojangapi.world.player.PlayerInformationCache; @Immutable public abstract class PlayerNbt { - public boolean tryWriteCoordinates(PlayerCoordinates coordinates) throws MojangApiParsingException { - if (tryBackup()) { - doWriteCoordinates(coordinates); - return true; - } else { - return false; - } - } - - protected abstract boolean tryBackup(); + private final PlayerCoordinates playerCoordinates; - protected abstract void doWriteCoordinates(PlayerCoordinates coordinates) throws MojangApiParsingException; + public PlayerNbt(PlayerCoordinates playerCoordinates) { + this.playerCoordinates = playerCoordinates; + } - public abstract PlayerCoordinates readCoordinates() throws IOException, MojangApiParsingException; + public PlayerCoordinates getPlayerCoordinates() { + return playerCoordinates; + } - public abstract Player createPlayer(PlayerInformationCache cache); + public abstract R map( + Supplier ifIsLevelDat, + Function ifIsPlayerdata, + Function ifIsPlayers); } diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerdataPlayerNbt.java b/src/main/java/amidst/mojangapi/file/nbt/player/PlayerdataPlayerNbt.java deleted file mode 100644 index f02be21dd..000000000 --- a/src/main/java/amidst/mojangapi/file/nbt/player/PlayerdataPlayerNbt.java +++ /dev/null @@ -1,43 +0,0 @@ -package amidst.mojangapi.file.nbt.player; - -import java.io.IOException; - -import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.nbt.NBTUtils; -import amidst.mojangapi.world.player.Player; -import amidst.mojangapi.world.player.PlayerCoordinates; -import amidst.mojangapi.world.player.PlayerInformationCache; - -@Immutable -public class PlayerdataPlayerNbt extends PlayerNbt { - private final SaveDirectory saveDirectory; - private final String playerUUID; - - public PlayerdataPlayerNbt(SaveDirectory saveDirectory, String playerUUID) { - this.saveDirectory = saveDirectory; - this.playerUUID = playerUUID; - } - - @Override - protected boolean tryBackup() { - return saveDirectory.tryBackupPlayerdataFile(playerUUID); - } - - @Override - protected void doWriteCoordinates(PlayerCoordinates coordinates) throws MojangApiParsingException { - PlayerLocationSaver.writeToPlayerFile(coordinates, saveDirectory.getPlayerdataFile(playerUUID)); - } - - @Override - public PlayerCoordinates readCoordinates() throws IOException, MojangApiParsingException { - return PlayerLocationLoader - .readFromPlayerFile(NBTUtils.readTagFromFile(saveDirectory.getPlayerdataFile(playerUUID))); - } - - @Override - public Player createPlayer(PlayerInformationCache cache) { - return new Player(cache.getByUUID(playerUUID), this); - } -} diff --git a/src/main/java/amidst/mojangapi/file/nbt/player/PlayersPlayerNbt.java b/src/main/java/amidst/mojangapi/file/nbt/player/PlayersPlayerNbt.java deleted file mode 100644 index 53c7cede9..000000000 --- a/src/main/java/amidst/mojangapi/file/nbt/player/PlayersPlayerNbt.java +++ /dev/null @@ -1,43 +0,0 @@ -package amidst.mojangapi.file.nbt.player; - -import java.io.IOException; - -import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.nbt.NBTUtils; -import amidst.mojangapi.world.player.Player; -import amidst.mojangapi.world.player.PlayerCoordinates; -import amidst.mojangapi.world.player.PlayerInformationCache; - -@Immutable -public class PlayersPlayerNbt extends PlayerNbt { - private final SaveDirectory saveDirectory; - private final String playerName; - - public PlayersPlayerNbt(SaveDirectory saveDirectory, String playerName) { - this.saveDirectory = saveDirectory; - this.playerName = playerName; - } - - @Override - protected boolean tryBackup() { - return saveDirectory.tryBackupPlayersFile(playerName); - } - - @Override - protected void doWriteCoordinates(PlayerCoordinates coordinates) throws MojangApiParsingException { - PlayerLocationSaver.writeToPlayerFile(coordinates, saveDirectory.getPlayersFile(playerName)); - } - - @Override - public PlayerCoordinates readCoordinates() throws IOException, MojangApiParsingException { - return PlayerLocationLoader - .readFromPlayerFile(NBTUtils.readTagFromFile(saveDirectory.getPlayersFile(playerName))); - } - - @Override - public Player createPlayer(PlayerInformationCache cache) { - return new Player(cache.getByName(playerName), this); - } -} diff --git a/src/main/java/amidst/mojangapi/file/service/AmidstBackupService.java b/src/main/java/amidst/mojangapi/file/service/AmidstBackupService.java new file mode 100644 index 000000000..ddce2fbd8 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/AmidstBackupService.java @@ -0,0 +1,61 @@ +package amidst.mojangapi.file.service; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.sql.Timestamp; + +import amidst.documentation.Immutable; +import amidst.mojangapi.file.directory.SaveDirectory; + +@Immutable +public class AmidstBackupService { + public boolean tryBackupPlayersFile(SaveDirectory saveDirectory, String playerName) { + File toDirectory = saveDirectory.getAmidst().getBackup().getPlayers(); + File from = saveDirectory.getPlayersFile(playerName); + File to = new File(toDirectory, playerName + ".dat" + "_" + millis()); + return tryBackup(toDirectory, from, to); + } + + public boolean tryBackupPlayerdataFile(SaveDirectory saveDirectory, String playerUUID) { + File toDirectory = saveDirectory.getAmidst().getBackup().getPlayerdata(); + File from = saveDirectory.getPlayerdataFile(playerUUID); + File to = new File(toDirectory, playerUUID + ".dat" + "_" + millis()); + return tryBackup(toDirectory, from, to); + } + + public boolean tryBackupLevelDat(SaveDirectory saveDirectory) { + File toDirectory = saveDirectory.getAmidst().getBackup().getRoot(); + File from = saveDirectory.getLevelDat(); + File to = new File(toDirectory, "level.dat" + "_" + millis()); + return tryBackup(toDirectory, from, to); + } + + private String millis() { + return new Timestamp(System.currentTimeMillis()) + .toString() + .replace(" ", "_") + .replace(":", "-") + .replace(".", "_"); + } + + private boolean tryBackup(File toDirectory, File from, File to) { + return ensureDirectoryExists(toDirectory) && tryCopy(from, to) && to.isFile(); + } + + private boolean ensureDirectoryExists(File directory) { + if (!directory.exists()) { + directory.mkdirs(); + } + return directory.isDirectory(); + } + + private boolean tryCopy(File from, File to) { + try { + Files.copy(from.toPath(), to.toPath()); + return true; + } catch (IOException e) { + return false; + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/service/ClassLoaderService.java b/src/main/java/amidst/mojangapi/file/service/ClassLoaderService.java new file mode 100644 index 000000000..d535d37aa --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/ClassLoaderService.java @@ -0,0 +1,153 @@ +package amidst.mojangapi.file.service; + +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.regex.Pattern; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.json.version.LibraryJson; +import amidst.mojangapi.file.json.version.LibraryRuleJson; +import amidst.mojangapi.file.json.version.LibraryRuleOsJson; +import amidst.util.OperatingSystemDetector; + +@Immutable +public class ClassLoaderService { + private static final String ACTION_ALLOW = "allow"; + + @NotNull + public URLClassLoader createClassLoader(File librariesDirectory, List libraries, File versionJarFile) + throws MalformedURLException { + List classLoaderUrls = getAllClassLoaderUrls(librariesDirectory, libraries, versionJarFile); + return new URLClassLoader(classLoaderUrls.toArray(new URL[classLoaderUrls.size()])); + } + + @NotNull + private List getAllClassLoaderUrls(File librariesDirectory, List libraries, File versionJarFile) + throws MalformedURLException { + List result = new LinkedList<>(getLibraryUrls(librariesDirectory, libraries)); + result.add(versionJarFile.toURI().toURL()); + return Collections.unmodifiableList(result); + } + + @NotNull + private List getLibraryUrls(File librariesDirectory, List libraries) { + List result = new ArrayList<>(); + AmidstLogger.info("Loading libraries."); + for (LibraryJson library : libraries) { + if (isLibraryActive(library)) { + Optional libraryFile = getLibraryFile(librariesDirectory, library); + if (libraryFile.isPresent()) { + try { + URL libraryUrl = libraryFile.get().toURI().toURL(); + result.add(libraryUrl); + AmidstLogger.info("Found library " + library.getName() + " at " + libraryUrl); + } catch (MalformedURLException e) { + AmidstLogger.warn(e, "Skipping erroneous library " + library.getName()); + } + } else { + AmidstLogger.warn("Skipping missing library " + library.getName()); + } + } else { + AmidstLogger.info("Skipping inactive library " + library.getName()); + } + } + AmidstLogger.info("Finished loading libraries."); + return result; + } + + private boolean isLibraryActive(LibraryJson library) { + return isLibraryActive(getOsName(), OperatingSystemDetector.getVersion(), library.getRules()); + } + + private String getOsName() { + if (OperatingSystemDetector.isWindows()) { + return "windows"; + } else if (OperatingSystemDetector.isMac()) { + return "osx"; + } else { + return "linux"; + } + } + + /** + * Note, that multiple rules might be applicable. We take the last + * applicable rule. However, this might be wrong so we need to take the most + * specific rule? For now this works fine. + */ + private boolean isLibraryActive(String osName, String osVersion, List rules) { + if (rules.isEmpty()) { + return true; + } + boolean result = false; + for (LibraryRuleJson rule : rules) { + if (isApplicable(osName, osVersion, rule)) { + result = isAllowed(rule); + } + } + return result; + } + + private boolean isApplicable(String osName, String osVersion, LibraryRuleJson rule) { + LibraryRuleOsJson osRule = rule.getOs(); + return osRule == null || Objects.equals(osRule.getName(), osName) + && (osRule.getVersion() == null || Pattern.matches(osRule.getVersion(), osVersion)); + } + + private boolean isAllowed(LibraryRuleJson rule) { + return Objects.equals(rule.getAction(), ACTION_ALLOW); + } + + private Optional getLibraryFile(File librariesDirectory, LibraryJson library) { + return Arrays + .stream(getLibrarySearchFiles(librariesDirectory, library)) + .filter(f -> hasFileExtension(f, "jar")) + .findFirst() + .filter(File::exists); + } + + private File[] getLibrarySearchFiles(File librariesDirectory, LibraryJson library) { + return getLibrarySearchFiles(getLibrarySearchPath(librariesDirectory.getAbsolutePath(), library.getName())); + } + + private File getLibrarySearchPath(String librariesDirectory, String libraryName) { + String result = librariesDirectory + "/"; + String[] split = libraryName.split(":"); + split[0] = split[0].replace('.', '/'); + for (String element : split) { + result += element + "/"; + } + return new File(result); + } + + private File[] getLibrarySearchFiles(File librarySearchPath) { + if (librarySearchPath.isDirectory()) { + return librarySearchPath.listFiles(); + } else { + return new File[0]; + } + } + + private boolean hasFileExtension(File file, String extension) { + return getFileExtension(file.getName()).equals(extension); + } + + private String getFileExtension(String fileName) { + int index = fileName.lastIndexOf('.'); + if (index > 0) { + return fileName.substring(index + 1); + } else { + return ""; + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/service/DotMinecraftDirectoryService.java b/src/main/java/amidst/mojangapi/file/service/DotMinecraftDirectoryService.java new file mode 100644 index 000000000..e10eeb3c5 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/DotMinecraftDirectoryService.java @@ -0,0 +1,212 @@ +package amidst.mojangapi.file.service; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.DotMinecraftDirectoryNotFoundException; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; +import amidst.mojangapi.file.directory.DotMinecraftDirectory; +import amidst.mojangapi.file.directory.ProfileDirectory; +import amidst.mojangapi.file.directory.VersionDirectory; +import amidst.mojangapi.file.json.ReleaseType; +import amidst.mojangapi.file.json.launcherprofiles.LauncherProfileJson; +import amidst.mojangapi.file.json.launcherprofiles.LauncherProfilesJson; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; +import amidst.util.OperatingSystemDetector; + +@Immutable +public class DotMinecraftDirectoryService { + private final FilenameService filenameService = new FilenameService(); + + @NotNull + public DotMinecraftDirectory createCustomDotMinecraftDirectory( + File libraries, + File saves, + File versions, + File launcherProfilesJson) throws DotMinecraftDirectoryNotFoundException { + return validate( + DotMinecraftDirectory + .newCustom(getMinecraftDirectory(), libraries, saves, versions, launcherProfilesJson)); + } + + @NotNull + public DotMinecraftDirectory createDotMinecraftDirectory(String preferredDotMinecraftDirectory) + throws DotMinecraftDirectoryNotFoundException { + return validate(new DotMinecraftDirectory(findDotMinecraftDirectory(preferredDotMinecraftDirectory))); + } + + @NotNull + private DotMinecraftDirectory validate(DotMinecraftDirectory dotMinecraftDirectory) + throws DotMinecraftDirectoryNotFoundException { + if (dotMinecraftDirectory.isValid()) { + return dotMinecraftDirectory; + } else { + throw new DotMinecraftDirectoryNotFoundException( + "invalid '.minecraft' directory at: '" + dotMinecraftDirectory.getRoot() + "'"); + } + } + + @NotNull + private File findDotMinecraftDirectory(String preferredDotMinecraftDirectory) { + if (preferredDotMinecraftDirectory != null) { + File result = new File(preferredDotMinecraftDirectory); + if (result.isDirectory()) { + return result; + } else { + AmidstLogger.warn( + "Unable to set Minecraft directory to: " + result + + " as that location does not exist or is not a folder."); + return getMinecraftDirectory(); + } + } else { + return getMinecraftDirectory(); + } + } + + @NotNull + private File getMinecraftDirectory() { + File home = new File(System.getProperty("user.home", ".")); + if (OperatingSystemDetector.isWindows()) { + File appData = new File(System.getenv("APPDATA")); + if (appData.isDirectory()) { + return new File(appData, ".minecraft"); + } + } else if (OperatingSystemDetector.isMac()) { + return new File(home, "Library/Application Support/minecraft"); + } + return new File(home, ".minecraft"); + } + + @NotNull + public LauncherProfilesJson readLauncherProfilesFrom(DotMinecraftDirectory dotMinecraftDirectory) + throws FormatException, + IOException { + return JsonReader.readLocation(dotMinecraftDirectory.getLauncherProfilesJson(), LauncherProfilesJson.class); + } + + @NotNull + public ProfileDirectory createValidProfileDirectory( + LauncherProfileJson launcherProfileJson, + DotMinecraftDirectory dotMinecraftDirectory) throws FileNotFoundException { + String gameDir = launcherProfileJson.getGameDir(); + ProfileDirectory result = createProfileDirectory(dotMinecraftDirectory, gameDir); + if (result.isValid()) { + return result; + } else { + throw new FileNotFoundException( + "cannot find valid profile directory for launcher profile '" + launcherProfileJson.getName() + "': " + + gameDir); + } + } + + @NotNull + private ProfileDirectory createProfileDirectory(DotMinecraftDirectory dotMinecraftDirectory, String gameDir) { + if (gameDir != null) { + return new ProfileDirectory(new File(gameDir)); + } else { + return dotMinecraftDirectory.asProfileDirectory(); + } + } + + @NotNull + public VersionDirectory createValidVersionDirectory( + LauncherProfileJson launcherProfileJson, + VersionList versionList, + DotMinecraftDirectory dotMinecraftDirectory) throws FileNotFoundException { + String lastVersionId = launcherProfileJson.getLastVersionId(); + if (lastVersionId != null) { + VersionDirectory result = createVersionDirectory(dotMinecraftDirectory, lastVersionId); + if (result.isValid()) { + return result; + } else { + // error + } + } else { + VersionDirectory result = tryFindFirstValidVersionDirectory( + launcherProfileJson.getAllowedReleaseTypes(), + versionList, + dotMinecraftDirectory); + if (result != null) { + return result; + } else { + // error + } + } + throw new FileNotFoundException( + "cannot find valid version directory for launcher profile '" + launcherProfileJson.getName() + "'"); + } + + private VersionDirectory tryFindFirstValidVersionDirectory( + List allowedReleaseTypes, + VersionList versionList, + DotMinecraftDirectory dotMinecraftDirectory) { + for (Version version : versionList.getVersions()) { + if (allowedReleaseTypes.contains(version.getType())) { + VersionDirectory versionDirectory = createVersionDirectory(dotMinecraftDirectory, version.getId()); + if (versionDirectory.isValid()) { + return versionDirectory; + } + } + } + return null; + } + + @NotNull + public VersionDirectory createValidVersionDirectory(File jar, File json) throws FileNotFoundException { + VersionDirectory versionDirectory = new VersionDirectory(jar, json); + if (versionDirectory.isValid()) { + return versionDirectory; + } else { + throw new FileNotFoundException( + "cannot find valid version directory for jar: '" + jar + "', json: '" + json + "'"); + } + } + + @NotNull + public VersionDirectory createValidVersionDirectory(DotMinecraftDirectory dotMinecraftDirectory, String versionId) + throws FileNotFoundException { + VersionDirectory versionDirectory = createVersionDirectory(dotMinecraftDirectory, versionId); + if (versionDirectory.isValid()) { + return versionDirectory; + } else { + throw new FileNotFoundException("cannot find valid version directory for version id '" + versionId + "'"); + } + } + + @NotNull + private VersionDirectory createVersionDirectory(DotMinecraftDirectory dotMinecraftDirectory, String versionId) { + File versions = dotMinecraftDirectory.getVersions(); + File jar = filenameService.getClientJarFile(versions, versionId); + File json = filenameService.getClientJsonFile(versions, versionId); + return new VersionDirectory(jar, json); + } + + public List findInstalledValidVersionDirectories(DotMinecraftDirectory dotMinecraftDirectory) { + return listFiles(dotMinecraftDirectory.getVersions()) + .stream() + .filter(File::isDirectory) + .map(File::getName) + .map(id -> createVersionDirectory(dotMinecraftDirectory, id)) + .filter(VersionDirectory::isValid) + .collect(Collectors.toList()); + } + + private List listFiles(File file) { + File[] files = file.listFiles(); + if (files != null) { + return Arrays.asList(files); + } else { + return Collections.emptyList(); + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/service/DownloadService.java b/src/main/java/amidst/mojangapi/file/service/DownloadService.java new file mode 100644 index 000000000..ed49ca896 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/DownloadService.java @@ -0,0 +1,81 @@ +package amidst.mojangapi.file.service; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import amidst.documentation.Immutable; +import amidst.logging.AmidstLogger; +import amidst.parsing.URIUtils; + +@Immutable +public class DownloadService { + private final FilenameService filenameService = new FilenameService(); + + public boolean hasServer(String versionId) { + return exists(filenameService.getRemoteServerJar(versionId)); + } + + public boolean hasClient(String versionId) { + return exists(filenameService.getRemoteClientJar(versionId)); + } + + private static boolean exists(String location) { + try { + HttpURLConnection connection = (HttpURLConnection) URIUtils.newURL(location).openConnection(); + connection.setRequestMethod("HEAD"); + return connection.getResponseCode() == HttpURLConnection.HTTP_OK; + } catch (IOException e) { + return false; + } + } + + public boolean tryDownloadServer(String prefix, String versionId) { + try { + downloadServer(prefix, versionId); + return true; + } catch (IOException e) { + AmidstLogger.warn(e, "unable to download server: " + versionId); + } + return false; + } + + public boolean tryDownloadClient(String prefix, String versionId) { + try { + downloadClient(prefix, versionId); + return true; + } catch (IOException e) { + AmidstLogger.warn(e, "unable to download client: " + versionId); + } + return false; + } + + public void downloadServer(String prefix, String versionId) throws IOException { + download(filenameService.getRemoteServerJar(versionId), filenameService.getServerJar(prefix, versionId)); + } + + public void downloadClient(String prefix, String versionId) throws IOException { + download(filenameService.getRemoteClientJar(versionId), filenameService.getClientJar(prefix, versionId)); + download(filenameService.getRemoteClientJson(versionId), filenameService.getClientJson(prefix, versionId)); + } + + private void download(String from, String to) throws IOException { + download(URIUtils.newURL(from), Paths.get(to)); + } + + private void download(URL from, Path to) throws IOException { + to.getParent().toFile().mkdirs(); + if (to.toFile().exists()) { + return; + } + Path part = Paths.get(to.toString() + ".part"); + InputStream in = URIUtils.newInputStream(from); + Files.copy(in, part, StandardCopyOption.REPLACE_EXISTING); + Files.move(part, to, StandardCopyOption.REPLACE_EXISTING); + } +} diff --git a/src/main/java/amidst/mojangapi/file/FilenameFactory.java b/src/main/java/amidst/mojangapi/file/service/FilenameService.java similarity index 56% rename from src/main/java/amidst/mojangapi/file/FilenameFactory.java rename to src/main/java/amidst/mojangapi/file/service/FilenameService.java index 09be06c65..599342609 100644 --- a/src/main/java/amidst/mojangapi/file/FilenameFactory.java +++ b/src/main/java/amidst/mojangapi/file/service/FilenameService.java @@ -1,60 +1,58 @@ -package amidst.mojangapi.file; +package amidst.mojangapi.file.service; import java.io.File; import amidst.documentation.Immutable; @Immutable -public enum FilenameFactory { - ; - +public class FilenameService { private static final String REMOTE_PREFIX = "https://s3.amazonaws.com/Minecraft.Download/versions/"; private static final String MIDDLE_SERVER = "/minecraft_server."; private static final String MIDDLE_CLIENT = "/"; private static final String JAR_FILE_EXTENSION = ".jar"; private static final String JSON_FILE_EXTENSION = ".json"; - public static File getClientJsonFile(File prefix, String versionId) { + public File getClientJsonFile(File prefix, String versionId) { return getClientFile(prefix, versionId, JSON_FILE_EXTENSION); } - public static File getClientJarFile(File prefix, String versionId) { + public File getClientJarFile(File prefix, String versionId) { return getClientFile(prefix, versionId, JAR_FILE_EXTENSION); } - private static File getClientFile(File prefix, String versionId, String fileExtension) { + private File getClientFile(File prefix, String versionId, String fileExtension) { return new File(prefix, getClientLocation("", versionId, fileExtension)); } - public static String getRemoteClientJson(String versionId) { + public String getRemoteClientJson(String versionId) { return getClientJson(REMOTE_PREFIX, versionId); } - public static String getClientJson(String prefix, String versionId) { + public String getClientJson(String prefix, String versionId) { return getClientLocation(prefix, versionId, JSON_FILE_EXTENSION); } - public static String getRemoteClientJar(String versionId) { + public String getRemoteClientJar(String versionId) { return getClientJar(REMOTE_PREFIX, versionId); } - public static String getClientJar(String prefix, String versionId) { + public String getClientJar(String prefix, String versionId) { return getClientLocation(prefix, versionId, JAR_FILE_EXTENSION); } - private static String getClientLocation(String prefix, String versionId, String fileExtension) { + private String getClientLocation(String prefix, String versionId, String fileExtension) { return prefix + versionId + MIDDLE_CLIENT + versionId + fileExtension; } - public static String getRemoteServerJar(String versionId) { + public String getRemoteServerJar(String versionId) { return getServerJar(REMOTE_PREFIX, versionId); } - public static String getServerJar(String prefix, String versionId) { + public String getServerJar(String prefix, String versionId) { return getServerLocation(prefix, versionId, JAR_FILE_EXTENSION); } - private static String getServerLocation(String prefix, String versionId, String fileExtension) { + private String getServerLocation(String prefix, String versionId, String fileExtension) { return prefix + versionId + MIDDLE_SERVER + versionId + fileExtension; } } diff --git a/src/main/java/amidst/mojangapi/file/service/PlayerInformationService.java b/src/main/java/amidst/mojangapi/file/service/PlayerInformationService.java new file mode 100644 index 000000000..a0a3d6473 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/PlayerInformationService.java @@ -0,0 +1,205 @@ +package amidst.mojangapi.file.service; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Optional; + +import javax.imageio.ImageIO; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.json.player.PlayerJson; +import amidst.mojangapi.file.json.player.PropertyJson; +import amidst.mojangapi.file.json.player.SKINJson; +import amidst.mojangapi.file.json.player.SimplePlayerJson; +import amidst.mojangapi.file.json.player.TexturesJson; +import amidst.mojangapi.file.json.player.TexturesPropertyJson; +import amidst.mojangapi.world.icon.WorldIconImage; +import amidst.mojangapi.world.icon.type.DefaultWorldIconTypes; +import amidst.mojangapi.world.player.PlayerInformation; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; + +@Immutable +public class PlayerInformationService { + private static final WorldIconImage DEFAULT_HEAD = DefaultWorldIconTypes.PLAYER.getImage(); + + private static final String SIMPLE_PLAYER_SKIN_URL = "http://s3.amazonaws.com/MinecraftSkins/"; + private static final String PLAYERNAME_TO_UUID = "https://api.mojang.com/users/profiles/minecraft/"; + private static final String UUID_TO_PROFILE = "https://sessionserver.mojang.com/session/minecraft/profile/"; + + @NotNull + public PlayerInformation fromUUID(String uuid) { + Optional optionalPlayer = tryGetPlayerJsonByUUID(uuid); + Optional head; + if (optionalPlayer.isPresent()) { + PlayerJson player = optionalPlayer.get(); + head = tryGetSkinUrl(player).flatMap(this::tryGetPlayerHeadBySkinUrl); + if (head.isPresent()) { + return new PlayerInformation(player.getId(), player.getName(), head.get()); + } else { + return new PlayerInformation(player.getId(), player.getName(), DEFAULT_HEAD); + } + } else { + return new PlayerInformation(uuid, null, DEFAULT_HEAD); + } + } + + @NotNull + public PlayerInformation fromName(String name) { + Optional optionalPlayer = tryGetPlayerJsonByName(name); + Optional head; + if (optionalPlayer.isPresent()) { + PlayerJson player = optionalPlayer.get(); + head = tryGetSkinUrl(player).flatMap(this::tryGetPlayerHeadBySkinUrl); + if (head.isPresent()) { + return new PlayerInformation(player.getId(), player.getName(), head.get()); + } else { + head = tryGetPlayerHeadByName(name); + if (head.isPresent()) { + return new PlayerInformation(player.getId(), player.getName(), head.get()); + } else { + return new PlayerInformation(player.getId(), player.getName(), DEFAULT_HEAD); + } + } + } else { + head = tryGetPlayerHeadByName(name); + if (head.isPresent()) { + return new PlayerInformation(null, name, head.get()); + } else { + return new PlayerInformation(null, name, DEFAULT_HEAD); + } + } + } + + @NotNull + private Optional tryGetSkinUrl(PlayerJson playerJson) { + return tryReadTexturesProperty(playerJson) + .map(TexturesPropertyJson::getTextures) + .map(TexturesJson::getSKIN) + .map(SKINJson::getUrl); + } + + @NotNull + private Optional tryReadTexturesProperty(PlayerJson playerJson) { + try { + for (PropertyJson property : playerJson.getProperties()) { + if (isTexturesProperty(property)) { + return Optional.of(JsonReader.readString(getDecodedValue(property), TexturesPropertyJson.class)); + } + } + return Optional.empty(); + } catch (FormatException e) { + return Optional.empty(); + } + } + + private boolean isTexturesProperty(PropertyJson propertyJson) throws FormatException { + String name = propertyJson.getName(); + if (name == null) { + throw new FormatException("property has no name"); + } else { + return name.equals("textures"); + } + } + + @NotNull + private String getDecodedValue(PropertyJson propertyJson) throws FormatException { + String value = propertyJson.getValue(); + if (value == null) { + throw new FormatException("unable to decode property value"); + } else { + return new String( + Base64.getDecoder().decode(value.getBytes(StandardCharsets.UTF_8)), + StandardCharsets.UTF_8); + } + } + + @NotNull + private Optional tryGetPlayerJsonByName(String name) { + try { + return Optional.of(getPlayerJsonByName(name)); + } catch (IOException | FormatException | NullPointerException e) { + AmidstLogger.warn("unable to load player information by name: " + name); + return Optional.empty(); + } + } + + @NotNull + private Optional tryGetPlayerJsonByUUID(String uuid) { + try { + return Optional.of(getPlayerJsonByUUID(uuid)); + } catch (IOException | FormatException | NullPointerException e) { + AmidstLogger.warn("unable to load player information by uuid: " + uuid); + return Optional.empty(); + } + } + + @NotNull + private PlayerJson getPlayerJsonByName(String name) throws FormatException, IOException { + return getPlayerJsonByUUID(getUUIDByName(name).getId()); + } + + @NotNull + private PlayerJson getPlayerJsonByUUID(String uuid) throws FormatException, IOException { + return JsonReader.readLocation(UUID_TO_PROFILE + uuid, PlayerJson.class); + } + + @NotNull + private SimplePlayerJson getUUIDByName(String name) throws FormatException, IOException { + return JsonReader.readLocation(PLAYERNAME_TO_UUID + name, SimplePlayerJson.class); + } + + @NotNull + private Optional tryGetPlayerHeadByName(String name) { + try { + return Optional.of(WorldIconImage.from(getPlayerHeadByName(name))); + } catch (IOException | NullPointerException e) { + AmidstLogger.warn("unable to load player head by name: " + name); + return Optional.empty(); + } + } + + @NotNull + private Optional tryGetPlayerHeadBySkinUrl(String skinUrl) { + try { + return Optional.of(WorldIconImage.from(getPlayerHeadBySkinUrl(skinUrl))); + } catch (IOException | NullPointerException e) { + AmidstLogger.warn("unable to load player head by skin url: " + skinUrl); + return Optional.empty(); + } + } + + @NotNull + private BufferedImage getPlayerHeadByName(String name) throws IOException { + return extractPlayerHead(new URL(SIMPLE_PLAYER_SKIN_URL + name + ".png")); + } + + @NotNull + private BufferedImage getPlayerHeadBySkinUrl(String skinUrl) throws IOException { + return extractPlayerHead(new URL(skinUrl)); + } + + @NotNull + private BufferedImage extractPlayerHead(URL url) throws IOException { + return extractPlayerHead(ImageIO.read(url)); + } + + @NotNull + private BufferedImage extractPlayerHead(BufferedImage skin) { + BufferedImage head = new BufferedImage(20, 20, BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = head.createGraphics(); + g2d.setColor(Color.black); + g2d.fillRect(0, 0, 20, 20); + g2d.drawImage(skin, 2, 2, 18, 18, 8, 8, 16, 16, null); + g2d.dispose(); + skin.flush(); + return head; + } +} diff --git a/src/main/java/amidst/mojangapi/file/service/SaveDirectoryService.java b/src/main/java/amidst/mojangapi/file/service/SaveDirectoryService.java new file mode 100644 index 000000000..c7cba8b88 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/SaveDirectoryService.java @@ -0,0 +1,267 @@ +package amidst.mojangapi.file.service; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.directory.SaveDirectory; +import amidst.mojangapi.file.nbt.LevelDatNbt; +import amidst.mojangapi.file.nbt.player.PlayerLocationLoader; +import amidst.mojangapi.file.nbt.player.PlayerLocationSaver; +import amidst.mojangapi.file.nbt.player.PlayerNbt; +import amidst.mojangapi.world.player.PlayerCoordinates; +import amidst.parsing.FormatException; + +@Immutable +public class SaveDirectoryService { + private final AmidstBackupService amidstBackupService = new AmidstBackupService(); + + /** + * Returns a new valid instance of the class SaveDirectory. It tries to use + * the given file. If that is not valid it tires to use its parent file. If + * that is also not valid it will throw a FileNotFoundException. + */ + @NotNull + public SaveDirectory newSaveDirectory(File file) throws FileNotFoundException { + File currentFile = file; + SaveDirectory result = null; + if (currentFile == null) { + // error + } else { + result = createValidSaveDirectory(currentFile); + currentFile = currentFile.getParentFile(); + if (result != null) { + return result; + } else if (currentFile == null) { + // error + } else { + result = createValidSaveDirectory(currentFile); + currentFile = currentFile.getParentFile(); + if (result != null) { + return result; + } else { + // error + } + } + } + throw new FileNotFoundException("unable to load save directory: " + file); + } + + private SaveDirectory createValidSaveDirectory(File currentFile) { + SaveDirectory result = new SaveDirectory(currentFile); + if (result.isValid()) { + return result; + } else { + return null; + } + } + + public LevelDatNbt readLevelDat(SaveDirectory saveDirectory) throws IOException, FormatException { + return LevelDatNbt.from(saveDirectory.getLevelDat()); + } + + /** + * We need to let the user decide if he wants to load the singleplayer + * player from the level.dat file or if he wants to load the multiplayer + * players. That is, because a singleplayer map will have the playerdata + * directory. It contains information about each player that ever played on + * the map. However, if the map is loaded as singleplayer map, minecraft + * will use the information that is stored in the level.dat file. It will + * also overwrite the file in the playerdata directory that belongs to the + * player that loaded the map in singleplayer mode. So, if we change the + * player location in the playerdata directory it will just be ignored if + * the map is used as singleplayer map. + */ + @NotNull + public Optional tryReadSingleplayerPlayerNbt(SaveDirectory saveDirectory) { + AmidstLogger.info("using player from level.dat"); + return tryReadCoordinatesFromLevelDat(saveDirectory).map(this::createLevelDatPlayerNbt); + } + + /** + * Since version 1.7.6, minecraft stores players in the playerdata directory + * and uses the player uuid as filename. + */ + @NotNull + public List tryReadMultiplayerPlayerNbts(SaveDirectory saveDirectory) { + List playerdataPlayers = listFiles(saveDirectory.getPlayerdata()) + .stream() + .filter(File::isFile) + .map(f -> createPlayerdataPlayerNbt(saveDirectory, f)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + if (!playerdataPlayers.isEmpty()) { + AmidstLogger.info("using players from the playerdata directory"); + return playerdataPlayers; + } else { + List playersPlayers = listFiles(saveDirectory.getPlayers()) + .stream() + .filter(File::isFile) + .map(f -> createPlayersPlayerNbt(saveDirectory, f)) + .filter(Optional::isPresent) + .map(Optional::get) + .collect(Collectors.toList()); + if (!playersPlayers.isEmpty()) { + AmidstLogger.info("using players from the players directory"); + return playersPlayers; + } else { + AmidstLogger.info("no multiplayer players found"); + return Collections.emptyList(); + } + } + } + + private List listFiles(File file) { + File[] files = file.listFiles(); + if (files != null) { + return Arrays.asList(files); + } else { + return Collections.emptyList(); + } + } + + private Optional createPlayerdataPlayerNbt(SaveDirectory saveDirectory, File playerdataFile) { + String playerUUID = getPlayerUUIDFromPlayerdataFile(playerdataFile); + return tryReadCoordinatesFromPlayerdata(saveDirectory, playerUUID) + .map(c -> createPlayerdataPlayerNbt(playerUUID, c)); + } + + private String getPlayerUUIDFromPlayerdataFile(File playerdataFile) { + return playerdataFile.getName().split("\\.")[0]; + } + + private Optional createPlayersPlayerNbt(SaveDirectory saveDirectory, File playersFile) { + String playerName = getPlayerNameFromPlayersFile(playersFile); + return tryReadCoordinatesFromPlayers(saveDirectory, playerName).map(c -> createPlayersPlayerNbt(playerName, c)); + } + + private String getPlayerNameFromPlayersFile(File playersFile) { + return playersFile.getName().split("\\.")[0]; + } + + private PlayerNbt createLevelDatPlayerNbt(PlayerCoordinates playerCoordinates) { + return new PlayerNbt(playerCoordinates) { + @Override + public R map( + Supplier ifIsLevelDat, + Function ifIsPlayerdata, + Function ifIsPlayers) { + return ifIsLevelDat.get(); + } + }; + } + + private PlayerNbt createPlayerdataPlayerNbt(String playerUUID, PlayerCoordinates playerCoordinates) { + return new PlayerNbt(playerCoordinates) { + @Override + public R map( + Supplier ifIsLevelDat, + Function ifIsPlayerdata, + Function ifIsPlayers) { + return ifIsPlayerdata.apply(playerUUID); + } + }; + } + + private PlayerNbt createPlayersPlayerNbt(String playerName, PlayerCoordinates playerCoordinates) { + return new PlayerNbt(playerCoordinates) { + @Override + public R map( + Supplier ifIsLevelDat, + Function ifIsPlayerdata, + Function ifIsPlayers) { + return ifIsPlayers.apply(playerName); + } + }; + } + + private Optional tryReadCoordinatesFromLevelDat(SaveDirectory saveDirectory) { + try { + return PlayerLocationLoader.tryReadFromLevelDat(saveDirectory.getLevelDat()); + } catch (IOException e) { + AmidstLogger.warn(e, "error while reading player coordinates from level.dat"); + return Optional.empty(); + } + } + + private Optional tryReadCoordinatesFromPlayerdata( + SaveDirectory saveDirectory, + String playerUUID) { + try { + return PlayerLocationLoader.tryReadFromPlayerFile(saveDirectory.getPlayerdataFile(playerUUID)); + } catch (IOException e) { + AmidstLogger.warn(e, "error while reading player coordinates for player " + playerUUID); + return Optional.empty(); + } + } + + private Optional tryReadCoordinatesFromPlayers(SaveDirectory saveDirectory, String playerName) { + try { + return PlayerLocationLoader.tryReadFromPlayerFile(saveDirectory.getPlayersFile(playerName)); + } catch (IOException e) { + AmidstLogger.warn(e, "error while reading player coordinates for player " + playerName); + return Optional.empty(); + } + } + + public boolean tryBackup(SaveDirectory saveDirectory, PlayerNbt playerNbt) { + return playerNbt.map( + () -> amidstBackupService.tryBackupLevelDat(saveDirectory), + playerUUID -> amidstBackupService.tryBackupPlayerdataFile(saveDirectory, playerUUID), + playerName -> amidstBackupService.tryBackupPlayersFile(saveDirectory, playerName)); + } + + public boolean tryWriteCoordinates( + SaveDirectory saveDirectory, + PlayerNbt playerNbt, + PlayerCoordinates coordinates) { + return playerNbt.map( + () -> tryWriteCoordinatesToLevelDat(saveDirectory, coordinates), + playerUUID -> tryWriteCoordinatesToPlayerdata(saveDirectory, playerUUID, coordinates), + playerName -> tryWriteCoordinatesToPlayers(saveDirectory, playerName, coordinates)); + } + + private boolean tryWriteCoordinatesToLevelDat(SaveDirectory saveDirectory, PlayerCoordinates coordinates) { + try { + return PlayerLocationSaver.tryWriteToLevelDat(coordinates, saveDirectory.getLevelDat()); + } catch (IOException e) { + AmidstLogger.warn(e, "error while writing player coordinates to level.dat"); + return false; + } + } + + private boolean tryWriteCoordinatesToPlayerdata( + SaveDirectory saveDirectory, + String playerUUID, + PlayerCoordinates coordinates) { + try { + return PlayerLocationSaver.tryWriteToPlayerFile(coordinates, saveDirectory.getPlayerdataFile(playerUUID)); + } catch (IOException e) { + AmidstLogger.warn(e, "error while writing player coordinates for player " + playerUUID); + return false; + } + } + + private boolean tryWriteCoordinatesToPlayers( + SaveDirectory saveDirectory, + String playerName, + PlayerCoordinates coordinates) { + try { + return PlayerLocationSaver.tryWriteToPlayerFile(coordinates, saveDirectory.getPlayersFile(playerName)); + } catch (IOException e) { + AmidstLogger.warn(e, "error while writing player coordinates for player " + playerName); + return false; + } + } +} diff --git a/src/main/java/amidst/mojangapi/file/service/VersionListService.java b/src/main/java/amidst/mojangapi/file/service/VersionListService.java new file mode 100644 index 000000000..93be46fe4 --- /dev/null +++ b/src/main/java/amidst/mojangapi/file/service/VersionListService.java @@ -0,0 +1,28 @@ +package amidst.mojangapi.file.service; + +import java.io.IOException; +import java.net.URL; + +import amidst.ResourceLoader; +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; + +@Immutable +public class VersionListService { + private static final String REMOTE_VERSION_LIST = "https://launchermeta.mojang.com/mc/game/version_manifest.json"; + private static final URL LOCAL_VERSION_LIST = ResourceLoader + .getResourceURL("/amidst/mojangapi/version_manifest.json"); + + @NotNull + public VersionListJson readRemoteVersionList() throws FormatException, IOException { + return JsonReader.readLocation(REMOTE_VERSION_LIST, VersionListJson.class); + } + + @NotNull + public VersionListJson readLocalVersionListFromResource() throws FormatException, IOException { + return JsonReader.readLocation(LOCAL_VERSION_LIST, VersionListJson.class); + } +} diff --git a/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterface.java b/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterface.java index 30116410f..92c8f43a0 100644 --- a/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterface.java +++ b/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterface.java @@ -4,8 +4,10 @@ import amidst.clazz.symbolic.SymbolicClass; import amidst.clazz.symbolic.SymbolicObject; +import amidst.clazz.translator.ClassTranslator; import amidst.documentation.ThreadSafe; import amidst.logging.AmidstLogger; +import amidst.mojangapi.file.LauncherProfile; import amidst.mojangapi.minecraftinterface.MinecraftInterface; import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.minecraftinterface.RecognisedVersion; @@ -13,6 +15,11 @@ @ThreadSafe public class LocalMinecraftInterface implements MinecraftInterface { + public static LocalMinecraftInterface create(ClassTranslator translator, LauncherProfile launcherProfile) + throws LocalMinecraftInterfaceCreationException { + return new LocalMinecraftInterfaceBuilder(translator).create(launcherProfile); + } + /** * A GenLayer instance, at quarter scale to the final biome layer (i.e. both * axis are divided by 4). Minecraft calculates biomes at diff --git a/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterfaceBuilder.java b/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterfaceBuilder.java index 011997c5f..9462d292f 100644 --- a/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterfaceBuilder.java +++ b/src/main/java/amidst/mojangapi/minecraftinterface/local/LocalMinecraftInterfaceBuilder.java @@ -1,7 +1,6 @@ package amidst.mojangapi.minecraftinterface.local; -import java.io.FileNotFoundException; -import java.net.MalformedURLException; +import java.io.IOException; import java.net.URLClassLoader; import java.util.Map; @@ -13,8 +12,7 @@ import amidst.documentation.Immutable; import amidst.documentation.NotNull; import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.minecraftinterface.MinecraftInterface; +import amidst.mojangapi.file.LauncherProfile; import amidst.mojangapi.minecraftinterface.RecognisedVersion; @Immutable @@ -26,13 +24,13 @@ public LocalMinecraftInterfaceBuilder(ClassTranslator translator) { } @NotNull - public MinecraftInterface create(VersionDirectory versionDirectory) + public LocalMinecraftInterface create(LauncherProfile launcherProfile) throws LocalMinecraftInterfaceCreationException { try { - URLClassLoader classLoader = versionDirectory.createClassLoader(); + URLClassLoader classLoader = launcherProfile.newClassLoader(); RecognisedVersion recognisedVersion = RecognisedVersion.from(classLoader); Map symbolicClassMap = Classes - .createSymbolicClassMap(versionDirectory.getJar(), classLoader, translator); + .createSymbolicClassMap(launcherProfile.getJar(), classLoader, translator); AmidstLogger.info("Minecraft load complete."); return new LocalMinecraftInterface( symbolicClassMap.get(SymbolicNames.CLASS_INT_CACHE), @@ -42,11 +40,10 @@ public MinecraftInterface create(VersionDirectory versionDirectory) symbolicClassMap.get(SymbolicNames.CLASS_GEN_OPTIONS_FACTORY), recognisedVersion); } catch ( - MalformedURLException - | ClassNotFoundException - | FileNotFoundException + ClassNotFoundException | JarFileParsingException - | SymbolicClassGraphCreationException e) { + | SymbolicClassGraphCreationException + | IOException e) { throw new LocalMinecraftInterfaceCreationException("unable to create local minecraft interface", e); } } diff --git a/src/main/java/amidst/mojangapi/world/World.java b/src/main/java/amidst/mojangapi/world/World.java index 37ec00a8a..6891a38b1 100644 --- a/src/main/java/amidst/mojangapi/world/World.java +++ b/src/main/java/amidst/mojangapi/world/World.java @@ -1,6 +1,7 @@ package amidst.mojangapi.world; import java.util.List; +import java.util.function.Consumer; import amidst.documentation.ThreadSafe; import amidst.mojangapi.minecraftinterface.RecognisedVersion; @@ -16,6 +17,8 @@ @ThreadSafe public class World { + private final Consumer onDisposeWorld; + private final WorldSeed worldSeed; private final WorldType worldType; private final String generatorOptions; @@ -37,6 +40,7 @@ public class World { private final WorldIconProducer> endCityProducer; public World( + Consumer onDisposeWorld, WorldSeed worldSeed, WorldType worldType, String generatorOptions, @@ -55,6 +59,7 @@ public World( WorldIconProducer oceanMonumentProducer, WorldIconProducer netherFortressProducer, WorldIconProducer> endCityProducer) { + this.onDisposeWorld = onDisposeWorld; this.worldSeed = worldSeed; this.worldType = worldType; this.generatorOptions = generatorOptions; @@ -162,4 +167,13 @@ public List getPlayerWorldIcons() { public void reloadPlayerWorldIcons() { playerProducer.resetCache(); } + + /** + * Unlocks the RunningLauncherProfile to allow the creation of another + * world. However, this does not actually prevent the usage of this world. + * If you keep using it, something will break. + */ + public void dispose() { + onDisposeWorld.accept(this); + } } diff --git a/src/main/java/amidst/mojangapi/world/WorldBuilder.java b/src/main/java/amidst/mojangapi/world/WorldBuilder.java index 9e2505e05..ba11182de 100644 --- a/src/main/java/amidst/mojangapi/world/WorldBuilder.java +++ b/src/main/java/amidst/mojangapi/world/WorldBuilder.java @@ -1,11 +1,12 @@ package amidst.mojangapi.world; import java.io.IOException; +import java.util.function.Consumer; import amidst.documentation.Immutable; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.nbt.LevelDatNbt; +import amidst.mojangapi.file.ImmutablePlayerInformationProvider; +import amidst.mojangapi.file.PlayerInformationProvider; +import amidst.mojangapi.file.SaveGame; import amidst.mojangapi.minecraftinterface.MinecraftInterface; import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.minecraftinterface.RecognisedVersion; @@ -27,10 +28,8 @@ import amidst.mojangapi.world.oracle.ImmutableWorldSpawnOracle; import amidst.mojangapi.world.oracle.SlimeChunkOracle; import amidst.mojangapi.world.oracle.WorldSpawnOracle; -import amidst.mojangapi.world.player.ImmutablePlayerInformationCache; import amidst.mojangapi.world.player.MovablePlayerList; import amidst.mojangapi.world.player.PlayerInformation; -import amidst.mojangapi.world.player.PlayerInformationCache; import amidst.mojangapi.world.player.WorldPlayerType; import amidst.mojangapi.world.versionfeatures.DefaultVersionFeatures; import amidst.mojangapi.world.versionfeatures.VersionFeatures; @@ -43,24 +42,28 @@ public class WorldBuilder { */ public static WorldBuilder createSilentPlayerless() { return new WorldBuilder( - new ImmutablePlayerInformationCache(PlayerInformation.theSingleplayerPlayer()), + new ImmutablePlayerInformationProvider(PlayerInformation.theSingleplayerPlayer()), SeedHistoryLogger.createDisabled()); } - private final PlayerInformationCache playerInformationCache; + private final PlayerInformationProvider playerInformationProvider; private final SeedHistoryLogger seedHistoryLogger; - public WorldBuilder(PlayerInformationCache playerInformationCache, SeedHistoryLogger seedHistoryLogger) { - this.playerInformationCache = playerInformationCache; + public WorldBuilder(PlayerInformationProvider playerInformationProvider, SeedHistoryLogger seedHistoryLogger) { + this.playerInformationProvider = playerInformationProvider; this.seedHistoryLogger = seedHistoryLogger; } - public World fromSeed(MinecraftInterface minecraftInterface, WorldSeed worldSeed, WorldType worldType) - throws MinecraftInterfaceException { + public World fromSeed( + MinecraftInterface minecraftInterface, + Consumer onDisposeWorld, + WorldSeed worldSeed, + WorldType worldType) throws MinecraftInterfaceException { BiomeDataOracle biomeDataOracle = new BiomeDataOracle(minecraftInterface); VersionFeatures versionFeatures = DefaultVersionFeatures.create(minecraftInterface.getRecognisedVersion()); return create( minecraftInterface, + onDisposeWorld, worldSeed, worldType, "", @@ -73,30 +76,30 @@ public World fromSeed(MinecraftInterface minecraftInterface, WorldSeed worldSeed versionFeatures.getValidBiomesForStructure_Spawn())); } - public World fromSaveGame(MinecraftInterface minecraftInterface, SaveDirectory saveDirectory) + public World fromSaveGame(MinecraftInterface minecraftInterface, Consumer onDisposeWorld, SaveGame saveGame) throws IOException, - MinecraftInterfaceException, - MojangApiParsingException { + MinecraftInterfaceException { VersionFeatures versionFeatures = DefaultVersionFeatures.create(minecraftInterface.getRecognisedVersion()); - LevelDatNbt levelDat = saveDirectory.createLevelDat(); MovablePlayerList movablePlayerList = new MovablePlayerList( - playerInformationCache, - saveDirectory, + playerInformationProvider, + saveGame, true, - WorldPlayerType.from(saveDirectory, levelDat)); + WorldPlayerType.from(saveGame)); return create( minecraftInterface, - WorldSeed.fromSaveGame(levelDat.getSeed()), - levelDat.getWorldType(), - levelDat.getGeneratorOptions(), + onDisposeWorld, + WorldSeed.fromSaveGame(saveGame.getSeed()), + saveGame.getWorldType(), + saveGame.getGeneratorOptions(), movablePlayerList, versionFeatures, new BiomeDataOracle(minecraftInterface), - new ImmutableWorldSpawnOracle(levelDat.getWorldSpawn())); + new ImmutableWorldSpawnOracle(saveGame.getWorldSpawn())); } private World create( MinecraftInterface minecraftInterface, + Consumer onDisposeWorld, WorldSeed worldSeed, WorldType worldType, String generatorOptions, @@ -109,6 +112,7 @@ private World create( long seed = worldSeed.getLong(); minecraftInterface.createWorld(seed, worldType, generatorOptions); return new World( + onDisposeWorld, worldSeed, worldType, generatorOptions, diff --git a/src/main/java/amidst/mojangapi/world/player/ImmutablePlayerInformationCache.java b/src/main/java/amidst/mojangapi/world/player/ImmutablePlayerInformationCache.java deleted file mode 100644 index d4b854ab2..000000000 --- a/src/main/java/amidst/mojangapi/world/player/ImmutablePlayerInformationCache.java +++ /dev/null @@ -1,25 +0,0 @@ -package amidst.mojangapi.world.player; - -import amidst.documentation.Immutable; -import amidst.documentation.NotNull; - -@Immutable -public class ImmutablePlayerInformationCache implements PlayerInformationCache { - private final PlayerInformation playerInformation; - - public ImmutablePlayerInformationCache(PlayerInformation playerInformation) { - this.playerInformation = playerInformation; - } - - @NotNull - @Override - public PlayerInformation getByUUID(String uuid) { - return playerInformation; - } - - @NotNull - @Override - public PlayerInformation getByName(String name) { - return playerInformation; - } -} diff --git a/src/main/java/amidst/mojangapi/world/player/MovablePlayerList.java b/src/main/java/amidst/mojangapi/world/player/MovablePlayerList.java index 2bc47ccd8..e0897944f 100644 --- a/src/main/java/amidst/mojangapi/world/player/MovablePlayerList.java +++ b/src/main/java/amidst/mojangapi/world/player/MovablePlayerList.java @@ -7,8 +7,9 @@ import amidst.documentation.CalledOnlyBy; import amidst.documentation.ThreadSafe; import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.nbt.player.PlayerNbt; +import amidst.mojangapi.file.PlayerInformationProvider; +import amidst.mojangapi.file.SaveGame; +import amidst.mojangapi.file.SaveGamePlayer; import amidst.threading.WorkerExecutor; @ThreadSafe @@ -19,20 +20,20 @@ public static MovablePlayerList dummy() { return DUMMY; } - private final PlayerInformationCache playerInformationCache; - private final SaveDirectory saveDirectory; + private final PlayerInformationProvider playerInformationProvider; + private final SaveGame saveGame; private final boolean isSaveEnabled; private volatile WorldPlayerType worldPlayerType; private volatile ConcurrentLinkedQueue players = new ConcurrentLinkedQueue<>(); public MovablePlayerList( - PlayerInformationCache playerInformationCache, - SaveDirectory saveDirectory, + PlayerInformationProvider playerInformationProvider, + SaveGame saveGame, boolean isSaveEnabled, WorldPlayerType worldPlayerType) { - this.playerInformationCache = playerInformationCache; - this.saveDirectory = saveDirectory; + this.playerInformationProvider = playerInformationProvider; + this.saveGame = saveGame; this.isSaveEnabled = isSaveEnabled; this.worldPlayerType = worldPlayerType; } @@ -46,11 +47,11 @@ public void setWorldPlayerType(WorldPlayerType worldPlayerType) { } public boolean canLoad() { - return saveDirectory != null; + return saveGame != null; } public void load(WorkerExecutor workerExecutor, Runnable onPlayerFinishedLoading) { - if (saveDirectory != null) { + if (saveGame != null) { AmidstLogger.info("loading player locations"); ConcurrentLinkedQueue players = new ConcurrentLinkedQueue<>(); this.players = players; @@ -70,17 +71,14 @@ private void loadPlayers( WorkerExecutor workerExecutor, ConcurrentLinkedQueue players, Runnable onPlayerFinishedLoading) { - for (PlayerNbt playerNbt : worldPlayerType.createPlayerNbts(saveDirectory)) { - workerExecutor.run(() -> loadPlayer(players, playerNbt), onPlayerFinishedLoading); + for (SaveGamePlayer saveGamePlayer : worldPlayerType.tryReadPlayers(saveGame)) { + workerExecutor.run(() -> loadPlayer(players, saveGamePlayer), onPlayerFinishedLoading); } } @CalledOnlyBy(AmidstThread.WORKER) - private void loadPlayer(ConcurrentLinkedQueue players, PlayerNbt playerNbt) { - Player player = playerNbt.createPlayer(playerInformationCache); - if (player.tryLoadLocation()) { - players.offer(player); - } + private void loadPlayer(ConcurrentLinkedQueue players, SaveGamePlayer saveGamePlayer) { + players.offer(new Player(saveGamePlayer.getPlayerInformation(playerInformationProvider), saveGamePlayer)); } public boolean canSave() { diff --git a/src/main/java/amidst/mojangapi/world/player/Player.java b/src/main/java/amidst/mojangapi/world/player/Player.java index 37506e330..da97a305b 100644 --- a/src/main/java/amidst/mojangapi/world/player/Player.java +++ b/src/main/java/amidst/mojangapi/world/player/Player.java @@ -1,11 +1,8 @@ package amidst.mojangapi.world.player; -import java.io.IOException; - import amidst.documentation.ThreadSafe; import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.nbt.player.PlayerNbt; +import amidst.mojangapi.file.SaveGamePlayer; import amidst.mojangapi.world.Dimension; import amidst.mojangapi.world.coordinates.CoordinatesInWorld; import amidst.mojangapi.world.icon.WorldIconImage; @@ -13,17 +10,19 @@ @ThreadSafe public class Player { private final PlayerInformation playerInformation; - private final PlayerNbt playerNbt; + private final SaveGamePlayer saveGamePlayer; private volatile PlayerCoordinates savedCoordinates; private volatile PlayerCoordinates currentCoordinates; - public Player(PlayerInformation playerInformation, PlayerNbt playerNbt) { + public Player(PlayerInformation playerInformation, SaveGamePlayer saveGamePlayer) { this.playerInformation = playerInformation; - this.playerNbt = playerNbt; + this.saveGamePlayer = saveGamePlayer; + this.savedCoordinates = saveGamePlayer.getPlayerCoordinates(); + this.currentCoordinates = savedCoordinates; } public String getPlayerName() { - return playerInformation.getNameOrUUID(); + return playerInformation.getNameOrElseUUID(); } public WorldIconImage getHead() { @@ -38,52 +37,22 @@ public void moveTo(CoordinatesInWorld coordinates, long height, Dimension dimens this.currentCoordinates = new PlayerCoordinates(coordinates, height, dimension); } - public boolean trySaveLocation() { - try { - if (saveLocation()) { - return true; - } else { - AmidstLogger.warn( - "skipping to save player location, because the backup file cannot be created for player: " - + getPlayerName()); - return false; - } - } catch (MojangApiParsingException e) { - AmidstLogger.warn(e, "error while writing player location for player: " + getPlayerName()); - return false; - } - } - /** * Returns true if the player was not moved or the new location was * successfully saved. */ - public synchronized boolean saveLocation() throws MojangApiParsingException { + public synchronized boolean trySaveLocation() { PlayerCoordinates currentCoordinates = this.currentCoordinates; if (savedCoordinates != currentCoordinates) { - if (playerNbt.tryWriteCoordinates(currentCoordinates)) { + if (saveGamePlayer.tryBackupAndWritePlayerCoordinates(currentCoordinates)) { savedCoordinates = currentCoordinates; return true; } else { + AmidstLogger.warn("error while writing player location for player: " + getPlayerName()); return false; } } else { return true; } } - - public boolean tryLoadLocation() { - try { - loadLocation(); - return true; - } catch (IOException | MojangApiParsingException e) { - AmidstLogger.warn(e, "error while reading player location for player: " + getPlayerName()); - return false; - } - } - - public synchronized void loadLocation() throws IOException, MojangApiParsingException { - this.savedCoordinates = playerNbt.readCoordinates(); - this.currentCoordinates = savedCoordinates; - } } diff --git a/src/main/java/amidst/mojangapi/world/player/PlayerInformation.java b/src/main/java/amidst/mojangapi/world/player/PlayerInformation.java index 893848d69..937e09c1d 100644 --- a/src/main/java/amidst/mojangapi/world/player/PlayerInformation.java +++ b/src/main/java/amidst/mojangapi/world/player/PlayerInformation.java @@ -1,12 +1,7 @@ package amidst.mojangapi.world.player; -import java.awt.image.BufferedImage; - import amidst.documentation.Immutable; import amidst.documentation.NotNull; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.PlayerInformationRetriever; -import amidst.mojangapi.file.json.player.PlayerJson; import amidst.mojangapi.world.icon.WorldIconImage; import amidst.mojangapi.world.icon.type.DefaultWorldIconTypes; @@ -18,64 +13,6 @@ public class PlayerInformation { "The Singleplayer Player", DEFAULT_HEAD); - @NotNull - public static PlayerInformation fromUUID(String uuid) { - PlayerJson player = PlayerInformationRetriever.tryGetPlayerJsonByUUID(uuid); - WorldIconImage head; - if (player != null) { - head = tryGetPlayerHeadBySkinUrl(player); - if (head != null) { - return new PlayerInformation(player.getId(), player.getName(), head); - } else { - return new PlayerInformation(player.getId(), player.getName(), DEFAULT_HEAD); - } - } else { - return new PlayerInformation(uuid, null, DEFAULT_HEAD); - } - } - - @NotNull - public static PlayerInformation fromName(String name) { - PlayerJson player = PlayerInformationRetriever.tryGetPlayerJsonByName(name); - WorldIconImage head; - if (player != null) { - head = tryGetPlayerHeadBySkinUrl(player); - if (head != null) { - return new PlayerInformation(player.getId(), player.getName(), head); - } else { - head = tryGetPlayerHeadByName(name); - if (head != null) { - return new PlayerInformation(player.getId(), player.getName(), head); - } else { - return new PlayerInformation(player.getId(), player.getName(), DEFAULT_HEAD); - } - } - } else { - head = tryGetPlayerHeadByName(name); - if (head != null) { - return new PlayerInformation(null, name, head); - } else { - return new PlayerInformation(null, name, DEFAULT_HEAD); - } - } - } - - private static WorldIconImage tryGetPlayerHeadBySkinUrl(PlayerJson player) { - BufferedImage head; - try { - head = PlayerInformationRetriever.tryGetPlayerHeadBySkinUrl(player.getSkinUrl()); - return head != null ? WorldIconImage.from(head) : null; - } catch (MojangApiParsingException e) { - return null; - } - } - - private static WorldIconImage tryGetPlayerHeadByName(String name) { - BufferedImage head; - head = PlayerInformationRetriever.tryGetPlayerHeadByName(name); - return head != null ? WorldIconImage.from(head) : null; - } - @NotNull public static PlayerInformation theSingleplayerPlayer() { return THE_SINGLEPLAYER_PLAYER; @@ -85,7 +22,7 @@ public static PlayerInformation theSingleplayerPlayer() { private final String name; private final WorldIconImage head; - private PlayerInformation(String uuid, String name, WorldIconImage head) { + public PlayerInformation(String uuid, String name, WorldIconImage head) { this.uuid = uuid; this.name = name; this.head = head; @@ -103,7 +40,7 @@ public WorldIconImage getHead() { return head; } - public String getNameOrUUID() { + public String getNameOrElseUUID() { if (name != null) { return name; } else { diff --git a/src/main/java/amidst/mojangapi/world/player/PlayerInformationCache.java b/src/main/java/amidst/mojangapi/world/player/PlayerInformationCache.java deleted file mode 100644 index e52aeed2a..000000000 --- a/src/main/java/amidst/mojangapi/world/player/PlayerInformationCache.java +++ /dev/null @@ -1,13 +0,0 @@ -package amidst.mojangapi.world.player; - -import amidst.documentation.NotNull; -import amidst.documentation.ThreadSafe; - -@ThreadSafe -public interface PlayerInformationCache { - @NotNull - PlayerInformation getByUUID(String uuid); - - @NotNull - PlayerInformation getByName(String name); -} diff --git a/src/main/java/amidst/mojangapi/world/player/WorldPlayerType.java b/src/main/java/amidst/mojangapi/world/player/WorldPlayerType.java index 222416708..21b50dad4 100644 --- a/src/main/java/amidst/mojangapi/world/player/WorldPlayerType.java +++ b/src/main/java/amidst/mojangapi/world/player/WorldPlayerType.java @@ -1,15 +1,13 @@ package amidst.mojangapi.world.player; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import amidst.documentation.Immutable; import amidst.documentation.NotNull; -import amidst.mojangapi.file.directory.SaveDirectory; -import amidst.mojangapi.file.nbt.LevelDatNbt; -import amidst.mojangapi.file.nbt.player.PlayerNbt; +import amidst.mojangapi.file.SaveGame; +import amidst.mojangapi.file.SaveGamePlayer; @Immutable public enum WorldPlayerType { @@ -26,9 +24,9 @@ public static List getSelectable() { return SELECTABLE; } - public static WorldPlayerType from(SaveDirectory saveDirectory, LevelDatNbt levelDat) { - boolean hasSingleplayerPlayer = levelDat.hasPlayer(); - boolean hasMultiplayerPlayers = saveDirectory.hasMultiplayerPlayers(); + public static WorldPlayerType from(SaveGame saveGame) { + boolean hasSingleplayerPlayer = saveGame.hasSingleplayerPlayer(); + boolean hasMultiplayerPlayers = saveGame.hasMultiplayerPlayers(); if (hasSingleplayerPlayer && hasMultiplayerPlayers) { return BOTH; } else if (hasSingleplayerPlayer) { @@ -51,16 +49,16 @@ public String getName() { } @NotNull - public List createPlayerNbts(SaveDirectory saveDirectory) { + public List tryReadPlayers(SaveGame saveGame) { if (this == BOTH) { - List result = new ArrayList<>(); - result.addAll(saveDirectory.createSingleplayerPlayerNbts()); - result.addAll(saveDirectory.createMultiplayerPlayerNbts()); - return result; + return saveGame.tryReadAllPlayers(); } else if (this == SINGLEPLAYER) { - return saveDirectory.createSingleplayerPlayerNbts(); + return saveGame + .tryReadSingleplayerPlayer() + .map(Collections::singletonList) + .orElseGet(Collections::emptyList); } else if (this == MULTIPLAYER) { - return saveDirectory.createMultiplayerPlayerNbts(); + return saveGame.tryReadMultiplayerPlayers(); } else { return Collections.emptyList(); } diff --git a/src/main/java/amidst/parsing/FormatException.java b/src/main/java/amidst/parsing/FormatException.java new file mode 100644 index 000000000..a6af43d9a --- /dev/null +++ b/src/main/java/amidst/parsing/FormatException.java @@ -0,0 +1,16 @@ +package amidst.parsing; + +@SuppressWarnings("serial") +public class FormatException extends Exception { + public FormatException(String message) { + super(message); + } + + public FormatException(Throwable cause) { + super(cause); + } + + public FormatException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/amidst/parsing/URIUtils.java b/src/main/java/amidst/parsing/URIUtils.java new file mode 100644 index 000000000..6c2d274f3 --- /dev/null +++ b/src/main/java/amidst/parsing/URIUtils.java @@ -0,0 +1,47 @@ +package amidst.parsing; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URI; +import java.net.URL; + +import amidst.documentation.Immutable; + +@Immutable +public enum URIUtils { + ; + + public static URI newURI(String location) throws IOException { + try { + return URI.create(location); + } catch (IllegalArgumentException e) { + throw new IOException("malformed uri: " + location, e); + } + } + + public static URL newURL(String location) throws IOException { + return newURI(location).toURL(); + } + + public static Reader newReader(String location) throws IOException { + return newReader(newURL(location)); + } + + public static Reader newReader(URL url) throws IOException { + return new InputStreamReader(newInputStream(url)); + } + + public static Reader newReader(File file) throws FileNotFoundException { + return new BufferedReader(new FileReader(file)); + } + + public static BufferedInputStream newInputStream(URL url) throws IOException { + return new BufferedInputStream(url.openStream()); + } +} diff --git a/src/main/java/amidst/parsing/json/JsonReader.java b/src/main/java/amidst/parsing/json/JsonReader.java new file mode 100644 index 000000000..005b3e04a --- /dev/null +++ b/src/main/java/amidst/parsing/json/JsonReader.java @@ -0,0 +1,67 @@ +package amidst.parsing.json; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.net.URL; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; + +import amidst.documentation.Immutable; +import amidst.documentation.NotNull; +import amidst.parsing.FormatException; +import amidst.parsing.URIUtils; + +@Immutable +public enum JsonReader { + ; + + private static final Gson GSON = new Gson(); + + @NotNull + public static T readLocation(File location, Class clazz) throws FormatException, IOException { + return readReader(URIUtils.newReader(location), clazz); + } + + @NotNull + public static T readLocation(URL location, Class clazz) throws FormatException, IOException { + return readReader(URIUtils.newReader(location), clazz); + } + + @NotNull + public static T readLocation(String location, Class clazz) throws FormatException, IOException { + return readReader(URIUtils.newReader(location), clazz); + } + + @NotNull + private static T readReader(Reader reader, Class clazz) throws FormatException, IOException { + try (Reader theReader = reader) { + T result = GSON.fromJson(theReader, clazz); + if (result != null) { + return result; + } else { + throw new FormatException("result was null"); + } + } catch (JsonSyntaxException e) { + throw new FormatException(e); + } catch (JsonIOException e) { + throw new IOException(e); + } + } + + @NotNull + public static T readString(String string, Class clazz) throws FormatException { + try { + T result = GSON.fromJson(string, clazz); + if (result != null) { + return result; + } else { + throw new FormatException("result was null"); + } + } catch (JsonSyntaxException e) { + throw new FormatException(e); + } + } +} diff --git a/src/main/java/amidst/settings/biomeprofile/BiomeProfileDirectory.java b/src/main/java/amidst/settings/biomeprofile/BiomeProfileDirectory.java index 8052ec548..10178352b 100644 --- a/src/main/java/amidst/settings/biomeprofile/BiomeProfileDirectory.java +++ b/src/main/java/amidst/settings/biomeprofile/BiomeProfileDirectory.java @@ -1,16 +1,12 @@ package amidst.settings.biomeprofile; -import java.io.BufferedReader; import java.io.File; -import java.io.FileReader; import java.io.IOException; -import com.google.gson.Gson; -import com.google.gson.JsonIOException; -import com.google.gson.JsonSyntaxException; - import amidst.documentation.Immutable; import amidst.logging.AmidstLogger; +import amidst.parsing.FormatException; +import amidst.parsing.json.JsonReader; @Immutable public class BiomeProfileDirectory { @@ -29,7 +25,6 @@ private static File getRoot(String root) { } private static final File DEFAULT_ROOT_DIRECTORY = new File("biome"); - private static final Gson GSON = new Gson(); private final File root; private final File defaultProfile; @@ -95,21 +90,12 @@ private BiomeProfile createFromFile(File file) { BiomeProfile profile = null; if (file.exists() && file.isFile()) { try { - profile = readProfile(file); - if (profile == null) { - throw new NullPointerException(); - } + profile = JsonReader.readLocation(file, BiomeProfile.class); profile.validate(); - } catch (JsonSyntaxException | JsonIOException | IOException | NullPointerException e) { + } catch (IOException | FormatException e) { AmidstLogger.warn(e, "Unable to load file: " + file); } } return profile; } - - private BiomeProfile readProfile(File file) throws IOException, JsonSyntaxException, JsonIOException { - try (BufferedReader reader = new BufferedReader(new FileReader(file))) { - return GSON.fromJson(reader, BiomeProfile.class); - } - } } diff --git a/src/test/java/amidst/devtools/DevToolRunner.java b/src/test/java/amidst/devtools/DevToolRunner.java index 6fb4b9898..b6dc35230 100644 --- a/src/test/java/amidst/devtools/DevToolRunner.java +++ b/src/test/java/amidst/devtools/DevToolRunner.java @@ -9,10 +9,10 @@ import amidst.AmidstVersion; import amidst.ResourceLoader; import amidst.devtools.settings.DevToolSettings; -import amidst.mojangapi.file.MojangApiParsingException; -import amidst.mojangapi.file.json.JsonReader; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.mojangapi.file.VersionList; import amidst.mojangapi.world.biome.Biome; +import amidst.parsing.FormatException; /** * Eclipse does not allow to run the main directly as a Java Application, @@ -22,37 +22,43 @@ public class DevToolRunner { @Ignore @Test - public void generateRecognisedVersionList() throws IOException, MojangApiParsingException { + public void generateInstalledVersionsList() throws FormatException, IOException { + new GenerateInstalledVersionsList(MinecraftInstallation.newLocalMinecraftInstallation()).run(); + } + + @Ignore + @Test + public void generateRecognisedVersionList() throws FormatException, IOException { new GenerateRecognisedVersionList(versionsDirectory(), librariesDirectory(), versionList()).run(); } @Ignore @Test - public void generateUpdateInformationJson() throws IOException, MojangApiParsingException { + public void generateUpdateInformationJson() { new GenerateUpdateInformationJson(amidstVersion()).run(); } @Ignore @Test - public void generateWorldTestData() throws IOException, MojangApiParsingException { + public void generateWorldTestData() throws FormatException, IOException { new GenerateWorldTestData(versionsDirectory(), librariesDirectory(), versionList()).run(); } @Ignore @Test - public void checkMinecraftJarFileDownloadAvailability() throws IOException, MojangApiParsingException { + public void checkMinecraftJarFileDownloadAvailability() throws FormatException, IOException { new MinecraftJarDownloadAvailabilityChecker(versionList()).run(); } @Ignore @Test - public void downloadMinecraftJarFiles() throws IOException, MojangApiParsingException { + public void downloadMinecraftJarFiles() throws FormatException, IOException { new MinecraftJarDownloader(versionsDirectory(), versionList()).run(); } @Ignore @Test - public void checkMinecraftVersionCompatibility() throws IOException, MojangApiParsingException { + public void checkMinecraftVersionCompatibility() throws FormatException, IOException { new MinecraftVersionCompatibilityChecker(versionsDirectory(), versionList()).run(); } @@ -62,8 +68,8 @@ public void generateBiomeColorImages() throws IOException { new GenerateBiomeColorImages(Biome.allBiomes(), new File(biomeColorImagesDirectory())).run(); } - private VersionListJson versionList() throws IOException, MojangApiParsingException { - return JsonReader.readRemoteVersionList(); + private VersionList versionList() throws FormatException, IOException { + return VersionList.newRemoteVersionList(); } private String librariesDirectory() { diff --git a/src/test/java/amidst/devtools/GenerateInstalledVersionsList.java b/src/test/java/amidst/devtools/GenerateInstalledVersionsList.java new file mode 100644 index 000000000..30d7fa6ab --- /dev/null +++ b/src/test/java/amidst/devtools/GenerateInstalledVersionsList.java @@ -0,0 +1,19 @@ +package amidst.devtools; + +import java.io.IOException; + +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.parsing.FormatException; + +public class GenerateInstalledVersionsList { + private final MinecraftInstallation minecraftInstallation; + + public GenerateInstalledVersionsList(MinecraftInstallation minecraftInstallation) { + this.minecraftInstallation = minecraftInstallation; + } + + public void run() throws FormatException, IOException { + minecraftInstallation.readInstalledVersionsAsLauncherProfiles().stream().map(p -> p.getVersionId()).forEach( + System.out::println); + } +} diff --git a/src/test/java/amidst/devtools/GenerateRecognisedVersionList.java b/src/test/java/amidst/devtools/GenerateRecognisedVersionList.java index 868054d89..e94001bd3 100644 --- a/src/test/java/amidst/devtools/GenerateRecognisedVersionList.java +++ b/src/test/java/amidst/devtools/GenerateRecognisedVersionList.java @@ -1,34 +1,34 @@ package amidst.devtools; import java.io.File; -import java.net.MalformedURLException; +import java.io.IOException; import java.net.URLClassLoader; import java.util.LinkedList; import java.util.List; import amidst.devtools.utils.RecognisedVersionEnumBuilder; import amidst.logging.AmidstLogger; -import amidst.mojangapi.file.FilenameFactory; -import amidst.mojangapi.file.directory.DotMinecraftDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.mojangapi.file.DotMinecraftDirectoryNotFoundException; +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; import amidst.mojangapi.minecraftinterface.RecognisedVersion; +import amidst.parsing.FormatException; public class GenerateRecognisedVersionList { private final String prefix; - private final VersionListJson versionList; - private final File versions; - private final DotMinecraftDirectory dotMinecraftDirectory; + private final VersionList versionList; + private final MinecraftInstallation minecraftInstallation; private final List versionsWithError = new LinkedList<>(); private final List downloadFailed = new LinkedList<>(); private final RecognisedVersionEnumBuilder builder = RecognisedVersionEnumBuilder.createPopulated(); - public GenerateRecognisedVersionList(String prefix, String libraries, VersionListJson versionList) { + public GenerateRecognisedVersionList(String prefix, String libraries, VersionList versionList) + throws DotMinecraftDirectoryNotFoundException { this.prefix = prefix; this.versionList = versionList; - this.versions = new File(prefix); - this.dotMinecraftDirectory = new DotMinecraftDirectory(null, new File(libraries)); + this.minecraftInstallation = MinecraftInstallation + .newCustomMinecraftInstallation(new File(libraries), null, new File(prefix), null); } public void run() { @@ -37,40 +37,32 @@ public void run() { } private void populate() { - for (VersionListEntryJson version : versionList.getVersions()) { + for (Version version : versionList.getVersions()) { process(version); } builder.calculateMaxLength(); } - private void process(VersionListEntryJson version) { - String versionId = version.getId(); + private void process(Version version) { if (version.tryDownloadClient(prefix)) { try { - process(versionId); - } catch (ClassNotFoundException | MalformedURLException | NoClassDefFoundError e) { + process(version.getId()); + } catch (ClassNotFoundException | NoClassDefFoundError | FormatException | IOException e) { e.printStackTrace(); - versionsWithError.add(versionId); + versionsWithError.add(version.getId()); } } else { - downloadFailed.add(versionId); + downloadFailed.add(version.getId()); } } - private void process(String versionId) throws MalformedURLException, ClassNotFoundException { + private void process(String versionId) throws ClassNotFoundException, FormatException, IOException { AmidstLogger.info("version " + versionId); - VersionDirectory versionDirectory = createVersionDirectory(versionId); - URLClassLoader classLoader = versionDirectory.createClassLoader(); + URLClassLoader classLoader = minecraftInstallation.newLauncherProfile(versionId).newClassLoader(); String magicString = RecognisedVersion.generateMagicString(classLoader); builder.addLauncherVersionId(versionId, magicString); } - private VersionDirectory createVersionDirectory(String versionId) { - File jar = FilenameFactory.getClientJarFile(versions, versionId); - File json = FilenameFactory.getClientJsonFile(versions, versionId); - return new VersionDirectory(dotMinecraftDirectory, versionId, jar, json); - } - private void print() { System.out.println(); System.out.println(); diff --git a/src/test/java/amidst/devtools/GenerateWorldTestData.java b/src/test/java/amidst/devtools/GenerateWorldTestData.java index c7da73505..bfae77569 100644 --- a/src/test/java/amidst/devtools/GenerateWorldTestData.java +++ b/src/test/java/amidst/devtools/GenerateWorldTestData.java @@ -5,40 +5,37 @@ import java.util.LinkedList; import java.util.List; -import amidst.mojangapi.file.FilenameFactory; -import amidst.mojangapi.file.directory.DotMinecraftDirectory; -import amidst.mojangapi.file.directory.VersionDirectory; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.clazz.translator.ClassTranslator; +import amidst.mojangapi.file.DotMinecraftDirectoryNotFoundException; +import amidst.mojangapi.file.LauncherProfile; +import amidst.mojangapi.file.MinecraftInstallation; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; import amidst.mojangapi.minecraftinterface.MinecraftInterfaceException; import amidst.mojangapi.minecraftinterface.local.DefaultClassTranslator; -import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceBuilder; +import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterface; import amidst.mojangapi.minecraftinterface.local.LocalMinecraftInterfaceCreationException; import amidst.mojangapi.world.testworld.TestWorldCache; import amidst.mojangapi.world.testworld.TestWorldDeclaration; +import amidst.parsing.FormatException; public class GenerateWorldTestData { - private static final LocalMinecraftInterfaceBuilder LOCAL_MINECRAFT_INTERFACE_BUILDER = new LocalMinecraftInterfaceBuilder( - DefaultClassTranslator.INSTANCE.get()); - private final String prefix; - private final File libraries; - private final VersionListJson versionList; - private final File versions; - private final DotMinecraftDirectory dotMinecraftDirectory; + private final VersionList versionList; + private final MinecraftInstallation minecraftInstallation; private final List failed = new LinkedList<>(); private final List successful = new LinkedList<>(); - public GenerateWorldTestData(String prefix, String libraries, VersionListJson versionList) { + public GenerateWorldTestData(String prefix, String libraries, VersionList versionList) + throws DotMinecraftDirectoryNotFoundException { this.prefix = prefix; - this.libraries = new File(libraries); this.versionList = versionList; - this.versions = new File(prefix); - this.dotMinecraftDirectory = new DotMinecraftDirectory(null, this.libraries); + this.minecraftInstallation = MinecraftInstallation + .newCustomMinecraftInstallation(new File(libraries), null, new File(prefix), null); } public void run() { - for (VersionListEntryJson version : versionList.getVersions()) { + for (Version version : versionList.getVersions()) { for (TestWorldDeclaration declaration : TestWorldDeclaration.values()) { if (declaration.getRecognisedVersion().getName().equals(version.getId())) { generate(declaration, version); @@ -49,29 +46,26 @@ public void run() { print("============== Failed ==============", failed); } - private void generate(TestWorldDeclaration declaration, VersionListEntryJson version) { - String versionId = version.getId(); + private void generate(TestWorldDeclaration declaration, Version version) { if (version.tryDownloadClient(prefix)) { try { - TestWorldCache.createAndPut( - declaration, - LOCAL_MINECRAFT_INTERFACE_BUILDER.create(createVersionDirectory(versionId))); - successful.add(versionId); - } catch (LocalMinecraftInterfaceCreationException | MinecraftInterfaceException | IOException e) { + ClassTranslator translator = DefaultClassTranslator.INSTANCE.get(); + LauncherProfile launcherProfile = minecraftInstallation.newLauncherProfile(version.getId()); + TestWorldCache.createAndPut(declaration, LocalMinecraftInterface.create(translator, launcherProfile)); + successful.add(version.getId()); + } catch ( + LocalMinecraftInterfaceCreationException + | MinecraftInterfaceException + | FormatException + | IOException e) { e.printStackTrace(); - failed.add(versionId); + failed.add(version.getId()); } } else { - failed.add(versionId); + failed.add(version.getId()); } } - private VersionDirectory createVersionDirectory(String versionId) { - File jar = FilenameFactory.getClientJarFile(versions, versionId); - File json = FilenameFactory.getClientJsonFile(versions, versionId); - return new VersionDirectory(dotMinecraftDirectory, versionId, jar, json); - } - private void print(String title, Iterable lines) { System.out.println(title); for (String line : lines) { diff --git a/src/test/java/amidst/devtools/MinecraftJarDownloadAvailabilityChecker.java b/src/test/java/amidst/devtools/MinecraftJarDownloadAvailabilityChecker.java index b32b7b3d8..6595dde92 100644 --- a/src/test/java/amidst/devtools/MinecraftJarDownloadAvailabilityChecker.java +++ b/src/test/java/amidst/devtools/MinecraftJarDownloadAvailabilityChecker.java @@ -1,19 +1,19 @@ package amidst.devtools; import amidst.devtools.utils.VersionStateRenderer; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; public class MinecraftJarDownloadAvailabilityChecker { private VersionStateRenderer renderer = new VersionStateRenderer(); - private VersionListJson versionList; + private VersionList versionList; - public MinecraftJarDownloadAvailabilityChecker(VersionListJson versionList) { + public MinecraftJarDownloadAvailabilityChecker(VersionList versionList) { this.versionList = versionList; } public void run() { - for (VersionListEntryJson version : versionList.getVersions()) { + for (Version version : versionList.getVersions()) { boolean hasServer = version.hasServer(); boolean hasClient = version.hasClient(); System.out.println(renderer.render(version, hasServer, hasClient)); diff --git a/src/test/java/amidst/devtools/MinecraftJarDownloader.java b/src/test/java/amidst/devtools/MinecraftJarDownloader.java index 9465e54e3..c69f3285b 100644 --- a/src/test/java/amidst/devtools/MinecraftJarDownloader.java +++ b/src/test/java/amidst/devtools/MinecraftJarDownloader.java @@ -1,21 +1,21 @@ package amidst.devtools; import amidst.devtools.utils.VersionStateRenderer; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; public class MinecraftJarDownloader { private VersionStateRenderer renderer = new VersionStateRenderer(); private String prefix; - private VersionListJson versionList; + private VersionList versionList; - public MinecraftJarDownloader(String prefix, VersionListJson versionList) { + public MinecraftJarDownloader(String prefix, VersionList versionList) { this.prefix = prefix; this.versionList = versionList; } public void run() { - for (VersionListEntryJson version : versionList.getVersions()) { + for (Version version : versionList.getVersions()) { boolean hasServer = version.tryDownloadServer(prefix); boolean hasClient = version.tryDownloadClient(prefix); System.out.println(renderer.render(version, hasServer, hasClient)); diff --git a/src/test/java/amidst/devtools/MinecraftVersionCompatibilityChecker.java b/src/test/java/amidst/devtools/MinecraftVersionCompatibilityChecker.java index c7525ddc4..0b0b62496 100644 --- a/src/test/java/amidst/devtools/MinecraftVersionCompatibilityChecker.java +++ b/src/test/java/amidst/devtools/MinecraftVersionCompatibilityChecker.java @@ -11,8 +11,8 @@ import amidst.clazz.real.JarFileParsingException; import amidst.clazz.symbolic.declaration.SymbolicClassDeclaration; import amidst.clazz.translator.ClassTranslator; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; -import amidst.mojangapi.file.json.versionlist.VersionListJson; +import amidst.mojangapi.file.Version; +import amidst.mojangapi.file.VersionList; import amidst.mojangapi.minecraftinterface.local.DefaultClassTranslator; /** @@ -23,17 +23,17 @@ */ public class MinecraftVersionCompatibilityChecker { private String prefix; - private VersionListJson versionList; + private VersionList versionList; - public MinecraftVersionCompatibilityChecker(String prefix, VersionListJson versionList) { + public MinecraftVersionCompatibilityChecker(String prefix, VersionList versionList) { this.prefix = prefix; this.versionList = versionList; } public void run() { - List supported = new ArrayList<>(); - List unsupported = new ArrayList<>(); - for (VersionListEntryJson version : versionList.getVersions()) { + List supported = new ArrayList<>(); + List unsupported = new ArrayList<>(); + for (Version version : versionList.getVersions()) { if (checkOne(version)) { supported.add(version); } else { @@ -44,10 +44,10 @@ public void run() { displayVersionList(unsupported, "================ UNSUPPORTED VERSIONS ================"); } - private boolean checkOne(VersionListEntryJson version) { + private boolean checkOne(Version version) { if (version.tryDownloadClient(prefix)) { try { - File jarFile = new File(version.getClientJar(prefix)); + File jarFile = version.getClientJarFile(new File(prefix)); ClassTranslator translator = DefaultClassTranslator.INSTANCE.get(); return isSupported(Classes.countMatches(jarFile, translator)); } catch (FileNotFoundException | JarFileParsingException e) { @@ -68,10 +68,10 @@ private boolean isSupported(Map matchesMap) { return true; } - private void displayVersionList(List supported, String message) { + private void displayVersionList(List versions, String message) { System.out.println(); System.out.println(message); - for (VersionListEntryJson version : supported) { + for (Version version : versions) { System.out.println(version.getId()); } } diff --git a/src/test/java/amidst/devtools/utils/VersionStateRenderer.java b/src/test/java/amidst/devtools/utils/VersionStateRenderer.java index 81ed359a9..b7e95864b 100644 --- a/src/test/java/amidst/devtools/utils/VersionStateRenderer.java +++ b/src/test/java/amidst/devtools/utils/VersionStateRenderer.java @@ -1,9 +1,9 @@ package amidst.devtools.utils; -import amidst.mojangapi.file.json.versionlist.VersionListEntryJson; +import amidst.mojangapi.file.Version; public class VersionStateRenderer { - public String render(VersionListEntryJson version, boolean hasServer, boolean hasClient) { + public String render(Version version, boolean hasServer, boolean hasClient) { return toBox(hasServer, 'S') + " " + toBox(hasClient, 'C') + " " + version.getType().getTypeChar() + " " + version.getId(); } diff --git a/src/test/java/amidst/mojangapi/mocking/FakeWorldBuilder.java b/src/test/java/amidst/mojangapi/mocking/FakeWorldBuilder.java index 2251c7952..e9b384bdd 100644 --- a/src/test/java/amidst/mojangapi/mocking/FakeWorldBuilder.java +++ b/src/test/java/amidst/mojangapi/mocking/FakeWorldBuilder.java @@ -1,6 +1,7 @@ package amidst.mojangapi.mocking; import java.util.Map; +import java.util.function.Consumer; import amidst.documentation.ThreadSafe; import amidst.mojangapi.minecraftinterface.MinecraftInterface; @@ -21,6 +22,8 @@ public static FakeWorldBuilder create(TestWorldDirectoryDeclaration directoryDec return new FakeWorldBuilder(WorldBuilder.createSilentPlayerless(), directoryDeclaration); } + private static final Consumer NOOP = disposedWorld -> { + }; private final WorldBuilder builder; private final TestWorldDirectoryDeclaration directoryDeclaration; @@ -31,8 +34,11 @@ public FakeWorldBuilder(WorldBuilder builder, TestWorldDirectoryDeclaration dire public World createRealWorld(TestWorldDeclaration worldDeclaration, MinecraftInterface realMinecraftInterface) throws MinecraftInterfaceException { - return builder - .fromSeed(realMinecraftInterface, worldDeclaration.getWorldSeed(), worldDeclaration.getWorldType()); + return builder.fromSeed( + realMinecraftInterface, + NOOP, + worldDeclaration.getWorldSeed(), + worldDeclaration.getWorldType()); } public World createFakeWorld(TestWorldDirectory worldDeclaration) throws MinecraftInterfaceException { @@ -55,6 +61,7 @@ private World createFakeWorld( BiomeDataJson fullBiomeData) throws MinecraftInterfaceException { return builder.fromSeed( createFakeMinecraftInterface(worldMetadata, quarterBiomeData, fullBiomeData), + NOOP, WorldSeed.fromUserInput(worldMetadata.getSeed() + ""), worldMetadata.getWorldType()); }