"Initial" commit

This commit is contained in:
Josh Larson
2018-06-10 14:49:22 -05:00
commit 0005ca0cf9
56 changed files with 4467 additions and 0 deletions

12
.gitmodules vendored Normal file
View File

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

1
client-holocore Submodule

Submodule client-holocore added at 450ce04c13

1
forwarder Submodule

Submodule forwarder added at 389d2323e4

1
pswgcommon Submodule

Submodule pswgcommon added at e2532b34ed

1
pswgcommonfx Submodule

Submodule pswgcommonfx added at 20f62b5b10

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<ServiceBase> 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
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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> 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;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<Card> announcementCards;
private final ConcurrentList<Card> serverListCards;
public AnnouncementsData() {
this.announcementCards = new ConcurrentList<>();
this.serverListCards = new ConcurrentList<>();
}
public ConcurrentList<Card> getAnnouncementCards() {
return announcementCards;
}
public ConcurrentList<Card> getServerListCards() {
return serverListCards;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LauncherTheme> theme;
private final ConcurrentReference<Locale> 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<LauncherTheme> getThemeProperty() {
return theme;
}
@NotNull
public ConcurrentReference<Locale> 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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<String, LauncherTheme> 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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LoginServer> servers;
public LoginData() {
this.servers = new ConcurrentSet<>(new CopyOnWriteArraySet<>());
}
@NotNull
public ConcurrentSet<LoginServer> getServers() {
return servers;
}
public void addServer(@NotNull LoginServer server) {
servers.add(server);
}
public void removeServer(@NotNull LoginServer server) {
servers.remove(server);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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> 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<UpdateServer> 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<UpdateServer> 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;
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<UpdateServer> servers;
public UpdateData() {
this.servers = new ConcurrentSet<>(new CopyOnWriteArraySet<>());
}
@NotNull
public ConcurrentSet<UpdateServer> getServers() {
return servers;
}
public void addServer(@NotNull UpdateServer server) {
servers.add(server);
}
public void removeServer(@NotNull UpdateServer server) {
servers.remove(server);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<RequiredFile> requiredFiles;
private final ConcurrentReference<UpdateServerStatus> 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<RequiredFile> getRequiredFiles() {
return requiredFiles;
}
@NotNull
public ConcurrentReference<UpdateServerStatus> 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;
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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();
});
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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();
});
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<Card> 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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LauncherUI> 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();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LoginServer> 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<Card> 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 <S, T> void addCenterAlignColumn(String name, double prefWidth, Function<S, T> conv, Function<LoginServer, ConcurrentBase<S>> transform) {
TableColumn<LoginServer, T> col = addColumn(name, prefWidth, conv, transform);
col.getStyleClass().add("center-table-cell");
}
private <S, T> TableColumn<LoginServer, T> addColumn(String name, double prefWidth, Function<S, T> conv, Function<LoginServer, ConcurrentBase<S>> transform) {
TableColumn<LoginServer, T> col = new TableColumn<>(name);
col.setPrefWidth(prefWidth);
col.setCellValueFactory(param -> {
ConcurrentBase<S> val = transform.apply(param.getValue());
SimpleObjectProperty<T> 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<LoginServer, LoginServer> 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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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) {
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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> loginServer;
private final AtomicReference<UpdateServer> 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)));
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LoginServer, LoginServer> {
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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<UpdateServer> 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<RequiredFile> 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]);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LauncherTheme> themeComboBox;
@FXML
private ComboBox<Locale> 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;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LoginServer> server;
@FXML
private Parent root;
@FXML
private ComboBox<LoginServer> nameComboBox;
@FXML
private TextField addressTextField, portTextField, usernameTextField;
@FXML
private PasswordField passwordField;
@FXML
private ComboBox<UpdateServer> 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<LoginServer> c) {
LoginServer s = server.get();
if (s != null)
c.accept(s);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<UpdateServer> server;
@FXML
private Parent root;
@FXML
private ComboBox<UpdateServer> 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<UpdateServer> 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;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<Double> callback;
public DownloadPatchIntent(@NotNull UpdateServer server, @Nullable Consumer<Double> callback) {
Objects.requireNonNull(server, "server");
this.server = server;
this.callback = callback;
}
@NotNull
public UpdateServer getServer() {
return server;
}
@Nullable
public Consumer<Double> getCallback() {
return callback;
}
public static void broadcast(@NotNull UpdateServer server) {
new DownloadPatchIntent(server, null).broadcast();
}
public static void broadcastWithCallback(@NotNull UpdateServer server, @NotNull Consumer<Double> callback) {
Objects.requireNonNull(callback, "callback");
new DownloadPatchIntent(server, callback).broadcast();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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();
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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 <T> PipelineCompiler<T> compile(String name) {
return new PipelineCompiler<>(name);
}
public static <T> PipelineExecutor<T> execute(String name) {
return new PipelineExecutor<>(name);
}
public static class PipelineCompiler<T> {
private final List<Predicate<T>> stages;
private final String name;
public PipelineCompiler(String name) {
this.stages = new CopyOnWriteArrayList<>();
this.name = name;
}
public PipelineCompiler<T> next(Predicate<T> stage) {
stages.add(stage);
return this;
}
public PipelineCompiler<T> next(Consumer<T> stage) {
return next(in -> {stage.accept(in); return true; });
}
public PipelineCompiler<T> next(Runnable stage) {
return next(in -> { stage.run(); return true; });
}
public void execute(T input) {
int stageIndex = 0;
try {
for (Predicate<T> 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<T> {
private final String name;
private boolean terminated;
public PipelineExecutor() {
this("");
}
public PipelineExecutor(String name) {
this.name = name;
this.terminated = false;
}
public PipelineExecutor<T> execute(Predicate<T> 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<T> execute(Consumer<T> 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<T> 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;
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<RequiredFile> 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<RequiredFile> serverList = info.getServer().getRequiredFiles();
List<RequiredFile> 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<RequiredFile> 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<RequiredFile> 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<RequiredFile> files) {
this.files = files;
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<String, String> 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<CardData> announcementCards = parseCards(announcements.getArray("announcements")).stream().map(this::downloadImage).collect(Collectors.toList());
List<CardData> 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<CardData> 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<String, String> var : VARIABLES.entrySet()) {
str = str.replaceAll(var.getKey(), var.getValue());
}
return str;
}
private static boolean passesVersionCheck(String specifiedVersionStr, BiPredicate<Integer, Integer> 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;
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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() {
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<UpdateServer, Thread> 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<RequiredFile> 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<Double> 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);
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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 <T> void ifPresent(Preferences p, String key, Function<String, T> transform, Consumer<T> setter) {
String val = p.get(key, null);
if (val != null)
setter.accept(transform.apply(val));
}
private static void ifPresent(Preferences p, String key, Consumer<String> 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;
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<LoginServer, LoginServerUpdater> 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);
}
}
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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<GameInstance> 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);
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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() {
}
}

View File

@@ -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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
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);
}
}

View File

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

View File

@@ -0,0 +1,48 @@
announcements=Ank<EFBFBD>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

View File

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

View File

@@ -0,0 +1,5 @@
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="root" fx:controller="com.projectswg.launcher.core.resources.gui.AnnouncementsController" xmlns:fx="http://javafx.com/fxml">
<FlowPane fx:id="cardContainer" styleClass="card-container" VBox.vgrow="ALWAYS" />
</VBox>

View File

@@ -0,0 +1,20 @@
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.VBox?>
<?import java.net.URL?>
<VBox fx:id="root" fx:controller="com.projectswg.launcher.core.resources.gui.NavigationController" xmlns:fx="http://javafx.com/fxml">
<stylesheets>
<URL value="@/theme/projectswg/css/theme.css"/>
</stylesheets>
<TabPane fx:id="tabPane" side="LEFT" VBox.vgrow="ALWAYS">
<Tab fx:id="announcementsTab" styleClass="background" text="%announcements" closable="false">
<fx:include source="announcements.fxml"/>
</Tab>
<Tab fx:id="serverListTab" styleClass="background" text="%servers" closable="false">
<fx:include source="servers.fxml"/>
</Tab>
<Tab fx:id="settingsTab" styleClass="background" text="%settings" closable="false">
<fx:include source="settings.fxml"/>
</Tab>
</TabPane>
</VBox>

View File

@@ -0,0 +1,17 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.*?>
<VBox fx:id="root" fx:controller="com.projectswg.launcher.core.resources.gui.ServerListController" xmlns:fx="http://javafx.com/fxml">
<ImageView fx:id="headerImage">
<Image url="/theme/projectswg/graphics/headers/server-table.png"/>
</ImageView>
<TableView fx:id="serverTable" focusTraversable="false">
<placeholder>
<Label text="%noServers"/>
</placeholder>
</TableView>
<Region prefHeight="5" />
<FlowPane fx:id="cardContainer" styleClass="card-container" VBox.vgrow="ALWAYS" />
</VBox>

View File

@@ -0,0 +1,12 @@
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.ScrollPane?>
<ScrollPane fx:id="root" fx:controller="com.projectswg.launcher.core.resources.gui.SettingsController" xmlns:fx="http://javafx.com/fxml" fitToWidth="true">
<VBox styleClass="background">
<fx:include source="settings/settings_general.fxml" />
<Separator />
<fx:include source="settings/settings_login.fxml" />
<Separator />
<fx:include source="settings/settings_update.fxml" />
</VBox>
</ScrollPane>

View File

@@ -0,0 +1,22 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.projectswg.launcher.core.resources.gui.settings.SettingsGeneralController">
<Label text="%settings.general.header" styleClass="settings-header-label" />
<HBox styleClass="settings-row">
<Label text="%settings.general.sound" />
<CheckBox fx:id="soundCheckbox" disable="true" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.general.theme" />
<ComboBox fx:id="themeComboBox" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.general.locale" />
<ComboBox fx:id="localeComboBox" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.general.wine" />
<TextField fx:id="wineTextField" disable="true" />
<Button fx:id="wineSelectionButton" styleClass="pathSelection" />
</HBox>
</VBox>

View File

@@ -0,0 +1,30 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<VBox fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.projectswg.launcher.core.resources.gui.settings.SettingsLoginController">
<Label text="%settings.login.header" styleClass="settings-header-label" />
<HBox styleClass="settings-row">
<Label text="%settings.login.name" />
<ComboBox fx:id="nameComboBox" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.address" />
<TextField fx:id="addressTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.port" />
<TextField fx:id="portTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.username" />
<TextField fx:id="usernameTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.password" />
<PasswordField fx:id="passwordField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.updateServer" />
<ComboBox fx:id="updateServerComboBox" />
</HBox>
</VBox>

View File

@@ -0,0 +1,29 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.layout.HBox?>
<VBox fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.projectswg.launcher.core.resources.gui.settings.SettingsUpdateController">
<Label text="%settings.update.header" styleClass="settings-header-label" />
<HBox styleClass="settings-row">
<Label text="%settings.update.name" />
<ComboBox fx:id="nameComboBox" />
<Button fx:id="scanButton" text="%settings.update.scan" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.update.address" />
<TextField fx:id="addressTextField" />
<Button fx:id="clientOptionsButton" text="%settings.update.clientOptions"/>
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.update.port" />
<TextField fx:id="portTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.update.basePath" />
<TextField fx:id="basePathTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.update.localPath" />
<TextField fx:id="localPathTextField" disable="true" />
<Button fx:id="localPathSelectionButton" styleClass="pathSelection" />
</HBox>
</VBox>

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,136 @@
/***********************************************************************************
* 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 <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.launcher.utility;
import me.joshlarson.json.JSONArray;
import me.joshlarson.json.JSONObject;
import me.joshlarson.json.JSONOutputStream;
import net.openhft.hashing.LongHashFunction;
import org.bouncycastle.jcajce.provider.digest.SHA3;
import org.bouncycastle.util.encoders.Hex;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.zip.Adler32;
public class CreateUpdateList {
public static void main(String [] args) throws IOException {
if (args.length <= 0) {
System.err.println("Invalid arguments. Expected: java -jar CreateUpdateList.jar <patch directory>");
return;
}
File patch = new File(args[0]);
if (!patch.isDirectory()) {
System.err.println("Invalid patch directory - not a directory: " + patch);
return;
}
patch = patch.getCanonicalFile();
System.out.println("Opening " + patch + " for reading...");
JSONArray files = new JSONArray();
createFileList(files, patch, patch.getAbsolutePath());
System.out.println("Saving to file...");
try (JSONOutputStream out = new JSONOutputStream(new FileOutputStream(new File("files.json")))) {
out.writeArray(files);
}
System.out.println("Done.");
}
private static void createFileList(JSONArray files, File directory, String filter) {
for (File child : Objects.requireNonNull(directory.listFiles())) {
if (child.isFile()) {
addFile(files, child, filter);
} else if (child.isDirectory()) {
createFileList(files, child, filter);
} else {
System.err.println("Unknown file: " + child);
}
}
}
private static void addFile(JSONArray files, File file, String filter) {
String path = file.getAbsolutePath().substring(filter.length());
if (!isValidFile(file)) {
System.out.println(" Ignoring " + path);
return;
}
System.out.println(" Adding " + path);
JSONObject obj = new JSONObject();
obj.put("path", path);
obj.put("length", file.length());
try (FileChannel fc = FileChannel.open(file.toPath())) {
ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, file.length());
obj.put("adler32", getAdler32(bb));
obj.put("xxhash", getXXHash(bb));
// obj.put("md5", getMD5(bb));
// obj.put("sha3", getSHA3(bb));
} catch (IOException e) {
e.printStackTrace();
}
files.add(obj);
}
private static long getAdler32(ByteBuffer bb) {
bb.position(0);
Adler32 adler = new Adler32();
adler.update(bb);
return adler.getValue();
}
private static long getXXHash(ByteBuffer bb) {
bb.position(0);
return LongHashFunction.xx().hashBytes(bb);
}
// private static String getMD5(ByteBuffer bb) {
// try {
// bb.position(0);
// MessageDigest digest = MessageDigest.getInstance("MD5");
// digest.update(bb);
// return Hex.toHexString(digest.digest());
// } catch (NoSuchAlgorithmException e) {
// return "";
// }
// }
// private static String getSHA3(ByteBuffer bb) {
// bb.position(0);
// MessageDigest digest = new SHA3.Digest512();
// digest.update(bb);
// return Hex.toHexString(digest.digest());
// }
private static boolean isValidFile(File file) {
String name = file.getName();
return !name.equals("files.json") && !name.equals("user.cfg") && !name.equals("options.cfg") && !name.endsWith(".log") && !name.endsWith(".iff");
}
}