commit 0005ca0cf91ae73fe2a569fec4a946adc8a16ae8 Author: Josh Larson Date: Sun Jun 10 14:49:22 2018 -0500 "Initial" commit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bb4db48 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "pswgcommon"] + path = pswgcommon + url = git@bitbucket.org:projectswg/pswgcommon.git +[submodule "pswgcommonfx"] + path = pswgcommonfx + url = git@bitbucket.org:projectswg/pswgcommonfx.git +[submodule "forwarder"] + path = forwarder + url = git@bitbucket.org:projectswg/forwarder.git +[submodule "client-holocore"] + path = client-holocore + url = git@bitbucket.org:projectswg/client-holocore.git diff --git a/client-holocore b/client-holocore new file mode 160000 index 0000000..450ce04 --- /dev/null +++ b/client-holocore @@ -0,0 +1 @@ +Subproject commit 450ce04c13ac4f80bb5061bb38a2abfe0939ed93 diff --git a/forwarder b/forwarder new file mode 160000 index 0000000..389d232 --- /dev/null +++ b/forwarder @@ -0,0 +1 @@ +Subproject commit 389d2323e47405e1691898a6e62ac9adad71f64f diff --git a/pswgcommon b/pswgcommon new file mode 160000 index 0000000..e2532b3 --- /dev/null +++ b/pswgcommon @@ -0,0 +1 @@ +Subproject commit e2532b34eda75626a06ace134211bd19c3ccbde5 diff --git a/pswgcommonfx b/pswgcommonfx new file mode 160000 index 0000000..20f62b5 --- /dev/null +++ b/pswgcommonfx @@ -0,0 +1 @@ +Subproject commit 20f62b5b1080aef6a722f736c5b6252da046087d diff --git a/src/main/java/com/projectswg/launcher/core/Launcher.java b/src/main/java/com/projectswg/launcher/core/Launcher.java new file mode 100644 index 0000000..c75ebc0 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/Launcher.java @@ -0,0 +1,77 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core; + +import com.projectswg.common.javafx.ResourceUtilities; +import com.projectswg.common.utilities.LocalUtilities; +import com.projectswg.launcher.core.services.data.DataManager; +import com.projectswg.launcher.core.services.launcher.LauncherManager; +import me.joshlarson.jlcommon.control.IntentManager; +import me.joshlarson.jlcommon.control.Manager; +import me.joshlarson.jlcommon.control.SafeMain; +import me.joshlarson.jlcommon.control.ServiceBase; +import me.joshlarson.jlcommon.log.Log; +import me.joshlarson.jlcommon.log.log_wrapper.ConsoleLogWrapper; +import me.joshlarson.jlcommon.log.log_wrapper.FileLogWrapper; +import me.joshlarson.jlcommon.utilities.ThreadUtilities; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class Launcher { + + private final List services; + + private Launcher() { + this.services = new ArrayList<>(); + } + + private void run() { + IntentManager intentManager = new IntentManager(Runtime.getRuntime().availableProcessors()); + intentManager.initialize(); + IntentManager.setInstance(intentManager); + services.clear(); + services.add(new DataManager()); + services.add(new LauncherManager()); + for (ServiceBase s : services) + s.setIntentManager(intentManager); + Manager.start(services); + Manager.run(services, 100); + Collections.reverse(services); // Allows the data services to stay alive longer + Manager.stop(services); + intentManager.terminate(); + IntentManager.setInstance(null); + ThreadUtilities.printActiveThreads(); + } + + public static void main(String [] args) { + LocalUtilities.setApplicationName(".projectswg/launcher"); + ResourceUtilities.setPrimarySource(Launcher.class); + Log.addWrapper(new ConsoleLogWrapper()); + Log.addWrapper(new FileLogWrapper(new File(LocalUtilities.getApplicationDirectory(), "log.txt"))); + + SafeMain.main("launcher", new Launcher()::run); + // No code can run after this point - SafeMain calls System.exit + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/LauncherData.java b/src/main/java/com/projectswg/launcher/core/resources/data/LauncherData.java new file mode 100644 index 0000000..3754841 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/LauncherData.java @@ -0,0 +1,85 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data; + +import com.projectswg.launcher.core.Launcher; +import com.projectswg.launcher.core.resources.data.announcements.AnnouncementsData; +import com.projectswg.launcher.core.resources.data.general.GeneralData; +import com.projectswg.launcher.core.resources.data.login.LoginData; +import com.projectswg.launcher.core.resources.data.update.UpdateData; +import javafx.stage.Stage; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.prefs.Preferences; + +public class LauncherData { + + public static final String VERSION = "1.0.11"; + public static final String UPDATE_ADDRESS = "login1.projectswg.com"; + + private static final LauncherData INSTANCE = new LauncherData(); + + private final AtomicReference stage; + private final AnnouncementsData announcementsData; + private final GeneralData generalData; + private final LoginData loginData; + private final UpdateData updateData; + + public LauncherData() { + this.stage = new AtomicReference<>(null); + this.announcementsData = new AnnouncementsData(); + this.generalData = new GeneralData(); + this.loginData = new LoginData(); + this.updateData = new UpdateData(); + } + + public Preferences getPreferences() { + return Preferences.userNodeForPackage(Launcher.class); + } + + public Stage getStage() { + return stage.get(); + } + + public AnnouncementsData getAnnouncements() { + return announcementsData; + } + + public GeneralData getGeneral() { + return generalData; + } + + public LoginData getLogin() { + return loginData; + } + + public UpdateData getUpdate() { + return updateData; + } + + public void setStage(Stage stage) { + this.stage.set(stage); + } + + public static LauncherData getInstance() { + return INSTANCE; + } +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/announcements/AnnouncementsData.java b/src/main/java/com/projectswg/launcher/core/resources/data/announcements/AnnouncementsData.java new file mode 100644 index 0000000..ef9104b --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/announcements/AnnouncementsData.java @@ -0,0 +1,44 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.announcements; + +import com.projectswg.launcher.core.resources.gui.Card; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentList; + +public class AnnouncementsData { + + private final ConcurrentList announcementCards; + private final ConcurrentList serverListCards; + + public AnnouncementsData() { + this.announcementCards = new ConcurrentList<>(); + this.serverListCards = new ConcurrentList<>(); + } + + public ConcurrentList getAnnouncementCards() { + return announcementCards; + } + + public ConcurrentList getServerListCards() { + return serverListCards; + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/general/GeneralData.java b/src/main/java/com/projectswg/launcher/core/resources/data/general/GeneralData.java new file mode 100644 index 0000000..624ce9e --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/general/GeneralData.java @@ -0,0 +1,100 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.general; + +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentBoolean; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentReference; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Locale; + +public class GeneralData { + + private final ConcurrentBoolean sound; + private final ConcurrentReference theme; + private final ConcurrentReference locale; + private final ConcurrentString wine; + + public GeneralData() { + this.sound = new ConcurrentBoolean(); + this.theme = new ConcurrentReference<>(LauncherTheme.DEFAULT); + this.locale = new ConcurrentReference<>(Locale.getDefault()); + this.wine = new ConcurrentString(); + } + + @NotNull + public ConcurrentBoolean getSoundProperty() { + return sound; + } + + @NotNull + public ConcurrentReference getThemeProperty() { + return theme; + } + + @NotNull + public ConcurrentReference getLocaleProperty() { + return locale; + } + + @NotNull + public ConcurrentString getWineProperty() { + return wine; + } + + public boolean isSound() { + return sound.get(); + } + + @NotNull + public LauncherTheme getTheme() { + return theme.get(); + } + + @NotNull + public Locale getLocale() { + return locale.get(); + } + + @Nullable + public String getWine() { + return wine.get(); + } + + public void setSound(boolean sound) { + this.sound.set(sound); + } + + public void setTheme(@NotNull LauncherTheme theme) { + this.theme.set(theme); + } + + public void setLocale(@NotNull Locale locale) { + this.locale.set(locale); + } + + public void setWine(@Nullable String wine) { + this.wine.set(wine); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/general/LauncherTheme.java b/src/main/java/com/projectswg/launcher/core/resources/data/general/LauncherTheme.java new file mode 100644 index 0000000..7168106 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/general/LauncherTheme.java @@ -0,0 +1,54 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.general; + +import java.util.HashMap; +import java.util.Map; + +public enum LauncherTheme { + DEFAULT ("projectswg"); + + private static final Map TAG_TO_THEME = new HashMap<>(); + + static { + for (LauncherTheme theme : values()) { + TAG_TO_THEME.put(theme.primaryTag, theme); + for (String tag : theme.tags) + TAG_TO_THEME.put(tag, theme); + } + } + + private final String [] tags; + private final String primaryTag; + + LauncherTheme(String primaryTag, String ... tags) { + this.tags = tags; + this.primaryTag = primaryTag; + } + + public String getTag() { + return primaryTag; + } + + public static LauncherTheme forThemeTag(String tag) { + return TAG_TO_THEME.getOrDefault(tag, LauncherTheme.DEFAULT); + } +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginData.java b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginData.java new file mode 100644 index 0000000..296416e --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginData.java @@ -0,0 +1,49 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.login; + +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentSet; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CopyOnWriteArraySet; + +public class LoginData { + + private final ConcurrentSet servers; + + public LoginData() { + this.servers = new ConcurrentSet<>(new CopyOnWriteArraySet<>()); + } + + @NotNull + public ConcurrentSet getServers() { + return servers; + } + + public void addServer(@NotNull LoginServer server) { + servers.add(server); + } + + public void removeServer(@NotNull LoginServer server) { + servers.remove(server); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServer.java b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServer.java new file mode 100644 index 0000000..a32ed14 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServer.java @@ -0,0 +1,168 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.login; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentBase; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentInteger; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentReference; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentString; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class LoginServer { + + private final String name; + private final ConcurrentString address; + private final ConcurrentInteger port; + private final ConcurrentString username; + private final ConcurrentString password; + private final ConcurrentReference updateServer; + private final LoginServerInstanceInfo instanceInfo; + + public LoginServer(@NotNull String name) { + this.name = name; + this.address = new ConcurrentString(""); + this.port = new ConcurrentInteger(0); + this.username = new ConcurrentString(""); + this.password = new ConcurrentString(""); + this.updateServer = new ConcurrentReference<>(null); + this.instanceInfo = new LoginServerInstanceInfo(); + + updateServer.addListener("login-server-"+name, this::updateServerListener); + instanceInfo.setUpdateStatus(UpdateServerStatus.UNKNOWN.getFriendlyName()); + updateServerListener(updateServer, null, null); + } + + @NotNull + public ConcurrentString getAddressProperty() { + return address; + } + + @NotNull + public ConcurrentInteger getPortProperty() { + return port; + } + + @NotNull + public ConcurrentString getUsernameProperty() { + return username; + } + + @NotNull + public ConcurrentString getPasswordProperty() { + return password; + } + + @NotNull + public ConcurrentReference getUpdateServerProperty() { + return updateServer; + } + + @NotNull + public LoginServerInstanceInfo getInstanceInfo() { + return instanceInfo; + } + + @NotNull + public String getName() { + return name; + } + + @NotNull + public String getAddress() { + return address.get(); + } + + public int getPort() { + return port.getValue(); + } + + @NotNull + public String getUsername() { + return username.get(); + } + + @NotNull + public String getPassword() { + return password.get(); + } + + @Nullable + public UpdateServer getUpdateServer() { + return updateServer.get(); + } + + public void setAddress(@NotNull String address) { + this.address.set(address); + } + + public void setPort(int port) { + this.port.set(port); + } + + public void setUsername(@NotNull String username) { + this.username.set(username); + } + + public void setPassword(@NotNull String password) { + this.password.set(password); + } + + public void setUpdateServer(@Nullable UpdateServer server) { + this.updateServer.set(server); + } + + @Override + public String toString() { + return name; + } + + private void updateServerListener(ConcurrentBase obs, UpdateServer prev, UpdateServer next) { + String listenerName = "login-server-"+name; + if (prev != null) { + prev.getStatusProperty().removeListener(listenerName); + } + if (next != null) { + next.getStatusProperty().addListener(listenerName, this::onUpdateServerStatusUpdated); + instanceInfo.setReadyToPlay(calculateReadyToPlay(next.getStatus())); + } else { + instanceInfo.setReadyToPlay(false); + } + } + + private void onUpdateServerStatusUpdated(UpdateServerStatus status) { + instanceInfo.setReadyToPlay(calculateReadyToPlay(status)); + instanceInfo.setUpdateStatus(status.getFriendlyName()); + } + + private boolean calculateReadyToPlay(UpdateServerStatus status) { + switch (status) { + case UNKNOWN: + case READY: + return true; + default: + return false; + } + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServerInstanceInfo.java b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServerInstanceInfo.java new file mode 100644 index 0000000..78ef29d --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/login/LoginServerInstanceInfo.java @@ -0,0 +1,78 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.login; + +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentBoolean; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentString; +import org.jetbrains.annotations.NotNull; + +public class LoginServerInstanceInfo { + + private final ConcurrentString loginStatus; + private final ConcurrentString updateStatus; + private final ConcurrentBoolean readyToPlay; + + public LoginServerInstanceInfo() { + this.loginStatus = new ConcurrentString(""); + this.updateStatus = new ConcurrentString(""); + this.readyToPlay = new ConcurrentBoolean(); + } + + @NotNull + public ConcurrentString getLoginStatusProperty() { + return loginStatus; + } + + @NotNull + public ConcurrentString getUpdateStatusProperty() { + return updateStatus; + } + + @NotNull + public ConcurrentBoolean getReadyToPlayProperty() { + return readyToPlay; + } + + public String getLoginStatus() { + return loginStatus.get(); + } + + public String getUpdateStatus() { + return updateStatus.get(); + } + + public boolean isReadyToPlay() { + return readyToPlay.get(); + } + + public void setLoginStatus(@NotNull String loginStatus) { + this.loginStatus.set(loginStatus); + } + + public void setUpdateStatus(@NotNull String updateStatus) { + this.updateStatus.set(updateStatus); + } + + public void setReadyToPlay(boolean readyToPlay) { + this.readyToPlay.set(readyToPlay); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateData.java b/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateData.java new file mode 100644 index 0000000..9d0cc33 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateData.java @@ -0,0 +1,49 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.update; + +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentSet; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.CopyOnWriteArraySet; + +public class UpdateData { + + private final ConcurrentSet servers; + + public UpdateData() { + this.servers = new ConcurrentSet<>(new CopyOnWriteArraySet<>()); + } + + @NotNull + public ConcurrentSet getServers() { + return servers; + } + + public void addServer(@NotNull UpdateServer server) { + servers.add(server); + } + + public void removeServer(@NotNull UpdateServer server) { + servers.remove(server); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateServer.java b/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateServer.java new file mode 100644 index 0000000..37d8698 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/data/update/UpdateServer.java @@ -0,0 +1,189 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.data.update; + +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentInteger; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentList; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentReference; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentString; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.net.URL; + +public class UpdateServer { + + private final String name; + private final ConcurrentString address; + private final ConcurrentInteger port; + private final ConcurrentString basePath; + private final ConcurrentString localPath; + private final ConcurrentList requiredFiles; + private final ConcurrentReference status; + + public UpdateServer(@NotNull String name) { + this.name = name; + this.address = new ConcurrentString(""); + this.port = new ConcurrentInteger(0); + this.basePath = new ConcurrentString(""); + this.localPath = new ConcurrentString(""); + this.requiredFiles = new ConcurrentList<>(); + this.status = new ConcurrentReference<>(UpdateServerStatus.UNKNOWN); + } + + @NotNull + public ConcurrentString getAddressProperty() { + return address; + } + + @NotNull + public ConcurrentInteger getPortProperty() { + return port; + } + + @NotNull + public ConcurrentString getUsernameProperty() { + return basePath; + } + + @NotNull + public ConcurrentString getPasswordProperty() { + return localPath; + } + + @NotNull + public ConcurrentList getRequiredFiles() { + return requiredFiles; + } + + @NotNull + public ConcurrentReference getStatusProperty() { + return status; + } + + @NotNull + public String getName() { + return name; + } + + @NotNull + public String getAddress() { + return address.get(); + } + + public int getPort() { + return port.getValue(); + } + + @NotNull + public String getBasePath() { + return basePath.get(); + } + + @NotNull + public String getLocalPath() { + return localPath.get(); + } + + + @NotNull + public UpdateServerStatus getStatus() { + return status.get(); + } + + public void setAddress(@NotNull String address) { + this.address.set(address); + } + + public void setPort(int port) { + this.port.set(port); + } + + public void setBasePath(@NotNull String basePath) { + this.basePath.set(basePath); + } + + public void setLocalPath(@NotNull String localPath) { + this.localPath.set(localPath); + } + + public void setStatus(@NotNull UpdateServerStatus status) { + this.status.set(status); + } + + @Override + public String toString() { + return name; + } + + public static class RequiredFile { + + private final File localPath; + private final URL remotePath; + private final long length; + private final long hash; + + public RequiredFile(@NotNull File localPath, @NotNull URL remotePath, long length, long hash) { + this.localPath = localPath; + this.remotePath = remotePath; + this.length = length; + this.hash = hash; + } + + @NotNull + public File getLocalPath() { + return localPath; + } + + @NotNull + public URL getRemotePath() { + return remotePath; + } + + public long getLength() { + return length; + } + + public long getHash() { + return hash; + } + + } + + public enum UpdateServerStatus { + UNKNOWN ("servers.status.unknown"), + SCANNING ("servers.status.scanning"), + REQUIRES_DOWNLOAD ("servers.status.requires_download"), + DOWNLOADING ("servers.status.downloading"), + READY ("servers.status.ready"); + + private final String friendlyName; + + UpdateServerStatus(String friendlyName) { + this.friendlyName = friendlyName; + } + + public String getFriendlyName() { + return friendlyName; + } + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/game/GameInstance.java b/src/main/java/com/projectswg/launcher/core/resources/game/GameInstance.java new file mode 100644 index 0000000..72cd3c4 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/game/GameInstance.java @@ -0,0 +1,190 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.game; + +import com.projectswg.forwarder.Forwarder; +import com.projectswg.forwarder.Forwarder.ForwarderData; +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import me.joshlarson.jlcommon.concurrency.BasicThread; +import me.joshlarson.jlcommon.concurrency.Delay; +import me.joshlarson.jlcommon.log.Log; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class GameInstance { + + private static final AtomicLong GAME_ID = new AtomicLong(0); + + private final BasicThread processThread; + private final BasicThread forwarderThread; + private final LoginServer server; + private Forwarder forwarder; + + public GameInstance(LoginServer server) { + this.server = server; + long gameId = GAME_ID.incrementAndGet(); + this.processThread = new BasicThread("game-process-"+gameId, this::runProcess); + this.forwarderThread = new BasicThread("game-forwarder-"+gameId, this::runForwarder); + this.forwarder = new Forwarder(); + } + + public void start() { + if (forwarder == null) + return; + ForwarderData data = forwarder.getData(); + data.setAddress(new InetSocketAddress(server.getAddress(), server.getPort())); + data.setUsername(server.getUsername()); + data.setPassword(server.getPassword()); + forwarderThread.start(); + } + + public void stop() { + if (processThread.isExecuting()) { + processThread.stop(true); + processThread.awaitTermination(2000); + } + } + + private void runForwarder() { + processThread.start(); + forwarder.run(); + forwarder = null; + } + + private void runProcess() { + try { + Process process = buildProcess(server, forwarder.getData()); + if (process == null) + return; + int ret; + try { + File crashLog = forwarder.readClientOutput(process.getInputStream()); + if (crashLog != null) + onCrash(crashLog); + ret = process.waitFor(); + } catch (InterruptedException e) { + Log.w("Thread %s interrupted", Thread.currentThread().getName()); + try { + process.destroyForcibly().waitFor(1, TimeUnit.SECONDS); + if (process.isAlive()) + ret = Integer.MIN_VALUE; + else + ret = process.exitValue(); + } catch (InterruptedException i) { + // Suppressed - no need to report interruption twice + ret = Integer.MIN_VALUE; + } + } + if (ret == Integer.MIN_VALUE) + Log.w("Failed to retrieve proper exit code - defaulting to MIN_VALUE"); + Log.i("Game thread %s terminated with exit code (%d)", Thread.currentThread().getName(), ret); + } finally { + forwarderThread.stop(true); + forwarderThread.awaitTermination(500); + } + } + + private void onCrash(File crashLog) { + Log.w("Crash Detected. ZIP: %s", crashLog); + reportWarning("Crash Detected", "A crash was detected. Please report this to the ProjectSWG team with this zip file: " + crashLog); + } + + @Nullable + private static Process buildProcess(@NotNull LoginServer server, @NotNull final ForwarderData data) { + Log.t("Waiting for forwarder to initialize..."); + long start = System.nanoTime(); + while (data.getLoginPort() == 0 && System.nanoTime() - start <= 1E9) { + Delay.sleepMilli(10); + } + int loginPort = data.getLoginPort(); + if (loginPort == 0) { + Log.e("Failed to build process. Forwarder did not initialize."); + reportError("Connection", "Failed to initialize the PSWG forwarder"); + return null; + } + String username = data.getUsername(); + if (username == null) { + Log.w("Issue when launching game. Username is null - setting to an empty string"); + username = ""; + } + File swgDirectory = null; + UpdateServer updateServer = server.getUpdateServer(); + if (updateServer != null) { + swgDirectory = new File(updateServer.getLocalPath()); + if (!swgDirectory.isDirectory()) { + Log.e("Failed to launch game. Invalid SWG directory: %s", swgDirectory); + reportError("Process", "Invalid SWG directory: " + swgDirectory); + return null; + } + } + if (swgDirectory == null) { + Log.e("Failed to launch game. No SWG directory defined"); + reportError("Process", "No SWG directory defined"); + return null; + } + Log.d("Building game... (login=%d)", loginPort); + return ProcessExecutor.INSTANCE.buildProcess(updateServer, "SwgClient_r.exe", + "--", + "-s", + "Station", + "subscriptionFeatures=1", + "gameFeatures=34374193", + "-s", + "ClientGame", + "loginServerPort0=" + loginPort, + "loginServerAddress0=127.0.0.1", + "loginClientID=" + username, + "autoConnectToLoginServer=" + !username.isEmpty(), + "logReportFatals=true", + "logStderr=true", + "0fd345d9=true"); + } + + private static void reportWarning(String title, String message) { + Platform.runLater(() -> { + Alert alert = new Alert(AlertType.WARNING); + alert.setTitle("Game Launch Warning"); + alert.setHeaderText(title); + alert.setContentText(message); + alert.showAndWait(); + }); + } + + private static void reportError(String title, String message) { + Platform.runLater(() -> { + Alert alert = new Alert(AlertType.ERROR); + alert.setTitle("Game Launch Error"); + alert.setHeaderText(title); + alert.setContentText(message); + alert.showAndWait(); + }); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/game/ProcessExecutor.java b/src/main/java/com/projectswg/launcher/core/resources/game/ProcessExecutor.java new file mode 100644 index 0000000..5b3c6b8 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/game/ProcessExecutor.java @@ -0,0 +1,165 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.game; + +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import me.joshlarson.jlcommon.log.Log; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; + +public enum ProcessExecutor { + INSTANCE; + + @Nullable + public Process buildProcess(@NotNull UpdateServer server, String executable, String ... args) { + File swgDirectory; + { + swgDirectory = new File(server.getLocalPath()); + if (!swgDirectory.isDirectory()) { + Log.e("Failed to launch. Invalid SWG directory: %s", swgDirectory); + reportError("Process", "Invalid SWG directory: " + swgDirectory); + return null; + } + } + File swg = new File(swgDirectory, executable); + if (!swg.isFile()) { + Log.e("Failed to launch. Invalid executable file (%s)", swg); + reportError("Process", "Invalid executable: " + swg); + return null; + } + String[] commands = isWindows() ? buildWindowsArgs(swg, args) : buildWineArgs(swg, args); + if (commands == null) + return null; + Log.d("Building process with arguments %s", Arrays.asList(commands)); + ProcessBuilder pb = new ProcessBuilder(commands); + pb.redirectErrorStream(true); + pb.directory(swg.getParentFile()); + pb.environment().put("WINEDEBUG", "-all"); + try { + Log.i("Starting executable %s", Thread.currentThread().getName()); + return pb.start(); + } catch (IOException e) { + Log.e("Failed to launch. %s: %s", e.getClass().getName(), e.getMessage()); + reportError("Game - Process", e.getClass().getName() + ": " + e.getMessage()); + return null; + } + } + + @Nullable + private String[] buildWineArgs(@NotNull final File swg, @NotNull final String [] args) { + File wine = getWine(); + if (wine == null) { + Log.e("Failed to launch game. Invalid wine configuration"); + reportError("Wine Initialization", "Failed to locate your local wine installation. Please set the correct path in settings"); + return null; + } + String[] baseArgs = buildWindowsArgs(swg, args); + String[] combined = new String[baseArgs.length+1]; + combined[0] = getFileCanonicalIfPossible(wine); + System.arraycopy(baseArgs, 0, combined, 1, baseArgs.length); + return combined; + } + + @NotNull + private String[] buildWindowsArgs(@NotNull final File swg, @NotNull final String [] baseArgs) { + String[] combined = new String[baseArgs.length+1]; + combined[0] = getFileCanonicalIfPossible(swg); + System.arraycopy(baseArgs, 0, combined, 1, baseArgs.length); + return combined; + } + + @NotNull + private String getFileCanonicalIfPossible(@NotNull final File file) { + try { + return file.getCanonicalPath(); + } catch (IOException e) { + String absolute = file.getAbsolutePath(); + Log.w("Issue when launching game. Could not get canonical path (%s: %s) defaulting to absolute: %s", e.getClass().getName(), e.getMessage(), absolute); + return absolute; + } + } + + private boolean isWindows() { + return System.getProperty("os.name").startsWith("Windows"); + } + + private File getWine() { + { + String wineStr = LauncherData.getInstance().getGeneral().getWine(); + if (wineStr != null) { + File wine = new File(wineStr); + if (wine.isFile()) { + return new File(wineStr); + } else { + Log.e("Invalid wine file: " + wineStr); + reportWarning("Wine Initialization", "Invalid wine setting. Searching for valid path..."); + } + } + } + Log.w("Wine binary is not defined - searching..."); + String pathStr = System.getenv("PATH"); + if (pathStr == null) + return null; + + for (String path : pathStr.split(File.pathSeparator)) { + Log.t("Testing wine binary at %s", path); + File test = new File(path, "wine"); + if (test.isFile()) { + try { + test = test.getCanonicalFile(); + Log.d("Found wine installation. Location: %s", test); + return test; + } catch (IOException e) { + Log.w("Failed to get canonical file location of possible wine location: %s", test); + } + } + } + return null; + } + + private void reportWarning(String title, String message) { + Platform.runLater(() -> { + Alert alert = new Alert(AlertType.WARNING); + alert.setTitle("Game Launch Warning"); + alert.setHeaderText(title); + alert.setContentText(message); + alert.showAndWait(); + }); + } + + private void reportError(String title, String message) { + Platform.runLater(() -> { + Alert alert = new Alert(AlertType.ERROR); + alert.setTitle("Game Launch Error"); + alert.setHeaderText(title); + alert.setContentText(message); + alert.showAndWait(); + }); + } +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/AnnouncementsController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/AnnouncementsController.java new file mode 100644 index 0000000..d0a8add --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/AnnouncementsController.java @@ -0,0 +1,70 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import com.projectswg.common.javafx.FXMLController; +import com.projectswg.launcher.core.resources.data.LauncherData; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; + +import java.net.URL; +import java.util.List; +import java.util.ResourceBundle; + +public class AnnouncementsController implements FXMLController { + + private static final String LISTENER_KEY = "announcements-controller"; + + @FXML + private Region root; + + @FXML + private Pane cardContainer; + + public AnnouncementsController() { + + } + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + LauncherData.getInstance().getAnnouncements().getAnnouncementCards().addCollectionChangedListener(LISTENER_KEY, this::updateAnnouncements); + updateAnnouncements(); + } + + private void updateAnnouncements() { + List cards = LauncherData.getInstance().getAnnouncements().getAnnouncementCards(); + for (Card card : cards) { + card.minWidthProperty().bind(cardContainer.widthProperty().subtract(10).divide(2)); + card.maxWidthProperty().bind(cardContainer.widthProperty().subtract(10).divide(2)); + card.minHeightProperty().bind(cardContainer.heightProperty().subtract(10).divide(2)); + card.maxHeightProperty().bind(cardContainer.heightProperty().subtract(10).divide(2)); + } + cardContainer.getChildren().setAll(cards); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/Card.java b/src/main/java/com/projectswg/launcher/core/resources/gui/Card.java new file mode 100644 index 0000000..5d89e94 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/Card.java @@ -0,0 +1,96 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import javafx.scene.control.Label; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.Separator; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.VBox; +import me.joshlarson.jlcommon.log.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; + +public class Card extends ScrollPane { + + private final VBox content; + private final ImageView headerImage; + private final Label title; + private final Label description; + private String link; + + public Card() { + this.content = new VBox(); + this.headerImage = new ImageView(); + this.title = new Label(""); + this.description = new Label(""); + this.link = null; + + headerImage.setPreserveRatio(true); + headerImage.fitWidthProperty().bind(maxWidthProperty().subtract(10)); + headerImage.setFitHeight(100); + + title.maxWidthProperty().bind(maxWidthProperty().subtract(10)); + description.maxWidthProperty().bind(maxWidthProperty().subtract(10)); + + getStyleClass().add("card"); + content.getStyleClass().add("card-content"); + headerImage.getStyleClass().add("header-image"); + title.getStyleClass().add("title"); + description.getStyleClass().add("description"); + + content.getChildren().addAll(headerImage, title, new Separator(), description); + setContent(content); + setFitToWidth(true); + setOnMouseClicked(e -> gotoLink()); + } + + public void setHeaderImage(File image) { + try { + this.headerImage.setImage(new Image(new FileInputStream(image))); + } catch (FileNotFoundException e) { + Log.e("Failed to set image. File not found: %s", image); + } + } + + public void setTitle(String title) { + this.title.setText(title); + } + + public void setDescription(String description) { + this.description.setText(description); + } + + public void setLink(String link) { + this.link = link; + } + + private void gotoLink() { + String link = this.link; + if (link == null) + return; + LauncherUI.getInstance().getHostServices().showDocument(link); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/LauncherUI.java b/src/main/java/com/projectswg/launcher/core/resources/gui/LauncherUI.java new file mode 100644 index 0000000..a0af926 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/LauncherUI.java @@ -0,0 +1,73 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import com.projectswg.common.javafx.FXMLUtilities; +import com.projectswg.launcher.core.resources.data.LauncherData; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +public class LauncherUI extends Application { + + private static final AtomicReference INSTANCE = new AtomicReference<>(null); + + private final AtomicBoolean operational; + + public LauncherUI() { + this.operational = new AtomicBoolean(true); + INSTANCE.set(this); + } + + public boolean isOperational() { + return operational.get(); + } + + @Override + public void start(Stage primaryStage) { + // TODO: Theme specific loading + LauncherData data = LauncherData.getInstance(); + NavigationController controller = (NavigationController) FXMLUtilities.loadFxmlAsClassResource("/theme/projectswg/fxml/navigation.fxml", data.getGeneral().getLocale()); + if (controller == null) { + operational.set(false); + throw new NullPointerException("Invalid navigation controller"); + } + primaryStage.setTitle("ProjectSWG Launcher"); + primaryStage.setScene(new Scene(controller.getRoot())); + primaryStage.setResizable(false); + primaryStage.setOnCloseRequest(e -> Platform.exit()); + primaryStage.show(); + } + + @Override + public void stop() { + operational.set(false); + } + + public static LauncherUI getInstance() { + return INSTANCE.get(); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/NavigationController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/NavigationController.java new file mode 100644 index 0000000..fa16b2b --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/NavigationController.java @@ -0,0 +1,68 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import com.projectswg.common.javafx.FXMLController; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import javafx.scene.paint.Color; + +import java.net.URL; +import java.util.ResourceBundle; + +public class NavigationController implements FXMLController { + + @FXML + private TabPane tabPane; + @FXML + public Tab announcementsTab, serverListTab, settingsTab; + @FXML + private Parent root; + + public NavigationController() { + + } + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + tabPane.getSelectionModel().select(serverListTab); + announcementsTab.setGraphic(createGlyph(FontAwesomeIcon.NEWSPAPER_ALT)); + serverListTab.setGraphic(createGlyph(FontAwesomeIcon.SERVER)); + settingsTab.setGraphic(createGlyph(FontAwesomeIcon.SLIDERS)); + } + + private static FontAwesomeIconView createGlyph(FontAwesomeIcon icon) { + FontAwesomeIconView view = new FontAwesomeIconView(icon); + view.setGlyphSize(24); + view.setFill(Color.GRAY); + return view; + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/ServerListController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/ServerListController.java new file mode 100644 index 0000000..8900d2f --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/ServerListController.java @@ -0,0 +1,124 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import com.projectswg.common.javafx.FXMLController; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.gui.servers.ServerPlayCell; +import javafx.beans.property.SimpleObjectProperty; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.image.ImageView; +import javafx.scene.layout.Pane; +import javafx.scene.layout.Region; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentBase; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentString; + +import java.net.URL; +import java.util.List; +import java.util.ResourceBundle; +import java.util.function.Function; + +public class ServerListController implements FXMLController { + + private static final String LISTENER_KEY = "server-list-controller"; + private static final double COL_WIDTH_LARGE = 150; + + @FXML + private Region root; + + @FXML + private ImageView headerImage; + + @FXML + private TableView serverTable; + + @FXML + private Pane cardContainer; + + public ServerListController() { + + } + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + addCenterAlignColumn(resources.getString("servers.column.name"), COL_WIDTH_LARGE, t->t, s -> new ConcurrentString(s.getName())); + addCenterAlignColumn(resources.getString("servers.column.remoteStatus"), COL_WIDTH_LARGE, t->t, s -> s.getInstanceInfo().getLoginStatusProperty()); + addCenterAlignColumn(resources.getString("servers.column.localStatus"), COL_WIDTH_LARGE, resources::getString, s -> s.getInstanceInfo().getUpdateStatusProperty()); + addPlayColumn(resources); + + LauncherData.getInstance().getLogin().getServers().addCollectionChangedListener(LISTENER_KEY, this::updateServerTable); + LauncherData.getInstance().getAnnouncements().getServerListCards().addCollectionChangedListener(LISTENER_KEY, this::updateAnnouncements); + updateServerTable(); + updateAnnouncements(); + } + + private void updateServerTable() { + serverTable.getItems().setAll(LauncherData.getInstance().getLogin().getServers()); + } + + private void updateAnnouncements() { + List cards = LauncherData.getInstance().getAnnouncements().getServerListCards(); + for (Card card : cards) { + card.minWidthProperty().bind(cardContainer.widthProperty().subtract(10).divide(2)); + card.maxWidthProperty().bind(cardContainer.widthProperty().subtract(10).divide(2)); + card.minHeightProperty().bind(cardContainer.heightProperty()); + card.maxHeightProperty().bind(cardContainer.heightProperty()); + } + cardContainer.getChildren().setAll(cards); + } + + private void addCenterAlignColumn(String name, double prefWidth, Function conv, Function> transform) { + TableColumn col = addColumn(name, prefWidth, conv, transform); + col.getStyleClass().add("center-table-cell"); + } + + private TableColumn addColumn(String name, double prefWidth, Function conv, Function> transform) { + TableColumn col = new TableColumn<>(name); + col.setPrefWidth(prefWidth); + col.setCellValueFactory(param -> { + ConcurrentBase val = transform.apply(param.getValue()); + SimpleObjectProperty obj = new SimpleObjectProperty<>(conv.apply(val.get())); + val.addTransformListener(LISTENER_KEY, conv, obj::set); + return obj; + }); + serverTable.getColumns().add(col); + return col; + } + + private void addPlayColumn(ResourceBundle resources) { + TableColumn col = new TableColumn<>(resources.getString("servers.column.play")); + col.setCellFactory(param -> new ServerPlayCell(resources)); + col.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue())); + col.getStyleClass().add("center-table-cell"); + col.setPrefWidth(COL_WIDTH_LARGE); + serverTable.getColumns().add(col); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/SettingsController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/SettingsController.java new file mode 100644 index 0000000..dc42d6a --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/SettingsController.java @@ -0,0 +1,44 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui; + +import com.projectswg.common.javafx.FXMLController; +import javafx.fxml.FXML; +import javafx.scene.Parent; + +import java.net.URL; +import java.util.ResourceBundle; + +public class SettingsController implements FXMLController { + + @FXML + private Parent root; + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + + } +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayButton.java b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayButton.java new file mode 100644 index 0000000..3844a35 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayButton.java @@ -0,0 +1,127 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.servers; + +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus; +import com.projectswg.launcher.core.resources.intents.CancelDownloadIntent; +import com.projectswg.launcher.core.resources.intents.DownloadPatchIntent; +import com.projectswg.launcher.core.resources.intents.LaunchGameIntent; +import javafx.application.Platform; +import javafx.scene.control.Button; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentDouble; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Handles the updating of the server play button between each state of the update server + */ +public class ServerPlayButton extends Button { + + private final ResourceBundle bundle; + private final AtomicReference loginServer; + private final AtomicReference updateServer; + private final ConcurrentDouble progressBar; + + public ServerPlayButton(@NotNull ResourceBundle bundle, @NotNull ConcurrentDouble progressBar) { + Objects.requireNonNull(bundle, "bundle"); + this.bundle = bundle; + this.loginServer = new AtomicReference<>(null); + this.updateServer = new AtomicReference<>(null); + this.progressBar = progressBar; + + setOnAction(e -> act()); + } + + public void setLoginServer(LoginServer server) { + this.loginServer.set(server); + } + + public void setUpdateServer(UpdateServer server) { + UpdateServer prev = this.updateServer.getAndSet(server); + teardown(prev); + setup(server); + } + + private void setup(UpdateServer server) { + if (server == null) + return; + server.getStatusProperty().addListener(this, this::update); + update(server.getStatus()); + setDisable(false); + } + + private void teardown(UpdateServer server) { + if (server == null) + return; + server.getStatusProperty().removeListener(this); + setDisable(true); + } + + private void update(UpdateServerStatus status) { + setDisable(status == UpdateServerStatus.SCANNING); + switch (status) { + case SCANNING: + case UNKNOWN: + case READY: + internalSetText("servers.play.play"); + break; + case REQUIRES_DOWNLOAD: + internalSetText("servers.play.update"); + break; + case DOWNLOADING: + internalSetText("servers.play.cancel"); + break; + } + } + + private void act() { + LoginServer loginServer = this.loginServer.get(); + UpdateServer updateServer = this.updateServer.get(); + + if (loginServer == null || updateServer == null) + return; + + switch (updateServer.getStatus()) { + case SCANNING: + break; + case UNKNOWN: + case READY: + LaunchGameIntent.broadcast(loginServer); + break; + case REQUIRES_DOWNLOAD: + DownloadPatchIntent.broadcastWithCallback(updateServer, progressBar::set); + break; + case DOWNLOADING: + CancelDownloadIntent.broadcast(updateServer); + break; + } + } + + private void internalSetText(String key) { + Platform.runLater(() -> setText(bundle.getString(key))); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayCell.java b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayCell.java new file mode 100644 index 0000000..3a575e5 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayCell.java @@ -0,0 +1,70 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.servers; + +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import javafx.scene.control.TableCell; +import javafx.scene.layout.VBox; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentDouble; + +import java.util.ResourceBundle; + +public class ServerPlayCell extends TableCell { + + private final ServerPlayButton button; + private final ServerPlayLabel label; + private final VBox cellContents; + + public ServerPlayCell(ResourceBundle resources) { + ConcurrentDouble progressBar = new ConcurrentDouble(-1); + this.button = new ServerPlayButton(resources, progressBar); + this.label = new ServerPlayLabel(resources, progressBar); + this.cellContents = new VBox(button, label); + cellContents.getStyleClass().add("server-play-cell"); + } + + @Override + protected void updateItem(LoginServer item, boolean empty) { + LoginServer previousLoginServer = getItem(); + if (previousLoginServer != null) { + previousLoginServer.getUpdateServerProperty().removeListener(this); + } + super.updateItem(item, empty); + if (item != null) { + item.getUpdateServerProperty().addListener(this, updateServer -> update(item, updateServer)); + } + update(item, item==null?null:item.getUpdateServer()); + if (empty) { + setGraphic(null); + } else { + setGraphic(cellContents); + } + setText(null); + } + + private void update(LoginServer loginServer, UpdateServer updateServer) { + button.setLoginServer(loginServer); + button.setUpdateServer(updateServer); + label.setUpdateServer(updateServer); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayLabel.java b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayLabel.java new file mode 100644 index 0000000..a30fd13 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/servers/ServerPlayLabel.java @@ -0,0 +1,107 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.servers; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.RequiredFile; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus; +import javafx.application.Platform; +import javafx.scene.control.Label; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentDouble; +import org.jetbrains.annotations.NotNull; + +import java.util.Collection; +import java.util.Objects; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; + +public class ServerPlayLabel extends Label { + + private static final String[] SIZE_SUFFIX = new String[] { "B", "kB", "MB", "GB" }; + + private final ResourceBundle bundle; + private final AtomicReference server; + private final ConcurrentDouble progressBar; + + public ServerPlayLabel(@NotNull ResourceBundle bundle, @NotNull ConcurrentDouble progressBar) { + Objects.requireNonNull(bundle, "bundle"); + this.bundle = bundle; + this.server = new AtomicReference<>(null); + this.progressBar = progressBar; + } + + public void setUpdateServer(UpdateServer server) { + UpdateServer prev = this.server.getAndSet(server); + teardown(prev); + setup(server); + } + + private void setup(UpdateServer server) { + if (server == null) + return; + server.getStatusProperty().addListener(this, status -> update(server, status)); + } + + private void teardown(UpdateServer server) { + if (server == null) + return; + server.getStatusProperty().removeListener(this); + } + + private void update(UpdateServer server, UpdateServerStatus status) { + progressBar.removeListener("server-play-label"); + switch (status) { + case UNKNOWN: + case READY: + case SCANNING: + progressBar.setValue(-1); + internalSetText(bundle.getString("servers.action_info.empty")); + break; + case REQUIRES_DOWNLOAD: + progressBar.setValue(-1); + internalSetText(calculateDownloadSize(server.getRequiredFiles()) + " " + bundle.getString("servers.action_info.required")); + break; + case DOWNLOADING: + progressBar.addListener("server-play-label", p -> internalSetText(String.format("%.2f%% %s", p*100, bundle.getString("servers.action_info.progress")))); + if (progressBar.get() == -1) + internalSetText(bundle.getString("servers.action_info.downloading")); + else + internalSetText(String.format("%.2f%% %s", progressBar.get(), bundle.getString("servers.action_info.progress"))); + break; + } + } + + private void internalSetText(String text) { + Platform.runLater(() -> setText(text)); + } + + private static String calculateDownloadSize(Collection files) { + double totalSize = files.stream().mapToLong(RequiredFile::getLength).sum(); + for (int i = 0; i < SIZE_SUFFIX.length; i++) { + if (i != 0) + totalSize /= 1024; + if (totalSize < 1024) + return String.format("%.2f%s", totalSize, SIZE_SUFFIX[i]); + } + return String.format("%.2f%s", totalSize, SIZE_SUFFIX[SIZE_SUFFIX.length - 1]); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsGeneralController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsGeneralController.java new file mode 100644 index 0000000..a5a8529 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsGeneralController.java @@ -0,0 +1,111 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.settings; + +import com.projectswg.common.javafx.FXMLController; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.general.GeneralData; +import com.projectswg.launcher.core.resources.data.general.LauncherTheme; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.stage.FileChooser; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Locale; +import java.util.ResourceBundle; + +public class SettingsGeneralController implements FXMLController { + + @FXML + private Parent root; + @FXML + private CheckBox soundCheckbox; + @FXML + private ComboBox themeComboBox; + @FXML + private ComboBox localeComboBox; + @FXML + private TextField wineTextField; + @FXML + private Button wineSelectionButton; + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + GeneralData data = LauncherData.getInstance().getGeneral(); + + wineSelectionButton.setGraphic(createFolderGlyph()); + + soundCheckbox.selectedProperty().addListener((obs, prev, s) -> data.setSound(s)); + themeComboBox.valueProperty().addListener((obs, prev, v) -> data.setTheme(v)); + localeComboBox.valueProperty().addListener((obs, prev, v) -> data.setLocale(v)); + wineTextField.textProperty().addListener((obs, prev, t) -> data.setWine(t)); + wineSelectionButton.setOnAction(this::processWineSelectionButtonAction); + + themeComboBox.getItems().setAll(LauncherTheme.values()); + localeComboBox.getItems().setAll(Locale.ENGLISH, Locale.GERMAN); + + soundCheckbox.setSelected(data.isSound()); + themeComboBox.setValue(data.getTheme()); + localeComboBox.setValue(data.getLocale()); + wineTextField.setText(data.getWine()); + } + + private static FontAwesomeIconView createFolderGlyph() { + FontAwesomeIconView view = new FontAwesomeIconView(FontAwesomeIcon.FOLDER_ALT); + view.setGlyphSize(16); + return view; + } + + private void processWineSelectionButtonAction(ActionEvent e) { + File selection = chooseOpenFile("Choose Wine Path"); + if (selection == null) + return; + try { + wineTextField.setText(selection.getCanonicalPath()); + } catch (IOException ex) { + wineTextField.setText(selection.getAbsolutePath()); + } + } + + private static File chooseOpenFile(String title) { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle(title); + File file = fileChooser.showOpenDialog(LauncherData.getInstance().getStage()); + if (file == null || !file.isFile()) + return null; + return file; + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsLoginController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsLoginController.java new file mode 100644 index 0000000..8c4e90c --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsLoginController.java @@ -0,0 +1,95 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.settings; + +import com.projectswg.common.javafx.FXMLController; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import javafx.collections.FXCollections; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.ComboBox; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; + +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class SettingsLoginController implements FXMLController { + + private final AtomicReference server; + + @FXML + private Parent root; + @FXML + private ComboBox nameComboBox; + @FXML + private TextField addressTextField, portTextField, usernameTextField; + @FXML + private PasswordField passwordField; + @FXML + private ComboBox updateServerComboBox; + + public SettingsLoginController() { + this.server = new AtomicReference<>(null); + } + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + // TODO: Add/remove login servers + addressTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setAddress(t))); + portTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setPort(Integer.parseInt(t)))); + usernameTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setUsername(t))); + passwordField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setPassword(t))); + updateServerComboBox.valueProperty().addListener((obs, prev, v) -> setIfPresent(s -> s.setUpdateServer(v))); + + nameComboBox.valueProperty().addListener((obs, prev, next) -> { server.set(next); updateFields(next); }); + nameComboBox.setItems(FXCollections.observableArrayList(LauncherData.getInstance().getLogin().getServers())); + updateServerComboBox.setItems(FXCollections.observableArrayList(LauncherData.getInstance().getUpdate().getServers())); + + LoginServer def = nameComboBox.getItems().get(0); + updateFields(def); + nameComboBox.setValue(def); + } + + private void updateFields(LoginServer server) { + addressTextField.setText(server.getAddress()); + portTextField.setText(Integer.toString(server.getPort())); + usernameTextField.setText(server.getUsername()); + passwordField.setText(server.getPassword()); + updateServerComboBox.setValue(server.getUpdateServer()); + } + + private void setIfPresent(Consumer c) { + LoginServer s = server.get(); + if (s != null) + c.accept(s); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsUpdateController.java b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsUpdateController.java new file mode 100644 index 0000000..6b1e2b0 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/gui/settings/SettingsUpdateController.java @@ -0,0 +1,157 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.gui.settings; + +import com.projectswg.common.javafx.FXMLController; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.game.ProcessExecutor; +import com.projectswg.launcher.core.resources.intents.RequestScanIntent; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon; +import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView; +import javafx.collections.FXCollections; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.Parent; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TextField; +import javafx.stage.DirectoryChooser; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ResourceBundle; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +public class SettingsUpdateController implements FXMLController { + + private final AtomicReference server; + + @FXML + private Parent root; + @FXML + private ComboBox nameComboBox; + @FXML + private TextField addressTextField, portTextField, basePathTextField, localPathTextField; + @FXML + private Button scanButton, clientOptionsButton, localPathSelectionButton; + + public SettingsUpdateController() { + this.server = new AtomicReference<>(null); + } + + @Override + public Parent getRoot() { + return root; + } + + @Override + public void initialize(URL location, ResourceBundle resources) { + localPathSelectionButton.setGraphic(createFolderGlyph()); + + // TODO: Add/remove login servers + scanButton.setOnAction(this::processScanButtonAction); + clientOptionsButton.setOnAction(this::processClientOptionsButtonAction); + addressTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setAddress(t))); + portTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setPort(Integer.parseInt(t)))); + basePathTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setBasePath(t))); + localPathTextField.textProperty().addListener((obs, prev, t) -> setIfPresent(s -> s.setLocalPath(t))); + localPathSelectionButton.setOnAction(this::processLocalPathSelectionButtonAction); + + nameComboBox.valueProperty().addListener((obs, prev, next) -> { server.set(next); updateFields(next); }); + nameComboBox.setItems(FXCollections.observableArrayList(LauncherData.getInstance().getUpdate().getServers())); + + UpdateServer def = nameComboBox.getItems().get(0); + updateFields(def); + nameComboBox.setValue(def); + } + + private void setIfPresent(Consumer c) { + UpdateServer s = server.get(); + if (s != null) + c.accept(s); + } + + private void updateFields(UpdateServer server) { + addressTextField.setText(server.getAddress()); + portTextField.setText(Integer.toString(server.getPort())); + basePathTextField.setText(server.getBasePath()); + localPathTextField.setText(server.getLocalPath()); + } + + private static FontAwesomeIconView createFolderGlyph() { + FontAwesomeIconView view = new FontAwesomeIconView(FontAwesomeIcon.FOLDER_ALT); + view.setGlyphSize(16); + return view; + } + + private void processScanButtonAction(ActionEvent e) { + UpdateServer server = this.server.get(); + if (server != null) + RequestScanIntent.broadcast(server); + } + + private void processClientOptionsButtonAction(ActionEvent e) { + UpdateServer server = this.server.get(); + if (server != null) + ProcessExecutor.INSTANCE.buildProcess(server, "SwgClientSetup_r.exe"); + } + + private void processLocalPathSelectionButtonAction(ActionEvent e) { + File selection = chooseOpenDirectory("Choose Local Installation Path", getCurrentDirectory()); + if (selection == null) + return; + try { + localPathTextField.setText(selection.getCanonicalPath()); + } catch (IOException ex) { + localPathTextField.setText(selection.getAbsolutePath()); + } + UpdateServer server = this.server.get(); + if (server != null) + RequestScanIntent.broadcast(server); + } + + private File getCurrentDirectory() { + UpdateServer server = this.server.get(); + if (server == null) + return new File("."); + String localPathString = server.getLocalPath(); + if (localPathString.isEmpty()) + return new File("."); + File localPath = new File(localPathString); + if (!localPath.isDirectory()) + return new File("."); + return localPath; + } + + private static File chooseOpenDirectory(String title, File currentDirectory) { + DirectoryChooser directoryChooser = new DirectoryChooser(); + directoryChooser.setTitle(title); + directoryChooser.setInitialDirectory(currentDirectory); + File file = directoryChooser.showDialog(LauncherData.getInstance().getStage()); + if (file == null || !file.isDirectory()) + return null; + return file; + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/intents/CancelDownloadIntent.java b/src/main/java/com/projectswg/launcher/core/resources/intents/CancelDownloadIntent.java new file mode 100644 index 0000000..4bf770b --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/intents/CancelDownloadIntent.java @@ -0,0 +1,50 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.intents; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import me.joshlarson.jlcommon.control.Intent; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +/** + * Requests to cancel a download + */ +public class CancelDownloadIntent extends Intent { + + private final UpdateServer server; + + public CancelDownloadIntent(@NotNull UpdateServer server) { + Objects.requireNonNull(server, "server"); + this.server = server; + } + + @NotNull + public UpdateServer getServer() { + return server; + } + + public static void broadcast(@NotNull UpdateServer server) { + new CancelDownloadIntent(server).broadcast(); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/intents/DownloadPatchIntent.java b/src/main/java/com/projectswg/launcher/core/resources/intents/DownloadPatchIntent.java new file mode 100644 index 0000000..6280f5c --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/intents/DownloadPatchIntent.java @@ -0,0 +1,63 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.intents; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import me.joshlarson.jlcommon.control.Intent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Consumer; + +/** + * Queues a download for all required files on the specified update server, with an optional callback for the current download progress + */ +public class DownloadPatchIntent extends Intent { + + private final UpdateServer server; + private final Consumer callback; + + public DownloadPatchIntent(@NotNull UpdateServer server, @Nullable Consumer callback) { + Objects.requireNonNull(server, "server"); + this.server = server; + this.callback = callback; + } + + @NotNull + public UpdateServer getServer() { + return server; + } + + @Nullable + public Consumer getCallback() { + return callback; + } + + public static void broadcast(@NotNull UpdateServer server) { + new DownloadPatchIntent(server, null).broadcast(); + } + + public static void broadcastWithCallback(@NotNull UpdateServer server, @NotNull Consumer callback) { + Objects.requireNonNull(callback, "callback"); + new DownloadPatchIntent(server, callback).broadcast(); + } +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/intents/LaunchGameIntent.java b/src/main/java/com/projectswg/launcher/core/resources/intents/LaunchGameIntent.java new file mode 100644 index 0000000..29b5a1a --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/intents/LaunchGameIntent.java @@ -0,0 +1,47 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.intents; + +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import me.joshlarson.jlcommon.control.Intent; +import org.jetbrains.annotations.NotNull; + +/** + * Requests a SWG client game launch for the specified login server + */ +public class LaunchGameIntent extends Intent { + + private final LoginServer server; + + public LaunchGameIntent(@NotNull LoginServer server) { + this.server = server; + } + + @NotNull + public LoginServer getServer() { + return server; + } + + public static void broadcast(@NotNull LoginServer server) { + new LaunchGameIntent(server).broadcast(); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/intents/RequestScanIntent.java b/src/main/java/com/projectswg/launcher/core/resources/intents/RequestScanIntent.java new file mode 100644 index 0000000..b22e4ce --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/intents/RequestScanIntent.java @@ -0,0 +1,47 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.intents; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import me.joshlarson.jlcommon.control.Intent; +import org.jetbrains.annotations.NotNull; + +/** + * Requests a scan of the specified update server's local files, to determine whether or not the files are up to date + */ +public class RequestScanIntent extends Intent { + + private final UpdateServer server; + + public RequestScanIntent(@NotNull UpdateServer server) { + this.server = server; + } + + @NotNull + public UpdateServer getServer() { + return server; + } + + public static void broadcast(@NotNull UpdateServer server) { + new RequestScanIntent(server).broadcast(); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/pipeline/Pipeline.java b/src/main/java/com/projectswg/launcher/core/resources/pipeline/Pipeline.java new file mode 100644 index 0000000..9830a04 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/pipeline/Pipeline.java @@ -0,0 +1,134 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.pipeline; + +import me.joshlarson.jlcommon.log.Log; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class Pipeline { + + public static PipelineCompiler compile(String name) { + return new PipelineCompiler<>(name); + } + + public static PipelineExecutor execute(String name) { + return new PipelineExecutor<>(name); + } + + public static class PipelineCompiler { + + private final List> stages; + private final String name; + + public PipelineCompiler(String name) { + this.stages = new CopyOnWriteArrayList<>(); + this.name = name; + } + + public PipelineCompiler next(Predicate stage) { + stages.add(stage); + return this; + } + + public PipelineCompiler next(Consumer stage) { + return next(in -> {stage.accept(in); return true; }); + } + + public PipelineCompiler next(Runnable stage) { + return next(in -> { stage.run(); return true; }); + } + + public void execute(T input) { + int stageIndex = 0; + try { + for (Predicate stage : stages) { + if (!stage.test(input)) + return; + stageIndex++; + } + } catch (Throwable t) { + Log.e("Pipeline '%s' failed during stage index %d!", name, stageIndex); + Log.e(t); + } + } + + } + + public static class PipelineExecutor { + + private final String name; + private boolean terminated; + + public PipelineExecutor() { + this(""); + } + + public PipelineExecutor(String name) { + this.name = name; + this.terminated = false; + } + + public PipelineExecutor execute(Predicate stage, T t) { + if (terminated) + return this; + try { + stage.test(t); + } catch (Throwable ex) { + Log.e("Pipeline '%s' failed!", name); + Log.e(ex); + terminated = true; + } + return this; + } + + public PipelineExecutor execute(Consumer stage, T t) { + if (terminated) + return this; + try { + stage.accept(t); + } catch (Throwable ex) { + Log.e("Pipeline '%s' failed!", name); + Log.e(ex); + terminated = true; + } + return this; + } + + public PipelineExecutor execute(Runnable stage) { + if (terminated) + return this; + try { + stage.run(); + } catch (Throwable t) { + Log.e("Pipeline '%s' failed!", name); + Log.e(t); + terminated = true; + } + return this; + } + + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/resources/pipeline/UpdateServerUpdater.java b/src/main/java/com/projectswg/launcher/core/resources/pipeline/UpdateServerUpdater.java new file mode 100644 index 0000000..9a74006 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/resources/pipeline/UpdateServerUpdater.java @@ -0,0 +1,196 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.resources.pipeline; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.RequiredFile; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus; +import me.joshlarson.jlcommon.log.Log; +import me.joshlarson.json.*; +import net.openhft.hashing.LongHashFunction; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +public class UpdateServerUpdater { + + private UpdateServerUpdater() { + + } + + public static void update(UpdateServer server) { + UpdateServerDownloaderInfo info = new UpdateServerDownloaderInfo(server); + if (!updateFileList(info)) + return; + filterValidFiles(info); + updateServerStatus(info); + } + + /** + * Stage 1: Download the file list from the update server, or fall back on the local copy. If neither are accessible, fail. + */ + private static boolean updateFileList(UpdateServerDownloaderInfo info) { + Log.t("Retrieving latest file list from %s...", info.getAddress()); + File localFileList = new File(info.getLocalPath(), "files.json"); + JSONArray files; + try (JSONInputStream in = new JSONInputStream(createURL(info, "files.json").openConnection().getInputStream())) { + files = in.readArray(); + try (JSONOutputStream out = new JSONOutputStream(new FileOutputStream(localFileList))) { + out.writeArray(files); + } catch (IOException e) { + Log.e("Failed to write updated file list to disk for update server %s", info.getName()); + } + } catch (IOException | JSONException e) { + Log.w("Failed to retrieve latest file list for update server %s (%s: %s). Falling back on local copy...", e.getClass().getName(), e.getMessage(), info.getName()); + try (JSONInputStream in = new JSONInputStream(new FileInputStream(localFileList))) { + files = in.readArray(); + } catch (JSONException | IOException t) { + Log.e("Failed to read file list from disk on update server %s with path %s. Aborting update.", info.getName(), localFileList); + return false; + } + } + info.setFiles(files.stream().filter(JSONObject.class::isInstance).map(JSONObject.class::cast).map(obj -> jsonObjectToRequiredFile(info, obj)).collect(Collectors.toList())); + return true; + } + + /** + * Stage 2: Scan each file and only keep the ones that need to be downloaded. + */ + private static void filterValidFiles(UpdateServerDownloaderInfo info) { + List files = Objects.requireNonNull(info.getFiles(), "File list was not read correctly"); + Log.d("%d known files. Scanning...", files.size()); + int total = files.size(); + info.getServer().setStatus(UpdateServerStatus.SCANNING); + files.removeIf(UpdateServerUpdater::isValidFile); + int valid = total - files.size(); + Log.d("Completed scan of update server %s. %d of %d valid.", info.getName(), valid, total); + } + + /** + * Stage 3: Update the UpdateServer status and the required files. + */ + private static void updateServerStatus(UpdateServerDownloaderInfo info) { + List serverList = info.getServer().getRequiredFiles(); + List updateList = info.getFiles(); + UpdateServerStatus updateStatus = updateList.isEmpty() ? UpdateServerStatus.READY : UpdateServerStatus.REQUIRES_DOWNLOAD; + + serverList.clear(); + serverList.addAll(updateList); + info.getServer().setStatus(updateStatus); + Log.d("Setting update server '%s' status to %s", info.getName(), updateStatus); + } + + private static boolean isValidFile(RequiredFile file) { + File localFile = file.getLocalPath(); + long length = localFile.length(); + if (!localFile.isFile() || length != file.getLength()) + return false; + try (FileChannel fc = FileChannel.open(localFile.toPath())) { + return file.getHash() == LongHashFunction.xx().hashBytes(fc.map(MapMode.READ_ONLY, 0, length)); + } catch (IOException e) { + Log.w("Failed to hash file: %s. Defaulting to invalid", file); + return false; + } + } + + private static RequiredFile jsonObjectToRequiredFile(UpdateServerDownloaderInfo info, JSONObject obj) { + String path = obj.getString("path"); + try { + return new RequiredFile(new File(info.getLocalPath(), path), createURL(info, path), obj.getLong("length"), obj.getLong("xxhash")); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private static URL createURL(UpdateServerDownloaderInfo info, String path) throws MalformedURLException { + String basePath = info.getBasePath(); + while (basePath.endsWith("/")) + basePath = basePath.substring(0, basePath.length()-1); + if (!path.startsWith("/")) + path = "/" + path; + basePath += path; + return new URL("http", info.getAddress(), info.getPort(), basePath); + } + + private static class UpdateServerDownloaderInfo { + + private final UpdateServer server; + private final String updateServerName; + private final String updateServerAddress; + private final int updateServerPort; + private final String updateServerBasePath; + private final File updateServerLocalPath; + + private List files; + + public UpdateServerDownloaderInfo(UpdateServer server) { + this.server = server; + this.updateServerName = server.getName(); + this.updateServerAddress = server.getAddress(); + this.updateServerPort = server.getPort(); + this.updateServerBasePath = server.getBasePath(); + this.updateServerLocalPath = new File(server.getLocalPath()); + this.files = null; + } + + public UpdateServer getServer() { + return server; + } + + public List getFiles() { + return files; + } + + public String getName() { + return updateServerName; + } + + public String getAddress() { + return updateServerAddress; + } + + public int getPort() { + return updateServerPort; + } + + public String getBasePath() { + return updateServerBasePath; + } + + public File getLocalPath() { + return updateServerLocalPath; + } + + public void setFiles(List files) { + this.files = files; + } + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/data/AnnouncementService.java b/src/main/java/com/projectswg/launcher/core/services/data/AnnouncementService.java new file mode 100644 index 0000000..df234e7 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/data/AnnouncementService.java @@ -0,0 +1,264 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.data; + +import com.projectswg.common.utilities.LocalUtilities; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.announcements.AnnouncementsData; +import com.projectswg.launcher.core.resources.gui.Card; +import javafx.application.Platform; +import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; +import me.joshlarson.jlcommon.control.Service; +import me.joshlarson.jlcommon.log.Log; +import me.joshlarson.json.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.StandardOpenOption; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.function.BiPredicate; +import java.util.stream.Collectors; + +public class AnnouncementService extends Service { + + private static final Map VARIABLES = new HashMap<>(); + + static { + VARIABLES.put("\\$\\{LAUNCHER.VERSION\\}", LauncherData.VERSION); + } + + private final ScheduledThreadPool executor; + + public AnnouncementService() { + this.executor = new ScheduledThreadPool(1, "announcement-service"); + } + + @Override + public boolean start() { + executor.start(); + executor.executeWithFixedDelay(0, TimeUnit.MINUTES.toMillis(30), this::update); + return true; + } + + @Override + public boolean stop() { + executor.stop(); + executor.awaitTermination(1000); + return true; + } + + private void update() { + JSONObject announcements = updateAnnouncements(); + if (announcements == null) + return; + + List announcementCards = parseCards(announcements.getArray("announcements")).stream().map(this::downloadImage).collect(Collectors.toList()); + List serverCards = parseCards(announcements.getArray("servers")).stream().map(this::downloadImage).collect(Collectors.toList()); + + Platform.runLater(() -> { + AnnouncementsData data = LauncherData.getInstance().getAnnouncements(); + data.getAnnouncementCards().clear(); + data.getAnnouncementCards().addAll(announcementCards.stream().map(this::dataToCard).collect(Collectors.toList())); + data.getServerListCards().clear(); + data.getServerListCards().addAll(serverCards.stream().map(this::dataToCard).collect(Collectors.toList())); + }); + } + + private Card dataToCard(CardData cd) { + Card card = new Card(); + if (cd.getImageUrl() != null) + card.setHeaderImage(new File(cd.getImageUrl())); + if (cd.getLink() != null) + card.setLink(cd.getLink()); + card.setTitle(cd.getTitle()); + card.setDescription(cd.getDescription()); + return card; + } + + private List parseCards(JSONArray descriptor) { + if (descriptor == null) + return Collections.emptyList(); + + return descriptor.stream().filter(JSONObject.class::isInstance).map(JSONObject.class::cast).filter(AnnouncementService::validateCard).map(this::parseCard).collect(Collectors.toList()); + } + + private CardData parseCard(JSONObject obj) { + String imageUrl = obj.getString("image"); + String title = obj.getString("title"); + String description = obj.getString("description"); + String link = obj.getString("link"); + if (title == null) + title = ""; + else + title = parseVariables(title); + if (description == null) + description = ""; + else + description = parseVariables(description); + + return new CardData(imageUrl, title, description, link); + } + + private CardData downloadImage(CardData card) { + String url = card.getImageUrl(); + if (url == null) + return card; + int lastSlash = url.lastIndexOf('/'); + if (lastSlash == -1) + return new CardData(null, card.getTitle(), card.getDescription(), card.getLink()); // Invalid url + + File cards = new File(LocalUtilities.getApplicationDirectory(), "cards"); + if (!cards.isDirectory() && !cards.mkdir()) + Log.w("Could not create card directory"); + File destination = new File(cards, Integer.toHexString(url.hashCode())); + download(url, destination); + return new CardData(destination.getAbsolutePath(), card.getTitle(), card.getDescription(), card.getLink()); + } + + private static void download(String url, File destination) { + if (destination.isFile()) + return; + Log.d("Downloading image '%s' to '%s'", url, destination); + try (ReadableByteChannel rbc = Channels.newChannel(new URL(url).openStream()); FileChannel fc = FileChannel.open(destination.toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer bb = ByteBuffer.allocateDirect(8*1024); + while (rbc.read(bb) >= 0) { + bb.flip(); + fc.write(bb); + bb.clear(); + } + Log.t("Completed download of %s", destination); + } catch (IOException e) { + Log.e("Failed to download file %s from %s with error: %s: %s", destination, url, e.getClass().getName(), e.getMessage()); + } + } + + private static boolean validateCard(JSONObject obj) { + JSONObject filter = obj.getObject("filter"); + if (filter == null) + return true; + String os = filter.getString("os"); + if (os != null) { + String currentOs = System.getProperty("os.name").toLowerCase(Locale.US); + os = os.toLowerCase(Locale.US); + switch (os) { + case "windows": + return currentOs.contains("win"); + case "mac": + return currentOs.contains("mac"); + case "linux": + return !currentOs.contains("win") && !currentOs.contains("mac"); + } + } + // Inclusive + return passesVersionCheck(filter.getString("minLauncherVersion"), (cur, b) -> cur >= b, true) && passesVersionCheck(filter.getString("maxLauncherVersion"), (cur, b) -> cur < b, false); + } + + private static String parseVariables(String str) { + for (Entry var : VARIABLES.entrySet()) { + str = str.replaceAll(var.getKey(), var.getValue()); + } + return str; + } + + private static boolean passesVersionCheck(String specifiedVersionStr, BiPredicate check, boolean def) { + if (specifiedVersionStr == null) + return true; + String [] currentVersion = LauncherData.VERSION.split("\\."); + String [] specifiedVersion = specifiedVersionStr.split("\\."); + for (int i = 0; i < currentVersion.length && i < specifiedVersion.length; i++) { + int cur = Integer.parseUnsignedInt(currentVersion[i]); + int spec = Integer.parseUnsignedInt(specifiedVersion[i]); + if (cur == spec) + continue; + return check.test(cur, spec); + } + return def; + } + + /** + * Stage 1: Download the file list from the update server, or fall back on the local copy. If neither are accessible, fail. + */ + private static JSONObject updateAnnouncements() { + File localFileList = new File(LocalUtilities.getApplicationDirectory(), "announcements.json"); + + Log.t("Retrieving latest announcements..."); + JSONObject announcements; + try (JSONInputStream in = new JSONInputStream(new URL("http", LauncherData.UPDATE_ADDRESS, 80, "/launcher/announcements.json").openConnection().getInputStream())) { + announcements = in.readObject(); + try (JSONOutputStream out = new JSONOutputStream(new FileOutputStream(localFileList))) { + out.writeObject(announcements); + } catch (IOException e) { + Log.e("Failed to write updated announcements to disk. %s: %s", e.getClass().getName(), e.getMessage()); + } + } catch (IOException | JSONException e) { + Log.w("Failed to retrieve latest announcements. Falling back on local copy..."); + try (JSONInputStream in = new JSONInputStream(new FileInputStream(localFileList))) { + announcements = in.readObject(); + } catch (JSONException | IOException t) { + Log.e("Failed to read announcements from disk. %s: %s", t.getClass().getName(), t.getMessage()); + return null; + } + } + return announcements; + } + + private static class CardData { + + private final String imageUrl; + private final String title; + private final String description; + private final String link; + + public CardData(String imageUrl, String title, String description, String link) { + this.imageUrl = imageUrl; + this.title = title; + this.description = description; + this.link = link; + } + + public String getImageUrl() { + return imageUrl; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public String getLink() { + return link; + } + + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/data/DataManager.java b/src/main/java/com/projectswg/launcher/core/services/data/DataManager.java new file mode 100644 index 0000000..b654243 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/data/DataManager.java @@ -0,0 +1,38 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.data; + +import me.joshlarson.jlcommon.control.Manager; +import me.joshlarson.jlcommon.control.ManagerStructure; + +@ManagerStructure(children = { + PreferencesDataService.class, + RemoteDataService.class, + DownloadService.class, + AnnouncementService.class +}) +public class DataManager extends Manager { + + public DataManager() { + + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/data/DownloadService.java b/src/main/java/com/projectswg/launcher/core/services/data/DownloadService.java new file mode 100644 index 0000000..2c5f1a4 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/data/DownloadService.java @@ -0,0 +1,149 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.data; + +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.RequiredFile; +import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus; +import com.projectswg.launcher.core.resources.intents.CancelDownloadIntent; +import com.projectswg.launcher.core.resources.intents.DownloadPatchIntent; +import com.projectswg.launcher.core.resources.intents.RequestScanIntent; +import me.joshlarson.jlcommon.concurrency.ThreadPool; +import me.joshlarson.jlcommon.concurrency.beans.ConcurrentLong; +import me.joshlarson.jlcommon.control.IntentHandler; +import me.joshlarson.jlcommon.control.Service; +import me.joshlarson.jlcommon.log.Log; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; + +public class DownloadService extends Service { + + private final ThreadPool threadPool; + private final Map threadDownloaders; + + public DownloadService() { + this.threadPool = new ThreadPool(4, "downloader-%d"); + this.threadDownloaders = new ConcurrentHashMap<>(); + } + + @Override + public boolean start() { + threadPool.start(); + return true; + } + + @Override + public boolean stop() { + threadPool.stop(true); + threadPool.awaitTermination(1000); + return true; + } + + @IntentHandler + private void handleCancelDownloadIntent(CancelDownloadIntent cdi) { + Thread thread = threadDownloaders.get(cdi.getServer()); + if (thread != null) + thread.interrupt(); + } + + @IntentHandler + private void handleDownloadPatchIntent(DownloadPatchIntent dpi) { + if (threadDownloaders.putIfAbsent(dpi.getServer(), Thread.currentThread()) != null) + return; + Log.t("Attempting download of patch files from %s", dpi.getServer()); + Collection files = new ArrayList<>(dpi.getServer().getRequiredFiles()); + if (files.isEmpty()) + return; + AtomicBoolean running = new AtomicBoolean(true); + ConcurrentLong dataTransferred = new ConcurrentLong(0); + Semaphore fileLockPool = new Semaphore(1 - files.size()); + + // Setup the overall data transfer callback + Consumer callback = dpi.getCallback(); + if (callback != null) { + final double totalTransfer = files.stream().mapToLong(RequiredFile::getLength).sum(); + dataTransferred.addTransformListener(t -> t / totalTransfer, callback); + } + + // Queue each download in the thread pool + for (RequiredFile file : files) { + Log.t("Downloading %s -> %s", file.getRemotePath(), file.getLocalPath()); + threadPool.execute(() -> download(file, fileLockPool, dataTransferred, running)); + } + + // Waits for all files to complete, then is able to grab the one remaining lock + try { + Log.d("Downloading %d files from %s...", files.size(), dpi.getServer()); + dpi.getServer().setStatus(UpdateServerStatus.DOWNLOADING); + fileLockPool.acquire(1); + Log.d("Completed all downloads (%d)", files.size()); + RequestScanIntent.broadcast(dpi.getServer()); + } catch (InterruptedException e) { + Log.w("Failed to complete all downloads. %d remaining", 1 -fileLockPool.availablePermits()); + running.set(false); + RequestScanIntent.broadcast(dpi.getServer()); + } finally { + threadDownloaders.remove(dpi.getServer()); + } + dpi.getServer().setStatus(UpdateServerStatus.UNKNOWN); + } + + private static void download(RequiredFile file, Semaphore fileLockPool, ConcurrentLong dataTransferred, AtomicBoolean running) { + File fileParent = file.getLocalPath().getParentFile(); + if (fileParent != null && !fileParent.isDirectory() && !fileParent.mkdirs()) + Log.w("Failed to create parent directory: %s", fileParent); + try (ReadableByteChannel rbc = Channels.newChannel(file.getRemotePath().openStream()); FileChannel fc = FileChannel.open(file.getLocalPath().toPath(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) { + ByteBuffer bb = ByteBuffer.allocateDirect(8*1024); + long downloaded = 0; + long expected = file.getLength(); + while (downloaded < expected && running.get()) { + bb.clear(); + long n = rbc.read(bb); + if (n == -1) + break; + bb.flip(); + dataTransferred.addAndGet(n); + downloaded += n; + fc.write(bb); + } + Log.t("Completed download of %s", file.getLocalPath()); + } catch (IOException e) { + Log.e("Failed to download file %s from %s with error: %s: %s", file.getLocalPath(), file.getRemotePath(), e.getClass().getName(), e.getMessage()); + } finally { + fileLockPool.release(1); + } + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/data/PreferencesDataService.java b/src/main/java/com/projectswg/launcher/core/services/data/PreferencesDataService.java new file mode 100644 index 0000000..6d5ea60 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/data/PreferencesDataService.java @@ -0,0 +1,224 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.data; + +import me.joshlarson.jlcommon.log.Log; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.general.GeneralData; +import com.projectswg.launcher.core.resources.data.general.LauncherTheme; +import com.projectswg.launcher.core.resources.data.login.LoginData; +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateData; +import com.projectswg.launcher.core.resources.data.update.UpdateServer; +import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; +import me.joshlarson.jlcommon.control.Service; + +import java.io.File; +import java.io.IOException; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; + +public class PreferencesDataService extends Service { + + private final LauncherData data; + private final Preferences preferences; + private final ScheduledThreadPool executor; + + public PreferencesDataService() { + this.data = LauncherData.getInstance(); + this.preferences = data.getPreferences(); + this.executor = new ScheduledThreadPool(1, 3, "data-executor-%d"); + } + + @Override + public boolean initialize() { + loadPreferences(); + createDefaults(); + executor.start(); + executor.executeWithFixedDelay(5*60000, 5*60000, this::savePreferences); + return true; + } + + @Override + public boolean terminate() { + executor.stop(); + executor.awaitTermination(1000); + savePreferences(); + return true; + } + + private void createDefaults() { + UpdateServer defaultUpdateServer = data.getUpdate().getServers().stream().filter(s -> s.getName().equals("ProjectSWG")).findFirst().orElse(null); + if (defaultUpdateServer == null) { + defaultUpdateServer = new UpdateServer("ProjectSWG"); + defaultUpdateServer.setAddress("login1.projectswg.com"); + defaultUpdateServer.setPort(80); + defaultUpdateServer.setBasePath("/launcher/patch"); + data.getUpdate().addServer(defaultUpdateServer); + } + if (data.getLogin().getServers().stream().noneMatch(s -> s.getName().equals("ProjectSWG"))) { + LoginServer defaultLive = new LoginServer("ProjectSWG"); + defaultLive.setAddress("login1.projectswg.com"); + defaultLive.setPort(44453); + defaultLive.setUpdateServer(defaultUpdateServer); + data.getLogin().addServer(defaultLive); + } + if (data.getLogin().getServers().stream().noneMatch(s -> s.getName().equals("localhost"))) { + LoginServer defaultLocalhost = new LoginServer("localhost"); + defaultLocalhost.setAddress("localhost"); + defaultLocalhost.setPort(44463); + defaultLocalhost.setUpdateServer(defaultUpdateServer); + data.getLogin().addServer(defaultLocalhost); + } + if (data.getGeneral().getWine() == null || data.getGeneral().getWine().isEmpty()) + data.getGeneral().setWine(getWinePath()); + } + + private void loadPreferences() { + try { + loadGeneralPreferences(data.getGeneral()); + loadUpdatePreferences(data.getUpdate()); + loadLoginPreferences(data.getLogin()); + } catch (BackingStoreException e) { + Log.w(e); + } + } + + private void savePreferences() { + try { + saveGeneralPreferences(data.getGeneral()); + saveUpdatePreferences(data.getUpdate()); + saveLoginPreferences(data.getLogin()); + preferences.flush(); + } catch (BackingStoreException e) { + Log.w(e); + } + } + + private void loadGeneralPreferences(GeneralData generalData) { + Preferences generalPreferences = preferences.node("general"); + ifPresent(generalPreferences, "sound", Boolean::valueOf, generalData::setSound); + ifPresent(generalPreferences, "theme", LauncherTheme::forThemeTag, generalData::setTheme); + ifPresent(generalPreferences, "locale", Locale::forLanguageTag, generalData::setLocale); + ifPresent(generalPreferences, "wine", generalData::setWine); + } + + private void saveGeneralPreferences(GeneralData generalData) { + Preferences generalPreferences = preferences.node("general"); + generalPreferences.putBoolean("sound", generalData.isSound()); + generalPreferences.put("theme", generalData.getTheme().getTag()); + generalPreferences.put("locale", generalData.getLocale().toLanguageTag()); + String wine = generalData.getWine(); + if (wine != null) + generalPreferences.put("wine", wine); + } + + private void loadLoginPreferences(LoginData loginData) throws BackingStoreException { + Preferences loginPreferences = preferences.node("login"); + for (String childNodeName : loginPreferences.childrenNames()) { + Preferences loginServerPreferences = loginPreferences.node(childNodeName); + LoginServer server = new LoginServer(childNodeName); + ifPresent(loginServerPreferences, "address", server::setAddress); + ifPresent(loginServerPreferences, "port", Integer::parseInt, server::setPort); + ifPresent(loginServerPreferences, "username", server::setUsername); + ifPresent(loginServerPreferences, "password", server::setPassword); + ifPresent(loginServerPreferences, "updateServer", name -> server.setUpdateServer(data.getUpdate().getServers().stream().filter(s -> s.getName().equals(name)).findFirst().orElse(null))); + loginData.getServers().add(server); + } + } + + private void saveLoginPreferences(LoginData loginData) throws BackingStoreException { + preferences.node("login").removeNode(); + Preferences loginPreferences = preferences.node("login"); + for (LoginServer server : loginData.getServers()) { + Preferences loginServerPreferences = loginPreferences.node(server.getName()); + loginServerPreferences.put("address", server.getAddress()); + loginServerPreferences.putInt("port", server.getPort()); + loginServerPreferences.put("username", server.getUsername()); + loginServerPreferences.put("password", server.getPassword()); + UpdateServer updateServer = server.getUpdateServer(); + if (updateServer != null) + loginServerPreferences.put("updateServer", updateServer.getName()); + } + } + + private void loadUpdatePreferences(UpdateData updateData) throws BackingStoreException { + Preferences updatePreferences = preferences.node("update"); + for (String childNodeName : updatePreferences.childrenNames()) { + Preferences updateServerPreferences = updatePreferences.node(childNodeName); + UpdateServer server = new UpdateServer(childNodeName); + ifPresent(updateServerPreferences, "address", server::setAddress); + ifPresent(updateServerPreferences, "port", Integer::parseInt, server::setPort); + ifPresent(updateServerPreferences, "basePath", server::setBasePath); + ifPresent(updateServerPreferences, "localPath", server::setLocalPath); + updateData.getServers().add(server); + } + } + + private void saveUpdatePreferences(UpdateData updateData) throws BackingStoreException { + preferences.node("update").removeNode(); + Preferences updatePreferences = preferences.node("update"); + for (UpdateServer server : updateData.getServers()) { + Preferences updateServerPreferences = updatePreferences.node(server.getName()); + updateServerPreferences.put("address", server.getAddress()); + updateServerPreferences.putInt("port", server.getPort()); + updateServerPreferences.put("basePath", server.getBasePath()); + updateServerPreferences.put("localPath", server.getLocalPath()); + } + } + + private static void ifPresent(Preferences p, String key, Function transform, Consumer setter) { + String val = p.get(key, null); + if (val != null) + setter.accept(transform.apply(val)); + } + + private static void ifPresent(Preferences p, String key, Consumer setter) { + String val = p.get(key, null); + if (val != null) + setter.accept(val); + } + + private static String getWinePath() { + String pathStr = System.getenv("PATH"); + if (pathStr == null) + return null; + + for (String path : pathStr.split(File.pathSeparator)) { + Log.t("Testing wine binary at %s", path); + File test = new File(path, "wine"); + if (test.isFile()) { + try { + test = test.getCanonicalFile(); + Log.d("Found wine installation. Location: %s", test); + return test.getAbsolutePath(); + } catch (IOException e) { + Log.w("Failed to get canonical file location of possible wine location: %s", test); + } + } + } + return null; + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/data/RemoteDataService.java b/src/main/java/com/projectswg/launcher/core/services/data/RemoteDataService.java new file mode 100644 index 0000000..40a1191 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/data/RemoteDataService.java @@ -0,0 +1,143 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.data; + +import com.projectswg.connection.HolocoreSocket; +import com.projectswg.launcher.core.resources.data.LauncherData; +import com.projectswg.launcher.core.resources.data.login.LoginData; +import com.projectswg.launcher.core.resources.data.login.LoginServer; +import com.projectswg.launcher.core.resources.data.update.UpdateData; +import com.projectswg.launcher.core.resources.intents.RequestScanIntent; +import com.projectswg.launcher.core.resources.pipeline.UpdateServerUpdater; +import me.joshlarson.jlcommon.collections.TransferSet; +import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool; +import me.joshlarson.jlcommon.control.IntentHandler; +import me.joshlarson.jlcommon.control.Service; +import me.joshlarson.jlcommon.log.Log; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +public class RemoteDataService extends Service { + + private static final String LISTENER_KEY = "RDS"; + + private final TransferSet loginServers; + private final ScheduledThreadPool executor; + + public RemoteDataService() { + this.loginServers = new TransferSet<>(LoginServer::getName, LoginServerUpdater::new); + this.executor = new ScheduledThreadPool(2, "remote-data-service"); + + loginServers.addDestroyCallback(LoginServerUpdater::terminate); + getLoginData().getServers().addCollectionChangedListener(LISTENER_KEY, loginServers::synchronize); + } + + @Override + public boolean start() { + executor.start(); + // Updates the status of the login server (OFFLINE/LOADING/UP/LOCKED) + executor.executeWithFixedDelay(0, TimeUnit.SECONDS.toMillis(10), this::updateLoginServers); + // Retrieves the latest file list for each update server + executor.executeWithFixedDelay(0, TimeUnit.MINUTES.toMillis(30), this::updateUpdateServers); + return true; + } + + @Override + public boolean stop() { + executor.stop(); + executor.awaitTermination(1000); + return true; + } + + @Override + public boolean terminate() { + loginServers.synchronize(Collections.emptyList()); + return true; + } + + @IntentHandler + private void handleRequestScanIntent(RequestScanIntent rsi) { + UpdateServerUpdater.update(rsi.getServer()); + } + + private void updateLoginServers() { + // Allows for parallel networking operations + loginServers.parallelStream().forEach(LoginServerUpdater::update); + } + + private void updateUpdateServers() { + getUpdateData().getServers().parallelStream().forEach(UpdateServerUpdater::update); + } + + private static LoginData getLoginData() { + return LauncherData.getInstance().getLogin(); + } + + private static UpdateData getUpdateData() { + return LauncherData.getInstance().getUpdate(); + } + + private static class LoginServerUpdater { + + private final LoginServer server; + private HolocoreSocket socket; + + public LoginServerUpdater(LoginServer server) { + this.server = server; + this.socket = null; + + server.getAddressProperty().addListener(LISTENER_KEY, addr -> updateSocket()); + server.getPortProperty().addListener(LISTENER_KEY, port -> updateSocket()); + updateSocket(); + } + + public void terminate() { + HolocoreSocket socket = this.socket; + if (socket != null) + socket.terminate(); + } + + public void update() { + HolocoreSocket socket = this.socket; + if (socket == null) + return; // Better luck next time + server.getInstanceInfo().setLoginStatus(socket.getServerStatus(5000)); + } + + private void updateSocket() { + try { + String addr = server.getAddress().trim(); + int port = server.getPort(); + if (addr.isEmpty() || port <= 0) + return; + this.socket = new HolocoreSocket(InetAddress.getByName(addr), port); + } catch (UnknownHostException e) { + this.socket = null; + Log.w(e); + } + } + + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/launcher/GameService.java b/src/main/java/com/projectswg/launcher/core/services/launcher/GameService.java new file mode 100644 index 0000000..a9939b1 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/launcher/GameService.java @@ -0,0 +1,57 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.launcher; + +import com.projectswg.launcher.core.resources.game.GameInstance; +import com.projectswg.launcher.core.resources.intents.LaunchGameIntent; +import me.joshlarson.jlcommon.control.IntentHandler; +import me.joshlarson.jlcommon.control.Service; +import me.joshlarson.jlcommon.log.Log; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class GameService extends Service { + + private final List instances; + + public GameService() { + this.instances = new CopyOnWriteArrayList<>(); + } + + @Override + public boolean stop() { + for (GameInstance instance : instances) { + instance.stop(); + } + instances.clear(); + return true; + } + + @IntentHandler + private void handleLaunchGameIntent(LaunchGameIntent lgi) { + Log.i("Launching Game Instance"); + GameInstance instance = new GameInstance(lgi.getServer()); + instance.start(); + instances.add(instance); + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/launcher/LauncherManager.java b/src/main/java/com/projectswg/launcher/core/services/launcher/LauncherManager.java new file mode 100644 index 0000000..d70373c --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/launcher/LauncherManager.java @@ -0,0 +1,36 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.launcher; + +import me.joshlarson.jlcommon.control.Manager; +import me.joshlarson.jlcommon.control.ManagerStructure; + +@ManagerStructure(children = { + GameService.class, + UserInterfaceService.class +}) +public class LauncherManager extends Manager { + + public LauncherManager() { + + } + +} diff --git a/src/main/java/com/projectswg/launcher/core/services/launcher/UserInterfaceService.java b/src/main/java/com/projectswg/launcher/core/services/launcher/UserInterfaceService.java new file mode 100644 index 0000000..37ef065 --- /dev/null +++ b/src/main/java/com/projectswg/launcher/core/services/launcher/UserInterfaceService.java @@ -0,0 +1,67 @@ +/*********************************************************************************** + * Copyright (C) 2018 /// Project SWG /// www.projectswg.com * + * * + * This file is part of the ProjectSWG Launcher. * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as published by * + * the Free Software Foundation, either version 3 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + ***********************************************************************************/ + +package com.projectswg.launcher.core.services.launcher; + +import com.projectswg.launcher.core.resources.gui.LauncherUI; +import javafx.application.Application; +import javafx.application.Platform; +import me.joshlarson.jlcommon.concurrency.ThreadPool; +import me.joshlarson.jlcommon.control.Service; + +public class UserInterfaceService extends Service { + + private final ThreadPool uiThread; + + public UserInterfaceService() { + this.uiThread = new ThreadPool(1, "user-interface"); + } + + @Override + public boolean initialize() { + uiThread.start(); + uiThread.execute(this::run); + return true; + } + + @Override + public boolean isOperational() { + LauncherUI ui = LauncherUI.getInstance(); + return ui == null || ui.isOperational(); + } + + @Override + public boolean stop() { + uiThread.stop(true); + Platform.exit(); + return true; + } + + @Override + public boolean terminate() { + uiThread.awaitTermination(5000); + return true; + } + + private void run() { + Application.launch(LauncherUI.class); + } + +} diff --git a/src/main/resources/bundles/strings/strings.properties b/src/main/resources/bundles/strings/strings.properties new file mode 100644 index 0000000..3642411 --- /dev/null +++ b/src/main/resources/bundles/strings/strings.properties @@ -0,0 +1,48 @@ +announcements=Announcements +servers=Servers +settings=Settings + +noServers=No servers found + +servers.column.name=Name +servers.column.remoteStatus=Remote Status +servers.column.localStatus=Local Status +servers.column.play=Play + +servers.play.play=Play +servers.play.update=Update +servers.play.cancel=Cancel + +servers.status.unknown= +servers.status.scanning=Scanning... +servers.status.requires_download=Requires Download +servers.status.downloading=Downloading... +servers.status.ready=Ready + +servers.action_info.empty= +servers.action_info.progress=complete +servers.action_info.required=required +servers.action_info.downloading=Downloading... + +settings.general.header=General +settings.general.sound=Sound +settings.general.theme=Theme +settings.general.locale=Locale +settings.general.wine=Wine + +settings.login.header=Login Servers +settings.login.name=Name +settings.login.address=Address +settings.login.port=Port +settings.login.username=Username +settings.login.password=Password +settings.login.updateServer=Update Server + +settings.update.header=Update Servers +settings.update.name=Name +settings.update.scan=Scan +settings.update.clientOptions=Client Options +settings.update.address=Address +settings.update.port=Port +settings.update.basePath=Base Path +settings.update.localPath=Local Path diff --git a/src/main/resources/bundles/strings/strings_de.properties b/src/main/resources/bundles/strings/strings_de.properties new file mode 100644 index 0000000..e0570c8 --- /dev/null +++ b/src/main/resources/bundles/strings/strings_de.properties @@ -0,0 +1,48 @@ +announcements=Ank�ndigungen +servers=Servers +settings=Einstellungen + +noServers=Keine Server gefunden + +servers.column.name=Name +servers.column.remoteStatus=Remote Status +servers.column.localStatus=Lokal Status +servers.column.play=Aktion + +servers.play.play=Spielen +servers.play.update=Update +servers.play.cancel=Abbruch + +servers.status.unknown=Unbekannt +servers.status.scanning=Scanne... +servers.status.requires_download=Benötigt Download +servers.status.downloading=Downloade... +servers.status.ready=Fertig + +servers.action_info.empty= +servers.action_info.progress=fertig +servers.action_info.required=benötigt +servers.action_info.downloading=Lade... + +settings.general.header=Allgemein +settings.general.sound=Ton +settings.general.theme=Stil +settings.general.locale=Sprache +settings.general.wine=Wine + +settings.login.header=Login Servers +settings.login.name=Name +settings.login.address=Adresse +settings.login.port=Port +settings.login.username=Nutzername +settings.login.password=Passwort +settings.login.updateServer=Update Server + +settings.update.header=Update Servers +settings.update.name=Name +settings.update.scan=Scannen +settings.update.clientOptions=Client-Optionen +settings.update.address=Adresse +settings.update.port=Port +settings.update.basePath=Basis Pfad +settings.update.localPath=Lokaler Pfad diff --git a/src/main/resources/theme/projectswg/css/theme.css b/src/main/resources/theme/projectswg/css/theme.css new file mode 100644 index 0000000..c9d209b --- /dev/null +++ b/src/main/resources/theme/projectswg/css/theme.css @@ -0,0 +1,179 @@ +#root, .background { + -fx-background: #393939; + -fx-background-color: #393939; + -fx-text-fill: white; +} + +.separator *.line { + -fx-border-style: solid; + -fx-border-width: 3 0 0 0; + -fx-border-color: #d5d5d5; + -fx-padding: 5 0 0 0; +} + +.scroll-pane { + -fx-background: transparent; +} + +/* Cards */ + + .card-container { + -fx-hgap: 10px; + -fx-vgap: 10px; + } + + .card { + -fx-background-color: #454545; + -fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0); + -fx-vbar-policy: as-needed; + -fx-hbar-policy: never; + } + + .card-content { + -fx-text-fill: white; + -fx-vgap: 5px; + -fx-alignment: top-center; + -fx-padding: 5px; + } + + .card .title { + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-wrap-text: true; + -fx-font-size: 14px; + } + + .card .separator { + -fx-padding: 0 0 10 0; + } + + .card .description { + -fx-text-fill: white; + -fx-wrap-text: true; + -fx-font-size: 12px; + } + +/* Tabs*/ + + .tab-pane { + -fx-tab-min-width: 40px; + -fx-tab-min-height: 40px; + -fx-tab-max-width: 40px; + -fx-tab-max-height: 40px; + } + + .tab-pane *.tab-header-area *.tab-header-background { + -fx-background-color: #313131; + } + + .tab-pane *.tab-content-area { + -fx-background-color: #393939; + -fx-padding: 8px; + } + + .tab-pane .tab { + -fx-background-color: #313131; + -fx-text-fill: gray; + -fx-padding: 5px; + } + + .tab-pane .tab .scroll-pane { + -fx-fit-to-height: true; + } + + .tab-pane .tab:selected { + -fx-background-color: #d5d5d5; + -fx-focus-color: transparent; + -fx-text-color: black; + } + + .tab-pane .tab-label { + -fx-content-display: graphic-only; + } + +/* Servers */ + + .server-play-cell { + -fx-alignment: center; + } + + #serverTable { + -fx-background-color: #4e4e4e; + -fx-border-width: 0; + -fx-padding: 0px; + -fx-max-height: 225px; + } + + #serverTable .table-column { + -fx-font-weight: bold; + } + + #serverTable .table-row-cell { + -fx-pref-height: 75px; + } + + #serverTable .table-row-cell:filled:selected { + -fx-background: -fx-control-inner-background ; + -fx-background-color: -fx-table-cell-border-color, -fx-background ; + -fx-background-insets: 0, 0 0 1 0 ; + -fx-table-cell-border-color: derive(-fx-color, 5%); + } + + #serverTable .table-row-cell:odd:filled:selected { + -fx-background: -fx-control-inner-background-alt ; + } + + .table-view .column-header .label { + -fx-text-alignment: center; + -fx-text-fill: black; + } + + .left-table-cell { + -fx-alignment: center-left; + -fx-text-fill: black; + } + + .center-table-cell { + -fx-alignment: center; + -fx-text-fill: black; + } + +/* Settings */ + + .settings-header-label { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-padding: 5 0 10 5; + } + + .settings-row { + -fx-pref-height: 25px; + -fx-padding: 5 5 5 50; + -fx-hgap: 5px; + } + + .settings-row .label { + -fx-pref-width: 100px; + -fx-pref-height: 25px; + -fx-text-fill: white; + } + + .settings-row .check-box { + -fx-pref-height: 25px; + -fx-text-fill: white; + } + + .settings-row .pathSelection { + -fx-min-width: 25px; + -fx-max-width: 25px; + -fx-min-height: 25px; + -fx-max-height: 25px; + -icons-color: black; + } + + .settings-row .combo-box, + .settings-row .text-field { + -fx-pref-width: 400px; + -fx-pref-height: 20px; + } diff --git a/src/main/resources/theme/projectswg/fxml/announcements.fxml b/src/main/resources/theme/projectswg/fxml/announcements.fxml new file mode 100644 index 0000000..845c875 --- /dev/null +++ b/src/main/resources/theme/projectswg/fxml/announcements.fxml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/theme/projectswg/fxml/navigation.fxml b/src/main/resources/theme/projectswg/fxml/navigation.fxml new file mode 100644 index 0000000..ffc4e6c --- /dev/null +++ b/src/main/resources/theme/projectswg/fxml/navigation.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/theme/projectswg/fxml/servers.fxml b/src/main/resources/theme/projectswg/fxml/servers.fxml new file mode 100644 index 0000000..5a25264 --- /dev/null +++ b/src/main/resources/theme/projectswg/fxml/servers.fxml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + diff --git a/src/main/resources/theme/projectswg/fxml/settings.fxml b/src/main/resources/theme/projectswg/fxml/settings.fxml new file mode 100644 index 0000000..48881de --- /dev/null +++ b/src/main/resources/theme/projectswg/fxml/settings.fxml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/main/resources/theme/projectswg/fxml/settings/settings_general.fxml b/src/main/resources/theme/projectswg/fxml/settings/settings_general.fxml new file mode 100644 index 0000000..3dbc278 --- /dev/null +++ b/src/main/resources/theme/projectswg/fxml/settings/settings_general.fxml @@ -0,0 +1,22 @@ + + + +