aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt44
-rw-r--r--src/cli/CMakeLists.txt12
-rw-r--r--src/cli/commands.c242
-rw-r--r--src/cli/commands.h15
-rw-r--r--src/cli/main.c50
-rw-r--r--src/cli/updater.c143
-rw-r--r--src/cli/updater.h6
-rw-r--r--src/fs.c136
-rw-r--r--src/fs.h24
-rw-r--r--src/hash/CMakeLists.txt2
-rw-r--r--src/hash/md5/CMakeLists.txt12
-rw-r--r--src/hash/md5/global.h32
-rw-r--r--src/hash/md5/md5.h38
-rw-r--r--src/hash/md5/md5c.c334
-rw-r--r--src/net.c128
-rw-r--r--src/net.h28
-rw-r--r--src/qt/CMakeLists.txt34
-rw-r--r--src/qt/assets.qrc15
-rw-r--r--src/qt/assets/background.bmpbin0 -> 2073738 bytes
-rw-r--r--src/qt/assets/discord.pngbin0 -> 2412 bytes
-rw-r--r--src/qt/assets/game.icobin0 -> 93062 bytes
-rw-r--r--src/qt/assets/gear-solid.pngbin0 -> 2550 bytes
-rw-r--r--src/qt/assets/globe.pngbin0 -> 2849 bytes
-rw-r--r--src/qt/assets/logo.pngbin0 -> 21482 bytes
-rw-r--r--src/qt/assets/tf2build.ttfbin0 -> 57168 bytes
-rw-r--r--src/qt/assets/version.rc.in21
-rw-r--r--src/qt/main.cpp12
-rw-r--r--src/qt/mainwindow.cpp249
-rw-r--r--src/qt/mainwindow.hpp50
-rw-r--r--src/qt/mainwindow.ui349
-rw-r--r--src/qt/settings.cpp44
-rw-r--r--src/qt/settings.hpp35
-rw-r--r--src/qt/settings.ui215
-rw-r--r--src/qt/workers.cpp299
-rw-r--r--src/qt/workers.hpp94
-rw-r--r--src/steam.c178
-rw-r--r--src/steam.h47
-rw-r--r--src/toast.c651
-rw-r--r--src/toast.h51
39 files changed, 3590 insertions, 0 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..e27aa46
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,44 @@
+
+find_package(Libcurl REQUIRED)
+find_package(JsonC REQUIRED)
+add_subdirectory(hash)
+
+set(CFLAGS
+ -Wall -Wextra -pedantic
+ -Wconversion -Wshadow -Wstrict-aliasing
+ -Winit-self -Wcast-align -Wpointer-arith
+ -Wmissing-declarations -Wmissing-include-dirs
+ -Wno-unused-parameter -Wuninitialized
+ ${LIBCURL_CFLAGS}
+ ${JSONC_CFLAGS}
+)
+
+list(APPEND
+ CORE_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/fs.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/fs.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/net.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/net.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/steam.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/steam.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/toast.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/toast.h
+)
+
+add_library(libofqt OBJECT ${CORE_SOURCES})
+target_compile_options(libofqt PUBLIC ${CFLAGS})
+
+target_include_directories(libofqt PUBLIC ${LIBCURL_INCLUDE_DIRS})
+target_include_directories(libofqt PUBLIC ${JSONC_INCLUDE_DIRS})
+target_include_directories(libofqt PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
+target_link_libraries(libofqt LINK_PUBLIC ${LIBCURL_LIBRARIES})
+target_link_libraries(libofqt LINK_PUBLIC ${JSONC_LIBRARIES})
+target_link_libraries(libofqt LINK_PUBLIC md5)
+
+if(BUILD_CLI)
+ add_subdirectory(cli)
+endif()
+
+if(BUILD_QT)
+ add_subdirectory(qt)
+endif()
diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt
new file mode 100644
index 0000000..fc46a42
--- /dev/null
+++ b/src/cli/CMakeLists.txt
@@ -0,0 +1,12 @@
+SET(FRONTEND_NAME "OFCL")
+
+SET(CLI_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/commands.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/commands.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/main.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/updater.c
+ ${CMAKE_CURRENT_SOURCE_DIR}/updater.h
+)
+
+add_executable(${FRONTEND_NAME} ${CLI_SOURCES})
+target_link_libraries(${FRONTEND_NAME} PRIVATE libofqt)
diff --git a/src/cli/commands.c b/src/cli/commands.c
new file mode 100644
index 0000000..4bac2f7
--- /dev/null
+++ b/src/cli/commands.c
@@ -0,0 +1,242 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "steam.h"
+#include "toast.h"
+
+#include "commands.h"
+#include "updater.h"
+
+#define ARRAY_LEN(arr) sizeof(arr) / sizeof(arr[0])
+
+static int install(int, char**);
+static int update(int, char**);
+static int run(int, char**);
+static int info(int, char**);
+
+const struct Command commands[] = {
+ { .name = "install", .func = install, .description = "Install OpenFortress"},
+ { .name = "update", .func = update, .description = "Update an existing install"},
+ { .name = "run", .func = run, .description = "Run OpenFortress"},
+ { .name = "info", .func = info, .description = "Show info about the current setup"},
+};
+const size_t commands_size = ARRAY_LEN(commands);
+
+static int install(int c, char** v)
+{
+ int exit_val = EXIT_SUCCESS;
+ char* of_dir = NULL;
+ char* remote = NULL;
+ for (int i = 1; i < c; ++i)
+ {
+ if (!strcmp(v[i], "--dir"))
+ {
+ if (!v[++i])
+ {
+ puts("No Directory specified");
+ return EXIT_FAILURE;
+ }
+ of_dir = strdup(v[i]);
+ printf("Using %s as the directory\n", of_dir);
+ }
+ else if (!strcmp(v[i], "--remote"))
+ {
+ if (!v[++i])
+ {
+ puts("No URL specified");
+ return EXIT_FAILURE;
+ }
+ remote = strdup(v[i]);
+ printf("Using %s as the server\n", remote);
+ }
+ else
+ {
+ if (strcmp(v[i], "--help"))
+ printf("%s is not a valid flag\n\n", v[i]);
+
+ puts(
+ "OFCL install\n"
+ "\t--dir\t\tspecify where to download OpenFortress to\n"
+ "\t--remote\tspecify the server to use\n"
+ "\t--help\t\tshows this text"
+ );
+ return EXIT_FAILURE;
+ }
+ }
+ if (!of_dir)
+ of_dir = getOpenFortressDir();
+
+ if (getLocalRevision(of_dir) >= 0)
+ {
+ puts("OpenFortress is already installed");
+ exit_val = EXIT_FAILURE;
+ goto install_cleanup;
+ }
+
+ if (!remote)
+ remote = getLocalRemote(of_dir);
+
+ int remote_rev = getLatestRemoteRevision(remote);
+
+ if (remote_rev != -1)
+ {
+ update_setup(of_dir, remote, 0, remote_rev);
+ }
+ else
+ {
+ puts("Failed to get the latest revision");
+ exit_val = EXIT_FAILURE;
+ }
+
+ install_cleanup:
+ if (remote)
+ free(remote);
+ free(of_dir);
+
+ return exit_val;
+}
+
+static int update(int c, char** v)
+{
+ int exit_val = EXIT_SUCCESS;
+ int force = 0;
+ char* of_dir = NULL;
+ char* remote = NULL;
+ for (int i = 1; i < c; ++i)
+ {
+ if (!strcmp(v[i], "--force"))
+ force = 1;
+ else if (!strcmp(v[i], "--dir"))
+ {
+ if (!v[++i])
+ {
+ puts("No Directory specified");
+ return EXIT_FAILURE;
+ }
+ of_dir = strdup(v[i]);
+ printf("Using %s as the directory\n", of_dir);
+ }
+ else if (!strcmp(v[i], "--remote"))
+ {
+ if (!v[++i])
+ {
+ puts("No URL specified");
+ return EXIT_FAILURE;
+ }
+ remote = strdup(v[i]);
+ printf("Using %s as the server\n", remote);
+ }
+ else
+ {
+ if (strcmp(v[i], "--help"))
+ printf("%s is not a valid flag\n\n", v[i]);
+
+ puts(
+ "OFCL update\n"
+ "\t--force\t\tforce update\n"
+ "\t--dir\t\tspecify where to download OpenFortress to\n"
+ "\t--remote\tspecify the server to use\n"
+ "\t--help\t\tshows this text"
+ );
+ return EXIT_FAILURE;
+ }
+ }
+
+ if (!of_dir)
+ of_dir = getOpenFortressDir();
+
+ int local_rev = getLocalRevision(of_dir);
+ if (force)
+ {
+ local_rev = 0;
+ }
+ else if (0 > local_rev)
+ {
+ puts("OpenFortress is not installed");
+ exit_val = EXIT_FAILURE;
+ goto update_cleanup;
+ }
+
+ if (!remote)
+ remote = getLocalRemote(of_dir);
+
+ if (!strlen(remote))
+ {
+ puts("Remote is invalid");
+ exit_val = EXIT_FAILURE;
+ goto update_cleanup;
+ }
+
+ int remote_rev = getLatestRemoteRevision(remote);
+
+ if (remote_rev == -1)
+ {
+ puts("Failed to get the latest revision");
+ exit_val = EXIT_FAILURE;
+ goto update_cleanup;
+ }
+ else if (remote_rev <= local_rev)
+ {
+ puts("Already up to date");
+ goto update_cleanup;
+ }
+
+ update_setup(of_dir, remote, local_rev, remote_rev);
+
+ update_cleanup:
+ if (remote)
+ free(remote);
+ free(of_dir);
+ return exit_val;
+}
+
+static int run(int c, char** v)
+{
+ int exit_val = EXIT_SUCCESS;
+ char* of_dir = getOpenFortressDir();
+
+ int local_rev = getLocalRevision(of_dir);
+ if (0 > local_rev)
+ {
+ puts("OpenFortress is not installed");
+ exit_val = EXIT_FAILURE;
+ goto run_cleanup;
+ }
+
+ if (getSteamPID() == -1)
+ {
+ puts("Steam is not running");
+ exit_val = EXIT_FAILURE;
+ goto run_cleanup;
+ }
+
+ runOpenFortress();
+
+ run_cleanup:
+ free(of_dir);
+ return exit_val;
+}
+
+static int info(int c, char** v)
+{
+ char* of_dir = getOpenFortressDir();
+ if (!of_dir)
+ of_dir = strdup("Not Found");
+ printf("Install Directory:\n\t%s\n", of_dir);
+
+ char* remote = getLocalRemote(of_dir);
+ printf("Server:\n\t%s\n", remote);
+ free(remote);
+
+ int local_rev = getLocalRevision(of_dir);
+ printf("Revision:\t\n");
+ if (local_rev == -1)
+ puts("\tNot installed");
+ else
+ printf("\t%i\n", local_rev);
+
+ free(of_dir);
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/cli/commands.h b/src/cli/commands.h
new file mode 100644
index 0000000..d55edb9
--- /dev/null
+++ b/src/cli/commands.h
@@ -0,0 +1,15 @@
+#ifndef COMMANDS_H
+#define COMMANDS_H
+
+#include <stddef.h>
+
+struct Command {
+ char* name;
+ int (*func)(int, char**);
+ char* description;
+};
+
+extern const struct Command commands[];
+extern const size_t commands_size;
+
+#endif \ No newline at end of file
diff --git a/src/cli/main.c b/src/cli/main.c
new file mode 100644
index 0000000..7971c7d
--- /dev/null
+++ b/src/cli/main.c
@@ -0,0 +1,50 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "net.h"
+#include "commands.h"
+
+static void help()
+{
+ fprintf(stderr, "OFCL <command>\n"); \
+
+ size_t longestStr;
+ size_t length;
+
+ if (commands_size)
+ {
+ longestStr = 0;
+
+ for (size_t i = 0; i < commands_size; ++i)
+ {
+ length = strlen(commands[i].name);
+
+ if (length > longestStr) longestStr = length;
+ }
+
+ fprintf(stderr, "\nList of commands:\n");
+ for (size_t i = 0; i < commands_size; ++i)
+ {
+ fprintf(stderr, "\t%-*s\t %s\n", (int)longestStr, commands[i].name, commands[i].description);
+ }
+ }
+}
+
+int main(int argc, char** argv)
+{
+ net_init();
+ atexit(net_deinit);
+
+ for (int i = 1; i < argc; ++i)
+ {
+ for (size_t j = 0; j < commands_size; ++j)
+ {
+ if (!strcmp(commands[j].name, argv[i]))
+ return commands[j].func(argc-i, argv+i);
+ }
+ }
+
+ help();
+ return EXIT_SUCCESS;
+} \ No newline at end of file
diff --git a/src/cli/updater.c b/src/cli/updater.c
new file mode 100644
index 0000000..8b0cc03
--- /dev/null
+++ b/src/cli/updater.c
@@ -0,0 +1,143 @@
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <pthread.h>
+
+#include "fs.h"
+#include "toast.h"
+
+#include "updater.h"
+
+#include <assert.h>
+
+#define THREAD_COUNT 8
+
+struct thread_object_info {
+ int working;
+
+ char* of_dir;
+ char* remote;
+ struct revision_t* rev;
+ size_t index;
+};
+
+static void* thread_download(void* pinfo)
+{
+ struct thread_object_info* info = pinfo;
+ if (info)
+ {
+ char* of_dir = info->of_dir;
+ char* remote = info->remote;
+ struct revision_t* rev = info->rev;
+ size_t i = info->index;
+
+ struct file_info* file = &rev->files[i];
+ if (file->type == TYPE_WRITE)
+ {
+ fprintf(stderr, "\rChecking %lu/%lu (%s)", i+1, rev->file_count, file->object);
+
+ if (verifyFileHash(of_dir, file))
+ {
+ fprintf(stderr, "\rDownloading %lu/%lu (%s)", i+1, rev->file_count, file->object);
+ downloadObject(of_dir, remote, file);
+ }
+ }
+ }
+
+ info->working = 0;
+ pthread_exit(0);
+
+ return NULL;
+}
+
+
+void update_setup(char* of_dir, char* remote, int local_rev, int remote_rev)
+{
+ struct revision_t* rev = fastFowardRevisions(remote, local_rev, remote_rev);
+
+ if (rev)
+ {
+ pthread_t download_threads[THREAD_COUNT] = {0};
+ struct thread_object_info thread_info[THREAD_COUNT] = {0};
+ size_t tindex = 0;
+
+ for (size_t i = 0; i < rev->file_count; ++i)
+ {
+ while (thread_info[tindex].working)
+ {
+ tindex = (tindex+1) % THREAD_COUNT;
+ }
+
+ pthread_t* thread = &download_threads[tindex];
+ struct thread_object_info* info = &thread_info[tindex];
+
+ info->working = 1;
+ info->of_dir = of_dir;
+ info->remote = remote;
+ info->rev = rev;
+ info->index = i;
+
+ pthread_create(thread, NULL, thread_download, info);
+ }
+
+ for (size_t i = 0; i < THREAD_COUNT; ++i)
+ {
+ pthread_t* thread = &download_threads[i];
+ if (*thread)
+ pthread_join(*thread, NULL);
+ }
+
+ for (size_t i = 0; i < rev->file_count; ++i)
+ {
+ struct file_info* file = &rev->files[i];
+ if (file->type != TYPE_MKDIR)
+ continue;
+
+ size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1;
+ char* buf = malloc(len);
+ snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path);
+ makeDir(buf);
+ free(buf);
+ }
+
+ fprintf(stderr, "\rInstalling...");
+ for (size_t i = 0; i < rev->file_count; ++i)
+ {
+ struct file_info* file = &rev->files[i];
+
+ switch (file->type)
+ {
+ case TYPE_WRITE:
+ case TYPE_MKDIR:
+ {
+ int k = applyObject(of_dir, file);
+ if (k)
+ {
+ printf("Failed to write %s\n", file->path);
+ }
+ }
+ break;
+
+ case TYPE_DELETE:
+ {
+ size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1;
+ char* buf = malloc(len);
+ snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path);
+ if (isFile(buf) && remove(buf))
+ {
+ printf("Failed to delete %s\n", file->path);
+ }
+ free(buf);
+ }
+ break;
+ }
+ }
+
+ removeObjects(of_dir);
+ setLocalRemote(of_dir, remote);
+ setLocalRevision(of_dir, remote_rev);
+
+ fprintf(stderr, "\rUpdated OpenFortress\n");
+ freeRevision(rev);
+ }
+}
diff --git a/src/cli/updater.h b/src/cli/updater.h
new file mode 100644
index 0000000..71fc751
--- /dev/null
+++ b/src/cli/updater.h
@@ -0,0 +1,6 @@
+#ifndef UPDATER_H
+#define UPDATER_H
+
+void update_setup(char*, char*, int, int);
+
+#endif \ No newline at end of file
diff --git a/src/fs.c b/src/fs.c
new file mode 100644
index 0000000..ee7a39d
--- /dev/null
+++ b/src/fs.c
@@ -0,0 +1,136 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+#include <limits.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <dirent.h>
+
+#include "fs.h"
+
+#ifdef _WIN32
+#define mkdir(path, perm) mkdir(path)
+#endif
+
+static struct stat getStat(const char* path)
+{
+ // fill with 0s by default in the case stat fails
+ struct stat sb = {0};
+
+ // the return value signifies if stat failes (e.g. file not found)
+ // unimportant for us if it fails it won't touch sb
+ stat(path, &sb);
+
+ return sb;
+}
+
+int isFile(const char* path)
+{
+ struct stat sb = getStat(path);
+
+#ifndef _WIN32
+ if (S_ISLNK(sb.st_mode))
+ {
+ char buf[PATH_MAX];
+ readlink(path, buf, sizeof(buf));
+
+ return isFile(buf);
+ }
+#endif
+ return S_ISREG(sb.st_mode);
+}
+
+int isDir(const char* path)
+{
+ struct stat sb = getStat(path);
+
+#ifndef _WIN32
+ if (S_ISLNK(sb.st_mode))
+ {
+ char buf[PATH_MAX];
+ readlink(path, buf, sizeof(buf));
+
+ return isDir(buf);
+ }
+#endif
+ return S_ISDIR(sb.st_mode);
+}
+
+int makeDir(const char* path)
+{
+ char pathcpy[PATH_MAX];
+ char *index;
+
+ strncpy(pathcpy, path, PATH_MAX-1); // make a mutable copy of the path
+
+ for(index = pathcpy+1; *index; ++index)
+ {
+
+ if (*index == *OS_PATH_SEP)
+ {
+ *index = '\0';
+
+ if (mkdir(pathcpy, 0755) != 0)
+ {
+ if (errno != EEXIST)
+ return -1;
+ }
+
+ *index = *OS_PATH_SEP;
+ }
+ }
+
+ return mkdir(path, 0755);
+}
+
+int removeDir(const char* path)
+{
+ DIR *d = opendir(path);
+ size_t path_len = strlen(path);
+ int r = -1;
+
+ if (d) {
+ struct dirent *p;
+
+ r = 0;
+ while (!r && (p = readdir(d))) {
+ char *buf;
+ size_t len;
+
+ // Skip the names "." and ".." as we don't want to recurse on them.
+ if (!strcmp(p->d_name, ".") || !strcmp(p->d_name, ".."))
+ continue;
+
+ len = path_len + strlen(p->d_name) + 2;
+ buf = malloc(len);
+
+ if (buf) {
+ struct stat statbuf = {0};
+
+ snprintf(buf, len, "%s/%s", path, p->d_name);
+ if (!stat(buf, &statbuf)) {
+ if (S_ISDIR(statbuf.st_mode))
+ r = removeDir(buf);
+#ifndef _WIN32
+ else if (S_ISLNK(statbuf.st_mode))
+ r = unlink(buf);
+#endif
+ else
+ r = remove(buf);
+ }
+ else // it is very likely that we found a dangling symlink which is not detected by stat
+ {
+ r = unlink(buf);
+ }
+ free(buf);
+ }
+ }
+ closedir(d);
+ }
+
+ if (!r)
+ r = rmdir(path);
+
+ return r;
+}
diff --git a/src/fs.h b/src/fs.h
new file mode 100644
index 0000000..26cdd8b
--- /dev/null
+++ b/src/fs.h
@@ -0,0 +1,24 @@
+#ifndef FS_H
+#define FS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#ifdef _WIN32
+#define OS_PATH_SEP "\\"
+#else
+#define OS_PATH_SEP "/"
+#endif
+
+int isFile(const char*);
+int isDir(const char*);
+
+int makeDir(const char*);
+int removeDir(const char*);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif \ No newline at end of file
diff --git a/src/hash/CMakeLists.txt b/src/hash/CMakeLists.txt
new file mode 100644
index 0000000..7290091
--- /dev/null
+++ b/src/hash/CMakeLists.txt
@@ -0,0 +1,2 @@
+
+add_subdirectory(md5) \ No newline at end of file
diff --git a/src/hash/md5/CMakeLists.txt b/src/hash/md5/CMakeLists.txt
new file mode 100644
index 0000000..ff560b1
--- /dev/null
+++ b/src/hash/md5/CMakeLists.txt
@@ -0,0 +1,12 @@
+
+list(APPEND
+ MD5_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/global.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/md5.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/md5c.c
+)
+
+add_library(md5 STATIC ${MD5_SOURCES})
+target_compile_options(md5 PUBLIC ${CFLAGS})
+
+target_include_directories(md5 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
diff --git a/src/hash/md5/global.h b/src/hash/md5/global.h
new file mode 100644
index 0000000..302b3aa
--- /dev/null
+++ b/src/hash/md5/global.h
@@ -0,0 +1,32 @@
+/* GLOBAL.H - RSAREF types and constants
+ */
+
+/* PROTOTYPES should be set to one if and only if the compiler supports
+ function argument prototyping.
+The following makes PROTOTYPES default to 0 if it has not already
+ been defined with C compiler flags.
+ */
+#ifndef PROTOTYPES
+#define PROTOTYPES 0
+#endif
+
+#include <stdint.h>
+
+/* POINTER defines a generic pointer type */
+typedef unsigned char *POINTER;
+
+/* UINT2 defines a two byte word */
+typedef uint16_t UINT2;
+
+/* UINT4 defines a four byte word */
+typedef uint32_t UINT4;
+
+/* PROTO_LIST is defined depending on how PROTOTYPES is defined above.
+If using PROTOTYPES, then PROTO_LIST returns the list, otherwise it
+ returns an empty list.
+ */
+#if PROTOTYPES
+#define PROTO_LIST(list) list
+#else
+#define PROTO_LIST(list) ()
+#endif \ No newline at end of file
diff --git a/src/hash/md5/md5.h b/src/hash/md5/md5.h
new file mode 100644
index 0000000..11b3ddd
--- /dev/null
+++ b/src/hash/md5/md5.h
@@ -0,0 +1,38 @@
+/* MD5.H - header file for MD5C.C
+ */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+#include "global.h"
+
+/* MD5 context. */
+typedef struct {
+ UINT4 state[4]; /* state (ABCD) */
+ UINT4 count[2]; /* number of bits, modulo 2^64 (lsb first) */
+ unsigned char buffer[64]; /* input buffer */
+} MD5_CTX;
+
+void MD5Init PROTO_LIST ((MD5_CTX *));
+void MD5Update PROTO_LIST
+ ((MD5_CTX *, unsigned char *, unsigned int));
+void MD5Final PROTO_LIST ((unsigned char [16], MD5_CTX *));
diff --git a/src/hash/md5/md5c.c b/src/hash/md5/md5c.c
new file mode 100644
index 0000000..3682361
--- /dev/null
+++ b/src/hash/md5/md5c.c
@@ -0,0 +1,334 @@
+/* MD5C.C - RSA Data Security, Inc., MD5 message-digest algorithm
+ */
+
+/* Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All
+rights reserved.
+
+License to copy and use this software is granted provided that it
+is identified as the "RSA Data Security, Inc. MD5 Message-Digest
+Algorithm" in all material mentioning or referencing this software
+or this function.
+
+License is also granted to make and use derivative works provided
+that such works are identified as "derived from the RSA Data
+Security, Inc. MD5 Message-Digest Algorithm" in all material
+mentioning or referencing the derived work.
+
+RSA Data Security, Inc. makes no representations concerning either
+the merchantability of this software or the suitability of this
+software for any particular purpose. It is provided "as is"
+without express or implied warranty of any kind.
+
+These notices must be retained in any copies of any part of this
+documentation and/or software.
+ */
+
+#include "global.h"
+#include "md5.h"
+
+/* Constants for MD5Transform routine.
+ */
+#define S11 7
+#define S12 12
+#define S13 17
+#define S14 22
+#define S21 5
+#define S22 9
+#define S23 14
+#define S24 20
+#define S31 4
+#define S32 11
+#define S33 16
+#define S34 23
+#define S41 6
+#define S42 10
+#define S43 15
+#define S44 21
+
+static void MD5Transform PROTO_LIST ((UINT4 [4], unsigned char [64]));
+static void Encode PROTO_LIST
+ ((unsigned char *, UINT4 *, unsigned int));
+static void Decode PROTO_LIST
+ ((UINT4 *, unsigned char *, unsigned int));
+static void MD5_memcpy PROTO_LIST ((POINTER, POINTER, unsigned int));
+static void MD5_memset PROTO_LIST ((POINTER, int, unsigned int));
+
+static unsigned char PADDING[64] = {
+ 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+};
+
+/* F, G, H and I are basic MD5 functions.
+ */
+#define F(x, y, z) (((x) & (y)) | ((~x) & (z)))
+#define G(x, y, z) (((x) & (z)) | ((y) & (~z)))
+#define H(x, y, z) ((x) ^ (y) ^ (z))
+#define I(x, y, z) ((y) ^ ((x) | (~z)))
+
+/* ROTATE_LEFT rotates x left n bits.
+ */
+#define ROTATE_LEFT(x, n) (((x) << (n)) | ((x) >> (32-(n))))
+
+/* FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4.
+Rotation is separate from addition to prevent recomputation.
+ */
+#define FF(a, b, c, d, x, s, ac) { \
+ (a) += F ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define GG(a, b, c, d, x, s, ac) { \
+ (a) += G ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define HH(a, b, c, d, x, s, ac) { \
+ (a) += H ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+#define II(a, b, c, d, x, s, ac) { \
+ (a) += I ((b), (c), (d)) + (x) + (UINT4)(ac); \
+ (a) = ROTATE_LEFT ((a), (s)); \
+ (a) += (b); \
+ }
+
+/* MD5 initialization. Begins an MD5 operation, writing a new context.
+ */
+void MD5Init (context)
+MD5_CTX *context; /* context */
+{
+ context->count[0] = context->count[1] = 0;
+ /* Load magic initialization constants.
+*/
+ context->state[0] = 0x67452301;
+ context->state[1] = 0xefcdab89;
+ context->state[2] = 0x98badcfe;
+ context->state[3] = 0x10325476;
+}
+
+/* MD5 block update operation. Continues an MD5 message-digest
+ operation, processing another message block, and updating the
+ context.
+ */
+void MD5Update (context, input, inputLen)
+MD5_CTX *context; /* context */
+unsigned char *input; /* input block */
+unsigned int inputLen; /* length of input block */
+{
+ unsigned int i, index, partLen;
+
+ /* Compute number of bytes mod 64 */
+ index = (unsigned int)((context->count[0] >> 3) & 0x3F);
+
+ /* Update number of bits */
+ if ((context->count[0] += ((UINT4)inputLen << 3))
+ < ((UINT4)inputLen << 3))
+ context->count[1]++;
+ context->count[1] += ((UINT4)inputLen >> 29);
+
+ partLen = 64 - index;
+
+ /* Transform as many times as possible.
+*/
+ if (inputLen >= partLen) {
+ MD5_memcpy
+ ((POINTER)&context->buffer[index], (POINTER)input, partLen);
+ MD5Transform (context->state, context->buffer);
+
+ for (i = partLen; i + 63 < inputLen; i += 64)
+ MD5Transform (context->state, &input[i]);
+
+ index = 0;
+ }
+ else
+ i = 0;
+
+ /* Buffer remaining input */
+ MD5_memcpy
+ ((POINTER)&context->buffer[index], (POINTER)&input[i],
+ inputLen-i);
+}
+
+/* MD5 finalization. Ends an MD5 message-digest operation, writing the
+ the message digest and zeroizing the context.
+ */
+void MD5Final (digest, context)
+unsigned char digest[16]; /* message digest */
+MD5_CTX *context; /* context */
+{
+ unsigned char bits[8];
+ unsigned int index, padLen;
+
+ /* Save number of bits */
+ Encode (bits, context->count, 8);
+
+ /* Pad out to 56 mod 64.
+*/
+ index = (unsigned int)((context->count[0] >> 3) & 0x3f);
+ padLen = (index < 56) ? (56 - index) : (120 - index);
+ MD5Update (context, PADDING, padLen);
+
+ /* Append length (before padding) */
+ MD5Update (context, bits, 8);
+
+ /* Store state in digest */
+ Encode (digest, context->state, 16);
+
+ /* Zeroize sensitive information.
+*/
+ MD5_memset ((POINTER)context, 0, sizeof (*context));
+}
+
+/* MD5 basic transformation. Transforms state based on block.
+ */
+static void MD5Transform (state, block)
+UINT4 state[4];
+unsigned char block[64];
+{
+ UINT4 a = state[0], b = state[1], c = state[2], d = state[3], x[16];
+
+ Decode (x, block, 64);
+
+ /* Round 1 */
+ FF (a, b, c, d, x[ 0], S11, 0xd76aa478); /* 1 */
+ FF (d, a, b, c, x[ 1], S12, 0xe8c7b756); /* 2 */
+ FF (c, d, a, b, x[ 2], S13, 0x242070db); /* 3 */
+ FF (b, c, d, a, x[ 3], S14, 0xc1bdceee); /* 4 */
+ FF (a, b, c, d, x[ 4], S11, 0xf57c0faf); /* 5 */
+ FF (d, a, b, c, x[ 5], S12, 0x4787c62a); /* 6 */
+ FF (c, d, a, b, x[ 6], S13, 0xa8304613); /* 7 */
+ FF (b, c, d, a, x[ 7], S14, 0xfd469501); /* 8 */
+ FF (a, b, c, d, x[ 8], S11, 0x698098d8); /* 9 */
+ FF (d, a, b, c, x[ 9], S12, 0x8b44f7af); /* 10 */
+ FF (c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */
+ FF (b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */
+ FF (a, b, c, d, x[12], S11, 0x6b901122); /* 13 */
+ FF (d, a, b, c, x[13], S12, 0xfd987193); /* 14 */
+ FF (c, d, a, b, x[14], S13, 0xa679438e); /* 15 */
+ FF (b, c, d, a, x[15], S14, 0x49b40821); /* 16 */
+
+ /* Round 2 */
+ GG (a, b, c, d, x[ 1], S21, 0xf61e2562); /* 17 */
+ GG (d, a, b, c, x[ 6], S22, 0xc040b340); /* 18 */
+ GG (c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */
+ GG (b, c, d, a, x[ 0], S24, 0xe9b6c7aa); /* 20 */
+ GG (a, b, c, d, x[ 5], S21, 0xd62f105d); /* 21 */
+ GG (d, a, b, c, x[10], S22, 0x2441453); /* 22 */
+ GG (c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */
+ GG (b, c, d, a, x[ 4], S24, 0xe7d3fbc8); /* 24 */
+ GG (a, b, c, d, x[ 9], S21, 0x21e1cde6); /* 25 */
+ GG (d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */
+ GG (c, d, a, b, x[ 3], S23, 0xf4d50d87); /* 27 */
+ GG (b, c, d, a, x[ 8], S24, 0x455a14ed); /* 28 */
+ GG (a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */
+ GG (d, a, b, c, x[ 2], S22, 0xfcefa3f8); /* 30 */
+ GG (c, d, a, b, x[ 7], S23, 0x676f02d9); /* 31 */
+ GG (b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */
+
+ /* Round 3 */
+ HH (a, b, c, d, x[ 5], S31, 0xfffa3942); /* 33 */
+ HH (d, a, b, c, x[ 8], S32, 0x8771f681); /* 34 */
+ HH (c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */
+ HH (b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */
+ HH (a, b, c, d, x[ 1], S31, 0xa4beea44); /* 37 */
+ HH (d, a, b, c, x[ 4], S32, 0x4bdecfa9); /* 38 */
+ HH (c, d, a, b, x[ 7], S33, 0xf6bb4b60); /* 39 */
+ HH (b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */
+ HH (a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */
+ HH (d, a, b, c, x[ 0], S32, 0xeaa127fa); /* 42 */
+ HH (c, d, a, b, x[ 3], S33, 0xd4ef3085); /* 43 */
+ HH (b, c, d, a, x[ 6], S34, 0x4881d05); /* 44 */
+ HH (a, b, c, d, x[ 9], S31, 0xd9d4d039); /* 45 */
+ HH (d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */
+ HH (c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */
+ HH (b, c, d, a, x[ 2], S34, 0xc4ac5665); /* 48 */
+
+ /* Round 4 */
+ II (a, b, c, d, x[ 0], S41, 0xf4292244); /* 49 */
+ II (d, a, b, c, x[ 7], S42, 0x432aff97); /* 50 */
+ II (c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */
+ II (b, c, d, a, x[ 5], S44, 0xfc93a039); /* 52 */
+ II (a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */
+ II (d, a, b, c, x[ 3], S42, 0x8f0ccc92); /* 54 */
+ II (c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */
+ II (b, c, d, a, x[ 1], S44, 0x85845dd1); /* 56 */
+ II (a, b, c, d, x[ 8], S41, 0x6fa87e4f); /* 57 */
+ II (d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */
+ II (c, d, a, b, x[ 6], S43, 0xa3014314); /* 59 */
+ II (b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */
+ II (a, b, c, d, x[ 4], S41, 0xf7537e82); /* 61 */
+ II (d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */
+ II (c, d, a, b, x[ 2], S43, 0x2ad7d2bb); /* 63 */
+ II (b, c, d, a, x[ 9], S44, 0xeb86d391); /* 64 */
+
+ state[0] += a;
+ state[1] += b;
+ state[2] += c;
+ state[3] += d;
+
+ /* Zeroize sensitive information.
+*/
+ MD5_memset ((POINTER)x, 0, sizeof (x));
+}
+
+/* Encodes input (UINT4) into output (unsigned char). Assumes len is
+ a multiple of 4.
+ */
+static void Encode (output, input, len)
+unsigned char *output;
+UINT4 *input;
+unsigned int len;
+{
+ unsigned int i, j;
+
+ for (i = 0, j = 0; j < len; i++, j += 4) {
+ output[j] = (unsigned char)(input[i] & 0xff);
+ output[j+1] = (unsigned char)((input[i] >> 8) & 0xff);
+ output[j+2] = (unsigned char)((input[i] >> 16) & 0xff);
+ output[j+3] = (unsigned char)((input[i] >> 24) & 0xff);
+ }
+}
+
+/* Decodes input (unsigned char) into output (UINT4). Assumes len is
+ a multiple of 4.
+ */
+static void Decode (output, input, len)
+UINT4 *output;
+unsigned char *input;
+unsigned int len;
+{
+ unsigned int i, j;
+
+ for (i = 0, j = 0; j < len; i++, j += 4)
+ output[i] = ((UINT4)input[j]) | (((UINT4)input[j+1]) << 8) |
+ (((UINT4)input[j+2]) << 16) | (((UINT4)input[j+3]) << 24);
+}
+
+/* Note: Replace "for loop" with standard memcpy if possible.
+ */
+
+static void MD5_memcpy (output, input, len)
+POINTER output;
+POINTER input;
+unsigned int len;
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++)
+ output[i] = input[i];
+}
+
+/* Note: Replace "for loop" with standard memset if possible.
+ */
+static void MD5_memset (output, value, len)
+POINTER output;
+int value;
+unsigned int len;
+{
+ unsigned int i;
+
+ for (i = 0; i < len; i++)
+ ((char *)output)[i] = (char)value;
+}
diff --git a/src/net.c b/src/net.c
new file mode 100644
index 0000000..0ae8e23
--- /dev/null
+++ b/src/net.c
@@ -0,0 +1,128 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <curl/curl.h>
+#include <json.h>
+
+#include "net.h"
+#include "fs.h"
+
+#define USER_AGENT NAME "/" VERSION
+
+void net_init()
+{
+ curl_global_init(CURL_GLOBAL_ALL);
+}
+
+void net_deinit()
+{
+ curl_global_cleanup();
+}
+
+static inline size_t memoryCallback(const void* contents, size_t size, size_t nmemb, void* userp)
+{
+ size_t realsize = size * nmemb;
+ struct MemoryStruct* mem = (struct MemoryStruct*)userp;
+
+ uint8_t* ptr = realloc(mem->memory, mem->size + realsize + 1);
+ if (!ptr)
+ return 0;
+
+ mem->memory = ptr;
+ memcpy(&(mem->memory[mem->size]), contents, realsize);
+ mem->size += realsize;
+ mem->memory[mem->size] = 0;
+
+ return realsize;
+}
+
+struct MemoryStruct* downloadToRam(const char* url)
+{
+ CURL* curl_handle;
+ CURLcode res = CURLE_OK;
+
+ struct MemoryStruct* chunk = malloc(sizeof(struct MemoryStruct));
+
+ if (chunk)
+ {
+ chunk->memory = malloc(1);
+ if (!chunk->memory)
+ {
+ free(chunk);
+ return NULL;
+ }
+ chunk->memory[0] = 0;
+ chunk->size = 0;
+
+ curl_handle = curl_easy_init();
+
+ curl_easy_setopt(curl_handle, CURLOPT_URL, url);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, memoryCallback);
+ curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*)chunk);
+ curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, USER_AGENT);
+ curl_easy_setopt(curl_handle, CURLOPT_FOLLOWLOCATION, 1L);
+ curl_easy_setopt(curl_handle, CURLOPT_NOPROGRESS, 1L);
+
+ res = curl_easy_perform(curl_handle);
+
+ long http_code = 0;
+ curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
+
+ if (res != CURLE_OK || http_code != 200)
+ {
+ freeDownload(chunk);
+ chunk = NULL;
+ }
+
+ curl_easy_cleanup(curl_handle);
+ }
+
+ return chunk;
+}
+
+size_t downloadToFile(const char* url, const char* path)
+{
+ size_t out_write = 0;
+ struct MemoryStruct* chunk = downloadToRam(url);
+
+ if (chunk)
+ {
+ FILE* fp = fopen(path, "wb");
+
+ if (fp)
+ {
+ out_write = fwrite(chunk->memory, sizeof(uint8_t), chunk->size, fp);
+ fclose(fp);
+ }
+
+ freeDownload(chunk);
+ }
+
+ return out_write;
+}
+
+void freeDownload(struct MemoryStruct* chunk)
+{
+ if (chunk)
+ {
+ if (chunk->memory)
+ free(chunk->memory);
+ free(chunk);
+ }
+}
+
+struct json_object* fetchJSON(const char* url)
+{
+ struct json_object* json = NULL;
+ struct MemoryStruct* chunk = downloadToRam(url);
+
+ if (chunk)
+ {
+ json = json_tokener_parse((char*)chunk->memory);
+ freeDownload(chunk);
+ }
+
+ return json;
+}
diff --git a/src/net.h b/src/net.h
new file mode 100644
index 0000000..fc6d34a
--- /dev/null
+++ b/src/net.h
@@ -0,0 +1,28 @@
+#ifndef NET_H
+#define NET_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <curl/curl.h>
+#include <json.h>
+
+struct MemoryStruct {
+ uint8_t* memory;
+ size_t size;
+};
+
+void net_init();
+void net_deinit();
+
+struct MemoryStruct* downloadToRam(const char* URL);
+size_t downloadToFile(const char*, const char*);
+void freeDownload(struct MemoryStruct* chunk);
+struct json_object* fetchJSON(const char*);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt
new file mode 100644
index 0000000..c0566d9
--- /dev/null
+++ b/src/qt/CMakeLists.txt
@@ -0,0 +1,34 @@
+SET(FRONTEND_NAME "OFQT")
+enable_language(CXX)
+
+find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Gui REQUIRED)
+find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Gui REQUIRED)
+
+set(CMAKE_AUTOUIC ON)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+
+list(APPEND
+ QT_SOURCES
+ ${CMAKE_CURRENT_SOURCE_DIR}/main.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.hpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/settings.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/settings.hpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/workers.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/workers.hpp
+
+ ${CMAKE_CURRENT_SOURCE_DIR}/mainwindow.ui
+ ${CMAKE_CURRENT_SOURCE_DIR}/assets.qrc
+)
+
+if(WIN32)
+ string(REPLACE "." "," CMAKE_PROJECT_COMMAVERSION ${CMAKE_PROJECT_VERSION})
+ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/assets/version.rc.in ${CMAKE_CURRENT_BINARY_DIR}/version.rc @ONLY)
+ list(APPEND QT_SOURCES ${CMAKE_CURRENT_BINARY_DIR}/version.rc)
+endif()
+
+add_executable(${FRONTEND_NAME} WIN32 ${QT_SOURCES})
+target_link_libraries(${FRONTEND_NAME} PRIVATE libofqt)
+target_link_libraries(${FRONTEND_NAME} PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
+set_property(TARGET ${FRONTEND_NAME} PROPERTY CXX_STANDARD 11)
diff --git a/src/qt/assets.qrc b/src/qt/assets.qrc
new file mode 100644
index 0000000..10bca84
--- /dev/null
+++ b/src/qt/assets.qrc
@@ -0,0 +1,15 @@
+<RCC>
+ <qresource prefix="background">
+ <file>assets/background.bmp</file>
+ </qresource>
+ <qresource prefix="icon">
+ <file>assets/globe.png</file>
+ <file>assets/logo.png</file>
+ <file>assets/gear-solid.png</file>
+ <file>assets/discord.png</file>
+ <file>assets/game.ico</file>
+ </qresource>
+ <qresource prefix="font">
+ <file>assets/tf2build.ttf</file>
+ </qresource>
+</RCC>
diff --git a/src/qt/assets/background.bmp b/src/qt/assets/background.bmp
new file mode 100644
index 0000000..37156be
--- /dev/null
+++ b/src/qt/assets/background.bmp
Binary files differ
diff --git a/src/qt/assets/discord.png b/src/qt/assets/discord.png
new file mode 100644
index 0000000..cc52539
--- /dev/null
+++ b/src/qt/assets/discord.png
Binary files differ
diff --git a/src/qt/assets/game.ico b/src/qt/assets/game.ico
new file mode 100644
index 0000000..7ebf98a
--- /dev/null
+++ b/src/qt/assets/game.ico
Binary files differ
diff --git a/src/qt/assets/gear-solid.png b/src/qt/assets/gear-solid.png
new file mode 100644
index 0000000..8540b00
--- /dev/null
+++ b/src/qt/assets/gear-solid.png
Binary files differ
diff --git a/src/qt/assets/globe.png b/src/qt/assets/globe.png
new file mode 100644
index 0000000..98374e3
--- /dev/null
+++ b/src/qt/assets/globe.png
Binary files differ
diff --git a/src/qt/assets/logo.png b/src/qt/assets/logo.png
new file mode 100644
index 0000000..ad04eca
--- /dev/null
+++ b/src/qt/assets/logo.png
Binary files differ
diff --git a/src/qt/assets/tf2build.ttf b/src/qt/assets/tf2build.ttf
new file mode 100644
index 0000000..d59eec9
--- /dev/null
+++ b/src/qt/assets/tf2build.ttf
Binary files differ
diff --git a/src/qt/assets/version.rc.in b/src/qt/assets/version.rc.in
new file mode 100644
index 0000000..06d3430
--- /dev/null
+++ b/src/qt/assets/version.rc.in
@@ -0,0 +1,21 @@
+IDI_ICON1 ICON DISCARDABLE "@CMAKE_CURRENT_SOURCE_DIR@/assets/game.ico"
+1 VERSIONINFO
+FILEVERSION @CMAKE_PROJECT_COMMAVERSION@
+PRODUCTVERSION @CMAKE_PROJECT_COMMAVERSION@
+BEGIN
+ BLOCK "StringFileInfo"
+ BEGIN
+ BLOCK "040904E4"
+ BEGIN
+ VALUE "FileVersion", "1.0"
+ VALUE "InternalName", "@CMAKE_PROJECT_NAME@"
+ VALUE "OriginalFilename", "@FRONTEND_NAME@.exe"
+ VALUE "ProductName", "@CMAKE_PROJECT_NAME@"
+ VALUE "ProductVersion", "@CMAKE_PROJECT_VERSION@"
+ END
+ END
+ BLOCK "VarFileInfo"
+ BEGIN
+ VALUE "Translation", 0x409, 1252
+ END
+END \ No newline at end of file
diff --git a/src/qt/main.cpp b/src/qt/main.cpp
new file mode 100644
index 0000000..9cf530b
--- /dev/null
+++ b/src/qt/main.cpp
@@ -0,0 +1,12 @@
+#include "mainwindow.hpp"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ MainWindow w;
+ w.show();
+
+ return a.exec();
+}
diff --git a/src/qt/mainwindow.cpp b/src/qt/mainwindow.cpp
new file mode 100644
index 0000000..35151ca
--- /dev/null
+++ b/src/qt/mainwindow.cpp
@@ -0,0 +1,249 @@
+#include <QApplication>
+#include <QMessageBox>
+#include <QFontDatabase>
+#include <QDesktopServices>
+#include <QUrl>
+
+#include <limits.h>
+#include <iostream>
+
+#include "steam.h"
+
+#include "mainwindow.hpp"
+#include "./ui_mainwindow.h"
+#include "workers.hpp"
+
+
+
+#define FONT "tf2build"
+
+MainWindow::MainWindow(QWidget *parent)
+ : QMainWindow(parent)
+ , ui(new Ui::MainWindow)
+{
+ ui->setupUi(this);
+
+ centralWidget()->layout()->setContentsMargins(0, 0, 0, 0);
+
+ qRegisterMetaType<Worker::Tasks_t>("Task_t");
+ qRegisterMetaType<Worker::Results_t>("Results_t");
+
+ QFontDatabase::addApplicationFont (":/font/assets/" FONT ".ttf");
+ QFont playFont(FONT, 20, QFont::Bold);
+ QFont progressFont(FONT, 10, QFont::Normal);
+
+ ui->mainButton->setFont(playFont);
+ ui->progressBar->setFont(progressFont);
+ ui->statusLabel->setFont(progressFont);
+ ui->infoLabel->setFont(progressFont);
+
+ QPixmap bkgnd(":/background/assets/background.bmp");
+ bkgnd = bkgnd.scaled(this->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
+
+ QPalette palette;
+ palette.setBrush(QPalette::Window, bkgnd);
+ this->setPalette(palette);
+
+ worker = new Worker();
+ worker->moveToThread(&thread);
+
+ connect(&thread, &QThread::finished, worker, &QObject::deleteLater);
+ connect(this, &MainWindow::workerOperate, worker, &Worker::doWork);
+ connect(worker, &Worker::resultReady, this, &MainWindow::workerResult);
+
+ thread.start();
+
+ //operateSVN(svnWorker::SVN_INSTALL);
+
+ connect(ui->settingsButton, SIGNAL(clicked()), this, SLOT(settingsWindow()));
+
+ settings = new Settings(worker, this);
+ settings->setModal(true);
+ //connect(settings, SIGNAL(visibleChanged()), this, SLOT(enable()));
+
+ ui->mainButton->setContextMenuPolicy(Qt::CustomContextMenu);
+
+ connect(ui->mainButton, SIGNAL(clicked()), this, SLOT(updateButton()));
+ connect(ui->mainButton, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(showButtonContext(const QPoint&)));
+ connect(ui->buttonDiscord, SIGNAL(clicked()), this, SLOT(openDiscordInvite()));
+ connect(ui->buttonWebsite, SIGNAL(clicked()), this, SLOT(openWebsite()));
+
+ ui->mainButton->setText("...");
+ ui->statusLabel->setText("...");
+ ui->infoLabel->setText("...");
+
+ in_progress = false;
+ installed = false;
+ uptodate = false;
+ workerOperate(Worker::TASK_INIT);
+}
+
+void MainWindow::workerResult(const enum Worker::Results_t& result)
+{
+ switch (result)
+ {
+
+ case Worker::RESULT_UNINSTALL_COMPLETE:
+ case Worker::RESULT_UNINSTALL_FAILURE:
+ // TODO find better use for these
+ case Worker::RESULT_NONE:
+ resetProgress();
+ break;
+
+ case Worker::RESULT_EXIT:
+ QCoreApplication::quit();
+ break;
+
+ case Worker::RESULT_UPDATE_TEXT:
+ ui->progressBar->setValue(worker->progress);
+ ui->infoLabel->setText(worker->infoText);
+ break;
+
+ case Worker::RESULT_IS_INSTALLED:
+ installed = true;
+ workerOperate(Worker::TASK_IS_UPTODATE);
+ break;
+
+ case Worker::RESULT_IS_NOT_INSTALLED:
+ ui->mainButton->setText("Install");
+ installed = false;
+ break;
+
+ case Worker::RESULT_IS_UPTODATE:
+ uptodate = true;
+ ui->mainButton->setText("Play");
+ ui->statusLabel->setText("Up to Date");
+ break;
+
+ case Worker::RESULT_IS_OUTDATED:
+ uptodate = false;
+ ui->mainButton->setText("Update");
+ ui->statusLabel->setText(QString("Revision %1 is available").arg(worker->getRemoteRevision()));
+ break;
+
+ case Worker::RESULT_INIT_COMPLETE:
+ ui->statusLabel->setText("");
+ ui->infoLabel->setText("");
+ workerOperate(Worker::TASK_IS_INSTALLED);
+ break;
+
+ case Worker::RESULT_INIT_FAILURE:
+ QMessageBox::information(this, windowTitle(), "Could not find install location.\nIs Steam installed?");
+ QCoreApplication::quit();
+ break;
+
+ case Worker::RESULT_INSTALL_COMPLETE:
+ resetProgress();
+ ui->statusLabel->setText("Installed");
+ workerOperate(Worker::TASK_IS_INSTALLED);
+ break;
+
+ case Worker::RESULT_INSTALL_FAILURE:
+ ui->progressBar->setFormat("Install failed");
+ break;
+
+ case Worker::RESULT_UPDATE_COMPLETE:
+ resetProgress();
+ ui->statusLabel->setText("Updated");
+ workerOperate(Worker::TASK_IS_UPTODATE);
+ break;
+
+ case Worker::RESULT_UPDATE_RUN:
+ ui->statusLabel->setText("Launching");
+ workerOperate(Worker::TASK_RUN);
+ break;
+
+ case Worker::RESULT_UPDATE_FAILURE:
+ ui->statusLabel->setText("Update failed");
+ break;
+
+ case Worker::RESULT_NO_STEAM:
+ resetProgress();
+ QMessageBox::information(this, windowTitle(), "Steam is not running" );
+ break;
+
+ }
+
+ in_progress = false;
+}
+
+void MainWindow::settingsWindow()
+{
+ settings->refresh();
+ settings->show();
+ //this->setEnabled(false);
+}
+
+void MainWindow::setupButton()
+{
+ workerOperate(Worker::TASK_IS_INSTALLED);
+}
+
+void MainWindow::updateButton()
+{
+ if (in_progress)
+ return;
+
+ if (installed)
+ {
+ if (!uptodate)
+ {
+ workerOperate(Worker::TASK_UPDATE);
+ ui->statusLabel->setText("Updating (may take a while)");
+ }
+ else
+ {
+ workerOperate(Worker::TASK_RUN);
+ }
+ }
+ else
+ {
+ workerOperate(Worker::TASK_INSTALL);
+ ui->statusLabel->setText("Installing (may take a while)");
+ }
+
+ in_progress = true;
+}
+
+void MainWindow::showButtonContext(const QPoint& pos)
+{
+ QPoint absPos = ui->mainButton->mapToGlobal(pos);
+
+ QMenu ctxMenu;
+ ctxMenu.addAction("Run without Update");
+
+ QAction* selectedItem = ctxMenu.exec(absPos);
+
+ if (selectedItem)
+ {
+ workerOperate(Worker::TASK_RUN);
+ }
+}
+
+
+void MainWindow::openDiscordInvite()
+{
+ QDesktopServices::openUrl(QUrl("https://discord.gg/mKjW2ACCrm", QUrl::TolerantMode));
+}
+
+void MainWindow::openWebsite()
+{
+ QDesktopServices::openUrl(QUrl("https://openfortress.fun/", QUrl::TolerantMode));
+}
+
+void MainWindow::resetProgress()
+{
+ ui->progressBar->setFormat("");
+ ui->progressBar->setValue(-1);
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+ delete settings;
+
+ worker->stop_work();
+ thread.quit();
+ thread.wait();
+}
+
diff --git a/src/qt/mainwindow.hpp b/src/qt/mainwindow.hpp
new file mode 100644
index 0000000..0d21efc
--- /dev/null
+++ b/src/qt/mainwindow.hpp
@@ -0,0 +1,50 @@
+#ifndef MAINWINDOW_HPP
+#define MAINWINDOW_HPP
+
+#include <QThread>
+#include <QMainWindow>
+#include <QMenu>
+
+#include "settings.hpp"
+#include "workers.hpp"
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+public:
+ MainWindow(QWidget* parent = nullptr);
+ ~MainWindow();
+
+private:
+ Ui::MainWindow *ui;
+ Settings* settings;
+ QThread thread;
+ Worker* worker;
+
+ bool in_progress;
+ bool installed;
+ bool uptodate;
+
+ void setupButton();
+ void resetProgress();
+
+public slots:
+ void workerResult(const Worker::Results_t&);
+
+private slots:
+ void settingsWindow();
+ void updateButton();
+ void showButtonContext(const QPoint&);
+ void openDiscordInvite();
+ void openWebsite();
+
+signals:
+ void workerOperate(const Worker::Tasks_t&);
+
+};
+#endif // MAINWINDOW_HPP
diff --git a/src/qt/mainwindow.ui b/src/qt/mainwindow.ui
new file mode 100644
index 0000000..574129b
--- /dev/null
+++ b/src/qt/mainwindow.ui
@@ -0,0 +1,349 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>736</width>
+ <height>413</height>
+ </rect>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>736</width>
+ <height>413</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>736</width>
+ <height>413</height>
+ </size>
+ </property>
+ <property name="windowTitle">
+ <string>OFQT</string>
+ </property>
+ <property name="windowIcon">
+ <iconset resource="assets.qrc">
+ <normaloff>:/icon/assets/game.ico</normaloff>:/icon/assets/game.ico</iconset>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <widget class="QWidget" name="centralwidget">
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <layout class="QVBoxLayout" name="layoutLeft">
+ <item>
+ <widget class="QLabel" name="logoOF">
+ <property name="styleSheet">
+ <string notr="true">QLabel {
+ margin: 10px;
+}</string>
+ </property>
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;img width=&quot;300&quot; src=&quot;:/icon/assets/logo.png&quot;/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="layoutRight">
+ <item>
+ <layout class="QHBoxLayout" name="layoutSocial">
+ <property name="leftMargin">
+ <number>6</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="rightMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="buttonWebsite">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>45</width>
+ <height>45</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QPushButton {
+ image: url(:/icon/assets/globe.png);
+ background-color: rgba(0, 0, 0, 0);
+ border: 0;
+}</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="buttonDiscord">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>45</width>
+ <height>45</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QPushButton {
+ image: url(:/icon/assets/discord.png);
+ background-color: rgba(0, 0, 0, 0);
+ border: 0;
+}</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="sizeConstraint">
+ <enum>QLayout::SetMinimumSize</enum>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="statusLayout">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_4">
+ <item>
+ <widget class="QLabel" name="statusLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel {
+ color: white;
+ margin-left: 5px;
+ margin-right: 5px;
+}</string>
+ </property>
+ <property name="text">
+ <string>STATUS</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignBottom|Qt::AlignLeading|Qt::AlignLeft</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="infoLabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QLabel {
+ color: white;
+ margin-left: 5px;
+ margin-right: 5px;
+}</string>
+ </property>
+ <property name="text">
+ <string>INFO</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignBottom|Qt::AlignRight|Qt::AlignTrailing</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progressBar">
+ <property name="styleSheet">
+ <string notr="true">QProgressBar
+{
+ text-align: center;
+ border: none;
+ background-color: rgba(0, 0, 0, 140);
+ color: white;
+}
+
+QProgressBar::chunk
+{
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0,
+ stop:0 rgba(42, 28, 77, 255),
+ stop:1 rgba(73, 43, 133, 255));
+ border: none;
+}</string>
+ </property>
+ <property name="minimum">
+ <number>0</number>
+ </property>
+ <property name="value">
+ <number>-1</number>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignCenter</set>
+ </property>
+ <property name="invertedAppearance">
+ <bool>false</bool>
+ </property>
+ <property name="textDirection">
+ <enum>QProgressBar::TopToBottom</enum>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QPushButton" name="settingsButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>60</width>
+ <height>60</height>
+ </size>
+ </property>
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QPushButton {
+ padding: 10px;
+ image: url(:/icon/assets/gear-solid.png);
+ background-color: rgba(0, 0, 0, 140);
+ border: none;
+ color: white;
+}
+
+QPushButton:pressed {
+ /*background-color: rgba(0, 0, 0, 200);*/
+}
+</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="mainButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>175</width>
+ <height>60</height>
+ </size>
+ </property>
+ <property name="styleSheet">
+ <string notr="true">QPushButton {
+ /*background-color: rgba(0, 0, 0, 140);*/
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(38, 33, 76, 255), stop:0.0534188 rgba(38, 33, 76, 255), stop:0.0566239 rgba(0, 0, 0, 140));
+ border: none;
+ color: white;
+}
+
+QPushButton:pressed {
+ /*background-color: rgba(0, 0, 0, 200);*/
+ background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0 rgba(38, 33, 76, 255), stop:0.0534188 rgba(38, 33, 76, 255), stop:0.0566239 rgba(0, 0, 0, 200));
+}
+</string>
+ </property>
+ <property name="text">
+ <string>MAIN_BUTTON</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ <tabstops>
+ <tabstop>mainButton</tabstop>
+ <tabstop>settingsButton</tabstop>
+ <tabstop>buttonDiscord</tabstop>
+ </tabstops>
+ <resources>
+ <include location="assets.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/qt/settings.cpp b/src/qt/settings.cpp
new file mode 100644
index 0000000..507e8c8
--- /dev/null
+++ b/src/qt/settings.cpp
@@ -0,0 +1,44 @@
+#include <QMessageBox>
+#include <iostream>
+
+#include "workers.hpp"
+#include "settings.hpp"
+#include "./ui_settings.h"
+
+Settings::Settings(Worker* pworker, QWidget *parent)
+ : QDialog(parent)
+ , ui(new Ui::Settings)
+{
+ this->worker = pworker;
+ ui->setupUi(this);
+
+ connect(ui->buttonBox, SIGNAL(accepted()), this, SLOT(applySettings()));
+ connect(ui->verifyButton, SIGNAL(clicked()), this, SLOT(verify()));
+ connect(this, &Settings::workerOperate, worker, &Worker::doWork);
+}
+
+void Settings::refresh()
+{
+ ui->serverEdit->setText(this->worker->getRemote());
+
+ ui->revisionLabel->setText(QString("%1").arg(this->worker->getRevision()));
+ ui->installLabel->setText(this->worker->getOfDir());
+}
+
+void Settings::applySettings()
+{
+ worker->setRemote(ui->serverEdit->text());
+ this->hide();
+}
+
+void Settings::verify()
+{
+ workerOperate(Worker::TASK_INSTALL);
+ QMessageBox::information(this, windowTitle(), "Verification Started" );
+}
+
+Settings::~Settings()
+{
+ delete ui;
+}
+
diff --git a/src/qt/settings.hpp b/src/qt/settings.hpp
new file mode 100644
index 0000000..3718f06
--- /dev/null
+++ b/src/qt/settings.hpp
@@ -0,0 +1,35 @@
+#ifndef SETTINGS_HPP
+#define SETTINGS_HPP
+
+#include <QWidget>
+#include <QDialog>
+
+#include "workers.hpp"
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class Settings; }
+QT_END_NAMESPACE
+
+class Settings : public QDialog
+{
+ Q_OBJECT
+
+public:
+ Settings(Worker* pworker, QWidget *parent = nullptr);
+ ~Settings();
+
+ void refresh();
+
+private:
+ Ui::Settings *ui;
+ Worker* worker;
+
+public slots:
+ void applySettings();
+ void verify();
+
+signals:
+ void workerOperate(const Worker::Tasks_t &);
+
+};
+#endif // SETTINGS_HPP
diff --git a/src/qt/settings.ui b/src/qt/settings.ui
new file mode 100644
index 0000000..4fde446
--- /dev/null
+++ b/src/qt/settings.ui
@@ -0,0 +1,215 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>Settings</class>
+ <widget class="QWidget" name="Settings">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>375</width>
+ <height>259</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Settings</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="serverTab">
+ <attribute name="title">
+ <string>Server</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="serverLabel">
+ <property name="text">
+ <string>Server</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="serverEdit"/>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="infoTab">
+ <attribute name="title">
+ <string>Info</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <item>
+ <layout class="QHBoxLayout" name="revisionLayout">
+ <item>
+ <widget class="QLabel" name="revisionText">
+ <property name="text">
+ <string>Current Revision</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="revisionLabel">
+ <property name="text">
+ <string>CURRENT_REVISION</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="installLayout">
+ <item>
+ <widget class="QLabel" name="installText">
+ <property name="text">
+ <string>Install Path</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_4">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="installLabel">
+ <property name="text">
+ <string>INSTALL_PATH</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>40</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <item>
+ <widget class="QLabel" name="verifyLabel">
+ <property name="text">
+ <string>Verify Files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="verifyButton">
+ <property name="text">
+ <string>Verify</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ <property name="centerButtons">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>Settings</receiver>
+ <slot>hide()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>187</x>
+ <y>235</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>187</x>
+ <y>129</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/qt/workers.cpp b/src/qt/workers.cpp
new file mode 100644
index 0000000..3fce706
--- /dev/null
+++ b/src/qt/workers.cpp
@@ -0,0 +1,299 @@
+#include <iostream>
+#include <limits.h>
+#include <pthread.h>
+
+#include "net.h"
+#include "steam.h"
+#include "toast.h"
+
+#include "./ui_mainwindow.h"
+#include "workers.hpp"
+
+#define THREAD_COUNT 4
+
+struct thread_object_info {
+ int working;
+
+ QString* infoText;
+ char* of_dir;
+ char* remote;
+ struct revision_t* rev;
+ size_t index;
+};
+
+static void* thread_download(void* pinfo)
+{
+ struct thread_object_info* info = (struct thread_object_info*)pinfo;
+ if (info)
+ {
+ QString* infoText = info->infoText;
+ char* of_dir = info->of_dir;
+ char* remote = info->remote;
+ struct revision_t* rev = info->rev;
+ size_t i = info->index;
+
+ struct file_info* file = &rev->files[i];
+ if (file->type == TYPE_WRITE)
+ {
+ *info->infoText = QString("Verifying %1").arg(file->object);
+ if (verifyFileHash(of_dir, file))
+ {
+ *infoText = QString("Downloading %1").arg(file->object);
+ downloadObject(of_dir, remote, file);
+ }
+ }
+ }
+
+ info->working = 0;
+ pthread_exit(0);
+
+ return NULL;
+}
+
+Worker::Worker()
+{
+ net_init();
+ of_dir = NULL;
+ remote = NULL;
+}
+
+Worker::~Worker()
+{
+ net_deinit();
+
+ if (of_dir)
+ free(of_dir);
+
+ if (remote)
+ free(remote);
+}
+
+QString Worker::getOfDir()
+{
+ return QString(of_dir);
+}
+
+QString Worker::getRemote()
+{
+ return QString(remote);
+}
+
+void Worker::setRemote(QString remotestr)
+{
+ if (remote)
+ free(remote);
+
+ remote = strdup(remotestr.toStdString().c_str());
+ setLocalRemote(of_dir, remote);
+}
+
+int Worker::getRevision()
+{
+ return getLocalRevision(of_dir);
+}
+
+int Worker::getRemoteRevision()
+{
+ return getLatestRemoteRevision(remote);
+}
+
+bool Worker::isOutdated()
+{
+ return getRemoteRevision() > getRevision();
+}
+
+void Worker::stop_work()
+{
+ do_work = false;
+}
+
+int Worker::update_setup(int local_rev, int remote_rev)
+{
+ if (!of_dir) return 1;
+
+ int retval = 0;
+
+
+ struct revision_t* rev = fastFowardRevisions(remote, local_rev, remote_rev);
+
+ if (rev)
+ {
+ pthread_t download_threads[THREAD_COUNT] = {0};
+ struct thread_object_info thread_info[THREAD_COUNT] = {0, NULL, NULL, NULL, NULL, 0};
+ size_t tindex = 0;
+ QString infoStrings[THREAD_COUNT];
+
+ for (size_t i = 0; i < rev->file_count && do_work; ++i)
+ {
+ while (thread_info[tindex].working)
+ {
+ tindex = (tindex+1) % THREAD_COUNT;
+ }
+
+ pthread_t* thread = &download_threads[tindex];
+ struct thread_object_info* info = &thread_info[tindex];
+ QString* threadString = &infoStrings[tindex];
+
+ if (!threadString->isEmpty())
+ {
+ infoText = *threadString;
+ emit resultReady(RESULT_UPDATE_TEXT);
+ }
+
+ info->working = 1;
+ info->infoText = threadString;
+ info->of_dir = of_dir;
+ info->remote = remote;
+ info->rev = rev;
+ info->index = i;
+ progress = (int)(((i * 100) + 1) / rev->file_count);
+
+ emit resultReady(RESULT_UPDATE_TEXT);
+ pthread_create(thread, NULL, thread_download, info);
+ }
+
+ for (size_t i = 0; i < THREAD_COUNT; ++i)
+ {
+ pthread_t* thread = &download_threads[i];
+ if (*thread)
+ pthread_join(*thread, NULL);
+ }
+
+ progress = 0;
+ infoText = QString("Processing");
+ emit resultReady(RESULT_UPDATE_TEXT);
+
+ for (size_t i = 0; i < rev->file_count && do_work; ++i)
+ {
+ struct file_info* file = &rev->files[i];
+ if (file->type != TYPE_MKDIR)
+ continue;
+
+ progress = (int)(((i * 100) + 1) / rev->file_count);
+ emit resultReady(RESULT_UPDATE_TEXT);
+
+ size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1;
+ char* buf = (char*)malloc(len);
+ snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path);
+ makeDir(buf);
+ free(buf);
+ }
+
+ for (size_t i = 0; i < rev->file_count && do_work; ++i)
+ {
+ struct file_info* file = &rev->files[i];
+
+ progress = (int)(((i * 100) + 1) / rev->file_count);
+ emit resultReady(RESULT_UPDATE_TEXT);
+
+ switch (file->type)
+ {
+ case TYPE_WRITE:
+ case TYPE_MKDIR:
+ {
+ retval += applyObject(of_dir, file);
+ }
+ break;
+
+ case TYPE_DELETE:
+ {
+ size_t len = strlen(of_dir) + strlen(OS_PATH_SEP) + strlen(file->path) + 1;
+ char* buf = (char*)malloc(len);
+ snprintf(buf, len, "%s%s%s", of_dir, OS_PATH_SEP, file->path);
+ if (isFile(buf))
+ retval += remove(buf);
+ free(buf);
+ }
+ break;
+ }
+ }
+
+ if (do_work)
+ {
+ removeObjects(of_dir);
+ setLocalRemote(of_dir, remote);
+ setLocalRevision(of_dir, remote_rev);
+ }
+
+ progress = 0;
+ infoText = QString("");
+ emit resultReady(RESULT_UPDATE_TEXT);
+
+ freeRevision(rev);
+ }
+
+ return retval;
+}
+
+
+void Worker::doWork(const enum Worker::Tasks_t &parameter) {
+ Results_t result = RESULT_NONE;
+
+
+ switch (parameter)
+ {
+ case TASK_INVALID:
+ break;
+
+ case TASK_INIT:
+ result = RESULT_INIT_FAILURE;
+ of_dir = getOpenFortressDir();
+
+ if (of_dir)
+ {
+ of_dir_len = strlen(of_dir);
+ remote = getLocalRemote(of_dir);
+
+ if (remote)
+ {
+ remote_len = strlen(remote);
+ result = RESULT_INIT_COMPLETE;
+ }
+ else
+ {
+ free(of_dir);
+ of_dir = NULL;
+ remote = NULL;
+ }
+ }
+ else
+ {
+ of_dir = NULL;
+ remote = NULL;
+ }
+
+ break;
+
+ case TASK_IS_INSTALLED:
+ result = getRevision() > -1 ? RESULT_IS_INSTALLED : RESULT_IS_NOT_INSTALLED;
+ break;
+
+ case TASK_IS_UPTODATE:
+ result = isOutdated() ? RESULT_IS_OUTDATED : RESULT_IS_UPTODATE;
+ break;
+
+ case TASK_UNINSTALL:
+ result = RESULT_UNINSTALL_FAILURE;
+ //if (direxists) result = svn_delete(mod) ? RESULT_UNINSTALL_FAILURE : RESULT_UNINSTALL_COMPLETE;
+ break;
+
+ case TASK_INSTALL:
+ result = update_setup(0, getRemoteRevision()) > 0 ? RESULT_INSTALL_FAILURE : RESULT_INSTALL_COMPLETE;
+ break;
+
+ case TASK_UPDATE:
+ result = update_setup(getRevision(), getRemoteRevision()) > 0 ? RESULT_UPDATE_FAILURE : RESULT_UPDATE_COMPLETE;
+ break;
+
+ case TASK_UPDATE_RUN:
+ result = update_setup(getRevision(), getRemoteRevision()) > 0 ? RESULT_UPDATE_FAILURE : RESULT_UPDATE_COMPLETE;
+ break;
+
+ case TASK_RUN:
+ result = getSteamPID() > -1 ? RESULT_EXIT : RESULT_NO_STEAM;
+ if (result == RESULT_EXIT) runOpenFortress();
+ break;
+ }
+
+ emit resultReady(result);
+} \ No newline at end of file
diff --git a/src/qt/workers.hpp b/src/qt/workers.hpp
new file mode 100644
index 0000000..a31d9eb
--- /dev/null
+++ b/src/qt/workers.hpp
@@ -0,0 +1,94 @@
+#ifndef WORKERS_HPP
+#define WORKERS_HPP
+
+#include <QObject>
+#include <limits.h>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class Worker : public QObject
+{
+ Q_OBJECT
+
+private:
+ char* of_dir;
+ size_t of_dir_len;
+
+ char* remote;
+ size_t remote_len;
+
+ bool do_work = true;
+
+public:
+ int progress = -1;
+ QString infoText;
+
+ Worker();
+ ~Worker();
+
+ QString getOfDir();
+ QString getRemote();
+ void setRemote(QString);
+
+ int getRevision();
+ int getRemoteRevision();
+ bool isOutdated();
+
+ void stop_work();
+
+ int update_setup(int, int);
+
+ enum Tasks_t
+ {
+ TASK_INVALID,
+
+ TASK_IS_INSTALLED,
+ TASK_IS_UPTODATE,
+
+ TASK_INIT,
+ TASK_INSTALL,
+ TASK_UNINSTALL,
+ TASK_UPDATE,
+ TASK_UPDATE_RUN,
+ TASK_RUN,
+ };
+ Q_ENUM(Tasks_t)
+
+ enum Results_t
+ {
+ RESULT_NONE,
+ RESULT_EXIT,
+
+ RESULT_UPDATE_TEXT,
+
+ RESULT_IS_INSTALLED,
+ RESULT_IS_NOT_INSTALLED,
+ RESULT_IS_UPTODATE,
+ RESULT_IS_OUTDATED,
+
+ RESULT_INIT_COMPLETE,
+ RESULT_INIT_FAILURE,
+ RESULT_INSTALL_COMPLETE,
+ RESULT_INSTALL_FAILURE,
+ RESULT_UNINSTALL_COMPLETE,
+ RESULT_UNINSTALL_FAILURE,
+ RESULT_UPDATE_COMPLETE,
+ RESULT_UPDATE_FAILURE,
+ RESULT_UPDATE_RUN,
+
+ RESULT_NO_STEAM
+ };
+ Q_ENUM(Results_t)
+
+public slots:
+ void doWork(const Tasks_t &);
+
+signals:
+ void resultReady(const Results_t &);
+
+};
+
+
+#endif \ No newline at end of file
diff --git a/src/steam.c b/src/steam.c
new file mode 100644
index 0000000..4213768
--- /dev/null
+++ b/src/steam.c
@@ -0,0 +1,178 @@
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <unistd.h>
+#include <dirent.h>
+#include <stdio.h>
+
+#include "steam.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <tlhelp32.h>
+#endif
+
+/**
+ * Returns a heap allocated path to the main steam directory
+ * If a problem occurs returns NULL
+ */
+char* getSteamDir()
+{
+#if defined(__linux__)
+ char* home = getenv("HOME");
+
+ if (!home || !isDir(home))
+ return NULL;
+
+ size_t len = strlen(home) + strlen(FLATPAK_DIR);
+ char* path = malloc(len+1);
+
+ strncpy(path, home, len);
+ strncat(path, STEAM_DIR, len-strlen(path));
+
+ if (isDir(path))
+ return path;
+
+ strncpy(path, home, len);
+ strncat(path, FLATPAK_DIR, len-strlen(path));
+
+ if (isDir(path))
+ return path;
+
+ free(path);
+#elif defined(_WIN32)
+ size_t size = PATH_MAX;
+ char* path = malloc(size+1);
+ if (!path)
+ return NULL;
+
+ LSTATUS res = RegGetValueA(HKEY_LOCAL_MACHINE, REG_PATH, "InstallPath", RRF_RT_REG_SZ, NULL, path, (LPDWORD)&size);
+
+ if (res == ERROR_SUCCESS && isDir(path))
+ return path;
+
+ strncpy(path, STEAM_PGRM_64, size);
+ path[size] = '\0';
+
+ if (isDir(path))
+ return path;
+
+ strncpy(path, STEAM_PGRM_86, size);
+ path[size] = '\0';
+
+ if (isDir(path))
+ return path;
+
+ free(path);
+
+#else
+ #error No Implementation
+#endif
+ return NULL;
+}
+
+/**
+ * Returns a heap allocated path to the sourcemod dirctory
+ * If a problem occurs returns NULL
+ */
+char* getSourcemodDir()
+{
+ char* steam = getSteamDir();
+ if (!steam)
+ return NULL;
+
+ steam = realloc(steam, strlen(steam) + strlen(OS_PATH_SEP) + strlen(SOURCEMOD_DIR) + 1);
+ strcat(steam, OS_PATH_SEP);
+ strcat(steam, SOURCEMOD_DIR);
+
+ return steam;
+}
+
+char* getOpenFortressDir()
+{
+ char* sm_dir = getSourcemodDir();
+ if (!sm_dir)
+ return NULL;
+
+ sm_dir = realloc(sm_dir, strlen(sm_dir) + strlen(OPEN_FORTRESS_DIR) + 1);
+ strcat(sm_dir, OPEN_FORTRESS_DIR);
+
+ return sm_dir;
+}
+
+/**
+ * function to fetch the PID of a running Steam process.
+ * If none were found returns -1
+ */
+long getSteamPID()
+{
+#ifdef __linux__
+ long pid;
+ char buf[PATH_MAX];
+ struct dirent* ent;
+ DIR* proc = opendir("/proc");
+ FILE* stat;
+
+ if (proc)
+ {
+ while ((ent = readdir(proc)) != NULL)
+ {
+ long lpid = atol(ent->d_name);
+ if (!lpid) continue;
+
+ snprintf(buf, sizeof(buf), "/proc/%ld/stat", lpid);
+ stat = fopen(buf, "r");
+
+ if (stat && (fscanf(stat, "%li (%[^)])", &pid, buf)) == 2)
+ {
+ if (!strcmp(buf, STEAM_PROC))
+ {
+ fclose(stat);
+ closedir(proc);
+ return pid;
+ }
+ fclose(stat);
+ }
+ }
+
+ closedir(proc);
+ }
+
+#elif _WIN32
+ HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ PROCESSENTRY32 pe32 = {0};
+ pe32.dwSize = sizeof(PROCESSENTRY32);
+ Process32First(hSnap,&pe32);
+
+ while(Process32Next(hSnap,&pe32))
+ {
+ if (!strcmp(pe32.szExeFile, STEAM_PROC))
+ return (long)pe32.th32ProcessID;
+ }
+#else
+ #error No Implementation
+#endif
+ return -1;
+}
+
+#define STEAM_LAUNCH
+
+int runOpenFortress()
+{
+#ifdef STEAM_LAUNCH
+ #ifdef _WIN32
+ return system("start steam://rungameid/11677091221058336806");
+ #else
+ return system("xdg-open steam://rungameid/11677091221058336806");
+ #endif
+
+#else
+ char* of_dir = getOpenFortressDir();
+ char* steam = getSteamDir();
+ steam = realloc(steam, strlen(steam) + strlen(OS_PATH_SEP) + strlen(STEAM_BIN) + 1);
+ strcat(steam, OS_PATH_SEP);
+ strcat(steam, STEAM_BIN);
+
+ return execl(steam, steam, "-applaunch", "243750", "-game", of_dir, "-secure", "-steam", NULL);
+#endif
+}
diff --git a/src/steam.h b/src/steam.h
new file mode 100644
index 0000000..9c24337
--- /dev/null
+++ b/src/steam.h
@@ -0,0 +1,47 @@
+#ifndef STEAM_H
+#define STEAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+#include <stdbool.h>
+
+#include "fs.h"
+
+#ifdef _WIN32
+#define STEAM_PROC "steam.exe"
+#define STEAM_BIN OS_PATH_SEP STEAM_PROC
+#else
+#define STEAM_PROC "steam"
+#define STEAM_BIN "steam.sh"
+#endif
+
+#define STEAM_APPID "243750"
+
+#ifdef _WIN32
+#define _STEAM_NAME "Steam"
+#define STEAM_PGRM_64 "C:\\Program Files (x86)\\" _STEAM_NAME
+#define STEAM_PGRM_86 "C:\\Program Files\\" _STEAM_NAME
+// TODO check if this is the right registry path for x86
+#define REG_PATH "SOFTWARE\\Wow6432Node\\Valve\\Steam"
+#else // _WIN32
+#define STEAM_DIR "/.local/share/Steam"
+#define FLATPAK_DIR "/.var/app/com.valvesoftware.Steam" STEAM_DIR
+#endif
+
+#define SOURCEMOD_DIR "steamapps" OS_PATH_SEP "sourcemods" OS_PATH_SEP
+#define OPEN_FORTRESS_DIR "open_fortress"
+
+char* getSteamDir();
+char* getSourcemodDir();
+char* getOpenFortressDir();
+long getSteamPID();
+int runOpenFortress();
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif \ No newline at end of file
diff --git a/src/toast.c b/src/toast.c
new file mode 100644
index 0000000..34d28b9
--- /dev/null
+++ b/src/toast.c
@@ -0,0 +1,651 @@
+#include <stdlib.h>
+#include <stddef.h>
+#include <string.h>
+#include <stdio.h>
+#include <assert.h>
+#include <json.h>
+#include <md5.h>
+
+#if defined(_WIN32)
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif
+#include <windows.h>
+#endif
+
+#include "fs.h"
+#include "net.h"
+#include "toast.h"
+
+#ifdef TOAST_DEFAULT_REMOTE
+#define TOAST_DEFAULT_REMOTE "http://toast.openfortress.fun/toast"
+#endif
+
+#define TVN_DIR ".tvn"
+#define LOCAL_REMOTE "remote"
+#define LOCAL_REVISION "revision"
+#define LOCAL_OBJECTS "objects"
+#define LOCAL_REMOTE_PATH TVN_DIR OS_PATH_SEP LOCAL_REMOTE
+#define LOCAL_REVISION_PATH TVN_DIR OS_PATH_SEP LOCAL_REVISION
+#define LOCAL_OBJECTS_PATH TVN_DIR OS_PATH_SEP LOCAL_OBJECTS
+#define OLD_LOCAL_REVISION_PATH ".revision"
+
+#define OBJECTS_ENDPOINT "objects"
+#define REVISIONS_ENDPOINT "revisions"
+#define LATEST_ENDPOINT REVISIONS_ENDPOINT "/latest"
+
+const char* TYPE_STRINGS[] = {
+ "Add/Modify",
+ "Create Directory",
+ "Delete"
+};
+
+static int fileHash(char*, char*);
+
+char* getToastDir(char* dir)
+{
+ if (!dir)
+ return NULL;
+
+ size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(TVN_DIR) + 1;
+ char* tvn_dir = malloc(len + 1);
+ if (!tvn_dir)
+ return NULL;
+
+ tvn_dir[0] = '\0';
+ snprintf(tvn_dir, len, "%s%s%s", dir, OS_PATH_SEP, TVN_DIR);
+
+ if (!isDir(tvn_dir))
+ {
+ makeDir(tvn_dir);
+#if defined(_WIN32)
+ SetFileAttributesA(tvn_dir, FILE_ATTRIBUTE_HIDDEN);
+#endif
+ }
+
+
+ return tvn_dir;
+}
+
+/**
+ * Returns a the latest Revision available on the server.
+ * If none could be fetched returns -1.
+ */
+int getLatestRemoteRevision(char* url)
+{
+ assert(url);
+ int revision = -1;
+ size_t len = strlen(url) + 1 + strlen(LATEST_ENDPOINT)+1;
+
+ char* latest_url = malloc(len);
+ if (!latest_url)
+ return revision;
+
+ snprintf(latest_url, len, "%s/%s", url, LATEST_ENDPOINT);
+
+ struct MemoryStruct* latest_data = downloadToRam(latest_url);
+ free(latest_url);
+
+ if (latest_data)
+ {
+ latest_data->memory = realloc(latest_data->memory, latest_data->size+1);
+ latest_data->memory[latest_data->size] = '\0';
+
+ revision = atoi((char*)latest_data->memory);
+ freeDownload(latest_data);
+ }
+
+ return revision;
+}
+
+/**
+ * Returns the current revision the local install is on
+ * If none could be found returns -1.
+ */
+int getLocalRevision(char* dir)
+{
+ int revision = -1;
+
+ if (!dir || !isDir(dir))
+ return revision;
+
+ size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(LOCAL_REVISION_PATH);
+ char* revision_path = malloc(len + 1);
+ if (!revision_path)
+ return revision;
+
+ // LEGACY BEHAVIOR
+ strncpy(revision_path, dir, len);
+ strncat(revision_path, OS_PATH_SEP, len - strlen(revision_path));
+ strncat(revision_path, OLD_LOCAL_REVISION_PATH, len - strlen(revision_path));
+
+ if (!isFile(revision_path))
+ {
+ revision_path[len-strlen(LOCAL_REVISION_PATH)] = '\0';
+ strncat(revision_path, LOCAL_REVISION_PATH, len - strlen(revision_path));
+ if (!isFile(revision_path))
+ {
+ free(revision_path);
+ return revision;
+ }
+ }
+
+ FILE* fd = fopen(revision_path, "r");
+ free(revision_path);
+
+ if (!fd)
+ return revision;
+
+ fseek(fd, 0L, SEEK_END);
+ size_t fd_size = (size_t)ftell(fd);
+ fseek(fd, 0L, SEEK_SET);
+
+ char* buf = malloc(fd_size+1);
+ fread(buf, sizeof(char), fd_size, fd);
+ buf[fd_size] = '\0';
+
+ fclose(fd);
+
+ revision = atoi(buf);
+ free(buf);
+
+ return revision;
+}
+
+/**
+ * Write a revision number to disk
+ */
+void setLocalRevision(char* dir, int rev)
+{
+ char* revision_path = getToastDir(dir);
+ if (!revision_path)
+ return;
+
+ // cleanup legacy behavior
+ {
+ char* old_revision_path = malloc(strlen(revision_path) + strlen(OLD_LOCAL_REVISION_PATH));
+ strcpy(old_revision_path, revision_path);
+
+ char* op = old_revision_path + strlen(old_revision_path);
+ while (*op != *OS_PATH_SEP) --op;
+ *++op = '\0';
+
+ strcat(old_revision_path, OLD_LOCAL_REVISION_PATH);
+
+ if (isFile(old_revision_path))
+ remove(old_revision_path);
+
+ free(old_revision_path);
+ }
+
+ size_t len = strlen(revision_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_REVISION) + 1;
+ revision_path = realloc(revision_path, len);
+
+ if (!isDir(revision_path))
+ makeDir(revision_path);
+
+ strncat(revision_path, OS_PATH_SEP, len - strlen(revision_path));
+ strncat(revision_path, LOCAL_REVISION, len - strlen(revision_path));
+
+ size_t rev_len = 1 + 1;
+ {
+
+ int rev_copy = rev;
+ while (rev_copy >= 10)
+ {
+ rev_copy /= 10;
+ rev_len++;
+ }
+ }
+ char* revision_string = malloc(rev_len);
+ if (!revision_string)
+ return;
+
+ snprintf(revision_string, rev_len, "%i", rev);
+
+ FILE* fd = fopen(revision_path, "w");
+ free(revision_path);
+ if (!fd)
+ return;
+
+ fwrite(revision_string, sizeof(char), rev_len-1, fd);
+ fclose(fd);
+
+ free(revision_string);
+}
+
+/**
+ * Get the remote URL for a local install
+ * If remote cannot be found returns TOAST_DEFAULT_REMOTE
+ *
+ * Results need to be freed
+ */
+char* getLocalRemote(char* dir)
+{
+ if (!dir || !isDir(dir))
+ return strdup(TOAST_DEFAULT_REMOTE);
+
+ size_t len = strlen(dir) + strlen(OS_PATH_SEP) + strlen(LOCAL_REMOTE_PATH);
+ char* remote_path = malloc(len + 1);
+ if (!remote_path)
+ return strdup(TOAST_DEFAULT_REMOTE);
+
+ strncpy(remote_path, dir, len);
+ strncat(remote_path, OS_PATH_SEP, len - strlen(remote_path));
+ strncat(remote_path, LOCAL_REMOTE_PATH, len - strlen(remote_path));
+
+ if (!isFile(remote_path))
+ {
+ free(remote_path);
+ return strdup(TOAST_DEFAULT_REMOTE);
+ }
+
+ FILE* fd = fopen(remote_path, "rb");
+ free(remote_path);
+
+ if (!fd)
+ return strdup(TOAST_DEFAULT_REMOTE);
+
+ fseek(fd, 0L, SEEK_END);
+ size_t fd_size = (size_t)ftell(fd);
+ fseek(fd, 0L, SEEK_SET);
+
+ char* buf = malloc(fd_size+1);
+ fread(buf, sizeof(char), fd_size, fd);
+ buf[fd_size] = '\0';
+
+ char* bufp = buf;
+ while (*bufp != '\0')
+ {
+ if (*bufp == '\n')
+ *bufp = '\0';
+ ++bufp;
+ }
+ if (buf[strlen(buf)] == '/')
+ buf[strlen(buf)] = '\0';
+
+ fclose(fd);
+
+ return buf;
+}
+
+/**
+ * Write a remote URL to disk
+ */
+void setLocalRemote(char* dir, char* remote)
+{
+ char* remote_path = getToastDir(dir);
+ if (!remote_path)
+ return;
+
+ size_t len = strlen(remote_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_REMOTE) + 1;
+ remote_path = realloc(remote_path, len);
+
+ if (!isDir(remote_path))
+ makeDir(remote_path);
+
+ strncat(remote_path, OS_PATH_SEP, len - strlen(remote_path));
+ strncat(remote_path, LOCAL_REMOTE, len - strlen(remote_path));
+
+ FILE* fd = fopen(remote_path, "w");
+ free(remote_path);
+
+ fwrite(remote, sizeof(char), strlen(remote), fd);
+
+ fclose(fd);
+}
+
+/**
+ * Gets revision data at the specified API url for the specified revision number
+ * Returns NULL on error
+ *
+ * Results need to be freed
+ */
+struct revision_t* getRevisionData(char* url, int rev)
+{
+ if (!url)
+ return NULL;
+
+ size_t rev_len = 1;
+ {
+
+ int rev_copy = rev;
+ while (rev_copy > 10)
+ {
+ rev_copy /= 10;
+ rev_len++;
+ }
+ }
+
+ size_t len = strlen(url) + 1 + strlen(REVISIONS_ENDPOINT) + 1 + rev_len + 1;
+ char* buf = malloc(len);
+ snprintf(buf, len, "%s/%s/%i", url, REVISIONS_ENDPOINT, rev);
+
+ struct json_object* revision_list = fetchJSON(buf);
+ free(buf);
+
+ if (!revision_list)
+ return NULL;
+
+ struct revision_t* revision = malloc(sizeof(struct revision_t));
+ if (!revision)
+ {
+ json_object_put(revision_list);
+ return NULL;
+ }
+
+ revision->file_count = (size_t)json_object_array_length(revision_list);
+ revision->files = malloc(sizeof(struct file_info) * revision->file_count);
+
+ struct json_object* temp;
+ for (size_t i = 0; i < revision->file_count; ++i)
+ {
+ struct json_object* file = json_object_array_get_idx(revision_list, i);
+
+ json_object_object_get_ex(file, "type", &temp);
+ assert(temp);
+ revision->files[i].type = json_object_get_int(temp);
+
+ json_object_object_get_ex(file, "path", &temp);
+ assert(temp);
+ revision->files[i].path = strdup(json_object_get_string(temp));
+
+ json_object_object_get_ex(file, "hash", &temp);
+ if (temp)
+ revision->files[i].hash = strdup(json_object_get_string(temp));
+ else
+ revision->files[i].hash = NULL;
+
+ json_object_object_get_ex(file, "object", &temp);
+ if (temp)
+ revision->files[i].object = strdup(json_object_get_string(temp));
+ else
+ revision->files[i].object = NULL;
+ }
+
+ json_object_put(revision_list);
+
+ return revision;
+}
+
+/**
+ * Get all revisions between two numbers and merges them together as far as possible
+ * Returns NULL on error
+ *
+ * Results need to be freed
+ */
+struct revision_t* fastFowardRevisions(char* url, int from, int to)
+{
+ struct revision_t* rev = NULL;
+
+ for (int rev_num = from; rev_num <= to; ++rev_num)
+ {
+ struct revision_t* cur_rev = getRevisionData(url, rev_num);
+
+ if (!rev)
+ {
+ rev = cur_rev;
+ continue;
+ }
+
+ for (size_t j = 0; j < cur_rev->file_count; ++j)
+ {
+ for (size_t i = 0; i < rev->file_count; ++i)
+ {
+ if (!strcmp(rev->files[i].path, cur_rev->files[j].path))
+ {
+ rev->files[i].type = cur_rev->files[j].type;
+
+ if (rev->files[i].hash)
+ free(rev->files[i].hash);
+
+ if (cur_rev->files[j].hash)
+ rev->files[i].hash = strdup(cur_rev->files[j].hash);
+ else
+ rev->files[i].hash = NULL;
+
+ if (rev->files[i].object)
+ free(rev->files[i].object);
+
+ if (cur_rev->files[j].object)
+ rev->files[i].object = strdup(cur_rev->files[j].object);
+ else
+ rev->files[i].object = NULL;
+ }
+ }
+
+ rev->files = realloc(rev->files, sizeof(struct file_info) * (rev->file_count+1));
+
+ rev->files[rev->file_count].type = cur_rev->files[j].type;
+
+ if (cur_rev->files[j].path)
+ rev->files[rev->file_count].path = strdup(cur_rev->files[j].path);
+ else
+ rev->files[rev->file_count].path = NULL;
+
+
+ if (cur_rev->files[j].hash)
+ rev->files[rev->file_count].hash = strdup(cur_rev->files[j].hash);
+ else
+ rev->files[rev->file_count].hash = NULL;
+
+ if (cur_rev->files[j].object)
+ rev->files[rev->file_count].object = strdup(cur_rev->files[j].object);
+ else
+ rev->files[rev->file_count].object = NULL;
+
+ rev->file_count++;
+ }
+
+ freeRevision(cur_rev);
+ }
+ return rev;
+}
+
+/**
+ * Frees a revision from the heap
+ */
+void freeRevision(struct revision_t* rev)
+{
+ if (!rev) return;
+
+ for (size_t i = 0; i < rev->file_count; ++i)
+ {
+ free(rev->files[i].path);
+ free(rev->files[i].hash);
+ free(rev->files[i].object);
+ }
+ free(rev->files);
+ free(rev);
+}
+
+
+/**
+ * Downloads an object from the specified API url to the specified game dir
+ *
+ * Returns bytes written
+ */
+size_t downloadObject(char* dir, char* url, struct file_info* info)
+{
+ size_t retval = 0;
+ if (!info)
+ return retval;
+
+ char* object = info->object;
+
+ char* buf_path = getToastDir(dir);
+ if (!buf_path)
+ return retval;
+
+ size_t len = strlen(buf_path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS) + strlen(OS_PATH_SEP) + strlen(object) + 1;
+ buf_path = realloc(buf_path, len);
+
+ strncat(buf_path, OS_PATH_SEP, len - strlen(buf_path));
+ strncat(buf_path, LOCAL_OBJECTS, len - strlen(buf_path));
+ strncat(buf_path, OS_PATH_SEP, len - strlen(buf_path));
+
+ if (!isDir(buf_path))
+ makeDir(buf_path);
+
+ strcat(buf_path, object);
+
+ // the object has the right hash so we ignore
+ if (isFile(buf_path) && !fileHash(buf_path, info->hash))
+ {
+ free(buf_path);
+ return 0;
+ }
+
+ len = strlen(url) + 1 + strlen(OBJECTS_ENDPOINT) + 1 + strlen(object) + 1;
+ char* buf_url = malloc(len);
+ snprintf(buf_url, len, "%s/%s/%s", url, OBJECTS_ENDPOINT, object);
+
+ retval = downloadToFile(buf_url, buf_path);
+
+ free(buf_url);
+ free(buf_path);
+
+ return retval;
+}
+
+/**
+ * Moves an object from the temporary object directory to its proper place
+ *
+ * Returns
+ * 0 on success
+ * 1 on missing object
+ * 2 on general failure
+ */
+int applyObject(char* path, struct file_info* info)
+{
+ if (!info)
+ return 2;
+
+ char* file = info->path;
+ char* object = info->object;
+
+ if (!path || !isDir(path))
+ return 2;
+ else if (!object)
+ return 0;
+
+ size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS_PATH) + strlen(OS_PATH_SEP) + strlen(object) + 1;
+ char* buf_obj = malloc(len);
+ snprintf(buf_obj, len, "%s%s%s%s%s", path, OS_PATH_SEP, LOCAL_OBJECTS_PATH, OS_PATH_SEP, object);
+
+ if (!isFile(buf_obj))
+ {
+ free(buf_obj);
+ len = strlen(path) + strlen(OS_PATH_SEP) + strlen(info->path) + 1;
+ buf_obj = malloc(len);
+ snprintf(buf_obj, len, "%s%s%s", path, OS_PATH_SEP, info->path);
+
+ int exists = isFile(buf_obj);
+ free(buf_obj);
+
+ return !exists;
+ }
+
+ len = strlen(path) + strlen(OS_PATH_SEP) + strlen(file) + 1;
+ char* buf_file = malloc(len);
+ snprintf(buf_file, len, "%s%s%s", path, OS_PATH_SEP, file);
+
+ rename(buf_obj, buf_file);
+
+ free(buf_obj);
+ free(buf_file);
+
+ return 0;
+}
+
+/**
+ * Removes temporarily stored objects
+ */
+void removeObjects(char* path)
+{
+ if (!path || !isDir(path))
+ return;
+
+ size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(LOCAL_OBJECTS_PATH) + strlen(OS_PATH_SEP) + 1;
+ char* buf = malloc(len);
+ snprintf(buf, len, "%s%s%s%s", path, OS_PATH_SEP, LOCAL_OBJECTS_PATH, OS_PATH_SEP);
+
+ removeDir(buf);
+
+ free(buf);
+}
+
+/**
+ * Internal function to generate check if a file matches a hash
+ */
+static int fileHash(char* path, char* hash)
+{
+ FILE* fd = fopen(path, "rb");
+ if (!fd)
+ return 1;
+
+ fseek(fd, 0L, SEEK_END);
+ size_t fd_size = (size_t)ftell(fd);
+ fseek(fd, 0L, SEEK_SET);
+
+ MD5_CTX context;
+ MD5Init(&context);
+
+ if (fd_size)
+ {
+ char* buf = malloc(fd_size);
+ if (!buf)
+ {
+ fclose(fd);
+ return 1;
+ }
+
+ fread(buf, sizeof(char), fd_size, fd);
+
+ MD5Update(&context, buf, fd_size);
+ free(buf);
+ }
+ fclose(fd);
+
+ unsigned char digest[16];
+ MD5Final(digest, &context);
+
+ char md5string[33];
+ for(int i = 0; i < 16; ++i)
+ snprintf(&md5string[i*2], 3, "%02x", (unsigned int)digest[i]);
+
+ if (!strncmp(md5string, hash, sizeof(md5string)))
+ return 0;
+
+ return 1;
+}
+
+/**
+ * Verifies the (md5) hash of a given file
+ *
+ * Returns
+ * 0 on success
+ * 1 on failure
+ */
+int verifyFileHash(char* path, struct file_info* info)
+{
+ if (!info)
+ return 1;
+
+ char* file = info->path;
+ char* hash = info->hash;
+
+ if (!path || !isDir(path))
+ return 1;
+
+ size_t len = strlen(path) + strlen(OS_PATH_SEP) + strlen(file) + 1;
+ char* buf = malloc(len);
+ if (!buf)
+ return 1;
+ snprintf(buf, len, "%s%s%s", path, OS_PATH_SEP, file);
+
+ int hash_matches = fileHash(buf, hash);
+ free(buf);
+
+ return hash_matches;
+}
diff --git a/src/toast.h b/src/toast.h
new file mode 100644
index 0000000..5a99fbb
--- /dev/null
+++ b/src/toast.h
@@ -0,0 +1,51 @@
+#ifndef TOAST_H
+#define TOAST_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <stddef.h>
+
+enum TYPE_ENUM {
+ TYPE_WRITE = 0,
+ TYPE_MKDIR = 1,
+ TYPE_DELETE = 2,
+};
+
+extern const char* TYPE_STRINGS[];
+
+struct file_info {
+ enum TYPE_ENUM type;
+ char* path;
+ char* hash;
+ char* object;
+};
+
+struct revision_t {
+ struct file_info* files;
+ size_t file_count;
+};
+
+char* getToastDir(char* dir);
+int getLatestRemoteRevision(char*);
+int getLocalRevision(char*);
+void setLocalRevision(char*, int);
+char* getLocalRemote(char*);
+void setLocalRemote(char*, char*);
+
+struct revision_t* getRevisionData(char*, int);
+struct revision_t* fastFowardRevisions(char* url, int from, int to);
+void freeRevision(struct revision_t* rev);
+
+size_t downloadObject(char*, char*, struct file_info*);
+int applyObject(char*, struct file_info*);
+void removeObjects(char*);
+
+int verifyFileHash(char*, struct file_info*);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif \ No newline at end of file