mirror of
https://github.com/ProjectSWGCore/launcher.git
synced 2026-01-16 23:04:25 -05:00
"Initial" commit
This commit is contained in:
12
.gitmodules
vendored
Normal file
12
.gitmodules
vendored
Normal 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
1
client-holocore
Submodule
Submodule client-holocore added at 450ce04c13
1
forwarder
Submodule
1
forwarder
Submodule
Submodule forwarder added at 389d2323e4
1
pswgcommon
Submodule
1
pswgcommon
Submodule
Submodule pswgcommon added at e2532b34ed
1
pswgcommonfx
Submodule
1
pswgcommonfx
Submodule
Submodule pswgcommonfx added at 20f62b5b10
77
src/main/java/com/projectswg/launcher/core/Launcher.java
Normal file
77
src/main/java/com/projectswg/launcher/core/Launcher.java
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
48
src/main/resources/bundles/strings/strings.properties
Normal file
48
src/main/resources/bundles/strings/strings.properties
Normal 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
|
||||
48
src/main/resources/bundles/strings/strings_de.properties
Normal file
48
src/main/resources/bundles/strings/strings_de.properties
Normal 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
|
||||
179
src/main/resources/theme/projectswg/css/theme.css
Normal file
179
src/main/resources/theme/projectswg/css/theme.css
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
20
src/main/resources/theme/projectswg/fxml/navigation.fxml
Normal file
20
src/main/resources/theme/projectswg/fxml/navigation.fxml
Normal 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>
|
||||
17
src/main/resources/theme/projectswg/fxml/servers.fxml
Normal file
17
src/main/resources/theme/projectswg/fxml/servers.fxml
Normal 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>
|
||||
12
src/main/resources/theme/projectswg/fxml/settings.fxml
Normal file
12
src/main/resources/theme/projectswg/fxml/settings.fxml
Normal 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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 |
BIN
src/main/resources/theme/projectswg/graphics/pswg_icon.png
Executable file
BIN
src/main/resources/theme/projectswg/graphics/pswg_icon.png
Executable file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user