diff --git a/openrct2.vcxproj b/openrct2.vcxproj
index 5952fcceda..f22ed97908 100644
--- a/openrct2.vcxproj
+++ b/openrct2.vcxproj
@@ -84,6 +84,7 @@
+
@@ -357,6 +358,7 @@
+
diff --git a/src/core/Nullable.hpp b/src/core/Nullable.hpp
index 37e3393cfe..67ba9b2c51 100644
--- a/src/core/Nullable.hpp
+++ b/src/core/Nullable.hpp
@@ -28,6 +28,12 @@ public:
_hasValue = false;
}
+ Nullable(nullptr_t)
+ {
+ _value = T();
+ _hasValue = false;
+ }
+
Nullable(const T &value)
{
_value = value;
diff --git a/src/network/NetworkUser.cpp b/src/network/NetworkUser.cpp
new file mode 100644
index 0000000000..2966fe0a8d
--- /dev/null
+++ b/src/network/NetworkUser.cpp
@@ -0,0 +1,222 @@
+#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers
+/*****************************************************************************
+ * OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
+ *
+ * OpenRCT2 is the work of many authors, a full list can be found in contributors.md
+ * For more information, visit https://github.com/OpenRCT2/OpenRCT2
+ *
+ * OpenRCT2 is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * A full copy of the GNU General Public License can be found in licence.txt
+ *****************************************************************************/
+#pragma endregion
+
+#include
+
+#include "../core/Json.hpp"
+#include "../core/Path.hpp"
+#include "../core/String.hpp"
+#include "NetworkUser.h"
+
+extern "C"
+{
+ #include "../platform/platform.h"
+}
+
+constexpr const utf8 * USER_STORE_FILENAME = "users.json";
+
+NetworkUser * NetworkUser::FromJson(json_t * json)
+{
+ const char * hash = json_string_value(json_object_get(json, "hash"));
+ const char * name = json_string_value(json_object_get(json, "name"));
+ const json_t * jsonGroupId = json_object_get(json, "groupId");
+
+ NetworkUser * user = nullptr;
+ if (hash != nullptr && name != nullptr) {
+ user = new NetworkUser();
+ user->Hash = std::string(hash);
+ user->Name = std::string(name);
+ if (!json_is_null(jsonGroupId)) {
+ user->GroupId = (uint8)json_integer_value(jsonGroupId);
+ }
+ return user;
+ }
+ return user;
+}
+
+json_t * NetworkUser::ToJson() const
+{
+ return ToJson(json_object());
+}
+
+json_t * NetworkUser::ToJson(json_t * json) const
+{
+ json_object_set_new(json, "hash", json_string(Hash.c_str()));
+ json_object_set_new(json, "name", json_string(Name.c_str()));
+
+ json_t * jsonGroupId;
+ if (GroupId.HasValue()) {
+ jsonGroupId = json_integer(GroupId.GetValue());
+ } else {
+ jsonGroupId = json_null();
+ }
+ json_object_set_new(json, "groupId", jsonGroupId);
+
+ return json;
+}
+
+NetworkUserManager::~NetworkUserManager()
+{
+ DisposeUsers();
+}
+
+void NetworkUserManager::DisposeUsers()
+{
+ for (const auto &kvp : _usersByHash)
+ {
+ delete kvp.second;
+ }
+ _usersByHash.clear();
+}
+
+void NetworkUserManager::Load()
+{
+ utf8 path[MAX_PATH];
+ GetStorePath(path, sizeof(path));
+
+ if (platform_file_exists(path))
+ {
+ DisposeUsers();
+
+ json_t * jsonUsers = Json::ReadFromFile(path);
+ size_t numUsers = json_array_size(jsonUsers);
+ for (size_t i = 0; i < numUsers; i++)
+ {
+ json_t * jsonUser = json_array_get(jsonUsers, i);
+ NetworkUser * networkUser = NetworkUser::FromJson(jsonUser);
+ if (networkUser != nullptr)
+ {
+ _usersByHash[networkUser->Hash] = networkUser;
+ }
+ }
+ json_decref(jsonUsers);
+ }
+}
+
+void NetworkUserManager::Save()
+{
+ utf8 path[MAX_PATH];
+ GetStorePath(path, sizeof(path));
+
+ json_t * jsonUsers = nullptr;
+ try
+ {
+ if (platform_file_exists(path))
+ {
+ jsonUsers = Json::ReadFromFile(path);
+ }
+ }
+ catch (Exception) { }
+
+ if (jsonUsers == nullptr)
+ {
+ jsonUsers = json_array();
+ }
+
+ // Update existing users
+ std::unordered_set savedHashes;
+ size_t numUsers = json_array_size(jsonUsers);
+ for (size_t i = 0; i < numUsers; i++)
+ {
+ json_t * jsonUser = json_array_get(jsonUsers, i);
+ const char * hash = json_string_value(json_object_get(jsonUser, "hash"));
+ if (hash != nullptr)
+ {
+ auto hashString = std::string(hash);
+ const NetworkUser * networkUser = GetUserByHash(hashString);
+ networkUser->ToJson(jsonUser);
+ savedHashes.insert(hashString);
+ }
+ }
+
+ // Add new users
+ for (const auto &kvp : _usersByHash)
+ {
+ const NetworkUser * networkUser = kvp.second;
+ if (savedHashes.find(networkUser->Hash) == savedHashes.end())
+ {
+ json_t * jsonUser = networkUser->ToJson();
+ json_array_append_new(jsonUsers, jsonUser);
+ }
+ }
+
+ Json::WriteToFile(path, jsonUsers, JSON_INDENT(4) | JSON_PRESERVE_ORDER);
+ json_decref(jsonUsers);
+}
+
+void NetworkUserManager::UnsetUsersOfGroup(uint8 groupId)
+{
+ for (const auto &kvp : _usersByHash)
+ {
+ NetworkUser * networkUser = kvp.second;
+ if (networkUser->GroupId.HasValue() &&
+ networkUser->GroupId.GetValue() == groupId)
+ {
+ networkUser->GroupId = nullptr;
+ }
+ }
+}
+
+NetworkUser * NetworkUserManager::GetUserByHash(const std::string &hash)
+{
+ auto it = _usersByHash.find(hash);
+ if (it != _usersByHash.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+const NetworkUser * NetworkUserManager::GetUserByHash(const std::string &hash) const
+{
+ auto it = _usersByHash.find(hash);
+ if (it != _usersByHash.end())
+ {
+ return it->second;
+ }
+ return nullptr;
+}
+
+const NetworkUser * NetworkUserManager::GetUserByName(const std::string &name) const
+{
+ for (auto kvp : _usersByHash)
+ {
+ const NetworkUser * networkUser = kvp.second;
+ if (String::Equals(name.c_str(), networkUser->Name.c_str(), true))
+ {
+ return networkUser;
+ }
+ }
+ return nullptr;
+}
+
+NetworkUser * NetworkUserManager::GetOrAddUser(const std::string &hash)
+{
+ NetworkUser * networkUser = GetUserByHash(hash);
+ if (networkUser == nullptr)
+ {
+ networkUser = new NetworkUser();
+ networkUser->Hash = hash;
+ _usersByHash[hash] = networkUser;
+ }
+ return networkUser;
+}
+
+void NetworkUserManager::GetStorePath(utf8 * buffer, size_t bufferSize)
+{
+ platform_get_user_directory(buffer, nullptr);
+ Path::Append(buffer, bufferSize, USER_STORE_FILENAME);
+}
diff --git a/src/network/NetworkUser.h b/src/network/NetworkUser.h
new file mode 100644
index 0000000000..f0983cceda
--- /dev/null
+++ b/src/network/NetworkUser.h
@@ -0,0 +1,65 @@
+#pragma region Copyright (c) 2014-2016 OpenRCT2 Developers
+/*****************************************************************************
+ * OpenRCT2, an open source clone of Roller Coaster Tycoon 2.
+ *
+ * OpenRCT2 is the work of many authors, a full list can be found in contributors.md
+ * For more information, visit https://github.com/OpenRCT2/OpenRCT2
+ *
+ * OpenRCT2 is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * A full copy of the GNU General Public License can be found in licence.txt
+ *****************************************************************************/
+#pragma endregion
+
+#pragma once
+
+#include "../common.h"
+#include "../core/Nullable.hpp"
+
+#include
+#include