Significant rewrite of the launcher - now with websockets

This commit is contained in:
Josh Larson
2022-07-05 00:42:15 -05:00
parent 0bcb394c23
commit 9afa9f570b
87 changed files with 2146 additions and 3066 deletions

6
.gitmodules vendored
View File

@@ -4,9 +4,3 @@
[submodule "forwarder"]
path = forwarder
url = https://github.com/ProjectSWGCore/forwarder.git
[submodule "client-holocore"]
path = client-holocore
url = https://github.com/ProjectSWGCore/client-holocore.git
[submodule "zero_allocation_hashing"]
path = zero_allocation_hashing
url = https://github.com/Josh-Larson/Zero-Allocation-Hashing.git

View File

@@ -1,24 +1,23 @@
//import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
application
java
idea
kotlin("jvm") version "1.4.32"
id("nebula.deb") version "8.4.1"
id("nebula.rpm") version "8.4.1"
id("edu.sc.seis.macAppBundle") version "2.3.0"
id("org.beryx.jlink") version "2.23.6"
kotlin("jvm") version "1.7.0"
id("org.beryx.jlink") version "2.25.0"
id("de.undercouch.download") version "5.0.5"
}
// Note: define javaVersion, javaMajorVersion, javaHomeLinux, javaHomeMac, and javaHomeWindows
// inside your gradle.properties file
val javaVersion: String by project
val javaMajorVersion: String by project
val kotlinTargetJdk: String by project
val javaHomeLinux: String by project
val javaHomeMac: String by project
val javaHomeWindows: String by project
val javaVersion = "17"
val javaMajorVersion = "17"
val kotlinTargetJdk = "17"
val osName: String = System.getProperty("os.name")
val platform: String = when {
osName.startsWith("Mac", ignoreCase = true) -> "mac"
osName.startsWith("Windows", ignoreCase = true) -> "win"
osName.startsWith("Linux", ignoreCase = true) -> "linux"
else -> ""
}
subprojects {
ext {
@@ -29,24 +28,22 @@ subprojects {
}
group = "com.projectswg.launcher"
version = "1.3.5"
version = "2.0.0"
application {
mainModule.set("com.projectswg.launcher")
mainClass.set("com.projectswg.launcher.core.LauncherKt")
mainClass.set("com.projectswg.launcher.LauncherKt")
applicationDefaultJvmArgs = listOf("--add-opens", "javafx.graphics/javafx.scene=tornadofx")
}
repositories {
maven("https://dev.joshlarson.me/maven2")
maven("https://oss.sonatype.org/content/repositories/snapshots")
mavenCentral()
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
jcenter()
}
sourceSets {
main {
java.outputDir = File(java.outputDir.toString().replace("\\${File.separatorChar}java", ""))
dependencies {
val jfxOptions = object {
val group = "org.openjfx"
@@ -54,25 +51,17 @@ sourceSets {
val fxModules = arrayListOf("javafx-base", "javafx-graphics", "javafx-controls", "javafx-fxml", "javafx-swing", "javafx-web", "javafx-media")
}
jfxOptions.run {
val osName = System.getProperty("os.name")
val platform = when {
osName.startsWith("Mac", ignoreCase = true) -> "mac"
osName.startsWith("Windows", ignoreCase = true) -> "win"
osName.startsWith("Linux", ignoreCase = true) -> "linux"
else -> ""
}
fxModules.forEach {
implementation("$group:$it:$version:$platform")
}
}
implementation(group="org.jetbrains", name="annotations", version="20.1.0")
implementation(project(":pswgcommon"))
implementation(project(":client-holocore"))
implementation(project(":forwarder"))
implementation(project(":zero_allocation_hashing"))
implementation("javax.json:javax.json-api:1.1.4")
implementation(group="me.joshlarson", name="fast-json", version="3.0.1")
implementation(group="me.joshlarson", name="jlcommon-fx", version="1.0.3")
implementation(group="me.joshlarson", name="jlcommon-fx", version="17.0.0")
implementation(group="no.tornado", name="tornadofx", version="2.0.0-SNAPSHOT") {
exclude(group="org.jetbrains.kotlin")
}
@@ -85,14 +74,7 @@ sourceSets {
}
test {
dependencies {
implementation(group="junit", name="junit", version="4.12")
}
}
create("utility") {
dependencies {
implementation(group="org.bouncycastle", name="bcprov-jdk15on", version="1.60")
implementation(group="me.joshlarson", name="fast-json", version="3.0.1")
implementation(project(":zero_allocation_hashing"))
testImplementation(group="junit", name="junit", version="4.12")
}
}
}
@@ -108,94 +90,48 @@ java {
modularity.inferModulePath.set(true)
}
task("downloadJmods", de.undercouch.gradle.tasks.download.Download::class) {
val zipPath = buildDir.absolutePath + "/jmods.zip"
val unzipPath = buildDir.absolutePath + "/jmods"
val baseUrl = "https://download2.gluonhq.com/openjfx/$javaVersion/"
val platformUrl = when(platform) {
"mac" -> "openjfx-${javaVersion}_osx-x64_bin-jmods.zip"
"win" -> "openjfx-${javaVersion}_windows-x64_bin-jmods.zip"
else -> "openjfx-${javaVersion}_linux-x64_bin-jmods.zip"
}
src(baseUrl + platformUrl)
dest(zipPath)
overwrite(false)
tasks.getByName("jlink").dependsOn(this)
doLast {
copy {
from(zipTree(zipPath))
into(unzipPath)
}
}
}
jlink {
addOptions("--strip-debug", "--compress", "2", "--no-header-files", "--no-man-pages")
javaHome.set(javaHomeLinux)
targetPlatform("linux", javaHomeLinux)
targetPlatform("mac", javaHomeMac)
targetPlatform("windows", javaHomeWindows)
forceMerge("kotlin-stdlib")
addExtraModulePath(buildDir.absolutePath + "/jmods")
launcher {
name = "projectswg"
jvmArgs = listOf("--add-opens", "javafx.graphics/javafx.scene=tornadofx")
}
}
/**
* Copies the JLink created JRE into a subdirectory in build/ that contains a /Contents/Home/jre directory.
* This has to happen because the Mac App Bundle plugin relies on that structure unfortunately.
*/
val macJreLocation = "$projectDir/build/mock-mac-jre/Contents/Home"
tasks.create<Copy>("createMacJREStructure") {
dependsOn(tasks.named("jlink"))
from("build/image/projectswg-mac")
include("**/*")
into("$macJreLocation/jre")
}
macAppBundle {
appName = "ProjectSWG"
dmgName = "ProjectSWG"
icon = "src/main/resources/graphics/ProjectSWGLaunchpad.icns"
mainClassName = application.mainClass.get()
jvmVersion = javaVersion
jreHome = macJreLocation
bundleJRE = true
}
// Enforce that the JRE is copied with a Mac based structure
tasks.named("bundleJRE") {
dependsOn(tasks.named("createMacJREStructure"))
}
tasks.create<com.netflix.gradle.plugins.deb.Deb>("linuxDeb") {
dependsOn("jlink")
release = "1"
packageName = "projectswg"
maintainer = "ProjectSWG"
preInstall(file("packaging/linux/preInstall.sh"))
postInstall(file("packaging/linux/postInstall.sh"))
preUninstall(file("packaging/linux/preUninstall.sh"))
postUninstall(file("packaging/linux/postUninstall.sh"))
from ("build/image/projectswg-linux") {
into("/opt/ProjectSWG")
jpackage {
imageName = "projectswg"
installerName = "ProjectSWG"
installerType = if (platform == "linux") "deb" else null
}
from ("packaging/linux") {
into("/opt/ProjectSWG")
}
link("/usr/share/applications/ProjectSWG.desktop", "/opt/ProjectSWG/ProjectSWG.desktop")
}
tasks.create<com.netflix.gradle.plugins.rpm.Rpm>("linuxRpm") {
dependsOn("jlink")
release = "1"
packageName = "projectswg"
maintainer = "ProjectSWG"
preInstall(file("packaging/linux/preInstall.sh"))
postInstall(file("packaging/linux/postInstall.sh"))
preUninstall(file("packaging/linux/preUninstall.sh"))
postUninstall(file("packaging/linux/postUninstall.sh"))
from ("build/image/projectswg-linux") {
into("/opt/ProjectSWG")
}
from ("packaging/linux") {
exclude("*.sh")
into("/opt/ProjectSWG")
}
link("/usr/share/applications/ProjectSWG.desktop", "/opt/ProjectSWG/ProjectSWG.desktop")
}
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = kotlinTargetJdk
}
destinationDir = sourceSets.main.get().java.outputDir
destinationDirectory.set(File(destinationDirectory.get().asFile.path.replace("kotlin", "java")))
}

Submodule client-holocore deleted from 242066cbb5

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

269
gradlew vendored
View File

@@ -1,7 +1,7 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright 2015 the original author or authors.
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,67 +17,101 @@
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
@@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
@@ -106,80 +140,95 @@ location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@@ -1,2 +1,2 @@
rootProject.name = "launcher"
include("pswgcommon", "client-holocore", "forwarder", "tornadofx", "zero_allocation_hashing")
include("pswgcommon", "forwarder", "tornadofx")

View File

@@ -18,19 +18,19 @@
* *
*/
package com.projectswg.launcher.core
package com.projectswg.launcher
import com.projectswg.common.utilities.LocalUtilities
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.gui.NavigationView
import com.projectswg.launcher.core.resources.gui.events.LauncherClosingEvent
import com.projectswg.launcher.core.resources.gui.style.Style
import com.projectswg.launcher.core.services.data.DataManager
import com.projectswg.launcher.core.services.launcher.LauncherManager
import com.projectswg.launcher.resources.gui.NavigationView
import com.projectswg.launcher.resources.gui.events.LauncherClosingEvent
import com.projectswg.launcher.resources.gui.style.Style
import com.projectswg.launcher.services.data.DataManager
import com.projectswg.launcher.services.launcher.LauncherManager
import javafx.scene.image.Image
import javafx.stage.Stage
import me.joshlarson.jlcommon.control.IntentManager
import me.joshlarson.jlcommon.control.Manager
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
@@ -56,18 +56,28 @@ class Launcher
class LauncherApp: App(NavigationView::class, Style::class) {
private val intentManager = IntentManager(Runtime.getRuntime().availableProcessors())
private val services = listOf(DataManager(), LauncherManager())
private val services: List<ServiceBase>
init {
IntentManager.setInstance(intentManager)
FX.localeProperty().bind(LauncherData.INSTANCE.general.localeProperty)
FX.messages = ResourceBundle.getBundle("strings.strings", LauncherData.INSTANCE.general.locale)
LauncherData.INSTANCE.general.localeProperty.addListener {_, _, locale ->
FX.messages = ResourceBundle.getBundle("strings.strings", locale)
FX.primaryStage.scene.reloadStylesheets()
FX.primaryStage.scene.findUIComponents().forEach {
FX.replaceComponent(it)
try {
services = listOf(DataManager(), LauncherManager())
IntentManager.setInstance(intentManager)
FX.localeProperty().bind(com.projectswg.launcher.resources.data.LauncherData.INSTANCE.general.localeProperty)
FX.messages = ResourceBundle.getBundle("strings.strings", com.projectswg.launcher.resources.data.LauncherData.INSTANCE.general.locale)
com.projectswg.launcher.resources.data.LauncherData.INSTANCE.general.localeProperty.addListener { _, _, locale ->
FX.messages = ResourceBundle.getBundle("strings.strings", locale)
FX.primaryStage.scene.reloadStylesheets()
FX.primaryStage.scene.findUIComponents().forEach {
FX.replaceComponent(it)
}
}
} catch (t: Error) {
t.printStackTrace()
throw t
} catch (t: Exception) {
t.printStackTrace()
throw t
}
}

View File

@@ -17,9 +17,9 @@
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core
package com.projectswg.launcher
import com.projectswg.launcher.core.resources.data.announcements.WebsitePostFeed
import com.projectswg.launcher.resources.data.announcements.WebsitePostFeed
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.jlcommon.log.log_wrapper.ConsoleLogWrapper
import java.io.File

View File

@@ -1,91 +0,0 @@
/***********************************************************************************
* 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.forwarder.ForwarderData;
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.application.Application;
import javafx.stage.Stage;
import tornadofx.FX;
import java.util.prefs.Preferences;
public enum LauncherData {
INSTANCE;
public static final String VERSION = "1.3.4";
public static final String UPDATE_ADDRESS = "login1.projectswg.com";
private final AnnouncementsData announcementsData;
private final GeneralData generalData;
private final LoginData loginData;
private final UpdateData updateData;
private final ForwarderData forwarderData;
LauncherData() {
this.announcementsData = new AnnouncementsData();
this.generalData = new GeneralData();
this.loginData = new LoginData();
this.updateData = new UpdateData();
this.forwarderData = new ForwarderData();
}
public Preferences getPreferences() {
return Preferences.userNodeForPackage(Launcher.class);
}
public Application getApplication() {
return FX.Companion.getApplication();
}
public Stage getStage() {
return FX.Companion.getPrimaryStage();
}
public AnnouncementsData getAnnouncements() {
return announcementsData;
}
public GeneralData getGeneral() {
return generalData;
}
public LoginData getLogin() {
return loginData;
}
public UpdateData getUpdate() {
return updateData;
}
public ForwarderData getForwarderData() {
return forwarderData;
}
public static LauncherData getInstance() {
return INSTANCE;
}
}

View File

@@ -1,4 +0,0 @@
package com.projectswg.launcher.core.resources.data.announcements
class CardData(val imageUrl: String, val title: String, val description: String, val link: String?)

View File

@@ -1,115 +0,0 @@
/***********************************************************************************
* 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.javafx.beans.ConcurrentBoolean;
import me.joshlarson.jlcommon.javafx.beans.ConcurrentReference;
import me.joshlarson.jlcommon.javafx.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;
private final ConcurrentBoolean admin;
public GeneralData() {
this.sound = new ConcurrentBoolean();
this.theme = new ConcurrentReference<>(LauncherTheme.DEFAULT);
this.locale = new ConcurrentReference<>(Locale.getDefault());
this.wine = new ConcurrentString();
this.admin = new ConcurrentBoolean();
}
@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;
}
@NotNull
public ConcurrentBoolean getAdminProperty() {
return admin;
}
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 boolean isAdmin() {
return admin.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);
}
public void setAdmin(boolean admin) {
this.admin.set(admin);
}
}

View File

@@ -1,54 +0,0 @@
/***********************************************************************************
* Copyright (C) 2018 /// Project SWG /// www.projectswg.com *
* *
* This file is part of the ProjectSWG Launcher. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.data.general;
import java.util.HashMap;
import java.util.Map;
public enum LauncherTheme {
DEFAULT ("projectswg");
private static final Map<String, LauncherTheme> TAG_TO_THEME = new HashMap<>();
static {
for (LauncherTheme theme : values()) {
TAG_TO_THEME.put(theme.primaryTag, theme);
for (String tag : theme.tags)
TAG_TO_THEME.put(tag, theme);
}
}
private final String [] tags;
private final String primaryTag;
LauncherTheme(String primaryTag, String ... tags) {
this.tags = tags;
this.primaryTag = primaryTag;
}
public String getTag() {
return primaryTag;
}
public static LauncherTheme forThemeTag(String tag) {
return TAG_TO_THEME.getOrDefault(tag, LauncherTheme.DEFAULT);
}
}

View File

@@ -1,78 +0,0 @@
/***********************************************************************************
* 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.javafx.beans.ConcurrentBoolean;
import me.joshlarson.jlcommon.javafx.beans.ConcurrentString;
import org.jetbrains.annotations.NotNull;
public class LoginServerInstanceInfo {
private final ConcurrentString loginStatus;
private final ConcurrentString updateStatus;
private final ConcurrentBoolean readyToPlay;
public LoginServerInstanceInfo() {
this.loginStatus = new ConcurrentString("");
this.updateStatus = new ConcurrentString("");
this.readyToPlay = new ConcurrentBoolean();
}
@NotNull
public ConcurrentString getLoginStatusProperty() {
return loginStatus;
}
@NotNull
public ConcurrentString getUpdateStatusProperty() {
return updateStatus;
}
@NotNull
public ConcurrentBoolean getReadyToPlayProperty() {
return readyToPlay;
}
public String getLoginStatus() {
return loginStatus.get();
}
public String getUpdateStatus() {
return updateStatus.get();
}
public boolean isReadyToPlay() {
return readyToPlay.get();
}
public void setLoginStatus(@NotNull String loginStatus) {
this.loginStatus.set(loginStatus);
}
public void setUpdateStatus(@NotNull String updateStatus) {
this.updateStatus.set(updateStatus);
}
public void setReadyToPlay(boolean readyToPlay) {
this.readyToPlay.set(readyToPlay);
}
}

View File

@@ -1,49 +0,0 @@
/***********************************************************************************
* 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.javafx.beans.ConcurrentSet;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CopyOnWriteArraySet;
public class UpdateData {
private final ConcurrentSet<UpdateServer> servers;
public UpdateData() {
this.servers = new ConcurrentSet<>(new CopyOnWriteArraySet<>());
}
@NotNull
public ConcurrentSet<UpdateServer> getServers() {
return servers;
}
public void addServer(@NotNull UpdateServer server) {
servers.add(server);
}
public void removeServer(@NotNull UpdateServer server) {
servers.remove(server);
}
}

View File

@@ -1,71 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.data.announcements.CardData
import com.projectswg.launcher.core.resources.gui.style.CardStyle
import javafx.scene.Cursor
import javafx.scene.control.Tooltip
import javafx.scene.layout.Priority
import tornadofx.*
class Card : Fragment() {
val data: CardData by param()
override val root = vbox {
importStylesheet(CardStyle::class)
isFillWidth = true
addClass(CardStyle.card)
hgrow = Priority.ALWAYS
vgrow = Priority.ALWAYS
imageview("file://"+data.imageUrl) {
isPreserveRatio = true
fitHeight = 108.0
addClass(CardStyle.cardTitle)
fitWidthProperty().bind(this@vbox.widthProperty())
if (data.link != null) {
// Clicking the image takes you to the link
Tooltip.install(this, Tooltip(data.link))
cursor = Cursor.HAND
setOnMouseClicked { LauncherData.INSTANCE.application.hostServices.showDocument(data.link) }
}
}
label(data.title) {
addClass(CardStyle.cardTitle)
maxWidthProperty().bind(this@vbox.widthProperty())
}
separator()
textarea(data.description) {
isEditable = false
addClass(CardStyle.cardDescription)
maxWidthProperty().bind(this@vbox.widthProperty())
}
}
}

View File

@@ -1,61 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui
import com.projectswg.launcher.core.resources.data.announcements.CardData
import com.projectswg.launcher.core.resources.gui.style.CardStyle
import javafx.application.Platform
import javafx.collections.ObservableList
import me.joshlarson.jlcommon.log.Log
import tornadofx.*
import kotlin.math.max
class CardContainer : Fragment() {
override val root = flowpane {
importStylesheet(CardStyle::class)
addClass(CardStyle.cardContainer)
minWidth = 300.0
minHeight = 300.0
}
val children: ObservableList<CardData> by param()
init {
root.bindChildren(children) {
val card = find<Card>(mapOf(Card::data to it)).root
card.prefWidthProperty().bind(root.widthProperty().doubleBinding { max -> createCardSize(max?.toDouble() ?: 0.0) })
card.prefHeightProperty().bind(root.heightProperty().doubleBinding { max -> createCardSize(max?.toDouble() ?: 0.0) })
// card.prefWidth = createCardSize(root.width)
// card.prefHeight = createCardSize(root.height)
Platform.runLater { root.applyCss() }
Log.t("Initializing card '${it.title}' with size ${card.prefWidth}x${card.prefHeight}")
card
}
}
private fun createCardSize(max: Double): Double {
val count = max(1, (max / 300).toInt())
return ((max - (count - 1) * 10) / count - 0.5).toInt().toDouble()
}
}

View File

@@ -1,191 +0,0 @@
/***********************************************************************************
* Copyright (C) 2020 /// 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.launcher.core.resources.gui.style.Style
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 com.projectswg.launcher.core.resources.gui.servers.WebsitePostFeedList
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Pos
import javafx.scene.control.TableColumn
import javafx.scene.layout.Priority
import me.joshlarson.jlcommon.log.Log
import tornadofx.*
import java.util.*
class ServerListView : View() {
private val feedList: WebsitePostFeedList by inject()
override val root = vbox {
imageview(url = "/graphics/headers/server-table.png") {
fitWidthProperty().bind(this@vbox.widthProperty())
}
tableview<LoginServer> {
items = LauncherData.INSTANCE.login.serversProperty
isFocusTraversable = false
placeholder = label(messages["noServers"])
setSortPolicy { _ -> Comparator.comparing<LoginServer, String> { it.name }; true }
column(messages["servers.column.name"], valueProvider={cellDataFeatures:TableColumn.CellDataFeatures<LoginServer, String> -> ReadOnlyStringWrapper(cellDataFeatures.value?.name) }).apply {
prefWidth = COL_WIDTH_LARGE
styleClass += "center-table-cell"
}
column(messages["servers.column.gameVersion"], valueProvider={cellDataFeatures:TableColumn.CellDataFeatures<LoginServer, String> -> cellDataFeatures.value?.updateServerProperty?.select { it.gameVersionProperty } ?: ReadOnlyStringWrapper("N/A") }).apply {
prefWidth = COL_WIDTH_MEDIUM
styleClass += "center-table-cell"
}
column(messages["servers.column.remoteStatus"], valueProvider={cellDataFeatures:TableColumn.CellDataFeatures<LoginServer, String> -> cellDataFeatures.value?.instanceInfo?.loginStatusProperty ?: ReadOnlyStringWrapper("") } ).apply {
cellFormat {
text = it
toggleClass(Style.statusFail, it == "OFFLINE")
toggleClass(Style.statusInProgress, it == FX.messages["servers.loginStatus.checking"] || it == "LOADING")
toggleClass(Style.statusGood, it == "UP")
}
prefWidth = COL_WIDTH_LARGE
styleClass += "center-table-cell"
}
column(messages["servers.column.localStatus"], valueProvider={cellDataFeatures:TableColumn.CellDataFeatures<LoginServer, String> -> cellDataFeatures.value?.instanceInfo?.updateStatusProperty ?: ReadOnlyStringWrapper("") } ).apply {
cellFormat {
text = messages[it]
}
prefWidth = COL_WIDTH_LARGE
styleClass += "center-table-cell"
}
column(messages["servers.column.play"], LoginServer::class) {
// setCellFactory { ServerPlayCell() }
cellFragment(ServerPlayCell::class)
setCellValueFactory { param -> SimpleObjectProperty(param.value) }
prefWidth = COL_WIDTH_LARGE
styleClass.add("center-table-cell")
}
}
region { prefHeight = 5.0 }
hbox {
isFillWidth = true
hboxConstraints {
this.marginRight = 5.0
}
vbox leftBox@ {
// TODO: RSS-based list of new posts
prefWidthProperty().bind(this@hbox.widthProperty().divide(2).subtract(5))
this += feedList.root
}
vbox rightBox@ {
prefWidthProperty().bind(this@hbox.widthProperty().divide(2).subtract(5))
// Login container
form {
val username = SimpleStringProperty()
val password = SimpleStringProperty()
fieldset(messages["servers.login.form.title"]) {
field(messages["servers.login.form.username"]) {
textfield(username)
}
field(messages["servers.login.form.password"]) {
passwordfield(password)
}
}
button(messages["servers.login.form.submit"]) {
action {
Log.i("Logging in with %s / %s", username.get(), password.get())
}
}
}
region { minHeight = 5.0; maxHeight = 5.0 }
separator()
region { minHeight = 5.0; maxHeight = 5.0 }
gridpane {
this.hgap = 5.0
this.vgap = 5.0
row {
button(messages["servers.login.buttons.website"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
action {
// TODO add hyperlink
}
}
button(messages["servers.login.buttons.create_account"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
action {
// TODO: this, somehow
}
}
}
row {
button(messages["servers.login.buttons.configuration"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
action {
// TODO add hyperlink
}
}
button(messages["servers.login.buttons.server_list"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
action {
// TODO: this, somehow
}
}
}
}
region { vgrow = Priority.ALWAYS }
label("%s: %s".format(messages["servers.login.launcher_version"], LauncherData.VERSION)) {
prefWidthProperty().bind(this@rightBox.widthProperty())
alignment = Pos.BASELINE_RIGHT
}
}
}
}
companion object {
private const val COL_WIDTH_MEDIUM = 110.0
private const val COL_WIDTH_LARGE = 150.0
}
}

View File

@@ -1,5 +0,0 @@
package com.projectswg.launcher.core.resources.gui.events
import tornadofx.FXEvent
object LauncherClosingEvent : FXEvent()

View File

@@ -1,127 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui.servers
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 com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus.*
import com.projectswg.launcher.core.resources.gui.admin.AdminDisplay
import com.projectswg.launcher.core.resources.game.GameInstance
import com.projectswg.launcher.core.resources.intents.CancelDownloadIntent
import com.projectswg.launcher.core.resources.intents.DownloadPatchIntent
import com.projectswg.launcher.core.resources.intents.GameLaunchedIntent
import javafx.beans.property.ReadOnlyBooleanWrapper
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.beans.property.SimpleStringProperty
import javafx.beans.value.ObservableStringValue
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.util.converter.NumberStringConverter
import tornadofx.*
class ServerPlayCell : TableCellFragment<LoginServer, LoginServer>() {
private val loginServerProperty = itemProperty
private val updateServerProperty = loginServerProperty.select { it.updateServerProperty }
private val updateServerStatusProperty = updateServerProperty.select { it.statusProperty }
override val root = vbox {
spacing = 5.0
padding = Insets(5.0)
alignment = Pos.CENTER
styleClass.add("server-play-cell")
button(updateServerStatusProperty.select { ReadOnlyStringWrapper(getButtonText(it)) }) {
disableProperty().bind(updateServerStatusProperty.select { ReadOnlyBooleanWrapper(getButtonDisabled(it)) })
setOnAction { onButtonClicked() }
}
label(updateServerStatusProperty.select { getLabelText(it) }) {
managedProperty().bind(updateServerStatusProperty.select { ReadOnlyBooleanWrapper(getLabelVisible(it)) })
}
}
private fun getButtonText(status: UpdateServer.UpdateServerStatus?): String {
return when (status) {
SCANNING, UNKNOWN, READY -> messages["servers.play.play"]
REQUIRES_DOWNLOAD -> messages["servers.play.update"]
DOWNLOADING -> messages["servers.play.cancel"]
null -> ""
}
}
private fun getButtonDisabled(status: UpdateServer.UpdateServerStatus?): Boolean = status == SCANNING
private fun onButtonClicked() {
when (updateServerStatusProperty.value) {
UNKNOWN, READY -> {
val loginServer = loginServerProperty.value ?: return
runAsync {
val gameInstance = GameInstance(loginServer)
gameInstance.start()
GameLaunchedIntent(gameInstance).broadcast()
gameInstance
// } ui {
// if (LauncherData.INSTANCE.general.isAdmin) {
// find<AdminDisplay>(AdminDisplay::forwarder to it.forwarder).openWindow()
// TODO: Fix admin display
// }
}
}
REQUIRES_DOWNLOAD -> DownloadPatchIntent(updateServerProperty.value ?: return).broadcast()
DOWNLOADING -> CancelDownloadIntent(updateServerProperty.value ?: return).broadcast()
else -> { }
}
}
private fun getLabelText(status: UpdateServer.UpdateServerStatus?): ObservableStringValue {
return when (status) {
null, UNKNOWN, READY, SCANNING -> ReadOnlyStringWrapper(messages["servers.action_info.empty"])
REQUIRES_DOWNLOAD -> ReadOnlyStringWrapper(calculateDownloadSize(updateServerProperty.value?.requiredFiles ?: return ReadOnlyStringWrapper("")) + " " + messages["servers.action_info.required"])
DOWNLOADING -> {
val wrapper = SimpleStringProperty()
wrapper.bindBidirectional(updateServerProperty.select { it.downloadProgressProperty }, NumberStringConverter("0.00% ${messages["servers.action_info.progress"]}"))
wrapper
}
}
}
private fun getLabelVisible(status: UpdateServer.UpdateServerStatus?): Boolean {
return status != UNKNOWN && status != READY && status != SCANNING
}
companion object {
private val SIZE_SUFFIX = listOf("B", "kB", "MB", "GB", "TB", "PB")
private fun calculateDownloadSize(files: Collection<UpdateServer.RequiredFile>): String {
var totalSize = files.stream().mapToLong { it.length }.sum().toDouble()
for ((i, suffix) in SIZE_SUFFIX.withIndex()) {
if (i != 0)
totalSize /= 1024.0
if (totalSize < 1024)
return String.format("%.2f%s", totalSize, suffix)
}
return String.format("%.2f%s", totalSize, SIZE_SUFFIX[SIZE_SUFFIX.size - 1])
}
}
}

View File

@@ -1,50 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui.settings
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.data.forwarder.ForwarderData
import javafx.scene.Parent
import javafx.scene.control.Button
import javafx.scene.control.TextField
import javafx.util.converter.NumberStringConverter
import tornadofx.View
class SettingsForwarderView : View() {
override val root: Parent by fxml()
private val sendIntervalTextField: TextField by fxid()
private val sendMaxTextField: TextField by fxid()
private val resetButton: Button by fxid()
init {
sendIntervalTextField.text = LauncherData.INSTANCE.forwarderData.sendInterval.toString()
sendMaxTextField.text = LauncherData.INSTANCE.forwarderData.sendMax.toString()
sendIntervalTextField.textProperty().bindBidirectional(LauncherData.INSTANCE.forwarderData.sendIntervalProperty, NumberStringConverter())
sendMaxTextField.textProperty().bindBidirectional(LauncherData.INSTANCE.forwarderData.sendMaxProperty, NumberStringConverter())
resetButton.setOnAction {
sendIntervalTextField.text = ForwarderData.DEFAULT_SEND_INTERVAL.toString()
sendMaxTextField.text = ForwarderData.DEFAULT_SEND_MAX.toString()
}
}
}

View File

@@ -1,80 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui.settings
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.data.general.LauncherTheme
import com.projectswg.launcher.core.resources.gui.createGlyph
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
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 tornadofx.View
import java.io.File
import java.io.IOException
import java.util.*
class SettingsGeneralView : View() {
override val root: Parent by fxml()
private val soundCheckbox: CheckBox by fxid()
private val themeComboBox: ComboBox<LauncherTheme> by fxid()
private val localeComboBox: ComboBox<Locale> by fxid()
private val wineTextField: TextField by fxid()
private val wineSelectionButton: Button by fxid()
private val adminCheckBox: CheckBox by fxid()
init {
val data = LauncherData.INSTANCE.general
wineSelectionButton.graphic = FontAwesomeIcon.FOLDER_ALT.createGlyph()
themeComboBox.items.setAll(*LauncherTheme.values())
localeComboBox.items.setAll(Locale.ENGLISH, Locale.GERMAN)
soundCheckbox.selectedProperty().bindBidirectional(data.soundProperty)
themeComboBox.valueProperty().bindBidirectional(data.themeProperty)
localeComboBox.valueProperty().bindBidirectional(data.localeProperty)
wineTextField.textProperty().bindBidirectional(data.wineProperty)
wineSelectionButton.setOnAction { this.processWineSelectionButtonAction() }
adminCheckBox.selectedProperty().bindBidirectional(data.adminProperty)
}
private fun processWineSelectionButtonAction() {
val selection = chooseOpenFile("Choose Wine Path") ?: return
try {
wineTextField.text = selection.canonicalPath
} catch (ex: IOException) {
wineTextField.text = selection.absolutePath
}
}
private fun chooseOpenFile(title: String): File? {
val fileChooser = FileChooser()
fileChooser.title = title
val file = fileChooser.showOpenDialog(LauncherData.INSTANCE.stage)
return if (file == null || !file.isFile) null else file
}
}

View File

@@ -1,88 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui.settings
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 com.projectswg.launcher.core.resources.gui.createGlyph
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.binding.BooleanBinding
import javafx.beans.property.ObjectProperty
import javafx.beans.property.ReadOnlyObjectWrapper
import javafx.collections.FXCollections
import javafx.scene.Node
import javafx.scene.Parent
import javafx.scene.control.*
import javafx.scene.control.skin.TextFieldSkin
import javafx.scene.paint.Color
import javafx.util.converter.NumberStringConverter
import tornadofx.View
import tornadofx.select
class SettingsLoginView : View() {
override val root: Parent by fxml()
private val nameComboBox: ComboBox<LoginServer> by fxid()
private val addressTextField: TextField by fxid()
private val portTextField: TextField by fxid()
private val usernameTextField: TextField by fxid()
private val passwordField: PasswordField by fxid()
private val hidePasswordButton: ToggleButton by fxid()
private val updateServerComboBox: ComboBox<UpdateServer> by fxid()
private val verifyServerCheckBox: CheckBox by fxid()
private val enableEncryptionCheckBox: CheckBox by fxid()
init {
val passwordSkin = PasswordFieldSkin(passwordField, hidePasswordButton.selectedProperty().not())
passwordField.skin = passwordSkin
hidePasswordButton.isSelected = false
hidePasswordButton.graphicProperty().bind(hidePasswordButton.selectedProperty().select { if (it) createGlyph(FontAwesomeIcon.EYE_SLASH) else createGlyph(FontAwesomeIcon.EYE) })
hidePasswordButton.selectedProperty().addListener { _, _, _ -> passwordField.text = passwordField.text }
// TODO: Add/remove login servers
addressTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.addressProperty })
portTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.portProperty }, NumberStringConverter("#"))
usernameTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.usernameProperty })
passwordField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.passwordProperty })
updateServerComboBox.valueProperty().bindBidirectional(nameComboBox.valueProperty().select { it.updateServerProperty })
verifyServerCheckBox.selectedProperty().bindBidirectional(nameComboBox.valueProperty().select { it.verifyServerProperty })
enableEncryptionCheckBox.selectedProperty().bindBidirectional(nameComboBox.valueProperty().select { it.enableEncryptionProperty })
nameComboBox.items = FXCollections.observableArrayList(LauncherData.INSTANCE.login.servers)
updateServerComboBox.items = FXCollections.observableArrayList(LauncherData.INSTANCE.update.servers)
nameComboBox.value = nameComboBox.items.getOrNull(0)
}
private fun createGlyph(icon: FontAwesomeIcon): ObjectProperty<Node> {
val node = icon.createGlyph(fill = Color.BLACK)
return ReadOnlyObjectWrapper(node)
}
inner class PasswordFieldSkin(passwordField: PasswordField, private val hiddenProperty: BooleanBinding?) : TextFieldSkin(passwordField) {
override fun maskText(txt: String): String = if (hiddenProperty?.value != false) "\u25CF".repeat(txt.length) else txt
}
}

View File

@@ -1,111 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.resources.gui.settings
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.gui.createGlyph
import com.projectswg.launcher.core.resources.intents.RequestScanIntent
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.scene.Parent
import javafx.scene.control.Button
import javafx.scene.control.ComboBox
import javafx.scene.control.TextField
import javafx.stage.DirectoryChooser
import javafx.util.converter.NumberStringConverter
import tornadofx.View
import tornadofx.select
import java.io.File
import java.io.IOException
class SettingsUpdateView: View() {
override val root: Parent by fxml()
private val nameComboBox: ComboBox<UpdateServer> by fxid()
private val addressTextField: TextField by fxid()
private val portTextField: TextField by fxid()
private val basePathTextField: TextField by fxid()
private val localPathTextField: TextField by fxid()
private val scanButton: Button by fxid()
private val clientOptionsButton: Button by fxid()
private val localPathSelectionButton: Button by fxid()
private val currentDirectory: File
get() {
val server = nameComboBox.value ?: return File(".")
val localPathString = server.localPath
if (localPathString.isEmpty())
return File(".")
val localPath = File(localPathString)
return if (!localPath.isDirectory) File(".") else localPath
}
init {
localPathSelectionButton.graphic = FontAwesomeIcon.FOLDER_ALT.createGlyph()
// TODO: Add/remove login servers
scanButton.onAction = EventHandler { this.processScanButtonAction() }
clientOptionsButton.onAction = EventHandler { this.processClientOptionsButtonAction() }
addressTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.addressProperty })
portTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.portProperty }, NumberStringConverter("#"))
basePathTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.basePathProperty })
localPathTextField.textProperty().bindBidirectional(nameComboBox.valueProperty().select { it.localPathProperty })
localPathSelectionButton.onAction = EventHandler { this.processLocalPathSelectionButtonAction() }
nameComboBox.items = FXCollections.observableArrayList(LauncherData.INSTANCE.update.servers)
nameComboBox.value = nameComboBox.items.getOrNull(0)
}
private fun processScanButtonAction() {
val server = nameComboBox.value ?: return
RequestScanIntent(server).broadcast()
}
private fun processClientOptionsButtonAction() {
val server = nameComboBox.value ?: return
ProcessExecutor.INSTANCE.buildProcess(server, "SwgClientSetup_r.exe")
}
private fun processLocalPathSelectionButtonAction() {
val selection = chooseOpenDirectory("Choose Local Installation Path", currentDirectory) ?: return
try {
localPathTextField.text = selection.canonicalPath
} catch (ex: IOException) {
localPathTextField.text = selection.absolutePath
}
val server = nameComboBox.value ?: return
RequestScanIntent(server).broadcast()
}
private fun chooseOpenDirectory(title: String, currentDirectory: File): File? {
val directoryChooser = DirectoryChooser()
directoryChooser.title = title
directoryChooser.initialDirectory = currentDirectory
val file = directoryChooser.showDialog(LauncherData.INSTANCE.stage)
return if (file == null || !file.isDirectory) null else file
}
}

View File

@@ -1,48 +0,0 @@
package com.projectswg.launcher.core.resources.gui.style
import javafx.geometry.Pos
import javafx.scene.control.ScrollPane
import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import tornadofx.*
class CardStyle : Stylesheet() {
companion object {
val cardContainer by cssclass()
val card by cssclass()
val cardContent by cssclass()
val cardImage by cssclass()
val cardTitle by cssclass()
val cardDescription by cssclass()
}
init {
cardContainer {
hgap = 10.px
vgap = 10.px
}
card {
backgroundColor += c("#454545")
vBarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED
hBarPolicy = ScrollPane.ScrollBarPolicy.NEVER
}
cardContent {
vgap = 5.px
alignment = Pos.CENTER
}
cardImage {
alignment = Pos.CENTER
}
cardTitle {
fontWeight = FontWeight.BOLD
textFill = Color.WHITE
wrapText = true
fontSize = 14.px
backgroundColor += c("#313131")
padding = box(5.px)
}
cardDescription {
fontSize = 12.px
}
}
}

View File

@@ -1,74 +0,0 @@
/***********************************************************************************
* Copyright (C) 2020 /// 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.style
import javafx.scene.paint.Color
import tornadofx.*
class Style : Stylesheet() {
companion object {
val statusNormal by cssclass()
val statusFail by cssclass()
val statusInProgress by cssclass()
val statusGood by cssclass()
val background by cssclass()
}
init {
statusNormal {
s(text) {
fill = Color.WHITE
}
}
statusFail {
s(text) {
fill = c("#FF0000")
}
}
statusInProgress {
s(text) {
fill = Color.YELLOW
}
}
statusGood {
s(text) {
fill = c("#00FF00")
}
}
background {
cell {
and(even) {
backgroundColor += c("#4e4e4e")
and(hover) {
backgroundColor += c("#404040")
}
}
and(odd) {
backgroundColor += c("#484848")
and(hover) {
backgroundColor += c("#404040")
}
}
}
}
}
}

View File

@@ -1,134 +0,0 @@
/***********************************************************************************
* Copyright (C) 2018 /// Project SWG /// www.projectswg.com *
* *
* This file is part of the ProjectSWG Launcher. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program. If not, see <https://www.gnu.org/licenses/>. *
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.pipeline;
import me.joshlarson.jlcommon.log.Log;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
import java.util.function.Predicate;
public class Pipeline {
public static <T> PipelineCompiler<T> compile(String name) {
return new PipelineCompiler<>(name);
}
public static <T> PipelineExecutor<T> execute(String name) {
return new PipelineExecutor<>(name);
}
public static class PipelineCompiler<T> {
private final List<Predicate<T>> stages;
private final String name;
public PipelineCompiler(String name) {
this.stages = new CopyOnWriteArrayList<>();
this.name = name;
}
public PipelineCompiler<T> next(Predicate<T> stage) {
stages.add(stage);
return this;
}
public PipelineCompiler<T> next(Consumer<T> stage) {
return next(in -> {stage.accept(in); return true; });
}
public PipelineCompiler<T> next(Runnable stage) {
return next(in -> { stage.run(); return true; });
}
public void execute(T input) {
int stageIndex = 0;
try {
for (Predicate<T> stage : stages) {
if (!stage.test(input))
return;
stageIndex++;
}
} catch (Throwable t) {
Log.e("Pipeline '%s' failed during stage index %d!", name, stageIndex);
Log.e(t);
}
}
}
public static class PipelineExecutor<T> {
private final String name;
private boolean terminated;
public PipelineExecutor() {
this("");
}
public PipelineExecutor(String name) {
this.name = name;
this.terminated = false;
}
public PipelineExecutor<T> execute(Predicate<T> stage, T t) {
if (terminated)
return this;
try {
stage.test(t);
} catch (Throwable ex) {
Log.e("Pipeline '%s' failed!", name);
Log.e(ex);
terminated = true;
}
return this;
}
public PipelineExecutor<T> execute(Consumer<T> stage, T t) {
if (terminated)
return this;
try {
stage.accept(t);
} catch (Throwable ex) {
Log.e("Pipeline '%s' failed!", name);
Log.e(ex);
terminated = true;
}
return this;
}
public PipelineExecutor<T> execute(Runnable stage) {
if (terminated)
return this;
try {
stage.run();
} catch (Throwable t) {
Log.e("Pipeline '%s' failed!", name);
Log.e(t);
terminated = true;
}
return this;
}
}
}

View File

@@ -1,187 +0,0 @@
/***********************************************************************************
* 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 javafx.application.Platform;
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.MalformedURLException;
import java.net.URL;
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");
List<Object> 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 (%s: %s)", info.getName(), e.getClass().getName(), e.getMessage());
}
} 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();
Platform.runLater(() -> 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);
Platform.runLater(() -> 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();
return localFile.isFile() && length == file.getLength();
}
private static RequiredFile jsonObjectToRequiredFile(UpdateServerDownloaderInfo info, JSONObject obj) {
String path = obj.getString("path");
try {
return new RequiredFile(new File(info.getLocalPath(), path), createURL(info, path), obj.getLong("length"), obj.getLong("xxhash"));
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
private static URL createURL(UpdateServerDownloaderInfo info, String path) throws MalformedURLException {
String basePath = info.getBasePath();
while (basePath.endsWith("/"))
basePath = basePath.substring(0, basePath.length()-1);
if (!path.startsWith("/"))
path = "/" + path;
basePath += path;
return new URL("http", info.getAddress(), info.getPort(), basePath);
}
private static class UpdateServerDownloaderInfo {
private final UpdateServer server;
private final String updateServerName;
private final String updateServerAddress;
private final int updateServerPort;
private final String updateServerBasePath;
private final File updateServerLocalPath;
private List<RequiredFile> files;
public UpdateServerDownloaderInfo(UpdateServer server) {
this.server = server;
this.updateServerName = server.getName();
this.updateServerAddress = server.getAddress();
this.updateServerPort = server.getPort();
this.updateServerBasePath = server.getBasePath();
this.updateServerLocalPath = new File(server.getLocalPath());
this.files = null;
}
public UpdateServer getServer() {
return server;
}
public List<RequiredFile> getFiles() {
return files;
}
public String getName() {
return updateServerName;
}
public String getAddress() {
return updateServerAddress;
}
public int getPort() {
return updateServerPort;
}
public String getBasePath() {
return updateServerBasePath;
}
public File getLocalPath() {
return updateServerLocalPath;
}
public void setFiles(List<RequiredFile> files) {
this.files = files;
}
}
}

View File

@@ -1,221 +0,0 @@
/***********************************************************************************
* 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.data.announcements.CardData;
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.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() {
try (JSONInputStream jsonInputStream = new JSONInputStream(new FileInputStream(new File(LocalUtilities.getApplicationDirectory(), "announcements.json")))) {
Map<String, Object> announcements = jsonInputStream.readObject();
update(announcements);
} catch (IOException | JSONException e) {
Log.w("Failed to load announcements from disk");
}
executor.start();
executor.executeWithFixedDelay(3000, TimeUnit.MINUTES.toMillis(30), this::update);
return true;
}
@Override
public boolean stop() {
executor.stop();
executor.awaitTermination(1000);
return true;
}
private void update() {
Map<String, Object> announcements = updateAnnouncements();
if (announcements == null)
return;
update(announcements);
}
private void update(Map<String, Object> announcements) {
List<CardData> announcementCards = parseCards(announcements.get("announcements")).stream().map(this::downloadImage).collect(Collectors.toList());
List<CardData> serverCards = parseCards(announcements.get("servers")).stream().map(this::downloadImage).collect(Collectors.toList());
Platform.runLater(() -> {
AnnouncementsData data = LauncherData.INSTANCE.getAnnouncements();
data.getAnnouncementCards().clear();
data.getAnnouncementCards().addAll(announcementCards);
data.getServerListCards().clear();
data.getServerListCards().addAll(serverCards);
});
}
private List<CardData> parseCards(Object descriptor) {
if (!(descriptor instanceof List))
return Collections.emptyList();
return ((List<?>) 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");
title = title == null ? "" : parseVariables(title);
description = description == null ? "" : 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 {
new URL(url).openStream().transferTo(new FileOutputStream(destination));
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) {
Map<String, Object> filter = obj.getObject("filter");
if (filter == null)
return true;
String os = (String) filter.get("os");
if (os != null) {
String currentOs = System.getProperty("os.name").toLowerCase(Locale.US);
switch (os.toLowerCase(Locale.US)) {
case "windows":
if (!currentOs.contains("win"))
return false;
break;
case "mac":
if (!currentOs.contains("mac"))
return false;
break;
case "linux":
if (currentOs.contains("win") || currentOs.contains("mac"))
return false;
break;
}
}
// Inclusive
return passesVersionCheck((String) filter.get("minLauncherVersion"), (cur, b) -> cur >= b, true) && passesVersionCheck((String) filter.get("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 Map<String, Object> updateAnnouncements() {
File localFileList = new File(LocalUtilities.getApplicationDirectory(), "announcements.json");
Log.t("Retrieving latest announcements...");
Map<String, Object> announcements;
try (JSONInputStream in = new JSONInputStream(new URL("http", LauncherData.UPDATE_ADDRESS, 80, "/launcher/announcements.json").openStream())) {
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;
}
}

View File

@@ -1,283 +0,0 @@
/***********************************************************************************
* 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.LauncherData;
import com.projectswg.launcher.core.resources.data.forwarder.ForwarderData;
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 me.joshlarson.jlcommon.log.Log;
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 Preferences preferences;
private final ScheduledThreadPool executor;
public PreferencesDataService() {
this.preferences = LauncherData.INSTANCE.getPreferences();
this.executor = new ScheduledThreadPool(1, 3, "data-executor-%d");
loadPreferences();
}
@Override
public boolean start() {
createDefaults();
executor.start();
executor.executeWithFixedDelay(5*60000, 5*60000, this::savePreferences);
return true;
}
@Override
public boolean stop() {
savePreferences();
executor.stop();
return executor.awaitTermination(1000);
}
private void createDefaults() {
createPSWG();
createTeamSWG();
if (LauncherData.INSTANCE.getGeneral().getWine() == null || LauncherData.INSTANCE.getGeneral().getWine().isEmpty())
LauncherData.INSTANCE.getGeneral().setWine(getWinePath());
}
private void createPSWG() {
UpdateServer pswgUpdateServer = LauncherData.INSTANCE.getUpdate().getServers().stream().filter(s -> s.getName().equals("ProjectSWG")).findFirst().orElse(null);
if (pswgUpdateServer == null) {
pswgUpdateServer = new UpdateServer("ProjectSWG");
pswgUpdateServer.setAddress("login1.projectswg.com");
pswgUpdateServer.setPort(80);
pswgUpdateServer.setBasePath("/launcher/patch");
pswgUpdateServer.setGameVersion("NGE");
LauncherData.INSTANCE.getUpdate().addServer(pswgUpdateServer);
} else if (pswgUpdateServer.getGameVersion().isEmpty()) {
// Migrate existing persisted update servers
pswgUpdateServer.setGameVersion("NGE");
}
if (LauncherData.INSTANCE.getLogin().getServers().stream().noneMatch(s -> s.getName().equals("ProjectSWG"))) {
LoginServer defaultLive = new LoginServer("ProjectSWG");
defaultLive.setAddress("login1.projectswg.com");
defaultLive.setPort(44453);
defaultLive.setUpdateServer(pswgUpdateServer);
LauncherData.INSTANCE.getLogin().addServer(defaultLive);
}
if (LauncherData.INSTANCE.getLogin().getServers().stream().noneMatch(s -> s.getName().equals("localhost"))) {
LoginServer defaultLocalhost = new LoginServer("localhost");
defaultLocalhost.setAddress("localhost");
defaultLocalhost.setPort(44463);
defaultLocalhost.setUpdateServer(pswgUpdateServer);
LauncherData.INSTANCE.getLogin().addServer(defaultLocalhost);
}
}
private void createTeamSWG() {
UpdateServer teamswgUpdateServer = LauncherData.INSTANCE.getUpdate().getServers().stream().filter(s -> s.getName().equals("TeamSWG")).findFirst().orElse(null);
if (teamswgUpdateServer == null) {
teamswgUpdateServer = new UpdateServer("TeamSWG");
teamswgUpdateServer.setAddress("patch.teamswg.com");
teamswgUpdateServer.setPort(80);
teamswgUpdateServer.setBasePath("/launcher/patch");
teamswgUpdateServer.setGameVersion("CU");
LauncherData.INSTANCE.getUpdate().addServer(teamswgUpdateServer);
}
if (LauncherData.INSTANCE.getLogin().getServers().stream().noneMatch(s -> s.getName().equals("Constrictor"))) {
LoginServer constrictor = new LoginServer("Constrictor");
constrictor.setAddress("game.teamswg.com");
constrictor.setPort(44463);
constrictor.setUpdateServer(teamswgUpdateServer);
LauncherData.INSTANCE.getLogin().addServer(constrictor);
}
}
private synchronized void loadPreferences() {
try {
loadGeneralPreferences(LauncherData.INSTANCE.getGeneral());
loadUpdatePreferences(LauncherData.INSTANCE.getUpdate());
loadLoginPreferences(LauncherData.INSTANCE.getLogin());
loadForwarderPreferences(LauncherData.INSTANCE.getForwarderData());
} catch (BackingStoreException e) {
Log.w(e);
}
}
private synchronized void savePreferences() {
try {
saveGeneralPreferences(LauncherData.INSTANCE.getGeneral());
saveUpdatePreferences(LauncherData.INSTANCE.getUpdate());
saveLoginPreferences(LauncherData.INSTANCE.getLogin());
saveForwarderPreferences(LauncherData.INSTANCE.getForwarderData());
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);
ifPresent(generalPreferences, "admin", Boolean::valueOf, generalData::setAdmin);
}
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());
generalPreferences.putBoolean("admin", generalData.isAdmin());
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(LauncherData.INSTANCE.getUpdate().getServers().stream().filter(s -> s.getName().equals(name)).findFirst().orElse(null)));
ifPresent(loginServerPreferences, "isVerifyServer", Boolean::parseBoolean, server::setVerifyServer);
ifPresent(loginServerPreferences, "isEncryptionEnabled", Boolean::parseBoolean, server::setEncryptionEnabled);
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());
loginServerPreferences.putBoolean("isVerifyServer", server.isVerifyServer());
loginServerPreferences.putBoolean("isEncryptionEnabled", server.isEncryptionEnabled());
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);
ifPresent(updateServerPreferences, "gameVersion", server::setGameVersion);
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());
updateServerPreferences.put("gameVersion", server.getGameVersion());
}
}
private void loadForwarderPreferences(ForwarderData forwarderData) {
Preferences forwarderPreferences = preferences.node("forwarder");
int version = forwarderPreferences.getInt("version", 0);
if (version < 1) {
forwarderData.setSendInterval(ForwarderData.DEFAULT_SEND_INTERVAL);
forwarderData.setSendMax(ForwarderData.DEFAULT_SEND_MAX);
} else {
ifPresent(forwarderPreferences, "sendInterval", Integer::valueOf, forwarderData::setSendInterval);
ifPresent(forwarderPreferences, "sendMax", Integer::valueOf, forwarderData::setSendMax);
}
}
private void saveForwarderPreferences(ForwarderData forwarderData) {
Preferences forwarderPreferences = preferences.node("forwarder");
forwarderPreferences.putInt("sendInterval", forwarderData.getSendInterval());
forwarderPreferences.putInt("sendMax", forwarderData.getSendMax());
forwarderPreferences.putInt("version", 1);
}
private static <T> void ifPresent(Preferences p, String key, Function<String, T> transform, Consumer<T> setter) {
String val = p.get(key, null);
if (val != null)
setter.accept(transform.apply(val));
}
private static void ifPresent(Preferences p, String key, Consumer<String> setter) {
String val = p.get(key, null);
if (val != null)
setter.accept(val);
}
private static String getWinePath() {
String pathStr = System.getenv("PATH");
if (pathStr == null)
return null;
for (String path : pathStr.split(File.pathSeparator)) {
Log.t("Testing wine binary at %s", path);
File test = new File(path, "wine");
if (test.isFile()) {
try {
test = test.getCanonicalFile();
Log.d("Found wine installation. Location: %s", test);
return test.getAbsolutePath();
} catch (IOException e) {
Log.w("Failed to get canonical file location of possible wine location: %s", test);
}
}
}
return null;
}
}

View File

@@ -1,156 +0,0 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.core.services.data
import com.projectswg.holocore.client.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 javafx.beans.InvalidationListener
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.javafx.beans.ConcurrentCollection
import me.joshlarson.jlcommon.log.Log
import tornadofx.FX
import tornadofx.get
import java.net.InetAddress
import java.net.UnknownHostException
import java.util.concurrent.TimeUnit
class RemoteDataService : Service() {
private val loginServers: TransferSet<LoginServer, LoginServerUpdater>
private val executor: ScheduledThreadPool
init {
this.loginServers = TransferSet( { it.name }, { LoginServerUpdater(it) })
this.executor = ScheduledThreadPool(2, "remote-data-service")
loginServers.addDestroyCallback { it.terminate() }
loginData.servers.addCollectionChangedListener(LISTENER_KEY, ConcurrentCollection.ComplexCollectionChangedListener<ConcurrentCollection<Set<LoginServer>, LoginServer>> { loginServers.synchronize(it) })
}
override fun initialize(): Boolean {
loginServers.synchronize(loginData.servers)
return true
}
override fun start(): Boolean {
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 fun stop(): Boolean {
executor.stop()
return executor.awaitTermination(1000)
}
override fun terminate(): Boolean {
loginServers.synchronize(emptyList())
return true
}
@IntentHandler
private fun handleRequestScanIntent(rsi: RequestScanIntent) {
UpdateServerUpdater.update(rsi.server)
}
private fun updateLoginServers() {
// Allows for parallel networking operations
loginServers.parallelStream().forEach { it.update() }
}
private fun updateUpdateServers() {
updateData.servers.parallelStream().forEach { UpdateServerUpdater.update(it) }
}
private class LoginServerUpdater(private val server: LoginServer) {
private var socket: HolocoreSocket? = null
init {
this.socket = null
server.addressProperty.addListener(InvalidationListener { updateSocket() })
server.portProperty.addListener(InvalidationListener { updateSocket() })
updateSocket()
}
fun terminate() {
socket?.close()
}
fun update() {
// Better luck next time
val socket = this.socket
if (socket == null) {
server.instanceInfo.loginStatus = ""
return
}
if (server.instanceInfo.loginStatus.isNullOrBlank())
server.instanceInfo.loginStatus = FX.messages["servers.loginStatus.checking"]
for (i in 0..4) {
val status = socket.getServerStatus(1000)
if (status != "OFFLINE") {
server.instanceInfo.loginStatus = status
return
}
}
server.instanceInfo.loginStatus = "OFFLINE"
}
private fun updateSocket() {
try {
val addr = server.address.trim { it <= ' ' }
val port = server.port
if (addr.isEmpty() || port <= 0)
return
this.socket = HolocoreSocket(InetAddress.getByName(addr), port)
} catch (e: UnknownHostException) {
this.socket = null
Log.w(e)
}
}
}
companion object {
private const val LISTENER_KEY = "RDS"
private val loginData: LoginData
get() = LauncherData.INSTANCE.login
private val updateData: UpdateData
get() = LauncherData.INSTANCE.update
}
}

View File

@@ -0,0 +1,67 @@
/***********************************************************************************
* Copyright (C) 2018 /// Project SWG /// www.projectswg.com *
* *
* This file is part of the ProjectSWG Launcher. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.data
import com.projectswg.launcher.resources.data.forwarder.ForwarderData
import com.projectswg.launcher.resources.data.general.GeneralData
import com.projectswg.launcher.resources.data.login.LoginData
import com.projectswg.launcher.resources.data.update.UpdateData
import javafx.application.Application
import javafx.stage.Stage
import tornadofx.FX
import tornadofx.FX.Companion.primaryStage
import java.io.File
import java.util.*
enum class LauncherData {
INSTANCE;
val general = GeneralData()
val login = LoginData()
val update = UpdateData()
val forwarder = ForwarderData()
val stage: Stage
get() = primaryStage
val application: Application
get() = FX.application
companion object {
const val VERSION = "2.0.0"
fun getApplicationDataDirectory(): File {
return when (getOS()) {
"windows" -> File(System.getenv("APPDATA"), "ProjectSWG")
"mac" -> File("${System.getProperty("user.home")}/Library/Application Support/ProjectSWG")
else -> File("${System.getProperty("user.home")}/.config/ProjectSWG")
}
}
private fun getOS(): String {
val currentOs = System.getProperty("os.name").lowercase(Locale.US)
return when {
currentOs.contains("win") -> "windows"
currentOs.contains("mac") -> "mac"
else -> "linux"
}
}
}
}

View File

@@ -18,7 +18,7 @@
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.data.announcements
package com.projectswg.launcher.resources.data.announcements
import com.rometools.rome.feed.synd.SyndFeed
import com.rometools.rome.io.SyndFeedInput
@@ -43,12 +43,12 @@ class WebsitePostFeed(feed: SyndFeed) {
date = it.publishedDate,
link = it.link,
description = StringEscapeUtils.unescapeXml(it.description.value),
image = when(it.categories.getOrNull(0)?.name?.toLowerCase(Locale.US)) {
image = when(it.categories.getOrNull(0)?.name?.lowercase(Locale.US)) {
"development update", "quality assurance" -> WebsitePostMessageImage.DEVELOPMENT
"community update" -> WebsitePostMessageImage.COMMUNITY
"holocore update" -> WebsitePostMessageImage.HOLOCORE
else -> {
Log.w("Unknown RSS category type: '%s'", it.categories.getOrNull(0)?.name?.toLowerCase(Locale.US))
Log.w("Unknown RSS category type: '%s'", it.categories.getOrNull(0)?.name?.lowercase(Locale.US))
WebsitePostMessageImage.OPERATIONS
}
}

View File

@@ -1,4 +1,4 @@
package com.projectswg.launcher.core.resources.data.announcements
package com.projectswg.launcher.resources.data.announcements
import java.util.Date

View File

@@ -18,7 +18,7 @@
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.data.announcements
package com.projectswg.launcher.resources.data.announcements
enum class WebsitePostMessageImage(val descriptionStr: String, val imagePath: String) {
COMMUNITY("Community Update", "/graphics/pswg_icon_big.png"),

View File

@@ -18,7 +18,7 @@
* *
*/
package com.projectswg.launcher.core.resources.data.forwarder
package com.projectswg.launcher.resources.data.forwarder
import javafx.beans.property.SimpleIntegerProperty
import tornadofx.getValue

View File

@@ -17,23 +17,25 @@
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.data.general
package com.projectswg.launcher.core.resources.data.announcements
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import java.util.*
import tornadofx.getValue
import tornadofx.setValue
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import me.joshlarson.jlcommon.javafx.beans.ConcurrentList
class AnnouncementsData {
class GeneralData {
val announcementCards = ConcurrentList<CardData>()
val announcementCardsProperty: ObservableList<CardData> = FXCollections.observableArrayList()
val serverListCards = ConcurrentList<CardData>()
val serverListCardsProperty: ObservableList<CardData> = FXCollections.observableArrayList()
val localeProperty = SimpleObjectProperty(Locale.getDefault())
val wineProperty = SimpleStringProperty()
val adminProperty = SimpleBooleanProperty(false)
val remoteVersionProperty = SimpleStringProperty("")
init {
announcementCards.addCollectionChangedListener("", Runnable { announcementCardsProperty.setAll(announcementCards) })
serverListCards.addCollectionChangedListener("", Runnable { serverListCardsProperty.setAll(serverListCards) })
}
var locale: Locale by localeProperty
var wine: String? by wineProperty
var isAdmin: Boolean by adminProperty
var remoteVersion: String by remoteVersionProperty
}
}

View File

@@ -0,0 +1,17 @@
package com.projectswg.launcher.resources.data.login
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
class AuthenticationData(val name: String) {
val nameProperty = ReadOnlyStringWrapper(name)
val usernameProperty = SimpleStringProperty("")
val passwordProperty = SimpleStringProperty("")
var username: String by usernameProperty
var password: String by passwordProperty
}

View File

@@ -0,0 +1,75 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.data.login
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import me.joshlarson.jlcommon.javafx.beans.ConcurrentSet
import tornadofx.getValue
import tornadofx.select
import tornadofx.setValue
import java.util.concurrent.CopyOnWriteArraySet
class LoginData {
val authenticationData = ConcurrentSet<AuthenticationData>(CopyOnWriteArraySet())
val authenticationProperty: ObservableList<AuthenticationData> = FXCollections.observableArrayList()
val servers = ConcurrentSet<LoginServer>(CopyOnWriteArraySet())
val serversProperty: ObservableList<LoginServer> = FXCollections.observableArrayList()
val activeServerProperty = SimpleObjectProperty<LoginServer>()
val lastSelectedServerProperty = SimpleStringProperty()
val localServerProperty = SimpleObjectProperty<LoginServer>()
var activeServer: LoginServer by activeServerProperty
val lastSelectedServer: String by lastSelectedServerProperty
var localServer: LoginServer by localServerProperty
init {
servers.addCollectionChangedListener("", Runnable { serversProperty.setAll(servers) })
authenticationData.addCollectionChangedListener("", Runnable { authenticationProperty.setAll(authenticationData) })
lastSelectedServerProperty.bind(activeServerProperty.select { it.nameProperty })
}
fun getAuthenticationData(authentication_name: String): AuthenticationData? {
for (authentication in authenticationData) {
if (authentication.name == authentication_name)
return authentication
}
return null
}
fun getServerByName(serverName: String): LoginServer? {
for (server in servers) {
if (server.name == serverName)
return server
}
return null
}
}

View File

@@ -18,10 +18,10 @@
* *
*/
package com.projectswg.launcher.core.resources.data.login
package com.projectswg.launcher.resources.data.login
import com.projectswg.launcher.core.resources.data.update.UpdateServer
import com.projectswg.launcher.core.resources.data.update.UpdateServer.UpdateServerStatus
import com.projectswg.launcher.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.data.update.UpdateServer.UpdateServerStatus
import javafx.beans.property.*
import javafx.beans.value.ObservableValue
import tornadofx.getValue
@@ -30,24 +30,16 @@ import tornadofx.setValue
class LoginServer(val name: String) {
val nameProperty = ReadOnlyStringWrapper(name)
val addressProperty = SimpleStringProperty("")
val portProperty = SimpleIntegerProperty(0)
val usernameProperty = SimpleStringProperty("")
val passwordProperty = SimpleStringProperty("")
val connectionUriProperty = SimpleStringProperty("")
val authenticationProperty = SimpleObjectProperty<AuthenticationData>(null)
val updateServerProperty = SimpleObjectProperty<UpdateServer>(null)
val verifyServerProperty = SimpleBooleanProperty(true)
val enableEncryptionProperty = SimpleBooleanProperty(true)
val instanceInfo = LoginServerInstanceInfo()
private val updateServerStatusCallback= { _: ObservableValue<*>, _: UpdateServerStatus, status: UpdateServerStatus -> onUpdateServerStatusUpdated(status) }
var address: String by addressProperty
var port: Int by portProperty
var username: String by usernameProperty
var password: String by passwordProperty
var connectionUri: String by connectionUriProperty
var authentication: AuthenticationData by authenticationProperty
var updateServer: UpdateServer? by updateServerProperty
var isVerifyServer: Boolean by verifyServerProperty
var isEncryptionEnabled: Boolean by enableEncryptionProperty
init {
updateServerProperty.addListener { _, oldValue, newValue -> updateServerListener(oldValue, newValue) }

View File

@@ -1,38 +1,37 @@
/***********************************************************************************
* 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/>. *
* *
***********************************************************************************/
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.data.login
package com.projectswg.launcher.core.services.data;
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
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 {
class LoginServerInstanceInfo {
public DataManager() {
}
val loginStatusProperty = SimpleStringProperty("")
val updateStatusProperty = SimpleStringProperty("")
val readyToPlayProperty = SimpleBooleanProperty(false)
}
var loginStatus: String by loginStatusProperty
var updateStatus: String by updateStatusProperty
var isReadyToPlay: Boolean by readyToPlayProperty
}

View File

@@ -17,30 +17,35 @@
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.data.update
package com.projectswg.launcher.core.resources.data.login
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
import javafx.collections.ObservableList
import me.joshlarson.jlcommon.javafx.beans.ConcurrentSet
import tornadofx.getValue
import tornadofx.setValue
import java.util.concurrent.CopyOnWriteArraySet
class LoginData {
class UpdateData {
val servers = ConcurrentSet<LoginServer>(CopyOnWriteArraySet())
val serversProperty: ObservableList<LoginServer> = FXCollections.observableArrayList()
val servers: ConcurrentSet<UpdateServer> = ConcurrentSet(CopyOnWriteArraySet())
val serversProperty: ObservableList<UpdateServer> = FXCollections.observableArrayList()
val localPathProperty = SimpleStringProperty("")
var localPath: String by localPathProperty
init {
servers.addCollectionChangedListener("", Runnable { serversProperty.setAll(servers) })
}
fun addServer(server: LoginServer) {
servers.add(server)
fun getServer(server_name: String): UpdateServer? {
for (server in servers) {
if (server.name == server_name)
return server
}
return null
}
fun removeServer(server: LoginServer) {
servers.remove(server)
}
}
}

View File

@@ -18,10 +18,10 @@
* *
*/
package com.projectswg.launcher.core.resources.data.update
package com.projectswg.launcher.resources.data.update
import com.projectswg.launcher.resources.data.LauncherData
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty
import javafx.collections.FXCollections
@@ -33,19 +33,20 @@ import java.net.URL
class UpdateServer(val name: String) {
val addressProperty = SimpleStringProperty("")
val portProperty = SimpleIntegerProperty(0)
val basePathProperty = SimpleStringProperty("")
val localPathProperty = SimpleStringProperty("")
val friendlyNameProperty = SimpleStringProperty("")
val urlProperty = SimpleStringProperty("")
val statusProperty = SimpleObjectProperty(UpdateServerStatus.UNKNOWN)
val gameVersionProperty = SimpleStringProperty("")
val requiredFiles: ObservableList<RequiredFile> = FXCollections.observableArrayList()
val downloadProgressProperty = SimpleDoubleProperty(-1.0)
var address: String by addressProperty
var port by portProperty
var basePath: String by basePathProperty
var localPath: String by localPathProperty
val localPath: File
get() {
return File(LauncherData.INSTANCE.update.localPath, gameVersion)
}
var friendlyName: String by friendlyNameProperty
var url: String by urlProperty
var status: UpdateServerStatus by statusProperty
var gameVersion: String by gameVersionProperty
var downloadProgress by downloadProgressProperty
@@ -54,7 +55,7 @@ class UpdateServer(val name: String) {
return name
}
class RequiredFile(val localPath: File, val remotePath: URL, val length: Long, val hash: Long)
class RequiredFile(val localPath: File, val remotePath: URL, val length: Long, val hash: String)
enum class UpdateServerStatus constructor(val friendlyName: String) {
UNKNOWN("servers.status.unknown"),

View File

@@ -18,12 +18,11 @@
* *
*/
package com.projectswg.launcher.core.resources.game
package com.projectswg.launcher.resources.game
import com.projectswg.forwarder.Forwarder
import com.projectswg.forwarder.Forwarder.ForwarderData
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.data.login.LoginServer
import com.projectswg.launcher.resources.data.login.LoginServer
import javafx.application.Platform
import javafx.scene.control.Alert
import javafx.scene.control.Alert.AlertType
@@ -31,7 +30,6 @@ import me.joshlarson.jlcommon.concurrency.BasicThread
import me.joshlarson.jlcommon.concurrency.Delay
import me.joshlarson.jlcommon.log.Log
import java.io.File
import java.net.InetSocketAddress
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicLong
@@ -44,19 +42,15 @@ class GameInstance(private val server: LoginServer) {
init {
val gameId = GAME_ID.incrementAndGet()
this.processThread = BasicThread("game-process-$gameId", Runnable { this.runProcess() })
this.forwarderThread = BasicThread("game-forwarder-$gameId", Runnable { this.runForwarder() })
this.processThread = BasicThread("game-process-$gameId") { this.runProcess() }
this.forwarderThread = BasicThread("game-forwarder-$gameId") { this.runForwarder() }
}
fun start() {
val data = forwarder.data
data.address = InetSocketAddress(server.address, server.port)
data.isVerifyServer = server.isVerifyServer
data.isEncryptionEnabled = server.isEncryptionEnabled
data.username = server.username
data.password = server.password
data.outboundTunerInterval = LauncherData.INSTANCE.forwarderData.sendInterval
data.outboundTunerMaxSend = LauncherData.INSTANCE.forwarderData.sendMax
data.baseConnectionUri = server.connectionUri
data.outboundTunerInterval = com.projectswg.launcher.resources.data.LauncherData.INSTANCE.forwarder.sendInterval
data.outboundTunerMaxSend = com.projectswg.launcher.resources.data.LauncherData.INSTANCE.forwarder.sendMax
forwarderThread.start()
}
@@ -101,7 +95,7 @@ class GameInstance(private val server: LoginServer) {
private fun onCrash(crashLog: File) {
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")
reportCrash("A crash was detected. Please report this to the ProjectSWG team with this zip file: $crashLog")
}
companion object {
@@ -131,7 +125,7 @@ class GameInstance(private val server: LoginServer) {
reportError("Process", "No update server defined")
return null
}
val swgDirectory = File(updateServer.localPath)
val swgDirectory = updateServer.localPath
if (!swgDirectory.isDirectory) {
Log.e("Failed to launch game. Invalid SWG directory: %s", swgDirectory)
reportError("Process", "Invalid SWG directory: $swgDirectory")
@@ -152,7 +146,7 @@ class GameInstance(private val server: LoginServer) {
"autoConnectToLoginServer=" + username.isNotEmpty(),
"logReportFatals=true",
"logStderr=true",
"0fd345d9=" + if (LauncherData.INSTANCE.general.isAdmin) "true" else "false",
"0fd345d9=" + if (com.projectswg.launcher.resources.data.LauncherData.INSTANCE.general.isAdmin) "true" else "false",
"-s",
"SharedNetwork",
"useTcp=false",
@@ -165,11 +159,11 @@ class GameInstance(private val server: LoginServer) {
"networkHandlerDispatchQueueSize=2000")
}
private fun reportWarning(title: String, message: String) {
private fun reportCrash(message: String) {
Platform.runLater {
val alert = Alert(AlertType.WARNING)
alert.title = "Game Launch Warning"
alert.headerText = title
alert.headerText = "Crash Detected"
alert.contentText = message
alert.showAndWait()
}

View File

@@ -18,10 +18,10 @@
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.game;
package com.projectswg.launcher.resources.game;
import com.projectswg.launcher.core.resources.data.LauncherData;
import com.projectswg.launcher.core.resources.data.update.UpdateServer;
import com.projectswg.launcher.resources.data.LauncherData;
import com.projectswg.launcher.resources.data.update.UpdateServer;
import javafx.application.Platform;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
@@ -40,7 +40,7 @@ public enum ProcessExecutor {
public Process buildProcess(@NotNull UpdateServer server, String executable, String ... args) {
File swgDirectory;
{
swgDirectory = new File(server.getLocalPath());
swgDirectory = server.getLocalPath();
if (!swgDirectory.isDirectory()) {
Log.e("Failed to launch. Invalid SWG directory: %s", swgDirectory);
reportError("Process", "Invalid SWG directory: " + swgDirectory);
@@ -120,7 +120,7 @@ public enum ProcessExecutor {
return new File(wineStr);
} else {
Log.e("Invalid wine file: " + wineStr);
reportWarning("Wine Initialization", "Invalid wine setting. Searching for valid path...");
reportInvalidWinePath();
}
}
}
@@ -145,12 +145,12 @@ public enum ProcessExecutor {
return null;
}
private void reportWarning(String title, String message) {
private void reportInvalidWinePath() {
Platform.runLater(() -> {
Alert alert = new Alert(AlertType.WARNING);
alert.setTitle("Game Launch Warning");
alert.setHeaderText(title);
alert.setContentText(message);
alert.setHeaderText("Wine Initialization");
alert.setContentText("Invalid wine setting. Searching for valid path...");
alert.showAndWait();
});
}

View File

@@ -1,4 +1,4 @@
package com.projectswg.launcher.core.resources.gui
package com.projectswg.launcher.resources.gui
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIconView

View File

@@ -18,27 +18,24 @@
* *
*/
package com.projectswg.launcher.core.resources.gui
package com.projectswg.launcher.resources.gui
import com.projectswg.launcher.resources.gui.style.Style
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.beans.property.Property
import javafx.beans.property.SimpleStringProperty
import javafx.geometry.Side
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import javafx.scene.text.Font
import tornadofx.*
class NavigationView : View("ProjectSWG Launcher") {
override val root = anchorpane {
stylesheets += "/css/theme.css"
addClass(Style.background)
var selectedTab: Property<String> = SimpleStringProperty("")
tabpane {
prefWidth = 825.0
prefHeight = 640.0
prefHeight = 400.0
vgrow = Priority.ALWAYS
side = Side.LEFT
@@ -47,10 +44,7 @@ class NavigationView : View("ProjectSWG Launcher") {
AnchorPane.setBottomAnchor(this, 0.0)
AnchorPane.setLeftAnchor(this, 0.0)
selectedTab = selectionModel.selectedItemProperty().select { it.textProperty() }
tab(messages["servers"]) {
styleClass += "background"
isClosable = false
graphic = FontAwesomeIcon.SERVER.createGlyph(glyphSize = 24, fill = Color.LIGHTGRAY)
graphic.tooltip(messages["servers"])
@@ -59,7 +53,6 @@ class NavigationView : View("ProjectSWG Launcher") {
selectionModel.select(this)
}
tab(messages["settings"]) {
styleClass += "background"
isClosable = false
graphic = FontAwesomeIcon.SLIDERS.createGlyph(glyphSize = 24, fill = Color.LIGHTGRAY)
graphic.tooltip(messages["settings"])
@@ -67,16 +60,6 @@ class NavigationView : View("ProjectSWG Launcher") {
this += find<SettingsView>().root
}
}
group {
AnchorPane.setBottomAnchor(this, 15.0)
AnchorPane.setLeftAnchor(this, 5.0)
label(selectedTab) {
rotate = -90.0
font = Font(28.0)
}
}
}
}

View File

@@ -0,0 +1,238 @@
/***********************************************************************************
* Copyright (C) 2020 /// 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.resources.gui
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.game.GameInstance
import com.projectswg.launcher.resources.game.ProcessExecutor
import com.projectswg.launcher.resources.gui.events.LauncherNewVersionEvent
import com.projectswg.launcher.resources.gui.servers.LauncherUpdatePopup
import com.projectswg.launcher.resources.gui.servers.WebsitePostFeedList
import com.projectswg.launcher.resources.gui.style.Style
import com.projectswg.launcher.resources.intents.DownloadPatchIntent
import com.projectswg.launcher.resources.intents.GameLaunchedIntent
import com.projectswg.launcher.resources.intents.RequestScanIntent
import javafx.beans.property.ReadOnlyBooleanWrapper
import javafx.beans.property.ReadOnlyDoubleWrapper
import javafx.beans.property.ReadOnlyObjectWrapper
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.geometry.Pos
import javafx.scene.layout.Priority
import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import javafx.stage.StageStyle
import tornadofx.*
class ServerListView : View() {
private val feedList: WebsitePostFeedList by inject()
override val root = vbox {
subscribe<LauncherNewVersionEvent>(times = 1) {
find<LauncherUpdatePopup>().openModal(stageStyle = StageStyle.UNDECORATED)
}
addClass(Style.serverList)
hbox {
isFillWidth = true
vbox leftBox@ {
prefWidthProperty().bind(this@hbox.widthProperty().subtract(5).divide(2))
hbox {
label(messages["servers.login.feed.title"]) {
addClass(Style.settingsHeaderLabel)
style {
padding = box(20.px)
}
}
style {
backgroundColor += Style.backgroundColorSecondary
}
}
this += feedList.root
}
region {
prefWidth = 5.0
}
vbox rightBox@ {
prefWidthProperty().bind(this@hbox.widthProperty().subtract(5).divide(2))
hbox {
label(messages["servers.login.form.title"]) {
addClass(Style.settingsHeaderLabel)
style {
padding = box(20.px)
}
}
style {
backgroundColor += Style.backgroundColorSecondary
}
}
// Login container
form {
fieldset {
field("Server:") {
combobox(LauncherData.INSTANCE.login.activeServerProperty) {
items = LauncherData.INSTANCE.login.serversProperty
valueProperty().bindBidirectional(LauncherData.INSTANCE.login.activeServerProperty)
prefWidth = 300.0
}
}
field(messages["servers.login.form.username"]) {
textfield(LauncherData.INSTANCE.login.activeServerProperty.select { it.authenticationProperty }.select { it.usernameProperty })
}
field(messages["servers.login.form.password"]) {
passwordfield(LauncherData.INSTANCE.login.activeServerProperty.select { it.authenticationProperty }.select { it.passwordProperty })
}
field(messages["servers.column.localStatus"]) {
val updateStatus = LauncherData.INSTANCE.login.activeServerProperty.select { it.updateServerProperty }.select { it.statusProperty }
label(observable=LauncherData.INSTANCE.login.activeServerProperty.select { it.instanceInfo.updateStatusProperty }.select { ReadOnlyStringWrapper(messages[it]) }) {
maxWidth = Double.POSITIVE_INFINITY
isFillWidth = true
textFillProperty().bind(updateStatus.select { when (it) {
UpdateServer.UpdateServerStatus.READY -> ReadOnlyObjectWrapper(Color.rgb(0, 255, 0))
UpdateServer.UpdateServerStatus.REQUIRES_DOWNLOAD -> ReadOnlyObjectWrapper(Color.RED)
else -> ReadOnlyObjectWrapper(Color.WHITE)
} })
style {
fontWeight = FontWeight.BOLD
}
}
button("Patch") {
val targetWidthProperty = updateStatus.select { ReadOnlyDoubleWrapper(if (it == UpdateServer.UpdateServerStatus.REQUIRES_DOWNLOAD) 75.0 else 0.0) }
visibleWhen { updateStatus.select { ReadOnlyBooleanWrapper(it == UpdateServer.UpdateServerStatus.REQUIRES_DOWNLOAD) } }
minWidthProperty().bind(targetWidthProperty)
maxWidthProperty().bind(targetWidthProperty)
setOnAction {
DownloadPatchIntent(LauncherData.INSTANCE.login.activeServer.updateServer ?: return@setOnAction).broadcast()
}
}
button("Scan") {
minWidth = 75.0
setOnAction {
RequestScanIntent(LauncherData.INSTANCE.login.activeServer.updateServer ?: return@setOnAction).broadcast()
}
}
}
}
button("Play") { // TODO: Get proper string for this
isFillWidth = true
maxWidth = Double.POSITIVE_INFINITY
setOnAction {
val gameInstance = GameInstance(LauncherData.INSTANCE.login.activeServer)
gameInstance.forwarder.data.username = LauncherData.INSTANCE.login.activeServer.authentication.username
gameInstance.forwarder.data.password = LauncherData.INSTANCE.login.activeServer.authentication.password
gameInstance.start()
GameLaunchedIntent(gameInstance).broadcast()
}
style {
fontSize = 15.px
fontWeight = FontWeight.BOLD
backgroundColor += Style.playButtonColor
textFill = Style.playButtonTextColor
}
}
}
region { minHeight = 20.0; maxHeight = 20.0 }
separator()
region { minHeight = 5.0; maxHeight = 5.0 }
gridpane {
this.hgap = 5.0
this.vgap = 5.0
paddingRight = 5.0
row {
button(messages["servers.login.buttons.website"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
setOnAction {
LauncherData.INSTANCE.application.hostServices.showDocument("https://projectswg.com")
}
}
button(messages["servers.login.buttons.create_account"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
setOnAction {
// TODO: this, somehow
}
isDisable = true
}
}
row {
button(messages["servers.login.buttons.configuration"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
setOnAction {
// TODO add hyperlink
}
isDisable = true
}
button(messages["servers.login.buttons.client_options"]) {
useMaxWidth = true
gridpaneColumnConstraints {
hgrow = Priority.ALWAYS
}
setOnAction {
val updateServer = LauncherData.INSTANCE.login.activeServer.updateServer ?: return@setOnAction
ProcessExecutor.INSTANCE.buildProcess(updateServer, "SwgClientSetup_r.exe")
}
}
}
}
region { vgrow = Priority.ALWAYS }
label("%s: %s".format(messages["servers.login.launcher_version"], LauncherData.VERSION)) {
paddingRight = 5.0
prefWidthProperty().bind(this@rightBox.widthProperty())
alignment = Pos.BASELINE_RIGHT
}
}
}
}
}

View File

@@ -18,24 +18,21 @@
* *
*/
package com.projectswg.launcher.core.resources.gui
package com.projectswg.launcher.resources.gui
import com.projectswg.launcher.core.resources.gui.settings.SettingsForwarderView
import com.projectswg.launcher.core.resources.gui.settings.SettingsGeneralView
import com.projectswg.launcher.core.resources.gui.settings.SettingsLoginView
import com.projectswg.launcher.core.resources.gui.settings.SettingsUpdateView
import tornadofx.View
import tornadofx.scrollpane
import tornadofx.separator
import tornadofx.vbox
import com.projectswg.launcher.resources.gui.settings.SettingsForwarderView
import com.projectswg.launcher.resources.gui.settings.SettingsGeneralView
import com.projectswg.launcher.resources.gui.settings.SettingsLoginView
import com.projectswg.launcher.resources.gui.settings.SettingsUpdateView
import com.projectswg.launcher.resources.gui.style.Style
import tornadofx.*
class SettingsView : View() {
override val root = scrollpane {
isFitToWidth = true
vbox {
styleClass += "background"
addClass(Style.background)
children.add(find<SettingsGeneralView>().root)
separator()

View File

@@ -1,4 +1,4 @@
package com.projectswg.launcher.core.resources.gui.admin
package com.projectswg.launcher.resources.gui.admin
import com.projectswg.common.network.NetBuffer
import com.projectswg.common.network.packets.PacketType

View File

@@ -1,4 +1,4 @@
package com.projectswg.launcher.core.resources.gui.admin
package com.projectswg.launcher.resources.gui.admin
import com.projectswg.common.network.packets.SWGPacket
import javafx.beans.property.ReadOnlyBooleanWrapper

View File

@@ -0,0 +1,6 @@
package com.projectswg.launcher.resources.gui.events
import tornadofx.FXEvent
object LauncherClosingEvent : FXEvent()
object LauncherNewVersionEvent : FXEvent()

View File

@@ -0,0 +1,65 @@
package com.projectswg.launcher.resources.gui.servers
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.gui.style.Style
import com.projectswg.launcher.resources.intents.DownloadLauncherIntent
import javafx.beans.property.ReadOnlyStringWrapper
import javafx.geometry.Pos
import javafx.scene.layout.Region
import javafx.scene.text.FontWeight
import javafx.scene.text.TextAlignment
import tornadofx.*
class LauncherUpdatePopup : Fragment() {
override val root = vbox {
alignment = Pos.CENTER
prefWidth = 200.0
maxWidth = 200.0
prefHeight = 100.0
maxHeight = 100.0
spacing = 5.0
padding = insets(5.0)
addClass(Style.popup)
imageview(resources.image("/graphics/large_load_elbow_grease.png")) {
fitWidth = 200.0
fitHeight = 200.0
isPreserveRatio = true
alignment = Pos.CENTER
}
label {
textProperty().bind(LauncherData.INSTANCE.general.remoteVersionProperty.select {
ReadOnlyStringWrapper("A new version of the launcher is available: $it")
})
minHeight = Region.USE_PREF_SIZE
textAlignment = TextAlignment.CENTER
isWrapText = true
style {
fontWeight = FontWeight.BOLD
}
}
buttonbar {
alignment = Pos.CENTER_RIGHT
button("Ignore") {
action {
close()
}
style {
backgroundColor += Style.backgroundColorTertiary
}
}
button("Download") {
action {
DownloadLauncherIntent().broadcast()
}
}
}
}
}

View File

@@ -18,14 +18,12 @@
* *
***********************************************************************************/
package com.projectswg.launcher.core.resources.gui.servers
package com.projectswg.launcher.resources.gui.servers
import com.projectswg.launcher.core.resources.data.LauncherData
import com.projectswg.launcher.core.resources.data.announcements.WebsitePostFeed
import com.projectswg.launcher.core.resources.data.announcements.WebsitePostMessage
import com.projectswg.launcher.core.resources.gui.events.LauncherClosingEvent
import com.projectswg.launcher.core.resources.gui.style.Style
import javafx.geometry.Pos
import com.projectswg.launcher.resources.data.announcements.WebsitePostFeed
import com.projectswg.launcher.resources.data.announcements.WebsitePostMessage
import com.projectswg.launcher.resources.gui.events.LauncherClosingEvent
import com.projectswg.launcher.resources.gui.style.Style
import javafx.scene.text.FontWeight
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool
import me.joshlarson.jlcommon.log.Log
@@ -51,22 +49,13 @@ class WebsitePostFeedList : View() {
}
onDoubleClick {
LauncherData.INSTANCE.application.hostServices.showDocument(item.link)
com.projectswg.launcher.resources.data.LauncherData.INSTANCE.application.hostServices.showDocument(item.link)
}
graphic = hbox {
maxHeight = 40.0
spacing = 10.0
vbox {
alignment = Pos.BASELINE_CENTER
imageview(resources.image(item.image.imagePath)) {
fitHeight = 40.0
isPreserveRatio = true
isSmooth = true
}
}
vbox {
label(item.title) {
style {

View File

@@ -0,0 +1,71 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.gui.settings
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.data.forwarder.ForwarderData
import com.projectswg.launcher.resources.gui.style.Style
import javafx.scene.control.TextField
import javafx.util.converter.NumberStringConverter
import tornadofx.*
class SettingsForwarderView : View() {
override val root = vbox {
val data = com.projectswg.launcher.resources.data.LauncherData.INSTANCE.forwarder
label(messages["settings.forwarder.header"]) {
addClass(Style.settingsHeaderLabel)
}
lateinit var sendIntervalTextField: TextField
lateinit var sendMaxTextField: TextField
hbox {
addClass(Style.settingsRow)
label(messages["settings.forwarder.sendInterval"])
sendIntervalTextField = textfield(LauncherData.INSTANCE.forwarder.sendInterval.toString()) {
textProperty().bindBidirectional(data.sendIntervalProperty, NumberStringConverter())
}
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.forwarder.sendMax"])
sendMaxTextField = textfield(LauncherData.INSTANCE.forwarder.sendMax.toString()) {
textProperty().bindBidirectional(data.sendMaxProperty, NumberStringConverter())
}
}
hbox {
addClass(Style.settingsRow)
label("")
button(messages["settings.forwarder.reset"]) {
setOnAction {
val textConverter = NumberStringConverter()
sendIntervalTextField.text = textConverter.toString(ForwarderData.DEFAULT_SEND_INTERVAL)
sendMaxTextField.text = textConverter.toString(ForwarderData.DEFAULT_SEND_MAX)
}
}
}
}
}

View File

@@ -0,0 +1,109 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.gui.settings
import com.projectswg.launcher.resources.gui.createGlyph
import com.projectswg.launcher.resources.gui.style.Style
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.scene.control.TextField
import javafx.scene.layout.Priority
import javafx.stage.FileChooser
import tornadofx.*
import java.io.File
import java.io.IOException
import java.util.*
class SettingsGeneralView : View() {
override val root = vbox {
val data = com.projectswg.launcher.resources.data.LauncherData.INSTANCE.general
label(messages["settings.general.header"]) {
addClass(Style.settingsHeaderLabel)
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.general.locale"])
combobox {
items.setAll(Locale.ENGLISH, Locale.GERMAN)
valueProperty().bindBidirectional(data.localeProperty)
}
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.general.wine"])
val winePathTextField = textfield {
isDisable = true
textProperty().bindBidirectional(data.wineProperty)
}
region {
prefWidth = 10.0
}
button {
graphic = FontAwesomeIcon.FOLDER_ALT.createGlyph()
setOnAction {
processWineSelectionButtonAction(winePathTextField)
}
style {
prefWidth = 30.px
}
}
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.general.admin"])
checkbox {
selectedProperty().bindBidirectional(data.adminProperty)
}
region {
prefWidth = 10.0
}
label(messages["settings.general.admin_disclaimer"]) {
maxWidth = Double.POSITIVE_INFINITY
hgrow = Priority.ALWAYS
style {
fontSize = 10.px
}
}
}
}
private fun processWineSelectionButtonAction(winePathTextField: TextField) {
val selection = chooseWinePath() ?: return
try {
winePathTextField.text = selection.canonicalPath
} catch (ex: IOException) {
winePathTextField.text = selection.absolutePath
}
}
private fun chooseWinePath(): File? {
val fileChooser = FileChooser()
fileChooser.title = "Choose Wine Path"
val file = fileChooser.showOpenDialog(com.projectswg.launcher.resources.data.LauncherData.INSTANCE.stage)
return if (file == null || !file.isFile) null else file
}
}

View File

@@ -0,0 +1,56 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.gui.settings
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.gui.style.Style
import tornadofx.*
class SettingsLoginView : View() {
override val root = vbox {
label(messages["settings.login.header"]) {
addClass(Style.settingsHeaderLabel)
}
val localServer = LauncherData.INSTANCE.login.localServer
hbox {
addClass(Style.settingsRow)
label(messages["settings.login.local_connection_url"])
textfield {
textProperty().bindBidirectional(localServer.connectionUriProperty)
}
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.login.update_server"])
combobox<UpdateServer> {
items = LauncherData.INSTANCE.update.serversProperty
valueProperty().bindBidirectional(localServer.updateServerProperty)
}
}
}
}

View File

@@ -0,0 +1,92 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.gui.settings
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.gui.createGlyph
import com.projectswg.launcher.resources.gui.style.Style
import com.projectswg.launcher.resources.intents.RequestScanIntent
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon
import javafx.scene.control.TextField
import javafx.stage.DirectoryChooser
import tornadofx.*
import java.io.File
import java.io.IOException
class SettingsUpdateView: View() {
override val root = vbox {
label(messages["settings.update.header"]) {
addClass(Style.settingsHeaderLabel)
}
hbox {
addClass(Style.settingsRow)
label(messages["settings.update.localPath"])
val localPathTextField = textfield(LauncherData.INSTANCE.update.localPathProperty) {
isDisable = true
}
region {
prefWidth = 10.0
}
button("") {
graphic = FontAwesomeIcon.FOLDER_ALT.createGlyph()
setOnAction {
processLocalPathSelectionButtonAction(localPathTextField)
}
style {
prefWidth = 30.px
}
}
}
region {
prefHeight = 10.0
}
}
private fun processLocalPathSelectionButtonAction(localPathTextField: TextField) {
val currentFolder = File(localPathTextField.text)
val popupFolder = if (currentFolder.exists()) currentFolder else File(System.getProperty("user.home"))
val selection = chooseLocalInstallationDirectory(popupFolder) ?: return
try {
localPathTextField.text = selection.canonicalPath
} catch (ex: IOException) {
localPathTextField.text = selection.absolutePath
}
for (server in LauncherData.INSTANCE.update.servers) {
RequestScanIntent(server).broadcast()
}
}
private fun chooseLocalInstallationDirectory(currentDirectory: File): File? {
val directoryChooser = DirectoryChooser()
directoryChooser.title = "Choose Local Installation Path"
directoryChooser.initialDirectory = currentDirectory
val file = directoryChooser.showDialog(com.projectswg.launcher.resources.data.LauncherData.INSTANCE.stage)
return if (file == null || !file.isDirectory) null else file
}
}

View File

@@ -0,0 +1,280 @@
/***********************************************************************************
* Copyright (C) 2020 /// 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.resources.gui.style
import javafx.geometry.Pos
import javafx.scene.layout.BorderStrokeStyle
import javafx.scene.paint.Color
import javafx.scene.text.FontWeight
import tornadofx.*
class Style : Stylesheet() {
companion object {
// Previous color scheme
// val backgroundColorPrimary = c("#484848")
// val backgroundColorSecondary = c("#4e4e4e")
// val backgroundColorTertiary = c("#313131")
// val textColorPrimary = c("#FFFFFF")
// val additionalColorPrimary = c("#007fcf")
// val additionalColorSecondary = c("#7f7f7f")
// val buttonColor = c("#007fcf")
// New color scheme
val backgroundColorPrimary = c("#213639")
val backgroundColorSecondary = c("#294749")
val backgroundColorTertiary = c("#092729")
val textColorPrimary = c("#FFFFFF")
val additionalColorPrimary = c("#ff8819")
val additionalColorSecondary = c("#6f9c99")
val buttonColor = c("#ff8819")
val playButtonColor: Color = Color.GREEN
val playButtonTextColor: Color = textColorPrimary
val statusNormal by cssclass()
val statusFail by cssclass()
val statusInProgress by cssclass()
val statusGood by cssclass()
val selectedTabLabel by cssclass()
// Tables
val leftTableCell by cssclass()
val centerTableCell by cssclass()
// Settings
val settingsHeaderLabel by cssclass()
val settingsRow by cssclass()
// Server List
val serverList by cssclass()
val background by cssclass()
val popup by cssclass()
}
init {
separator {
s(line) {
borderStyle += BorderStrokeStyle.SOLID
borderWidth += box(3.px, 0.px, 0.px, 0.px)
borderColor += box(additionalColorPrimary)
}
}
scrollPane {
backgroundColor += Color.TRANSPARENT
}
selectedTabLabel {
fontSize = 28.px
rotate = (-90).deg
}
// Various arrows
s(decrementArrow, incrementArrow, arrow) {
backgroundColor += textColorPrimary
}
s(filler, track) {
backgroundColor += backgroundColorTertiary
}
// ProgressBar, TextField, ListView, and ComboBox inner-component styling
s( textField,
listView,
progressBar child track,
comboBox child listCell) {
backgroundColor += backgroundColorTertiary
backgroundRadius += box(0.px)
highlightFill = additionalColorPrimary
textFill = textColorPrimary
}
// ComboBox, Button, and ScrollBar button styling
s( comboBox,
button,
scrollBar child decrementButton,
scrollBar child incrementButton) {
backgroundColor += additionalColorPrimary
textFill = textColorPrimary
}
s(scrollBar child thumb) {
backgroundColor += additionalColorSecondary
backgroundRadius += box(0.px)
backgroundInsets += box(0.px)
}
/* ------------------
* ----- Tables -----
* ------------------ */
cell {
backgroundColor += backgroundColorPrimary
and(even and filled) {
backgroundColor += backgroundColorPrimary
// and(hover) {
// backgroundColor += backgroundColorHover
// }
}
and(odd and filled) {
backgroundColor += backgroundColorSecondary
// and(hover) {
// backgroundColor += backgroundColorHover
// }
}
}
tableView {
padding = box(0.px)
}
tableColumn {
fontWeight = FontWeight.BOLD
borderWidth += box(0.px)
}
columnHeader {
backgroundColor += backgroundColorTertiary
}
columnHeaderBackground {
borderColor += box(additionalColorPrimary)
borderWidth += box(0.px, 0.px, 3.px, 0.px)
}
leftTableCell {
alignment = Pos.CENTER_LEFT
}
centerTableCell {
alignment = Pos.CENTER
}
// OTHER
s(text, label, content) {
fill = textColorPrimary
textFill = textColorPrimary
}
// Buttons
s(comboBox, button, decrementButton, incrementButton) {
textFill = textColorPrimary
backgroundColor += buttonColor
backgroundInsets += box(0.px)
backgroundRadius += box(0.px)
}
statusNormal {
s(text) {
fill = Color.WHITE
}
}
statusFail {
s(text) {
fill = c("#FF0000")
}
}
statusInProgress {
s(text) {
fill = Color.YELLOW
}
}
statusGood {
s(text) {
fill = c("#00FF00")
}
}
settingsHeaderLabel {
fontSize = 14.px
fontWeight = FontWeight.BOLD
padding = box(5.px, 0.px, 10.px, 5.px)
}
settingsRow {
prefHeight = 25.px
padding = box(5.px, 5.px, 5.px, 50.px)
hgap = 5.px
s(button) {
prefWidth = 150.px
}
s(label) {
prefWidth = 150.px
prefHeight= 25.px
}
s(checkBox) {
prefHeight = 25.px
}
s(comboBox, textField) {
prefWidth = 400.px
prefHeight = 20.px
}
}
// Server List
serverList {
// padding = box(5.px)
}
background {
backgroundColor += backgroundColorPrimary
}
s(".tab-header-background") {
backgroundColor += backgroundColorPrimary
}
s(tabContentArea) {
backgroundColor += backgroundColorPrimary
}
tab {
backgroundColor += backgroundColorSecondary
and(selected) {
backgroundColor += backgroundColorTertiary
}
}
s(focusIndicator) {
borderColor += box(Color.TRANSPARENT)
}
popup {
backgroundColor += backgroundColorPrimary
borderWidth += box(5.px)
borderColor += box(Color.WHITE)
s(label) {
fill = Color.WHITE
}
}
}
}

View File

@@ -1,6 +1,6 @@
package com.projectswg.launcher.core.resources.intents
package com.projectswg.launcher.resources.intents
import com.projectswg.launcher.core.resources.game.GameInstance
import com.projectswg.launcher.resources.game.GameInstance
import me.joshlarson.jlcommon.control.Intent
data class GameLaunchedIntent(val gameInstance: GameInstance): Intent()

View File

@@ -1,8 +1,9 @@
package com.projectswg.launcher.core.resources.intents
package com.projectswg.launcher.resources.intents
import com.projectswg.launcher.core.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.data.update.UpdateServer
import me.joshlarson.jlcommon.control.Intent
data class CancelDownloadIntent(val server: UpdateServer): Intent()
data class DownloadPatchIntent(val server: UpdateServer): Intent()
data class RequestScanIntent(val server: UpdateServer): Intent()
class DownloadLauncherIntent: Intent()

View File

@@ -0,0 +1,80 @@
package com.projectswg.launcher.resources.pipeline
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.gui.events.LauncherNewVersionEvent
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.json.JSONInputStream
import tornadofx.FX
import java.io.IOException
import java.net.URL
import java.util.*
import java.util.concurrent.atomic.AtomicReference
object LauncherConfigurationUpdater {
private const val REMOTE_CONFIGURATION_URL = "https://projectswg.com/launcher/launcher.json"
private val downloadUrl = AtomicReference<String>(null)
fun update() {
try {
Log.d("Retrieving server configuration from %s", REMOTE_CONFIGURATION_URL)
JSONInputStream(URL(REMOTE_CONFIGURATION_URL).openStream()).use { input ->
val configuration = input.readObject()
Log.t("Remote configuration: %s", configuration)
val version = configuration["download_version"] as? String ?: return
@Suppress("UNCHECKED_CAST")
val download = configuration["download"] as? Map<String, Any?> ?: return
val downloadUrl = download[getOS()] as? String ?: return
val needsUpdate = isNewVersionAvailable(version)
Log.d("Server configuration: version=%s download_url=%s needs_update=%s", version, downloadUrl, needsUpdate)
LauncherData.INSTANCE.general.remoteVersion = version
this.downloadUrl.set(downloadUrl)
if (needsUpdate)
FX.eventbus.fire(LauncherNewVersionEvent)
}
} catch (e: IOException) {
Log.e("Failed to retrieve updated server configuration")
}
}
fun download() {
val downloadUrl = this.downloadUrl.get()
if (downloadUrl == null) {
Log.w("Cannot update - download url is empty")
return
}
Log.i("Launching download URL: %s", downloadUrl)
LauncherData.INSTANCE.application.hostServices.showDocument(downloadUrl)
}
private fun isNewVersionAvailable(specifiedVersionStr: String?): Boolean {
if (specifiedVersionStr == null) return true
val currentVersion = LauncherData.VERSION.split(".")
val specifiedVersion = specifiedVersionStr.split(".")
for ((cur, spec) in currentVersion.zip(specifiedVersion)) {
val curInt = Integer.parseUnsignedInt(cur)
val specInt = Integer.parseUnsignedInt(spec)
if (curInt == specInt)
continue
if (curInt < specInt)
return true
}
return false
}
private fun getOS(): String {
val os = System.getProperty("os.name").lowercase(Locale.US)
if (os.contains("win"))
return "windows"
if (os.contains("mac"))
return "mac"
return "linux"
}
}

View File

@@ -0,0 +1,160 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.resources.pipeline
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.data.update.UpdateServer.RequiredFile
import com.projectswg.launcher.resources.data.update.UpdateServer.UpdateServerStatus
import javafx.application.Platform
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.json.JSONException
import me.joshlarson.json.JSONInputStream
import me.joshlarson.json.JSONOutputStream
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.util.*
import java.util.stream.Collectors
object UpdateServerUpdater {
fun update(server: UpdateServer) {
val localPath = LauncherData.INSTANCE.update.localPath
if (!File(localPath).isDirectory) {
Log.e("Not a valid local path: %s", localPath)
return
}
val info = UpdateServerDownloaderInfo(server, localPath)
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 fun updateFileList(info: UpdateServerDownloaderInfo): Boolean {
Log.t("Retrieving latest file list from %s...", info.url)
val localFileList = File(info.localPath, "files.json")
var files: List<Any?>
try {
JSONInputStream(createURL(info, "files.json").openConnection().getInputStream()).use { `in` ->
files = `in`.readArray()
try {
JSONOutputStream(FileOutputStream(localFileList)).use { out -> out.writeArray(files) }
} catch (e: IOException) {
Log.e("Failed to write updated file list to disk for update server %s (%s: %s)", info.name, e.javaClass.name, e.message)
}
}
} catch (e: IOException) {
Log.w("Failed to retrieve latest file list for update server %s (%s: %s). Falling back on local copy...", e.javaClass.name, e.message, info.name)
try {
JSONInputStream(FileInputStream(localFileList)).use { `in` -> files = `in`.readArray() }
} catch (t: JSONException) {
Log.e("Failed to read file list from disk on update server %s with path %s. Aborting update.", info.name, localFileList)
return false
} catch (t: IOException) {
Log.e("Failed to read file list from disk on update server %s with path %s. Aborting update.", info.name, localFileList)
return false
}
} catch (e: JSONException) {
Log.w("Failed to retrieve latest file list for update server %s (%s: %s). Falling back on local copy...", e.javaClass.name, e.message, info.name)
try {
JSONInputStream(FileInputStream(localFileList)).use { `in` -> files = `in`.readArray() }
} catch (t: JSONException) {
Log.e("Failed to read file list from disk on update server %s with path %s. Aborting update.", info.name, localFileList)
return false
} catch (t: IOException) {
Log.e("Failed to read file list from disk on update server %s with path %s. Aborting update.", info.name, localFileList)
return false
}
}
info.files = files.stream()
.filter { obj: Any? -> MutableMap::class.java.isInstance(obj) }
.map { obj: Any? -> MutableMap::class.java.cast(obj) }
.map { obj -> @Suppress("UNCHECKED_CAST") jsonObjectToRequiredFile(info, obj as? Map<String, Any> ?: return@map null ) }
.filter { it != null }
.collect(Collectors.toList())
return true
}
/**
* Stage 2: Scan each file and only keep the ones that need to be downloaded.
*/
private fun filterValidFiles(info: UpdateServerDownloaderInfo) {
val files: MutableList<RequiredFile> = Objects.requireNonNull<MutableList<RequiredFile>?>(info.files, "File list was not read correctly")
Log.d("%d known files. Scanning...", files.size)
val total = files.size
Platform.runLater { info.server.status = UpdateServerStatus.SCANNING }
files.removeIf { obj: RequiredFile -> isValidFile(obj) }
val valid = total - files.size
Log.d("Completed scan of update server %s. %d of %d valid.", info.name, valid, total)
}
/**
* Stage 3: Update the UpdateServer status and the required files.
*/
private fun updateServerStatus(info: UpdateServerDownloaderInfo) {
val serverList: MutableList<RequiredFile> = info.server.requiredFiles
val updateList: List<RequiredFile> = info.files ?: return
val updateStatus = if (updateList.isEmpty()) UpdateServerStatus.READY else UpdateServerStatus.REQUIRES_DOWNLOAD
serverList.clear()
serverList.addAll(updateList)
Platform.runLater { info.server.status = updateStatus }
Log.d("Setting update server '%s' status to %s", info.name, updateStatus)
}
private fun isValidFile(file: RequiredFile): Boolean {
val localFile = file.localPath
val length = localFile.length()
return localFile.isFile && length == file.length
}
private fun jsonObjectToRequiredFile(info: UpdateServerDownloaderInfo, obj: Map<String, Any>): RequiredFile {
val path = obj["path"] as String? ?: throw RuntimeException("no path defined for file")
return try {
RequiredFile(File(info.localPath, path), createURL(info, path), obj["length"] as Long, (obj["xxhash"] as String?)!!)
} catch (e: MalformedURLException) {
throw RuntimeException(e)
}
}
@Throws(MalformedURLException::class)
private fun createURL(info: UpdateServerDownloaderInfo, path: String): URL {
var url = info.url
while (url.endsWith("/"))
url = url.removeSuffix("/")
return URL(url + "/" + path.removePrefix("/"))
}
private class UpdateServerDownloaderInfo(val server: UpdateServer, localPath: String) {
val name: String = server.name
val url: String = server.url
val localPath: File = File(localPath, server.gameVersion)
var files: MutableList<RequiredFile>? = null
}
}

View File

@@ -0,0 +1,30 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.services.data
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure
@ManagerStructure(children = [
PreferencesDataService::class,
DownloadService::class,
RemoteDataService::class
])
class DataManager : Manager()

View File

@@ -18,14 +18,14 @@
* *
*/
package com.projectswg.launcher.core.services.data
package com.projectswg.launcher.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 com.projectswg.launcher.resources.data.update.UpdateServer
import com.projectswg.launcher.resources.data.update.UpdateServer.RequiredFile
import com.projectswg.launcher.resources.data.update.UpdateServer.UpdateServerStatus
import com.projectswg.launcher.resources.intents.CancelDownloadIntent
import com.projectswg.launcher.resources.intents.DownloadPatchIntent
import com.projectswg.launcher.resources.intents.RequestScanIntent
import javafx.application.Platform
import javafx.beans.binding.NumberBinding
import javafx.beans.property.LongProperty

View File

@@ -0,0 +1,333 @@
/***********************************************************************************
* 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:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.services.data
import com.projectswg.launcher.resources.data.LauncherData
import com.projectswg.launcher.resources.data.login.AuthenticationData
import com.projectswg.launcher.resources.data.login.LoginServer
import com.projectswg.launcher.resources.data.update.UpdateServer
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log
import me.joshlarson.json.JSONInputStream
import me.joshlarson.json.JSONOutputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.net.URL
import java.util.*
class PreferencesDataService : Service() {
private val executor: ScheduledThreadPool = ScheduledThreadPool(1, 3, "data-executor-%d")
init {
loadPreferences()
}
override fun start(): Boolean {
createDefaults()
executor.start()
executor.executeWithFixedDelay((5 * 60000).toLong(), (5 * 60000).toLong()) { savePreferences() }
return true
}
override fun stop(): Boolean {
savePreferences()
executor.stop()
return executor.awaitTermination(1000)
}
private fun createDefaults() {
if (LauncherData.INSTANCE.general.wine == null || LauncherData.INSTANCE.general.wine!!.isEmpty())
LauncherData.INSTANCE.general.wine = winePath
}
@Synchronized
private fun loadPreferences() {
val applicationDataDirectory = LauncherData.getApplicationDataDirectory()
if (!applicationDataDirectory.exists())
applicationDataDirectory.mkdirs()
else if (!applicationDataDirectory.isDirectory) {
applicationDataDirectory.deleteRecursively()
applicationDataDirectory.mkdirs()
}
lateinit var lastSelectedServerName: String
lateinit var localServerConfiguration: Map<String, Any?>
try {
JSONInputStream(FileInputStream(File(LauncherData.getApplicationDataDirectory(), "settings.json"))).use {
val settings = it.readObject()
loadSettingsGeneral(settings.getMap("general"))
loadSettingsUpdate(settings.getMap("update"))
loadSettingsLogin(settings.getMap("login"))
loadSettingsForwarder(settings.getMap("forwarder"))
lastSelectedServerName = settings.getMap("login").getString("last_selected_server", "")
localServerConfiguration = settings.getMap("login").getMap("local_server")
}
} catch (fnf: FileNotFoundException) {
loadSettingsGeneral(mapOf())
loadSettingsLogin(mapOf())
loadSettingsForwarder(mapOf())
}
loadServers()
// Add the local server last so that it's always last in the menu
loadLocalServer(localServerConfiguration)
val lastSelectedServer = LauncherData.INSTANCE.login.getServerByName(lastSelectedServerName)
if (lastSelectedServer != null)
LauncherData.INSTANCE.login.activeServer = lastSelectedServer
}
private fun loadSettingsGeneral(settings: Map<String, Any?>) {
LauncherData.INSTANCE.general.locale = Locale.forLanguageTag(settings.getString("locale", Locale.US.toLanguageTag()))
LauncherData.INSTANCE.general.wine = settings.getString("wine", "")
LauncherData.INSTANCE.general.isAdmin = settings.getBool("admin", false)
}
private fun loadSettingsUpdate(settings: Map<String, Any?>) {
LauncherData.INSTANCE.update.localPath = settings.getString("local_path", "")
}
private fun loadSettingsLogin(settings: Map<String, Any?>) {
for (e in settings.getMap("authentications")) {
val data = AuthenticationData(e.key)
data.username = castMap(e.value).getString("username", "")
data.password = castMap(e.value).getString("password", "")
LauncherData.INSTANCE.login.authenticationData.add(data)
}
}
private fun loadLocalServer(localServerSettings: Map<String, Any?>) {
val localAuthenticationSource = AuthenticationData(LOCAL_AUTHENTICATION_NAME)
if (LauncherData.INSTANCE.login.getAuthenticationData(LOCAL_AUTHENTICATION_NAME) == null)
LauncherData.INSTANCE.login.authenticationData.add(localAuthenticationSource)
val defaultLocalServer = LoginServer("local")
defaultLocalServer.connectionUri = "ws://127.0.0.1:44463/game"
defaultLocalServer.updateServer = LauncherData.INSTANCE.update.serversProperty.getOrNull(0)
defaultLocalServer.authentication = localAuthenticationSource
val localServer = loadLoginServer(localServerSettings) ?: defaultLocalServer
LauncherData.INSTANCE.login.localServer = localServer
LauncherData.INSTANCE.login.servers.add(LauncherData.INSTANCE.login.localServer)
}
private fun loadSettingsForwarder(settings: Map<String, Any?>) {
LauncherData.INSTANCE.forwarder.sendInterval = settings.getInt("send_interval", 1000)
LauncherData.INSTANCE.forwarder.sendMax = settings.getInt("send_max", 400)
}
private fun loadServers() {
try {
loadRemoteServers()
JSONInputStream(FileInputStream(File(LauncherData.getApplicationDataDirectory(), "servers.json"))).use {
val serverConfigurations = it.readObject()
loadUpdateServers(serverConfigurations.getListOfMap("update_servers"))
loadLoginServers(serverConfigurations.getListOfMap("login_servers"))
}
} catch (fnf: FileNotFoundException) {
loadSettingsGeneral(mapOf())
loadSettingsLogin(mapOf())
loadSettingsForwarder(mapOf())
}
}
private fun loadRemoteServers() {
try {
Log.d("Retrieving server configuration from %s", REMOTE_SERVER_CONFIGURATION_URL)
JSONInputStream(URL(REMOTE_SERVER_CONFIGURATION_URL).openStream()).use { input ->
JSONOutputStream(FileOutputStream(File(LauncherData.getApplicationDataDirectory(), "servers.json"))).use { output ->
output.writeObject(input.readObject())
}
}
} catch (e: IOException) {
Log.e("Failed to retrieve updated server configuration")
}
}
private fun loadUpdateServers(serverConfigurations: List<Map<String, Any?>>) {
for (updateServerConfiguration in serverConfigurations) {
val updateServer = UpdateServer(updateServerConfiguration["name"] as? String ?: continue)
updateServer.url = updateServerConfiguration["url"] as? String ?: continue
updateServer.gameVersion = updateServerConfiguration["game_version"] as? String ?: continue
updateServer.friendlyName = updateServerConfiguration["friendly_name"] as? String ?: continue
LauncherData.INSTANCE.update.servers.add(updateServer)
}
}
private fun loadLoginServers(serverConfigurations: List<Map<String, Any?>>) {
val unusedAuthenticationServers = HashSet(LauncherData.INSTANCE.login.authenticationData)
for (loginServerConfiguration in serverConfigurations) {
val loginServer = loadLoginServer(loginServerConfiguration) ?: continue
unusedAuthenticationServers.remove(loginServer.authentication)
LauncherData.INSTANCE.login.servers.add(loginServer)
}
// Don't want to perpetually store credentials
for (unusedAuthentication in unusedAuthenticationServers) {
if (unusedAuthentication.name == LOCAL_AUTHENTICATION_NAME)
continue // This will be used later
LauncherData.INSTANCE.login.authenticationData.remove(unusedAuthentication)
}
}
private fun loadLoginServer(loginServerConfiguration: Map<String, Any?>): LoginServer? {
val loginServer = LoginServer(loginServerConfiguration["name"] as? String ?: return null)
loginServer.connectionUri = loginServerConfiguration["url"] as? String ?: return null
loginServer.updateServer = LauncherData.INSTANCE.update.getServer(loginServerConfiguration["update_server"] as? String ?: return null) ?: return null
val authenticationSource = loginServerConfiguration["authentication_source"] as? String ?: return null
var authentication = LauncherData.INSTANCE.login.getAuthenticationData(authenticationSource)
if (authentication == null) {
authentication = AuthenticationData(loginServerConfiguration["authentication_source"] as? String ?: return null)
LauncherData.INSTANCE.login.authenticationData.add(authentication)
}
loginServer.authentication = authentication
return loginServer
}
private fun castMap(obj: Any?): Map<String, Any?> {
@Suppress("UNCHECKED_CAST")
return obj as? Map<String, Any?> ?: mapOf()
}
private fun castListOfMap(obj: Any?): List<Map<String, Any?>> {
@Suppress("UNCHECKED_CAST")
return obj as? List<Map<String, Any?>> ?: listOf()
}
@Synchronized
private fun savePreferences() {
JSONOutputStream(FileOutputStream(File(LauncherData.getApplicationDataDirectory(), "settings.json"))).use {
it.writeObject(jsonSettings())
}
}
private fun jsonSettings(): Map<String, Any?> {
return mapOf(
"general" to jsonSettingsGeneral(),
"update" to jsonSettingsUpdate(),
"login" to jsonSettingsLogin(),
"forwarder" to jsonSettingsForwarder()
)
}
private fun jsonSettingsGeneral(): Map<String, Any?> {
return mapOf(
"locale" to LauncherData.INSTANCE.general.locale.toLanguageTag(),
"wine" to LauncherData.INSTANCE.general.wine,
"admin" to LauncherData.INSTANCE.general.isAdmin
)
}
private fun jsonSettingsUpdate(): Map<String, Any?> {
return mapOf(
"local_path" to LauncherData.INSTANCE.update.localPath
)
}
private fun jsonSettingsLogin(): Map<String, Any?> {
val localServer = LauncherData.INSTANCE.login.localServer
return mapOf(
"last_selected_server" to LauncherData.INSTANCE.login.lastSelectedServer,
"authentications" to jsonSettingsLoginAuthentication(),
"local_server" to mapOf(
"name" to localServer.name,
"url" to localServer.connectionUri,
"update_server" to (localServer.updateServer?.name ?: ""),
"authentication_source" to localServer.authentication.name
)
)
}
private fun jsonSettingsLoginAuthentication(): Map<String, Any?> {
val authentications = HashMap<String, Any?>()
for (auth in LauncherData.INSTANCE.login.authenticationData) {
authentications[auth.name] = mapOf(
"username" to auth.username,
"password" to auth.password
)
}
return authentications
}
private fun jsonSettingsForwarder(): Map<String, Any?> {
return mapOf(
"send_interval" to LauncherData.INSTANCE.forwarder.sendInterval,
"send_max" to LauncherData.INSTANCE.forwarder.sendMax
)
}
private fun Map<String, Any?>?.getMap(key: String): Map<String, Any?> {
return castMap(this?.getOrDefault(key, mapOf<String, Any?>()))
}
private fun Map<String, Any?>?.getListOfMap(key: String): List<Map<String, Any?>> {
return castListOfMap(this?.getOrDefault(key, listOf<Map<String, Any?>>()))
}
private fun Map<String, Any?>?.getString(key: String, defaultValue: String): String {
return (this?.getOrDefault(key, defaultValue) as? String?) ?: defaultValue
}
private fun Map<String, Any?>?.getInt(key: String, defaultValue: Int): Int {
return ((this?.getOrDefault(key, defaultValue) as? Number?) ?: defaultValue).toInt()
}
private fun Map<String, Any?>?.getBool(key: String, defaultValue: Boolean): Boolean {
return (this?.getOrDefault(key, defaultValue) as? Boolean?) ?: defaultValue
}
companion object {
private const val LOCAL_AUTHENTICATION_NAME = "local"
private const val REMOTE_SERVER_CONFIGURATION_URL = "https://projectswg.com/launcher/servers.json"
private val winePath: String?
get() {
val pathStr = System.getenv("PATH") ?: return null
for (path in pathStr.split(File.pathSeparator.toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) {
Log.t("Testing wine binary at %s", path)
var test = File(path, "wine")
if (test.isFile) {
try {
test = test.canonicalFile
Log.d("Found wine installation. Location: %s", test)
return test.absolutePath
} catch (e: IOException) {
Log.w("Failed to get canonical file location of possible wine location: %s", test)
}
}
}
return null
}
}
}

View File

@@ -0,0 +1,73 @@
/***********************************************************************************
* Copyright (C) 2018 /// Project SWG /// www.projectswg.com *
* *
* This file is part of the ProjectSWG Launcher. *
* *
* This program is free software: you can redistribute it and/or modify *
* it under the terms of the GNU Affero General Public License as published by *
* the Free Software Foundation, either version 3 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Affero General Public License for more details. *
* *
* You should have received a copy of the GNU Affero General Public License *
* along with this program. If not, see <https:></https:>//www.gnu.org/licenses/>. *
* *
*/
package com.projectswg.launcher.services.data
import com.projectswg.launcher.resources.intents.DownloadLauncherIntent
import com.projectswg.launcher.resources.intents.RequestScanIntent
import com.projectswg.launcher.resources.pipeline.LauncherConfigurationUpdater
import com.projectswg.launcher.resources.pipeline.UpdateServerUpdater
import me.joshlarson.jlcommon.concurrency.ScheduledThreadPool
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import java.util.concurrent.TimeUnit
class RemoteDataService : Service() {
private val executor: ScheduledThreadPool = ScheduledThreadPool(2, "remote-data-service")
override fun start(): Boolean {
executor.start()
// Retrieves the latest file list for each update server
executor.executeWithFixedDelay(0, TimeUnit.MINUTES.toMillis(30)) { this.updateUpdateServers() }
executor.executeWithFixedDelay(0, TimeUnit.MINUTES.toMillis(30)) { this.updateRemoteVersion() }
return true
}
override fun stop(): Boolean {
executor.stop()
return executor.awaitTermination(1000)
}
@IntentHandler
private fun handleRequestScanIntent(rsi: RequestScanIntent) {
UpdateServerUpdater.update(rsi.server)
}
@IntentHandler
private fun handleDownloadLauncherIntent(dli: DownloadLauncherIntent) {
LauncherConfigurationUpdater.download()
}
private fun updateUpdateServers() {
updateData.servers.parallelStream().forEach { UpdateServerUpdater.update(it) }
}
private fun updateRemoteVersion() {
LauncherConfigurationUpdater.update()
}
companion object {
private val updateData: com.projectswg.launcher.resources.data.update.UpdateData
get() = com.projectswg.launcher.resources.data.LauncherData.INSTANCE.update
}
}

View File

@@ -18,10 +18,10 @@
* *
*/
package com.projectswg.launcher.core.services.launcher
package com.projectswg.launcher.services.launcher
import com.projectswg.launcher.core.resources.game.GameInstance
import com.projectswg.launcher.core.resources.intents.GameLaunchedIntent
import com.projectswg.launcher.resources.game.GameInstance
import com.projectswg.launcher.resources.intents.GameLaunchedIntent
import me.joshlarson.jlcommon.control.IntentHandler
import me.joshlarson.jlcommon.control.Service
import me.joshlarson.jlcommon.log.Log

View File

@@ -18,7 +18,7 @@
* *
*/
package com.projectswg.launcher.core.services.launcher
package com.projectswg.launcher.services.launcher
import me.joshlarson.jlcommon.control.Manager
import me.joshlarson.jlcommon.control.ManagerStructure

View File

@@ -4,13 +4,11 @@ open module com.projectswg.launcher {
requires me.joshlarson.jlcommon;
requires me.joshlarson.jlcommon.javafx;
requires fast.json;
requires net.openhft.hashing;
requires org.bouncycastle.provider;
requires org.jetbrains.annotations;
requires com.projectswg.common;
requires com.projectswg.forwarder;
requires com.projectswg.holocore.client;
requires jdk.crypto.ec;
requires java.prefs;
@@ -25,6 +23,4 @@ open module com.projectswg.launcher {
requires kotlin.stdlib;
requires kotlin.reflect;
exports com.projectswg.launcher.core.resources.data.forwarder;
}

View File

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

View File

@@ -1,32 +0,0 @@
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.net.URL?>
<?import javafx.scene.Group?>
<AnchorPane fx:id="root" fx:controller="com.projectswg.launcher.core.resources.gui.NavigationView" xmlns:fx="http://javafx.com/fxml" prefWidth="825" prefHeight="640">
<stylesheets>
<URL value="@/css/theme.css"/>
</stylesheets>
<TabPane fx:id="tabPane" side="LEFT" VBox.vgrow="ALWAYS" AnchorPane.topAnchor="0" AnchorPane.rightAnchor="0" AnchorPane.bottomAnchor="0" AnchorPane.leftAnchor="0">
<Tab fx:id="announcementsTab" styleClass="background" text="%announcements" closable="false">
<tooltip>
<Tooltip text="%announcements" />
</tooltip>
<fx:include source="AnnouncementView.fxml"/>
</Tab>
<Tab fx:id="serverListTab" styleClass="background" text="%servers" closable="false">
<tooltip>
<Tooltip text="%servers" />
</tooltip>
<fx:include source="ServerListView.fxml"/>
</Tab>
<Tab fx:id="settingsTab" styleClass="background" text="%settings" closable="false">
<tooltip>
<Tooltip text="%settings" />
</tooltip>
<fx:include source="SettingsView.fxml"/>
</Tab>
</TabPane>
<Group AnchorPane.bottomAnchor="15" AnchorPane.leftAnchor="5">
<Label fx:id="selectedTabLabel" />
</Group>
</AnchorPane>

View File

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

View File

@@ -1,14 +0,0 @@
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.VBox?>
<ScrollPane fx:id="root" xmlns:fx="http://javafx.com/fxml" fitToWidth="true">
<VBox styleClass="background">
<fx:include source="settings/SettingsGeneralView.fxml" />
<Separator />
<fx:include source="settings/SettingsLoginView.fxml" />
<Separator />
<fx:include source="/com/projectswg/launcher/core/resources/gui/settings/SettingsUpdateView.fxml" />
<Separator />
<fx:include source="settings/SettingsForwarderView.fxml" />
</VBox>
</ScrollPane>

View File

@@ -1,18 +0,0 @@
<?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">
<Label text="%settings.forwarder.header" styleClass="settings-header-label" />
<HBox styleClass="settings-row">
<Label text="%settings.forwarder.sendInterval" />
<TextField fx:id="sendIntervalTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.forwarder.sendMax" />
<TextField fx:id="sendMaxTextField" />
</HBox>
<HBox styleClass="settings-row">
<Label />
<Button fx:id="resetButton" text="%settings.forwarder.reset" />
</HBox>
</VBox>

View File

@@ -1,29 +0,0 @@
<?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">
<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" />
<Region prefWidth="10" />
<Button fx:id="wineSelectionButton" styleClass="pathSelection" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.general.admin" />
<CheckBox fx:id="adminCheckBox" />
<Region prefWidth="10" />
<Label text="%settings.general.admin_disclaimer" style="-fx-font-size: 10px;" maxWidth="Infinity" HBox.hgrow="ALWAYS" />
</HBox>
</VBox>

View File

@@ -1,41 +0,0 @@
<?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">
<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" />
<Region prefWidth="10" />
<ToggleButton fx:id="hidePasswordButton" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.updateServer" />
<ComboBox fx:id="updateServerComboBox" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.verify_server" />
<CheckBox fx:id="verifyServerCheckBox" />
<Region prefWidth="10" />
<Label text="%settings.login.verify_server_disclaimer" style="-fx-font-size: 10px;" maxWidth="Infinity" HBox.hgrow="ALWAYS" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.login.enable_encryption" />
<CheckBox fx:id="enableEncryptionCheckBox" />
</HBox>
</VBox>

View File

@@ -1,31 +0,0 @@
<?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">
<Label text="%settings.update.header" styleClass="settings-header-label" />
<HBox styleClass="settings-row">
<Label text="%settings.update.name" />
<ComboBox fx:id="nameComboBox" />
<Region prefWidth="10" />
<Button fx:id="scanButton" text="%settings.update.scan" />
</HBox>
<HBox styleClass="settings-row">
<Label text="%settings.update.address" />
<TextField fx:id="addressTextField" />
<Region prefWidth="10" />
<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" />
<Region prefWidth="10" />
<Button fx:id="localPathSelectionButton" styleClass="pathSelection" />
</HBox>
</VBox>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

View File

@@ -16,7 +16,7 @@ servers.play.cancel=Cancel
servers.status.unknown=
servers.status.scanning=Scanning...
servers.status.requires_download=Requires Download
servers.status.requires_download=Needs Update
servers.status.downloading=Downloading...
servers.status.ready=Ready
@@ -27,7 +27,8 @@ servers.action_info.progress=complete
servers.action_info.required=required
servers.action_info.downloading=Downloading...
servers.login.form.title=Login
servers.login.feed.title=Website Feed
servers.login.form.title=Game
servers.login.form.username=Username
servers.login.form.password=Password
servers.login.form.submit=Login
@@ -35,7 +36,7 @@ servers.login.form.submit=Login
servers.login.buttons.website=Website
servers.login.buttons.create_account=Create Account
servers.login.buttons.configuration=Configuration
servers.login.buttons.server_list=Server List
servers.login.buttons.client_options=Client Options
servers.login.launcher_version=Launcher Version
settings.general.header=General
@@ -46,25 +47,14 @@ settings.general.wine=Wine
settings.general.admin=Admin Commands
settings.general.admin_disclaimer=(This does not grant admin rights on the server)
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.login.verify_server=SSL Verification
settings.login.enable_encryption=TCP Encryption
settings.login.verify_server_disclaimer=(This is an important security feature and should be enabled at all times)
settings.login.header=Local Server
settings.login.local_connection_url=Connection URL
settings.login.update_server=Update Server
settings.update.header=Update Servers
settings.update.name=Name
settings.update.header=Update Server
settings.update.localPath=Local Path
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
settings.forwarder.header=Forwarder
settings.forwarder.sendInterval=Packet Interval

View File

@@ -16,7 +16,7 @@ servers.play.cancel=Abbruch
servers.status.unknown=Unbekannt
servers.status.scanning=Scanne...
servers.status.requires_download=Benötigt Download
servers.status.requires_download=Ben. Update
servers.status.downloading=Downloade...
servers.status.ready=Fertig
@@ -27,6 +27,7 @@ servers.action_info.progress=fertig
servers.action_info.required=benötigt
servers.action_info.downloading=Lade...
servers.login.feed.title=Website Feed
servers.login.form.title=Anmelden
servers.login.form.username=Benutzername
servers.login.form.password=Passwort
@@ -35,7 +36,7 @@ servers.login.form.submit=Login
servers.login.buttons.website=Webseite
servers.login.buttons.create_account=Konto erstellen
servers.login.buttons.configuration=Konfiguration
servers.login.buttons.server_list=Serverliste
servers.login.buttons.client_options=Client-Optionen
servers.login.launcher_version=Launcher Version
settings.general.header=Allgemein
@@ -44,21 +45,11 @@ 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.login.verify_server=SSL Verification
settings.login.verify_server_disclaimer=(This is an important security feature and should be enabled at all times)
settings.login.header=Lokal Servers
settings.login.local_connection_url=Verbindungs URL
settings.login.update_server=Update Server
settings.update.header=Update Servers
settings.update.name=Name
settings.update.localPath=Lokaler Pfad
settings.update.scan=Scannen
settings.update.clientOptions=Client-Optionen
settings.update.address=Adresse
settings.update.port=Port
settings.update.basePath=Basis Pfad
settings.update.localPath=Lokaler Pfad

View File

@@ -1,132 +0,0 @@
/***********************************************************************************
* 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 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.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");
}
}