diff --git a/AutoReplayUploader.sln b/AutoReplayUploader.sln index f0853f4..f3c2258 100644 --- a/AutoReplayUploader.sln +++ b/AutoReplayUploader.sln @@ -5,13 +5,37 @@ VisualStudioVersion = 15.0.27004.2010 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AutoReplayUploader", "AutoReplayUploader\AutoReplayUploader.vcxproj", "{05D2ABAD-B8F6-4ABA-BE31-EAF8EB557BA5}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ConsoleUploader", "ConsoleUploader\ConsoleUploader.vcxproj", "{9EBBEBF6-E373-4725-BF89-1A1AE087BAB6}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Uploader", "Uploader\Uploader.vcxproj", "{7C4C5DE3-8223-4BB5-99B3-DD2124C6015E}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UnitTests", "UnitTests\UnitTests.vcxproj", "{0560EFD2-3FC5-4BE0-B710-108B528257C4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x86 = Debug|x86 Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {05D2ABAD-B8F6-4ABA-BE31-EAF8EB557BA5}.Debug|x86.ActiveCfg = Release|Win32 {05D2ABAD-B8F6-4ABA-BE31-EAF8EB557BA5}.Release|x86.ActiveCfg = Release|Win32 {05D2ABAD-B8F6-4ABA-BE31-EAF8EB557BA5}.Release|x86.Build.0 = Release|Win32 + {9EBBEBF6-E373-4725-BF89-1A1AE087BAB6}.Debug|x86.ActiveCfg = Debug|Win32 + {9EBBEBF6-E373-4725-BF89-1A1AE087BAB6}.Debug|x86.Build.0 = Debug|Win32 + {9EBBEBF6-E373-4725-BF89-1A1AE087BAB6}.Release|x86.ActiveCfg = Release|Win32 + {9EBBEBF6-E373-4725-BF89-1A1AE087BAB6}.Release|x86.Build.0 = Release|Win32 + {7C4C5DE3-8223-4BB5-99B3-DD2124C6015E}.Debug|x86.ActiveCfg = Debug|Win32 + {7C4C5DE3-8223-4BB5-99B3-DD2124C6015E}.Debug|x86.Build.0 = Debug|Win32 + {7C4C5DE3-8223-4BB5-99B3-DD2124C6015E}.Release|x86.ActiveCfg = Release|Win32 + {7C4C5DE3-8223-4BB5-99B3-DD2124C6015E}.Release|x86.Build.0 = Release|Win32 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Debug|x64.ActiveCfg = Debug|x64 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Debug|x64.Build.0 = Debug|x64 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Debug|x86.ActiveCfg = Debug|Win32 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Debug|x86.Build.0 = Debug|Win32 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Release|x64.ActiveCfg = Release|x64 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Release|x64.Build.0 = Release|x64 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Release|x86.ActiveCfg = Release|Win32 + {0560EFD2-3FC5-4BE0-B710-108B528257C4}.Release|x86.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AutoReplayUploader/AutoReplayUploader.vcxproj b/AutoReplayUploader/AutoReplayUploader.vcxproj index 9affef8..de2106a 100644 --- a/AutoReplayUploader/AutoReplayUploader.vcxproj +++ b/AutoReplayUploader/AutoReplayUploader.vcxproj @@ -30,7 +30,9 @@ - + + true + Level3 @@ -38,16 +40,17 @@ true true true - %(AdditionalIncludeDirectories) + C:\Program Files %28x86%29\Steam\steamapps\common\rocketleague\Binaries\Win32\bakkesmod\bakkesmodsdk\include;..\Uploader;%(AdditionalIncludeDirectories) _WINDLL;%(PreprocessorDefinitions);_CRT_SECURE_NO_WARNINGS; true true + BakkesMod.lib;%(AdditionalDependencies) + C:\Program Files %28x86%29\Steam\steamapps\common\rocketleague\Binaries\Win32\bakkesmod\bakkesmodsdk\lib;%(AdditionalLibraryDirectories) - - + python "C:\Users\Tyler\source\repos\bakkes_patchplugin.py" "$(TargetPath)" @@ -56,7 +59,14 @@ + + + + + + {7c4c5de3-8223-4bb5-99b3-dd2124c6015e} + + - - + \ No newline at end of file diff --git a/AutoReplayUploader/AutoReplayUploader.vcxproj.filters b/AutoReplayUploader/AutoReplayUploader.vcxproj.filters index a99b808..ee04bb9 100644 --- a/AutoReplayUploader/AutoReplayUploader.vcxproj.filters +++ b/AutoReplayUploader/AutoReplayUploader.vcxproj.filters @@ -24,4 +24,7 @@ Source Files + + + \ No newline at end of file diff --git a/AutoReplayUploader/AutoReplayUploaderPlugin.cpp b/AutoReplayUploader/AutoReplayUploaderPlugin.cpp index ecc68ec..8809a5a 100644 --- a/AutoReplayUploader/AutoReplayUploaderPlugin.cpp +++ b/AutoReplayUploader/AutoReplayUploaderPlugin.cpp @@ -1,343 +1,395 @@ #include "AutoReplayUploaderPlugin.h" + +#include +#include + #include "bakkesmod/wrappers/GameEvent/ReplayWrapper.h" #include "bakkesmod/wrappers/GameEvent/ReplayDirectorWrapper.h" #include "bakkesmod/wrappers/GameEvent/ReplaySoccarWrapper.h" -#include "utils/io.h" -#include -#include -BAKKESMOD_PLUGIN(AutoReplayUploaderPlugin, "Auto replay uploader plugin", "0.1", 0) +#include "Utils.h" +#include "Ballchasing.h" +#include "Calculated.h" +#include "Match.h" +#include "Player.h" +#include "Replay.h" + +using namespace std; -HTTPRequestHandle hdl; +BAKKESMOD_PLUGIN(AutoReplayUploaderPlugin, "Auto replay uploader plugin", "0.1", 0); -AutoReplayUploaderPlugin::AutoReplayUploaderPlugin() +// Constant CVAR variable names +#define CVAR_REPLAY_EXPORT_PATH "cl_autoreplayupload_filepath" +#define CVAR_REPLAY_EXPORT "cl_autoreplayupload_save" +#define CVAR_UPLOAD_TO_CALCULATED "cl_autoreplayupload_calculated" +#define CVAR_UPLOAD_TO_BALLCHASING "cl_autoreplayupload_ballchasing" +#define CVAR_REPLAY_NAME_TEMPLATE "cl_autoreplayupload_replaynametemplate" +#define CVAR_REPLAY_SEQUENCE_NUM "cl_autoreplayupload_replaysequence" +#define CVAR_PLUGIN_SHOW_NOTIFICATIONS "cl_autoreplayupload_notifications" +#define CVAR_BALLCHASING_AUTH_KEY "cl_autoreplayupload_ballchasing_authkey" +#define CVAR_BALLCHASING_AUTH_TEST_RESULT "cl_autoreplayupload_ballchasing_testkeyresult" +#define CVAR_BALLCHASING_REPLAY_VISIBILITY "cl_autoreplayupload_ballchasing_visibility" + +string GetPlaylistName(int playlistId); + +void Log(void* object, string message) { - std::stringstream userAgentStream; - userAgentStream << exports.className << "/" << exports.pluginVersion << " BakkesModAPI/" << BAKKESMOD_PLUGIN_API_VERSION; - userAgent = userAgentStream.str(); + auto plugin = (AutoReplayUploaderPlugin*)object; + plugin->cvarManager->log(message); } -std::string GenerateUrl(std::string baseUrl, std::map getParams) +void UploadComplete(AutoReplayUploaderPlugin* plugin, bool result, string endpoint) { - std::stringstream urlStream; - urlStream << baseUrl; - if (!getParams.empty()) - { - urlStream << "?"; - - for (auto it = getParams.begin(); it != getParams.end(); it++) +#ifdef TOAST + if (*(plugin->showNotifications)) { + std::string message = "Uploaded replay to " + endpoint + " successfully!"; + std::string logo_to_use = endpoint + "_logo"; + uint8_t toastType = ToastType_OK; + if (!result) { - if (it != getParams.begin()) - urlStream << "&"; - urlStream << (*it).first << "=" << (*it).second; + message = "Unable to upload replay to " + endpoint + "."; + toastType = ToastType_Error; + } + if (endpoint.find(".")) + { + logo_to_use = endpoint.substr(0, endpoint.find(".") - 1) + "_logo"; } + plugin->gameWrapper->Toast("Autoreplayuploader", message, logo_to_use, 3.5f, toastType); } - return urlStream.str(); +#endif } -void AutoReplayUploaderPlugin::onLoad() +void CalculatedUploadComplete(void* object, bool result) { - HMODULE steamApi = GetModuleHandle("steam_api.dll"); - if (steamApi == NULL) - { - cvarManager->log("Steam API dll not loaded, note sure how this is possible!"); - return; - } - ISteamClient* steamClient = (ISteamClient*)((uintptr_t(__cdecl*)(void))GetProcAddress(steamApi, "SteamClient"))(); - - if (steamClient == NULL) - { - cvarManager->log("Could not find Steam client, cancelling plugin load"); - return; - } - - HSteamUser steamUser = (HSteamUser)((uintptr_t(__cdecl*)(void))GetProcAddress(steamApi, "SteamAPI_GetHSteamUser"))(); - - if (steamUser == NULL) - { - cvarManager->log("Could not find Steam user, cancelling plugin load"); - return; - } + UploadComplete((AutoReplayUploaderPlugin*)object, result, "calculated"); +} - HSteamPipe steamPipe = (HSteamPipe)((uintptr_t(__cdecl*)(void))GetProcAddress(steamApi, "SteamAPI_GetHSteamPipe"))(); - - if (steamPipe == NULL) - { - cvarManager->log("Could not find Steam pipe, cancelling plugin load"); - return; - } +void BallchasingUploadComplete(void* object, bool result) +{ + UploadComplete((AutoReplayUploaderPlugin*)object, result, "ballchasing"); +} - ISteamHTTP* steamHTTPInstanceRet = (ISteamHTTP*)steamClient->GetISteamHTTP(steamUser, steamPipe, "STEAMHTTP_INTERFACE_VERSION002"); - - if (steamHTTPInstanceRet == NULL) - { - cvarManager->log("Could not find Steam HTTP instance, cancelling plugin load"); - return; - } - - steamHTTPInstance = steamHTTPInstanceRet; - SteamAPI_RunCallbacks_Function = (SteamAPI_RunCallbacks_typedef)(GetProcAddress(steamApi, "SteamAPI_RunCallbacks")); - SteamAPI_RegisterCallResult_Function = (SteamAPI_RegisterCallResult_typedef)(GetProcAddress(steamApi, "SteamAPI_RegisterCallResult")); - SteamAPI_UnregisterCallResult_Function = (SteamAPI_RegisterCallResult_typedef)(GetProcAddress(steamApi, "SteamAPI_UnregisterCallResult")); +void BallchasingAuthTestComplete(void* object, bool result) +{ + auto plugin = (AutoReplayUploaderPlugin*)object; + string msg = result ? "Auth key correct!" : "Invalid auth key!"; + plugin->cvarManager->getCvar(CVAR_BALLCHASING_AUTH_TEST_RESULT).setValue(msg); +} - if (SteamAPI_RunCallbacks_Function == NULL || SteamAPI_RegisterCallResult_Function == NULL || SteamAPI_UnregisterCallResult_Function == NULL) - { - cvarManager->log("Could not find all functions in SteamAPI DLL!"); - return; - } +#pragma region AutoReplayUploaderPlugin Implementation - gameWrapper->HookEventWithCaller("Function TAGame.GameEvent_Soccar_TA.EventMatchEnded", - std::bind(&AutoReplayUploaderPlugin::OnGameComplete, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +/** +* OnLoad event called when the plugin is loaded by BakkesMod +*/ +void AutoReplayUploaderPlugin::onLoad() +{ + stringstream userAgentStream; + userAgentStream << exports.className << "/" << exports.pluginVersion << " BakkesModAPI/" << BAKKESMOD_PLUGIN_API_VERSION; + string userAgent = userAgentStream.str(); + + // Setup upload handlers + ballchasing = new Ballchasing(userAgent, "----BakkesModFileUpload90m8924r390j34f0", &Log, &BallchasingUploadComplete, &BallchasingAuthTestComplete, this); + calculated = new Calculated(userAgent, "----BakkesModFileUpload90m8924r390j34f0", &Log, &CalculatedUploadComplete, this); + + InitializeVariables(); + + // Register for Game ending event + gameWrapper->HookEventWithCaller( + "Function TAGame.GameEvent_Soccar_TA.EventMatchEnded", + bind( + &AutoReplayUploaderPlugin::OnGameComplete, + this, + placeholders::_1, + placeholders::_2, + placeholders::_3 + ) + ); + + // Initialize notification plugin assets +#ifdef TOAST + gameWrapper->LoadToastTexture("calculated_logo", "./bakkesmod/data/assets/calculated_logo.tga"); + gameWrapper->LoadToastTexture("ballchasing_logo", "./bakkesmod/data/assets/ballchasing_logo.tga"); +#endif +} - //cvarManager->registerCvar("cl_autoreplayupload_filepath", "./bakkesmod/data/autoreplaysave.replay", "Path to save to be uploaded replay to."); - cvarManager->registerCvar("cl_autoreplayupload_calculated", "0", "Upload to replays to calculated.gg automatically", true, true, 0, true, 1).bindTo(uploadToCalculated); +/** +* OnUnload event called when the plugin is unloaded by BakkesMod +*/ +void AutoReplayUploaderPlugin::onUnload() +{ + delete ballchasing; + delete calculated; +} - cvarManager->registerCvar("cl_autoreplayupload_ballchasing", "0", "Upload to replays to ballchasing.com automatically", true, true, 0, true, 1).bindTo(uploadToBallchasing); +void AutoReplayUploaderPlugin::InitializeVariables() +{ + // What endpoints should we upload to? + cvarManager->registerCvar(CVAR_UPLOAD_TO_CALCULATED, "0", "Upload to replays to calculated.gg automatically", true, true, 0, true, 1).bindTo(uploadToCalculated); + cvarManager->registerCvar(CVAR_UPLOAD_TO_BALLCHASING, "0", "Upload to replays to ballchasing.com automatically", true, true, 0, true, 1).bindTo(uploadToBallchasing); + // Ball Chasing variables + cvarManager->registerCvar(CVAR_BALLCHASING_REPLAY_VISIBILITY, "public", "Replay visibility when uploading to ballchasing.com", false, false, 0, false, 0, true).bindTo(ballchasing->visibility); + cvarManager->registerCvar(CVAR_BALLCHASING_AUTH_TEST_RESULT, "Untested", "Auth token needed to upload replays to ballchasing.com", false, false, 0, false, 0, false); + cvarManager->registerCvar(CVAR_BALLCHASING_AUTH_KEY, "", "Auth token needed to upload replays to ballchasing.com").bindTo(ballchasing->authKey); + cvarManager->getCvar(CVAR_BALLCHASING_AUTH_KEY).addOnValueChanged([this](string oldVal, CVarWrapper cvar) + { + if (ballchasing->authKey->size() > 0 && // We don't test the auth key if the size of the auth key is empty + ballchasing->authKey->compare(oldVal) != 0) // We don't test unless the value has changed + { + // value changed so test auth key + ballchasing->TestAuthKey(); + } + }); - //Auth token response, stored in cvar so we can display it in the plugins tab. Should not be exposed to user! - cvarManager->registerCvar("cl_autoreplayupload_ballchasing_testkeyresult", "Untested", "Auth token needed to upload replays to ballchasing.com", false, false, 0, false, 0, false); - cvarManager->registerCvar("cl_autoreplayupload_ballchasing_visibility", "public", "Replay visibility when uploading to ballchasing.com", false, false, 0, false, 0, false).addOnValueChanged([this](std::string oldValue, CVarWrapper cvar) + // Replay Name template variables + cvarManager->registerCvar(CVAR_REPLAY_SEQUENCE_NUM, "0", "Current Reqlay Sequence number to be used in replay name", true, true, 0, false, 0, true).bindTo(templateSequence); + cvarManager->registerCvar(CVAR_REPLAY_NAME_TEMPLATE, DEFAULT_REPLAY_NAME_TEMPLATE, "Template for in game name of replay", true, true, 0, true, 0, true).bindTo(replayNameTemplate); + cvarManager->getCvar(CVAR_REPLAY_NAME_TEMPLATE).addOnValueChanged([this](string oldVal, CVarWrapper cvar) { - //cvarManager->log(GenerateUrl(BALLCHASING_ENDPOINT_DEFAULT, { { "visibility", cvar.getStringValue() } })); - });; - cvarManager->registerCvar("cl_autoreplayupload_ballchasing_authkey", "", "Auth token needed to upload replays to ballchasing.com").addOnValueChanged([this](std::string oldVal, CVarWrapper cvar) - { - //User changed authkey, reset testkeyresult - cvarManager->getCvar("cl_autoreplayupload_ballchasing_testkeyresult").setValue("Untested"); + if (SanitizeReplayNameTemplate(replayNameTemplate, DEFAULT_REPLAY_NAME_TEMPLATE)) + { + cvarManager->getCvar(CVAR_REPLAY_NAME_TEMPLATE).setValue(*replayNameTemplate); + } }); - cvarManager->registerNotifier("cl_autoreplayupload_ballchasing_testkey", std::bind(&AutoReplayUploaderPlugin::TestBallchasingAuth, this, std::placeholders::_1), - "Checks whether ballchasing authkey is valid", PERMISSION_ALL); - //cvarManager->registerCvar("cl_autoreplayupload_calculated_endpoint", CALCULATED_ENDPOINT_DEFAULT, "URL to upload replay to when uploading to calculated.gg instance"); - - /* - Load notification assets - */ - gameWrapper->LoadToastTexture("calculated_logo", "./bakkesmod/data/assets/calculated_logo.tga"); - gameWrapper->LoadToastTexture("ballchasing_logo", "./bakkesmod/data/assets/ballchasing_logo.tga"); - cvarManager->registerCvar("cl_autoreplayupload_notifications", "1", "Show notifications on successful uploads", true, true, 0, true, 1).bindTo(showNotifications); -} + // Path to export replays to + cvarManager->registerCvar(CVAR_REPLAY_EXPORT, "0", "Save all replay files to export filepath above.", true, true, 0, true, 1).bindTo(saveReplay); + cvarManager->registerCvar(CVAR_REPLAY_EXPORT_PATH, DEAULT_EXPORT_PATH, "Path to export replays to.").bindTo(exportPath); + cvarManager->getCvar(CVAR_REPLAY_EXPORT_PATH).addOnValueChanged([this](string oldVal, CVarWrapper cvar) + { + if (SanitizeExportPath(exportPath, DEAULT_EXPORT_PATH)) + { + cvarManager->getCvar(CVAR_REPLAY_EXPORT_PATH).setValue(*exportPath); + } + }); -void AutoReplayUploaderPlugin::onUnload() -{ +#ifdef TOAST + // Notification variables + cvarManager->registerCvar(CVAR_PLUGIN_SHOW_NOTIFICATIONS, "1", "Show notifications on successful uploads", true, true, 0, true, 1).bindTo(showNotifications); +#endif } -void AutoReplayUploaderPlugin::OnGameComplete(ServerWrapper caller, void * params, std::string eventName) +/** +* OnGameComplete event called when on Function TAGame.GameEvent_Soccar_TA.EventMatchEnded event when an online game ends. +* Params: +* caller - ServerWraper this event was called from +* params - Event parameters +* eventName - Event name +*/ +void AutoReplayUploaderPlugin::OnGameComplete(ServerWrapper caller, void * params, string eventName) { - if (!*uploadToCalculated && !*uploadToBallchasing) + if (!*uploadToCalculated && !*uploadToBallchasing) // Bail if we aren't uploading replays { return; //Not uploading replays } + + // Get ReplayDirector ReplayDirectorWrapper replayDirector = caller.GetReplayDirector(); if (replayDirector.IsNull()) { cvarManager->log("Could not upload replay, director is NULL!"); - if(*showNotifications) gameWrapper->Toast("Autoreplayuploader", "Error exporting replay! (1)", "default", 3.5f, ToastType_Error); return; } + + // Get Replay wrapper ReplaySoccarWrapper soccarReplay = replayDirector.GetReplay(); if (soccarReplay.memory_address == NULL) { cvarManager->log("Could not upload replay, replay is NULL!"); - if (*showNotifications) gameWrapper->Toast("Autoreplayuploader", "Error exporting replay! (2)", "default", 3.5f, ToastType_Error); return; } - std::string replayPath = "./bakkesmod/data/autoreplaysave.replay";// cvarManager->getCvar("cl_autoreplayupload_filepath").getStringValue(); //"./bakkesmod/data/autoreplaysave.replay";// - if (file_exists(replayPath)) - { - cvarManager->log("Removing existing file: " + replayPath); - remove(replayPath.c_str()); - } - cvarManager->log("Exporting replay to " + replayPath); - soccarReplay.ExportReplay(replayPath); - cvarManager->log("Replay exported!"); - if (*uploadToCalculated) + + // If we have a template for the replay name then set the replay name based off that template else use default template + string replayName = SetReplayName(caller, soccarReplay); + + // Export the replay to a file for upload + string replayPath = ExportReplay(soccarReplay, replayName); + + // Upload replay + if (*uploadToCalculated) { - UploadReplayToEndpoint(replayPath, CALCULATED_ENDPOINT_DEFAULT, "replays", "", "calculated.gg"); + calculated->UploadReplay(replayPath); } if (*uploadToBallchasing) { - std::string authKey = cvarManager->getCvar("cl_autoreplayupload_ballchasing_authkey").getStringValue(); - if (authKey.empty()) - { - cvarManager->log("Cannot upload to ballchasing.com, no authkey set!"); - if (*showNotifications) gameWrapper->Toast("Autoreplayuploader", "Cannot upload to ballchasing.com, no authkey set!", "ballchasing_logo", 3.5f, ToastType_Error); - } - else - { - std::string visibility = cvarManager->getCvar("cl_autoreplayupload_ballchasing_visibility").getStringValue(); - UploadReplayToEndpoint(replayPath, GenerateUrl(BALLCHASING_ENDPOINT_DEFAULT, { {"visibility", visibility} }), "file", authKey, "ballchasing.com"); - } + ballchasing->UploadReplay(replayPath); } - CheckFileUploadProgress(gameWrapper.get()); -} - - -void AutoReplayUploaderPlugin::UploadReplayToEndpoint(std::string filename, std::string endpointUrl, std::string postName, std::string authKey, std::string endpointBaseUrl) -{ - std::vector data = LoadReplay(filename); - if (data.size() < 1) + // If we aren't saving the replay remove it after we've uploaded + if ((*saveReplay) == false) { - cvarManager->log("Export failed! Aborting upload"); - if (*showNotifications) gameWrapper->Toast("Autoreplayuploader", "Error exporting replay! (3)", "default", 3.5f, ToastType_Error); - return; + cvarManager->log("Removing replay file: " + replayPath); + remove(replayPath.c_str()); } - cvarManager->log("Uploading replay to " + endpointUrl); - HTTPRequestHandle hdl; - hdl = steamHTTPInstance->CreateHTTPRequest(k_EHTTPMethodPOST, endpointUrl.c_str()); - SteamAPICall_t* callHandle = NULL; - steamHTTPInstance->SetHTTPRequestHeaderValue(hdl, "User-Agent", userAgent.c_str()); - if (!authKey.empty()) +#ifdef TOAST + else if(*showNotifications) { - steamHTTPInstance->SetHTTPRequestHeaderValue(hdl, "Authorization", authKey.c_str()); + bool exported = file_exists(replayPath); + string msg = exported ? "Exported replay to: " + replayPath : "Failed to export replay to: " + replayPath; + gameWrapper->Toast("Autoreplayuploader", msg, "deafult", 3.5f, exported ? ToastType_OK : ToastType_Error); } - std::stringstream postBody; - postBody << "--" << UPLOAD_BOUNDARY << "\r\n"; - postBody << "Content-Disposition: form-data; name=\"" << postName << "\"; filename=\"autosavedreplay.replay\"" << "\r\n"; - postBody << "Content-Type: multipart/form-data" << "\r\n"; - postBody << "\r\n"; - postBody << std::string(data.begin(), data.end()); - postBody << "\r\n"; - postBody << "--" << UPLOAD_BOUNDARY << "--" << "\r\n"; - - auto postBodyString = postBody.str(); - postData = std::vector(postBodyString.begin(), postBodyString.end()); - - std::stringstream contentType; - contentType << "multipart/form-data;boundary=" << UPLOAD_BOUNDARY << ""; - steamHTTPInstance->SetHTTPRequestHeaderValue(hdl, "Content-Length", std::to_string(postData.size()).c_str()); - - if (!steamHTTPInstance->SetHTTPRequestRawPostBody(hdl, contentType.str().c_str(), &postData[0], postData.size())) +#endif +} + +Player ConstructPlayer(PriWrapper wrapper) +{ + Player p; + p.Name = wrapper.GetPlayerName().ToString(); + p.UniqueId = wrapper.GetUniqueId().ID; + p.Team = wrapper.GetTeamNum(); + p.Score = wrapper.GetScore(); + p.Goals = wrapper.GetMatchGoals(); + p.Assists = wrapper.GetMatchAssists(); + p.Saves = wrapper.GetMatchSaves(); + p.Shots = wrapper.GetMatchShots(); + p.Demos = wrapper.GetMatchDemolishes(); + return p; +} + +/** +* SetReplayName - Called to set the name of the replay in the replay file. +* Params: +* server - ServerWrapper +* soccarReplay - Replay to set name of +* replayName - A templatized string that accepts the following tokens for replacement. +* {PLAYER} - Name of current steam user +* {MODE} - Game mode of replay (Private, Ranked Standard, etc...) +* {NUM} - Current sequence number to allow for uniqueness +* {YEAR} - Year since 1900 % 100, eg. 2019 returns 19 +* {MONTH} - Month 1-12 +* {DAY} - Day of the month 1-31 +* {HOUR} - Hour of the day 0-23 +* {MIN} - Min of the hour 0-59 +* {WL} - W or L depending on if the player won or lost +* {WINLOSS} - Win or Loss depending on if the player won or lost +*/ +string AutoReplayUploaderPlugin::SetReplayName(ServerWrapper& server, ReplaySoccarWrapper& soccarReplay) +{ + string replayName = *replayNameTemplate; + cvarManager->log("Using replay name template: " + replayName); + + Match match; + + // Get Gamemode game was in + auto playlist = server.GetPlaylist(); + match.GameMode = GetPlaylistName(playlist.GetPlaylistId()); + + // Get local primary player + match.PrimaryPlayer = ConstructPlayer(server.GetLocalPrimaryPlayer().GetPRI()); + + // Get all players + auto players = server.GetLocalPlayers(); + for (int i = 0; i < players.Count(); i++) { - cvarManager->log("Could not set post body, not uploading replay!"); - steamHTTPInstance->ReleaseHTTPRequest(hdl); - return; + match.Players.push_back(ConstructPlayer(players.Get(i).GetPRI())); } - cvarManager->log("Full request body size: " + std::to_string(postData.size())); - ReplayFileUploadData* uploadData = new ReplayFileUploadData(); - uploadData->requestHandle = hdl; - uploadData->endpoint = endpointBaseUrl; - uploadData->requester = this; - steamHTTPInstance->SendHTTPRequest(uploadData->requestHandle, &uploadData->apiCall); - uploadData->requestCompleteCallback.Set(uploadData->apiCall, uploadData, &FileUploadData::OnRequestComplete); - fileUploadsInProgress.push_back(uploadData); + // Get Team scores + match.Team0Score = soccarReplay.GetTeam0Score(); + match.Team1Score = soccarReplay.GetTeam1Score(); -} + // Get current Sequence number + auto seq = *templateSequence; -std::vector AutoReplayUploaderPlugin::LoadReplay(std::string filename) -{ - std::ifstream replayFile(filename, std::ios::binary | std::ios::ate); - std::streamsize replayFileSize = replayFile.tellg(); - if (replayFileSize < 100) + replayName = ApplyNameTemplate(replayName, match, &seq); + + // Did sequence number change if so update setting + if (seq != *templateSequence) { - cvarManager->log("Replay size is too low, replay didn't export correctly?"); - return std::vector(); + *templateSequence = seq; + cvarManager->getCvar(CVAR_REPLAY_SEQUENCE_NUM).setValue(seq); + cvarManager->executeCommand("writeconfig"); // since we change this variable ourselves we want to write the config when it changes so it persists across loads } - replayFile.seekg(0, std::ios::beg); - cvarManager->log("Replay size: " + std::to_string(replayFileSize)); - std::vector data(replayFileSize, 0); - data.reserve(replayFileSize); - replayFile.read(reinterpret_cast(&data[0]), replayFileSize); - cvarManager->log("Replay data size: " + std::to_string(data.size())); - replayFile.close(); - return data; + + cvarManager->log("ReplayName: " + replayName); + soccarReplay.SetReplayName(replayName); + + return replayName; } -void AutoReplayUploaderPlugin::CheckFileUploadProgress(GameWrapper * gw) +string AutoReplayUploaderPlugin::ExportReplay(ReplaySoccarWrapper& soccarReplay, string replayName) { - cvarManager->log("Running callback, files left to upload: " + std::to_string(fileUploadsInProgress.size())); - SteamAPI_RunCallbacks_Function(); - cvarManager->log("Executed Steam callbacks"); - for (auto it = fileUploadsInProgress.begin(); it != fileUploadsInProgress.end();) + string replayPath = CalculateReplayPath(*exportPath, replayName); + + // Remove file if it already exists + if (file_exists(replayPath)) { - if ((*it)->canBeDeleted) - { - uint8 buf[4096]; - buf[0] = buf[4095] = '\0'; - uint32 body_size = 0; - steamHTTPInstance->GetHTTPResponseBodySize((*it)->requestHandle, &body_size); - //Let buffer max be 4096 (save last byte for nullbyte) - body_size = min(4095, body_size); - steamHTTPInstance->GetHTTPResponseBodyData((*it)->requestHandle, buf, body_size); - - - cvarManager->log("Request successful: " + std::to_string((*it)->successful)); - cvarManager->log("Response code: " + std::to_string((*it)->statusCode)); - cvarManager->log("Response body size: " + std::to_string(body_size)); - cvarManager->log("Response body: " + std::string(buf, buf + body_size)); - steamHTTPInstance->ReleaseHTTPRequest((*it)->requestHandle); - delete (*it); - cvarManager->log("Erased request"); - it = fileUploadsInProgress.erase(it); - } - else - { - it++; - } + cvarManager->log("Removing duplicate replay file: " + replayPath); + remove(replayPath.c_str()); } - if (!fileUploadsInProgress.empty()) + + // Export Replay + soccarReplay.ExportReplay(replayPath); + cvarManager->log("Exported replay to: " + replayPath); + + // Check to see if replay exists, if not then export to default path + if (!file_exists(replayPath)) { - gw->SetTimeout(std::bind(&AutoReplayUploaderPlugin::CheckFileUploadProgress, this, std::placeholders::_1), .5f); + cvarManager->log("Export failed to path: " + replayPath + " exporting to default path."); + replayPath = string(DEAULT_EXPORT_PATH) + "/autosaved.replay"; + + soccarReplay.ExportReplay(replayPath); + cvarManager->log("Exported replay to: " + replayPath); } -} -void AutoReplayUploaderPlugin::TestBallchasingAuth(std::vector params) -{ - HTTPRequestHandle hdl = steamHTTPInstance->CreateHTTPRequest(k_EHTTPMethodGET, "https://ballchasing.com/api/"); - SteamAPICall_t* callHandle = NULL; - steamHTTPInstance->SetHTTPRequestHeaderValue(hdl, "User-Agent", userAgent.c_str()); - - std::string authKey = cvarManager->getCvar("cl_autoreplayupload_ballchasing_authkey").getStringValue(); - steamHTTPInstance->SetHTTPRequestHeaderValue(hdl, "Authorization", authKey.c_str()); - - AuthKeyCheckUploadData* uploadData = new AuthKeyCheckUploadData(cvarManager); - uploadData->requestHandle = hdl; - uploadData->requester = this; - steamHTTPInstance->SendHTTPRequest(uploadData->requestHandle, &uploadData->apiCall); - uploadData->requestCompleteCallback.Set(uploadData->apiCall, uploadData, &FileUploadData::OnRequestComplete); - - fileUploadsInProgress.push_back(uploadData); - CheckFileUploadProgress(gameWrapper.get()); + return replayPath; } -void ReplayFileUploadData::OnRequestComplete(HTTPRequestCompleted_t * pCallback, bool failure) -{ - HTTPRequestData::OnRequestComplete(pCallback, failure); - if (*((AutoReplayUploaderPlugin*)requester)->showNotifications) { - std::string message = "Uploaded replay to " + endpoint + " successfully!"; - std::string logo_to_use = endpoint + "_logo"; - uint8_t toastType = ToastType_OK; - if (!successful) - { - message = "Unable to upload replay to " + endpoint + ". (Network error)"; - toastType = ToastType_Error; - } - else if (!(statusCode >= 200 && statusCode < 300)) - { - message = "Unable to upload replay to " + endpoint + ". (Server returned " + std::to_string(statusCode) + ")"; - toastType = ToastType_Error; - } - if (endpoint.find(".")) - { - logo_to_use = endpoint.substr(0, endpoint.find(".") - 1) + "_logo"; - } - requester->gameWrapper->Toast("Autoreplayuploader", message, logo_to_use, 3.5f, toastType); +#pragma endregion + +#pragma region Utility Functions + +string GetPlaylistName(int playlistId) { + switch (playlistId) { + case(1): + return "Casual Duel"; + break; + case(2): + return "Casual Doubles"; + break; + case(3): + return "Casual Standard"; + break; + case(4): + return "Casual Chaos"; + break; + case(6): + return "Private"; + break; + case(10): + return "Ranked Duel"; + break; + case(11): + return "Ranked Doubles"; + break; + case(12): + return "Ranked Solo Standard"; + break; + case(13): + return "Ranked Standard"; + break; + case(14): + return "Mutator Mashup"; + break; + case(22): + return "Tournament"; + break; + case(27): + return "Ranked Hoops"; + break; + case(28): + return "Ranked Rumble"; + break; + case(29): + return "Ranked Dropshot"; + break; + case(30): + return "Ranked Snowday"; + break; + default: + return ""; + break; } } -void AuthKeyCheckUploadData::OnRequestComplete(HTTPRequestCompleted_t * pCallback, bool failure) -{ - HTTPRequestData::OnRequestComplete(pCallback, failure); - std::string result = "Invalid auth key!"; - uint8_t toastType = ToastType_Warning; - if (statusCode == 200) - { - result = "Auth key correct!"; - toastType = ToastType_OK; - } - cvarManager->getCvar("cl_autoreplayupload_ballchasing_testkeyresult").setValue(result); - if (*((AutoReplayUploaderPlugin*)requester)->showNotifications) requester->gameWrapper->Toast("Autoreplayuploader", result, "ballchasing_logo", 3.5f, toastType); -} +#pragma endregion \ No newline at end of file diff --git a/AutoReplayUploader/AutoReplayUploaderPlugin.h b/AutoReplayUploader/AutoReplayUploaderPlugin.h index f08857d..182ce41 100644 --- a/AutoReplayUploader/AutoReplayUploaderPlugin.h +++ b/AutoReplayUploader/AutoReplayUploaderPlugin.h @@ -1,195 +1,54 @@ -#pragma once -#pragma comment( lib, "bakkesmod.lib" ) -#include "bakkesmod/plugin/bakkesmodplugin.h" -#include "bakkesmod/wrappers/GameEvent/ServerWrapper.h" -#include "ISteamHTTP.h" -#include - -#define CALCULATED_ENDPOINT_DEFAULT "https://calculated.gg/api/upload" -#define BALLCHASING_ENDPOINT_DEFAULT "https://ballchasing.com/api/upload" -#define UPLOAD_BOUNDARY "----BakkesModFileUpload90m8924r390j34f0" - -//S_API void SteamAPI_RegisterCallResult( class CCallbackBase *pCallback, SteamAPICall_t hAPICall ); -//S_API void SteamAPI_UnregisterCallResult(class CCallbackBase *pCallback, SteamAPICall_t hAPICall); - - -typedef void(__cdecl* SteamAPI_RunCallbacks_typedef)(); -typedef void*(__cdecl* SteamAPI_ISteamClient_GetISteamHTTP_typedef)(void* steamClient); -typedef void(__cdecl* SteamAPI_RegisterCallResult_typedef)(class CCallbackBase *pCallback, SteamAPICall_t hAPICall); -typedef void(__cdecl* SteamAPI_UnregisterCallResult_typedef)(class CCallbackBase *pCallback, SteamAPICall_t hAPICall); - -SteamAPI_RunCallbacks_typedef SteamAPI_RunCallbacks_Function; -SteamAPI_RegisterCallResult_typedef SteamAPI_RegisterCallResult_Function; -SteamAPI_UnregisterCallResult_typedef SteamAPI_UnregisterCallResult_Function; - -//----------------------------------------------------------------------------- -// Purpose: base for callbacks, -// used only by CCallback, shouldn't be used directly -//----------------------------------------------------------------------------- -class CCallbackBase -{ -public: - CCallbackBase() { m_nCallbackFlags = 0; m_iCallback = 0; } - // don't add a virtual destructor because we export this binary interface across dll's - virtual void Run(void *pvParam) = 0; - virtual void Run(void *pvParam, bool bIOFailure, SteamAPICall_t hSteamAPICall) = 0; - int GetICallback() { return m_iCallback; } - virtual int GetCallbackSizeBytes() = 0; - -protected: - enum { k_ECallbackFlagsRegistered = 0x01, k_ECallbackFlagsGameServer = 0x02 }; - uint8 m_nCallbackFlags; - int m_iCallback; - friend class CCallbackMgr; -}; - - -//----------------------------------------------------------------------------- -// Purpose: maps a steam async call result to a class member function -// template params: T = local class, P = parameter struct -//----------------------------------------------------------------------------- -template< class T, class P > -class CCallResult : private CCallbackBase -{ -public: - typedef void (T::*func_t)(P*, bool); - - CCallResult() - { - m_hAPICall = k_uAPICallInvalid; - m_pObj = NULL; - m_Func = NULL; - m_iCallback = P::k_iCallback; - } - - void Set(SteamAPICall_t hAPICall, T *p, func_t func) - { - if (m_hAPICall) - SteamAPI_UnregisterCallResult_Function(this, m_hAPICall); - - m_hAPICall = hAPICall; - m_pObj = p; - m_Func = func; - - if (hAPICall) - SteamAPI_RegisterCallResult_Function(this, hAPICall); - } - - bool IsActive() const - { - return (m_hAPICall != k_uAPICallInvalid); - } - - void Cancel() - { - if (m_hAPICall != k_uAPICallInvalid) - { - SteamAPI_UnregisterCallResult_Function(this, m_hAPICall); - m_hAPICall = k_uAPICallInvalid; - } - - } - - ~CCallResult() - { - Cancel(); - } - - void SetGameserverFlag() { m_nCallbackFlags |= k_ECallbackFlagsGameServer; } -private: - virtual void Run(void *pvParam) - { - m_hAPICall = k_uAPICallInvalid; // caller unregisters for us - (m_pObj->*m_Func)((P *)pvParam, false); - } - void Run(void *pvParam, bool bIOFailure, SteamAPICall_t hSteamAPICall) - { - if (hSteamAPICall == m_hAPICall) - { - m_hAPICall = k_uAPICallInvalid; // caller unregisters for us - (m_pObj->*m_Func)((P *)pvParam, bIOFailure); - } - } - int GetCallbackSizeBytes() - { - return sizeof(P); - } - - SteamAPICall_t m_hAPICall; - T *m_pObj; - func_t m_Func; -}; - - -struct HTTPRequestData -{ -public: - BakkesMod::Plugin::BakkesModPlugin* requester = NULL; - HTTPRequestHandle requestHandle = NULL; - SteamAPICall_t apiCall = NULL; - bool canBeDeleted = false; - bool successful = false; - EHTTPStatusCode statusCode = k_EHTTPStatusCodeInvalid; - virtual void OnRequestComplete(HTTPRequestCompleted_t* pCallback, bool failure) - { - successful = pCallback->m_bRequestSuccessful; - statusCode = pCallback->m_eStatusCode; - canBeDeleted = true; - } - - CCallResult< HTTPRequestData, HTTPRequestCompleted_t > requestCompleteCallback; -}; - -struct FileUploadData : public HTTPRequestData -{ - CCallResult< FileUploadData, HTTPRequestCompleted_t > requestCompleteCallback; -}; - -struct ReplayFileUploadData : public FileUploadData -{ -public: - std::string endpoint; - - // - virtual void OnRequestComplete(HTTPRequestCompleted_t* pCallback, bool failure); - -}; - -struct AuthKeyCheckUploadData : public HTTPRequestData -{ -public: - std::shared_ptr cvarManager; - AuthKeyCheckUploadData(std::shared_ptr cvm) - { - cvarManager = cvm; - } - - void OnRequestComplete(HTTPRequestCompleted_t* pCallback, bool failure); - - - CCallResult< AuthKeyCheckUploadData, HTTPRequestCompleted_t > requestCompleteCallback; -}; - -class AutoReplayUploaderPlugin : public BakkesMod::Plugin::BakkesModPlugin -{ -private: - std::string userAgent; - ISteamHTTP* steamHTTPInstance = NULL; - std::shared_ptr uploadToCalculated = std::make_shared(false); - std::shared_ptr uploadToBallchasing = std::make_shared(false); - - std::vector fileUploadsInProgress; - std::vector postData; - bool fileUploadThreadActive = false; - -public: - std::shared_ptr showNotifications = std::make_shared(true); - AutoReplayUploaderPlugin(); - virtual void onLoad(); - virtual void onUnload(); - void OnGameComplete(ServerWrapper caller, void* params, std::string eventName); - void UploadReplayToEndpoint(std::string filename, std::string endpointUrl, std::string postName, std::string authKey, std::string endpointBaseUrl); - std::vector LoadReplay(std::string filename); - void CheckFileUploadProgress(GameWrapper* gw); - void TestBallchasingAuth(std::vector params); +#pragma once +#include +#include + +#include "bakkesmod/plugin/bakkesmodplugin.h" + +#include "Ballchasing.h" +#include "Calculated.h" + +#pragma comment( lib, "bakkesmod.lib" ) + +using namespace std; + +#define DEAULT_EXPORT_PATH "./bakkesmod/data/" +#define DEFAULT_REPLAY_NAME_TEMPLATE "{YEAR}-{MONTH}-{DAY}.{HOUR}.{MIN} {PLAYER} {MODE} {WINLOSS}" + +// TODO: uncomment or remove #ifdef's when new Bakkes mod API becomes available that has Toast notifications +//#define TOAST + +class AutoReplayUploaderPlugin : public BakkesMod::Plugin::BakkesModPlugin +{ +private: + // Upload handlers + Ballchasing* ballchasing; + Calculated* calculated; + + // Which endpoints to upload to + shared_ptr uploadToCalculated = make_shared(false); + shared_ptr uploadToBallchasing = make_shared(false); + + // Replay name template variables + shared_ptr templateSequence = make_shared(0); + shared_ptr replayNameTemplate = make_shared(DEFAULT_REPLAY_NAME_TEMPLATE); + + // Export replay variables + shared_ptr saveReplay = make_shared(false); + shared_ptr exportPath = make_shared(DEAULT_EXPORT_PATH); + + // Initializes all variables from bakkes mod settings menu + void InitializeVariables(); + + string SetReplayName(ServerWrapper& server, ReplaySoccarWrapper& soccarReplay); + string ExportReplay(ReplaySoccarWrapper& soccarReplay, string replayName); + +public: + virtual void onLoad(); + virtual void onUnload(); + + void OnGameComplete(ServerWrapper caller, void* params, string eventName); + +#ifdef TOAST + shared_ptr showNotifications = make_shared(true); +#endif }; \ No newline at end of file diff --git a/ConsoleUploader/ConsoleUploader.vcxproj b/ConsoleUploader/ConsoleUploader.vcxproj new file mode 100644 index 0000000..014d0df --- /dev/null +++ b/ConsoleUploader/ConsoleUploader.vcxproj @@ -0,0 +1,134 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {9EBBEBF6-E373-4725-BF89-1A1AE087BAB6} + ConsoleUploader + 10.0.17763.0 + + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + Level3 + MaxSpeed + true + true + true + true + ..\Uploader;%(AdditionalIncludeDirectories) + + + true + true + + + + + Level3 + Disabled + true + true + ..\Uploader;%(AdditionalIncludeDirectories) + + + %(AdditionalDependencies) + %(AdditionalLibraryDirectories) + + + + + Level3 + Disabled + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + + + true + true + + + + + + + + {7c4c5de3-8223-4bb5-99b3-dd2124c6015e} + + + + + + \ No newline at end of file diff --git a/ConsoleUploader/ConsoleUploader.vcxproj.filters b/ConsoleUploader/ConsoleUploader.vcxproj.filters new file mode 100644 index 0000000..a91ad0e --- /dev/null +++ b/ConsoleUploader/ConsoleUploader.vcxproj.filters @@ -0,0 +1,22 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Source Files + + + \ No newline at end of file diff --git a/ConsoleUploader/main.cpp b/ConsoleUploader/main.cpp new file mode 100644 index 0000000..e0b5f3d --- /dev/null +++ b/ConsoleUploader/main.cpp @@ -0,0 +1,50 @@ +#include "Ballchasing.h" +#include "Calculated.h" +#include "Utils.h" +#include "Replay.h" + +void Log(void* object, string message) +{ + cout << message << endl; +} + +void SetVariable(void* object, string name, string value) +{ + cout << "Set: " << name << " to: " << value << endl; +} + +void CalculatedUploadComplete(void* object, bool result) +{ + cout << "Calculated upload completed with result: " << result; +} + +void BallchasingUploadComplete(void* object, bool result) +{ + cout << "Ballchasing upload completed with result: " << result; +} + +void BallchasingAuthTestComplete(void* object, bool result) +{ + cout << "Ballchasing authtest completed with result: " << result; +} + +int main() +{ + string replayFile = "C:/Program Files (x86)/Steam/steamapps/common/rocketleague/Binaries/Win32/bakkesmod/data/autoupload.replay"; + + string exportDir = "C:/Program Files (x86)/Steam/steamapps/common/rocketleague/Binaries/Win32/bakkesmod/data/"; + string replayName = "tyni"; + + string replayPath = CalculateReplayPath(exportDir, replayName); + + Calculated* calculated = new Calculated("consoleuploader", "----boundary", &Log, &CalculatedUploadComplete, NULL); + Ballchasing* ballchasing = new Ballchasing("consoleuploader", "----boundary", &Log, &BallchasingUploadComplete, &BallchasingAuthTestComplete, NULL); + + *(ballchasing->authKey) = ""; + *(ballchasing->visibility) = "public"; + + ballchasing->UploadReplay(replayFile); + calculated->UploadReplay(replayFile); + + system("PAUSE"); +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d26de2 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# AutoReplayUploader +BakkesMod plugin that automatically uploads Rocket League replays to other services (calculated.gg, ballchasing.com) when a match ends. + +## Description of Source Projects + +**AutoReplayUploader:** Contains only the plugin code and anything that has to interact with the Bakkes API. + +*NOTE: In order to build, the AutoReplayUploader project has to be updated to add the BakkesMod include directory to the compiler properties as well as adding the folder that contains the bakkesmod.dll to the additional library directory in the linker.* + +**Uploader:** Contains all code to upload replays to an endpoint and does not have any dependencies on the Bakkes API. + +**ConsoleUploader:** A simple console app to run the uploader outside of Rocket League. + +**UnitTests:** UnitTests for the Uploader.vcxproj project. + +## Settings available in BakesMod Settings Console + +Upload to Calculated - Enable automatic replay uploading to calculated.gg +Upload to Balchasing - Enable automatic replay uploading to ballchasing.com + +Replay visibility - Sets the replay visiblity on ballchasing.com when it uploads a replay. Possible values include: +* public +* private +* unlisted + +Ballchasing auth key - an authentication key required by ballchasing.com to autoupload replays. Get one at https://ballchasing.com/upload + +Replay Name Template - A templatized string to name your replays. Possible token's that will be replaced are: +* {PLAYER} - Name of current player +* {MODE} - Game mode of replay (Private, Ranked Standard, etc...) +* {NUM} - Current sequence number to allow for uniqueness +* {YEAR} - Year since 1900 % 100, eg. 2019 returns 19 +* {MONTH} - Month 1-12 +* {DAY} - Day of the month 1-31 +* {HOUR} - Hour of the day 0-23 +* {MIN} - Min of the hour 0-59 +* {WL} - W or L depending on if the player won or lost +* {WINLOSS} - Win or Loss depending on if the player won or lost +* Default = {YEAR}-{MONTH}-{DAY}.{HOUR}.{MIN} {PLAYER} {MODE} {WINLOSS} + +Replay Sequence Number - Value use in the {NUM} token in the replay name template above. Used to give uniqueness to games in a session or series. Usually you want this to start at 1 and it will auto increment from there every time it saves a replay. + +Save Replays - Enable exporting all replay files to ExportPath setting + +ExportPath - The path the plugin will save replays to diff --git a/UnitTests/ExportPathUnitTest.cpp b/UnitTests/ExportPathUnitTest.cpp new file mode 100644 index 0000000..65a05ac --- /dev/null +++ b/UnitTests/ExportPathUnitTest.cpp @@ -0,0 +1,89 @@ +#include "stdafx.h" +#include "CppUnitTest.h" + +#include "Replay.h" + +using namespace Microsoft::VisualStudio::CppUnitTestFramework; + +#define DEFAULT_EXPORT_PATH "./defaultPath" +namespace UnitTests +{ + TEST_CLASS(ExportPathUnitTests) + { + public: + + TEST_METHOD(SanitizeExportPath_ValidReturnsFalse) + { + shared_ptr exportPath = make_shared("C:/test"); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure nothing changed and we returned false + Assert::AreEqual("C:/test", exportPath->c_str()); + Assert::AreEqual(false, changed); + + // try a couple more valid values + *exportPath = "./test"; + changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + Assert::AreEqual("./test", exportPath->c_str()); + Assert::AreEqual(false, changed); + + *exportPath = "te/st"; + changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + Assert::AreEqual("te/st", exportPath->c_str()); + Assert::AreEqual(false, changed); + } + + TEST_METHOD(SanitizeExportPath_ConvertsAllBSlashToFSlash) + { + shared_ptr exportPath = make_shared("C:\\test\\"); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure it no longer contains back slash + // ensure we replaced with forward + // ensure we removed trailing slash + Assert::AreEqual("C:/test", exportPath->c_str()); + Assert::AreEqual(true, changed); + } + + TEST_METHOD(SanitizeExportPath_EmptyPathReturnsDefault) + { + shared_ptr exportPath = make_shared(""); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure an empty path returns the default + Assert::AreEqual(DEFAULT_EXPORT_PATH, exportPath->c_str()); + Assert::AreEqual(true, changed); + } + + TEST_METHOD(SanitizeExportPath_IllegalCharsRemoved) + { + shared_ptr exportPath = make_shared("|C:\\*t?e\"s"); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure all illegal chars are removed + Assert::AreEqual("C:/test", exportPath->c_str()); + Assert::AreEqual(true, changed); + } + + TEST_METHOD(SanitizeExportPath_OnlyIllegalCharsReturnsDefault) + { + shared_ptr exportPath = make_shared("*?\"<>|"); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure if orignal is all illegal strings the we return the default + Assert::AreEqual(DEFAULT_EXPORT_PATH, exportPath->c_str()); + Assert::AreEqual(true, changed); + } + + TEST_METHOD(SanitizeExportPath_OnlyTrailingSlashReturnsDefault) + { + shared_ptr exportPath = make_shared("/"); + bool changed = SanitizeExportPath(exportPath, DEFAULT_EXPORT_PATH); + + // ensure only a trailing slash returns the default + Assert::AreEqual(DEFAULT_EXPORT_PATH, exportPath->c_str()); + Assert::AreEqual(true, changed); + } + + }; +} \ No newline at end of file diff --git a/UnitTests/UnitTests.vcxproj b/UnitTests/UnitTests.vcxproj new file mode 100644 index 0000000..fd9eab3 --- /dev/null +++ b/UnitTests/UnitTests.vcxproj @@ -0,0 +1,175 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 15.0 + {0560EFD2-3FC5-4BE0-B710-108B528257C4} + Win32Proj + UnitTests + 10.0.17763.0 + NativeUnitTestProject + + + + DynamicLibrary + true + v141 + Unicode + false + + + DynamicLibrary + false + v141 + true + Unicode + false + + + DynamicLibrary + true + v141 + Unicode + false + + + DynamicLibrary + false + v141 + true + Unicode + false + + + + + + + + + + + + + + + + + + + + + true + + + true + + + true + + + true + + + + Level3 + Use + MaxSpeed + true + true + ..\Uploader;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;NDEBUG;%(PreprocessorDefinitions) + true + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + Level3 + Disabled + ..\Uploader;$(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + WIN32;_DEBUG;%(PreprocessorDefinitions) + true + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Use + Level3 + Disabled + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + _DEBUG;%(PreprocessorDefinitions) + true + + + Windows + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + Level3 + Use + MaxSpeed + true + true + $(VCInstallDir)UnitTest\include;%(AdditionalIncludeDirectories) + NDEBUG;%(PreprocessorDefinitions) + true + + + Windows + true + true + $(VCInstallDir)UnitTest\lib;%(AdditionalLibraryDirectories) + + + + + + + + + Create + Create + Create + Create + + + + + + {7c4c5de3-8223-4bb5-99b3-dd2124c6015e} + + + + + + \ No newline at end of file diff --git a/UnitTests/UnitTests.vcxproj.filters b/UnitTests/UnitTests.vcxproj.filters new file mode 100644 index 0000000..ed88491 --- /dev/null +++ b/UnitTests/UnitTests.vcxproj.filters @@ -0,0 +1,33 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/UnitTests/stdafx.cpp b/UnitTests/stdafx.cpp new file mode 100644 index 0000000..7185e11 --- /dev/null +++ b/UnitTests/stdafx.cpp @@ -0,0 +1,8 @@ +// stdafx.cpp : source file that includes just the standard includes +// UnitTests.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/UnitTests/stdafx.h b/UnitTests/stdafx.h new file mode 100644 index 0000000..43280fc --- /dev/null +++ b/UnitTests/stdafx.h @@ -0,0 +1,13 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once + +#include "targetver.h" + +// Headers for CppUnitTest +#include "CppUnitTest.h" + +// TODO: reference additional headers your program requires here diff --git a/UnitTests/targetver.h b/UnitTests/targetver.h new file mode 100644 index 0000000..87c0086 --- /dev/null +++ b/UnitTests/targetver.h @@ -0,0 +1,8 @@ +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/Uploader/Ballchasing.cpp b/Uploader/Ballchasing.cpp new file mode 100644 index 0000000..3be0f28 --- /dev/null +++ b/Uploader/Ballchasing.cpp @@ -0,0 +1,92 @@ +#include "Ballchasing.h" + +#include "HttpClient.h" +#include + +using namespace std; + +Ballchasing::Ballchasing(string userAgent, string uploadBoundary, void(*Log)(void *object, string message), void(*NotifyUploadResult)(void* object, bool result), void(*NotifyAuthResult)(void *object, bool result), void * Client) +{ + this->UserAgent = userAgent; + this->uploadBoundary = uploadBoundary; + this->Log = Log; + this->NotifyUploadResult = NotifyUploadResult; + this->NotifyAuthResult = NotifyAuthResult; + this->Client = Client; +} + +void BallchasingRequestComplete(HttpRequestObject* ctx) +{ + auto ballchasing = (Ballchasing*)ctx->Requester; + + if (ctx->RequestId == 1) + { + ballchasing->Log(ballchasing->Client, "Ballchasing::UploadCompleted with status: " + to_string(ctx->Status)); + ballchasing->NotifyUploadResult(ballchasing->Client, (ctx->Status >= 200 && ctx->Status < 300)); + + delete[] ctx->ReqData; + delete[] ctx->RespData; + delete ctx; + } + else if(ctx->RequestId == 2) + { + ballchasing->Log(ballchasing->Client, "Ballchasing::AuthTest completed with status: " + to_string(ctx->Status)); + ballchasing->NotifyAuthResult(ballchasing->Client, ctx->Status == 200); + + delete[] ctx->RespData; + delete ctx; + } +} + +void Ballchasing::UploadReplay(string replayPath) +{ + if (UserAgent.empty() || authKey->empty() || visibility->empty() || replayPath.empty()) + { + Log(Client, "Ballchasing::UploadReplay Parameters were empty."); + Log(Client, "UserAgent: " + UserAgent); + Log(Client, "ReplayPath: " + replayPath); + Log(Client, "AuthKey: " + *authKey); + Log(Client, "Visibility: " + *visibility); + return; + } + + // Fire new thread and make request, dont't wait for response + HttpFileUploadAsync( + "ballchasing.com", + AppendGetParams("api/upload", { {"visibility", *visibility} }), + UserAgent, + replayPath, + "file", + "Authorization: " + *authKey, + uploadBoundary, + 1, + this, + &BallchasingRequestComplete); +} + +/** +* Tests the authorization key for Ballchasing.com +*/ +void Ballchasing::TestAuthKey() +{ + HttpRequestObject* ctx = new HttpRequestObject(); + ctx->RequestId = 2; + ctx->Requester = this; + ctx->Headers = "Authorization: " + *authKey; + ctx->Server = "ballchasing.com"; + ctx->Page = "api/"; + ctx->Method = "GET"; + ctx->UserAgent = UserAgent; + ctx->Port = INTERNET_DEFAULT_HTTPS_PORT; + ctx->RespData = new char[4096]; + ctx->RespDataSize = 4096; + ctx->RequestComplete = &BallchasingRequestComplete; + ctx->Flags = INTERNET_FLAG_SECURE; + + // Fire new thread and make request, dont't wait for response + HttpRequestAsync(ctx); +} + +Ballchasing::~Ballchasing() +{ +} \ No newline at end of file diff --git a/Uploader/Ballchasing.h b/Uploader/Ballchasing.h new file mode 100644 index 0000000..11ce065 --- /dev/null +++ b/Uploader/Ballchasing.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +using namespace std; + +class Ballchasing +{ +private: + string UserAgent; + string uploadBoundary; + +public: + Ballchasing(string userAgent, string uploadBoundary, void(*Log)(void *object, string message), void(*NotifyUpload)(void* object, bool result), void(*NotifyAuthResult)(void *object, bool result), void * Client); + ~Ballchasing(); + + shared_ptr authKey = make_shared(""); + shared_ptr visibility = make_shared("public"); + + void(*Log)(void* object, string message); + void(*NotifyAuthResult)(void* object, bool result); + void(*NotifyUploadResult)(void* object, bool result); + void* Client; + + void UploadReplay(string replayPath); + void TestAuthKey(); +}; + diff --git a/Uploader/Calculated.cpp b/Uploader/Calculated.cpp new file mode 100644 index 0000000..fb07543 --- /dev/null +++ b/Uploader/Calculated.cpp @@ -0,0 +1,57 @@ +#include "Calculated.h" + +#include "HttpClient.h" + +using namespace std; + +Calculated::Calculated(string userAgent, string uploadBoundary, void(*log)(void* object, string message), void(*NotifyUploadResult)(void* object, bool result), void* client) +{ + this->UserAgent = userAgent; + this->uploadBoundary = uploadBoundary; + this->Log = log; + this->NotifyUploadResult = NotifyUploadResult; + this->Client = client; +} + +void CalculatedRequestComplete(HttpRequestObject* ctx) +{ + auto calculated = (Calculated*)ctx->Requester; + + calculated->Log(calculated->Client, "Calculated::UploadCompleted with status: " + to_string(ctx->Status)); + calculated->NotifyUploadResult(calculated->Client, (ctx->Status >= 200 && ctx->Status < 300)); + + delete[] ctx->ReqData; + delete[] ctx->RespData; + delete ctx; +} + +/** +* Posts the replay file to Calculated.gg +*/ +void Calculated::UploadReplay(string replayPath) +{ + if (UserAgent.empty() || replayPath.empty()) + { + Log(Client, "Calculated::UploadReplay Parameters were empty."); + Log(Client, "UserAgent: " + UserAgent); + Log(Client, "ReplayPath: " + replayPath); + return; + } + + // Fire new thread and make request, dont't wait for response + HttpFileUploadAsync( + "calculated.gg", + "api/upload", + UserAgent, + replayPath, + "replays", + "", + uploadBoundary, + 1, + this, + &CalculatedRequestComplete); +} + +Calculated::~Calculated() +{ +} diff --git a/Uploader/Calculated.h b/Uploader/Calculated.h new file mode 100644 index 0000000..c92eff9 --- /dev/null +++ b/Uploader/Calculated.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +using namespace std; + +class Calculated +{ +private: + string UserAgent; + string uploadBoundary; + +public: + Calculated(string userAgent, string uploadBoundary, void(*log)(void* object, string message), void(*NotifyUploadResult)(void* object, bool result), void* client); + ~Calculated(); + + void(*Log)(void* object, string message); + void(*NotifyUploadResult)(void* object, bool result); + void* Client; + + void UploadReplay(string replayPath); +}; + diff --git a/Uploader/HttpClient.cpp b/Uploader/HttpClient.cpp new file mode 100644 index 0000000..6e1cc7a --- /dev/null +++ b/Uploader/HttpClient.cpp @@ -0,0 +1,433 @@ +#include "HttpClient.h" + +#include +#include +#include +#include + +HttpClient::HttpClient(void) +{ + m_hConnectedEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + m_hRequestOpenedEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + m_hRequestCompleteEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + + m_hInstance = NULL; + m_hConnect = NULL; + m_hRequest = NULL; +} + +HttpClient::~HttpClient(void) +{ + if (m_hConnectedEvent) + CloseHandle(m_hConnectedEvent); + if (m_hRequestOpenedEvent) + CloseHandle(m_hRequestOpenedEvent); + if (m_hRequestCompleteEvent) + CloseHandle(m_hRequestCompleteEvent); + + Close(); +} + +void WINAPI HttpClient::Callback(HINTERNET hInternet, + DWORD dwContext, + DWORD dwInternetStatus, + LPVOID lpStatusInfo, + DWORD dwStatusInfoLen) +{ + SContext* pContext = (SContext*)dwContext; + + switch (pContext->dwContext) + { + case CONTEXT_CONNECT: + if (dwInternetStatus == INTERNET_STATUS_HANDLE_CREATED) + { + INTERNET_ASYNC_RESULT *pRes = (INTERNET_ASYNC_RESULT *)lpStatusInfo; + pContext->pObj->m_hConnect = (HINTERNET)pRes->dwResult; + SetEvent(pContext->pObj->m_hConnectedEvent); + } + break; + + case CONTEXT_REQUESTHANDLE: // Request handle + { + switch (dwInternetStatus) + { + case INTERNET_STATUS_HANDLE_CREATED: + { + INTERNET_ASYNC_RESULT *pRes = (INTERNET_ASYNC_RESULT *)lpStatusInfo; + pContext->pObj->m_hRequest = (HINTERNET)pRes->dwResult; + SetEvent(pContext->pObj->m_hRequestOpenedEvent); + } + break; + + case INTERNET_STATUS_REQUEST_SENT: + { + DWORD *lpBytesSent = (DWORD*)lpStatusInfo; + } + break; + + case INTERNET_STATUS_REQUEST_COMPLETE: + { + INTERNET_ASYNC_RESULT *pAsyncRes = (INTERNET_ASYNC_RESULT *)lpStatusInfo; + SetEvent(pContext->pObj->m_hRequestCompleteEvent); + } + break; + + case INTERNET_STATUS_REDIRECT: + //string strRealAddr = (LPSTR) lpStatusInfo; + break; + + case INTERNET_STATUS_RECEIVING_RESPONSE: + break; + + case INTERNET_STATUS_RESPONSE_RECEIVED: + { + DWORD *dwBytesReceived = (DWORD*)lpStatusInfo; + //if (*dwBytesReceived == 0) + // bAllDone = TRUE; + + } + } + } + } +} + +void HttpClient::Close() +{ + if (m_hInstance) + { + InternetCloseHandle(m_hInstance); + m_hInstance = NULL; + } + + if (m_hConnect) + { + InternetCloseHandle(m_hConnect); + m_hConnect = NULL; + } + + if (m_hRequest) + { + InternetCloseHandle(m_hRequest); + m_hRequest = NULL; + } +} + + +BOOL HttpClient::Connect(LPCTSTR lpszAddr, USHORT uPort, LPCTSTR lpszAgent, DWORD dwTimeOut) +{ + Close(); + + ResetEvent(m_hConnectedEvent); + ResetEvent(m_hRequestOpenedEvent); + ResetEvent(m_hRequestCompleteEvent); + + + if (!(m_hInstance = InternetOpen(lpszAgent, + INTERNET_OPEN_TYPE_PRECONFIG, + NULL, + NULL, + INTERNET_FLAG_ASYNC))) + { + return FALSE; + } + + + if (InternetSetStatusCallback(m_hInstance, + (INTERNET_STATUS_CALLBACK)&Callback) + == INTERNET_INVALID_STATUS_CALLBACK) + { + return FALSE; + } + + + m_context.dwContext = CONTEXT_CONNECT; + m_context.pObj = this; + + m_hConnect = InternetConnect(m_hInstance, + lpszAddr, + uPort, + NULL, + NULL, + INTERNET_SERVICE_HTTP, + INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_CACHE_WRITE, + (DWORD)&m_context); + + + if (m_hConnect == NULL) + { + if (GetLastError() != ERROR_IO_PENDING) + return FALSE; + + if (WaitForSingleObject(m_hConnectedEvent, dwTimeOut) == WAIT_TIMEOUT) + return FALSE; + } + + if (m_hConnect == NULL) + return FALSE; + + + return TRUE; +} + +BOOL HttpClient::Request(string method, string page, string headers, char* data, size_t data_size, DWORD flags, DWORD dwTimeOut) +{ + + LPCTSTR szAcceptType = _T("*/*"); + + + m_context.dwContext = CONTEXT_REQUESTHANDLE; + m_context.pObj = this; + + m_hRequest = HttpOpenRequest(m_hConnect, + method.c_str(), + page.c_str(), + NULL, + NULL, + NULL, + INTERNET_FLAG_RELOAD | INTERNET_FLAG_KEEP_CONNECTION + | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_FORMS_SUBMIT + | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS + | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | flags, + (DWORD)&m_context); + + + if (m_hRequest == NULL) + { + if (GetLastError() != ERROR_IO_PENDING) + return FALSE; + + if (WaitForSingleObject(m_hRequestOpenedEvent, dwTimeOut) == WAIT_TIMEOUT) + return FALSE; + } + + + if (m_hRequest == NULL) + return FALSE; + + + HttpAddRequestHeaders(m_hRequest, headers.c_str(), headers.length(), HTTP_ADDREQ_FLAG_ADD); + + string contentLength = "Content-Length" + to_string(data_size); + HttpAddRequestHeaders(m_hRequest, contentLength.c_str(), contentLength.length(), HTTP_ADDREQ_FLAG_ADD); + + if (!HttpSendRequest(m_hRequest, + NULL, + 0, + data, + data_size)) + { + if (GetLastError() != ERROR_IO_PENDING) + return FALSE; + + } + + + if (WaitForSingleObject(m_hRequestCompleteEvent, dwTimeOut) == WAIT_TIMEOUT) + { + Close(); + return FALSE; + } + + + return TRUE; +} + +DWORD HttpClient::Read(PBYTE pBuffer, DWORD dwSize, DWORD dwTimeOut) +{ + INTERNET_BUFFERS InetBuff; + + FillMemory(&InetBuff, sizeof(InetBuff), 0); + + InetBuff.dwStructSize = sizeof(InetBuff); + InetBuff.lpvBuffer = pBuffer; + InetBuff.dwBufferLength = dwSize - 1; + + + m_context.dwContext = CONTEXT_REQUESTHANDLE; + m_context.pObj = this; + + if (!InternetReadFileEx(m_hRequest, + &InetBuff, + 0, + (DWORD)&m_context)) + { + if (GetLastError() == ERROR_IO_PENDING) + { + if (WaitForSingleObject(m_hRequestCompleteEvent, dwTimeOut) == WAIT_TIMEOUT) + return FALSE; + } + else + return FALSE; + } + + + return InetBuff.dwBufferLength; +} + +DWORD HttpClient::GetStatusCode() +{ + DWORD statusCode = 0; + DWORD length = sizeof(DWORD); + HttpQueryInfo( + m_hRequest, + HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, + &statusCode, + &length, + NULL + ); + return statusCode; +} + + +DWORD WINAPI HttpPostThread(void* data) { + + auto ctx = (HttpRequestObject*)data; + + HttpClient client; + if (!client.Connect(ctx->Server.c_str(), ctx->Port, ctx->UserAgent.c_str(), ctx->Timeout)) + return 1; + + if (!client.Request(ctx->Method, ctx->Page, ctx->Headers, ctx->ReqData, ctx->ReqDataSize, ctx->Flags, ctx->Timeout)) + return 1; + + ctx->Status = client.GetStatusCode(); + + if (ctx->RespData != NULL) + { + DWORD nLen; + while ((nLen = client.Read((PBYTE)ctx->RespData, ctx->RespDataSize)) > 0) + { + ctx->RespData[nLen] = 0; + } + } + + client.Close(); + + ctx->RequestComplete(ctx); + + return 0; +} + +void HttpRequestAsync(HttpRequestObject* request) +{ + std::thread http(HttpPostThread, (void*)request); + http.detach(); +} + +vector GetFileBytes(string filename) +{ + // open the file + std::streampos fileSize; + std::ifstream file(filename, ios::binary); + + // get its size + file.seekg(0, ios::end); + fileSize = file.tellg(); + + if (fileSize <= 0) // in case the file does not exist for some reason + { + fileSize = 0; + } + + // initialize byte vector to size of replay + vector fileData(fileSize); + + // read replay file from the beginning + file.seekg(0, ios::beg); + file.read((char*)&fileData[0], fileSize); + file.close(); + + return fileData; +} + +string GetFileName(const string& s) +{ + char sep = '/'; + + size_t i = s.rfind(sep, s.length()); + if (i != string::npos) + { + return(s.substr(i + 1, s.length() - i)); + } + + return(""); +} + +string AppendGetParams(string baseUrl, map getParams) +{ + std::stringstream urlStream; + urlStream << baseUrl; + if (!getParams.empty()) + { + urlStream << "?"; + + for (auto it = getParams.begin(); it != getParams.end(); it++) + { + if (it != getParams.begin()) + urlStream << "&"; + urlStream << (*it).first << "=" << (*it).second; + } + } + return urlStream.str(); +} + +char* CopyToCharPtr(vector& vector) +{ + char *reqData = new char[vector.size() + 1]; + for (int i = 0; i < vector.size(); i++) + reqData[i] = vector[i]; + reqData[vector.size()] = '\0'; + return reqData; +} + +void HttpFileUploadAsync(string server, string path, string userAgent, string filepath, string paramName, string additionalHeaders, string uploadBoundary, int requestId, void* requester, void(*RequestComplete)(HttpRequestObject*)) +{ + string filename = GetFileName(filepath); + + // Get Replay file bytes to upload + auto bytes = GetFileBytes(filepath); + + // Construct headers + stringstream headers; + headers << "Content-Type: multipart/form-data;boundary=" << uploadBoundary; + if (!additionalHeaders.empty()) + headers << "\r\n" << additionalHeaders; + auto header_str = headers.str(); + + // Construct body + stringstream body; + body << "--" << uploadBoundary << "\r\n"; + body << "Content-Disposition: form-data; name=\"" << paramName << "\"; filename=\"" << filename << "\"" << "\r\n"; + body << "Content-Type: application/form-data" << "\r\n"; + body << "\r\n"; + body << string(bytes.begin(), bytes.end()); + body << "\r\n"; + body << "--" << uploadBoundary << "--" << "\r\n"; + + // Convert body to vector of bytes instead of using str() which may have trouble with null termination chars + vector buffer; + const string& str = body.str(); + buffer.insert(buffer.end(), str.begin(), str.end()); + + // Copy vector to char* for upload + char *reqData = CopyToCharPtr(buffer); + + // Setup Http Request context + HttpRequestObject* ctx = new HttpRequestObject(); + ctx->RequestId = requestId; + ctx->Requester = requester; + ctx->Headers = header_str; + ctx->Server = server; + ctx->Page = path; + ctx->Method = "POST"; + ctx->UserAgent = userAgent; + ctx->Port = INTERNET_DEFAULT_HTTPS_PORT; + ctx->ReqData = reqData; + ctx->ReqDataSize = buffer.size(); + ctx->RespData = new char[4096]; + ctx->RespDataSize = 4096; + ctx->RequestComplete = RequestComplete; + ctx->Flags = INTERNET_FLAG_SECURE; + + HttpRequestAsync(ctx); +} \ No newline at end of file diff --git a/Uploader/HttpClient.h b/Uploader/HttpClient.h new file mode 100644 index 0000000..6bf51bc --- /dev/null +++ b/Uploader/HttpClient.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include +#include +#include + +using namespace std; + +#pragma comment( lib, "wininet" ) + +class HttpClient; + +struct HttpRequestObject { + unsigned int RequestId = 0; + void* Requester = NULL; + + string Server; + + string Method = "GET"; + int Port = 80; + string Page = "/"; + + string UserAgent = "AsyncClient"; + string Headers = ""; + DWORD Flags = 0; + DWORD Timeout = 30000; + + DWORD Status = 0; + + char* ReqData = NULL; + size_t ReqDataSize = 0; + + char* RespData = NULL; + size_t RespDataSize = 0; + + void(*RequestComplete)(HttpRequestObject*); +}; + +void HttpRequestAsync(HttpRequestObject* object); +void HttpFileUploadAsync(string server, string path, string userAgent, string filepath, string paramName, string additionalHeaders, string uploadBoundary, int requestId, void* requester, void(*RequestComplete)(HttpRequestObject*)); +string AppendGetParams(string baseUrl, map getParams); + +struct SContext +{ + HttpClient* pObj; + DWORD dwContext; +}; + + +class HttpClient +{ + +protected: + + SContext m_context; + + HANDLE m_hConnectedEvent; + HANDLE m_hRequestOpenedEvent; + HANDLE m_hRequestCompleteEvent; + + HINTERNET m_hInstance; + HINTERNET m_hConnect; + HINTERNET m_hRequest; + + +public: + + HttpClient(void); + ~HttpClient(void); + + +public: + + enum + { + CONTEXT_CONNECT, + CONTEXT_REQUESTHANDLE + }; + + BOOL Connect(LPCTSTR lpszAddr, + USHORT uPort = INTERNET_DEFAULT_HTTP_PORT, + LPCTSTR lpszAgent = _T("AsyncClient"), + DWORD dwTimeOut = 30000); + + BOOL Request(string method, string page, string headers = "", char* data = NULL, size_t data_size = 0, DWORD flags = 0, DWORD dwTimeOut = 30000); + DWORD Read(PBYTE pBuffer, DWORD dwSize, DWORD dwTimeOut = 30000); + DWORD GetStatusCode(); + + void Close(); + + + static void WINAPI Callback(HINTERNET hInternet, + DWORD dwContext, + DWORD dwInternetStatus, + LPVOID lpStatusInfo, + DWORD dwStatusInfoLen); + +}; diff --git a/Uploader/Match.cpp b/Uploader/Match.cpp new file mode 100644 index 0000000..990fdee --- /dev/null +++ b/Uploader/Match.cpp @@ -0,0 +1,9 @@ +#include "Match.h" + +Match::Match() +{ +} + +Match::~Match() +{ +} \ No newline at end of file diff --git a/Uploader/Match.h b/Uploader/Match.h new file mode 100644 index 0000000..98a3fc3 --- /dev/null +++ b/Uploader/Match.h @@ -0,0 +1,23 @@ +#pragma once + +#include "Player.h" + +#include +#include + +using namespace std; + +class Match +{ +public: + + string GameMode; + Player PrimaryPlayer; + vector Players; + + int Team0Score; + int Team1Score; + + Match(); + ~Match(); +}; \ No newline at end of file diff --git a/Uploader/Player.cpp b/Uploader/Player.cpp new file mode 100644 index 0000000..4cbabdf --- /dev/null +++ b/Uploader/Player.cpp @@ -0,0 +1,14 @@ +#include "Player.h" + +Player::Player() +{ +} + +Player::~Player() +{ +} + +bool Player::WonMatch(int team0Score, int team1Score) +{ + return Team == 0 ? team0Score > team1Score : team1Score > team0Score; +} diff --git a/Uploader/Player.h b/Uploader/Player.h new file mode 100644 index 0000000..188bf7d --- /dev/null +++ b/Uploader/Player.h @@ -0,0 +1,25 @@ +#pragma once + +#include + +using namespace std; + +class Player +{ +public: + string Name; + unsigned long long UniqueId; + int Team; + + int Score; + int Goals; + int Assists; + int Saves; + int Shots; + int Demos; + + Player(); + ~Player(); + + bool WonMatch(int team0Score, int team1Score); +}; \ No newline at end of file diff --git a/Uploader/Replay.cpp b/Uploader/Replay.cpp new file mode 100644 index 0000000..e9afd51 --- /dev/null +++ b/Uploader/Replay.cpp @@ -0,0 +1,114 @@ +#include "Replay.h" + +#include +#include +#include +#include + +#include "Utils.h" + +bool SanitizeReplayNameTemplate(shared_ptr replayNameTemplate, string defaultValue) +{ + // Remove illegal characters for filename + vector illegalChars{ '\\', '/', ':', '*', '?', '\"', '<', '>', '|' }; + bool changed = RemoveChars(replayNameTemplate, illegalChars, false); + + // If empty use default + if (replayNameTemplate->empty()) + { + *replayNameTemplate = defaultValue; + changed = true; + } + + return changed; +} + +string ApplyNameTemplate(string& nameTemplate, Match& match, int* matchIndex) +{ + // Get date string + auto t = time(0); + auto now = localtime(&t); + + auto month = to_string(now->tm_mon + 1); + month.insert(month.begin(), 2 - month.length(), '0'); + + auto day = to_string(now->tm_mday); + day.insert(day.begin(), 2 - day.length(), '0'); + + auto year = to_string(now->tm_year + 1900); + + auto hour = to_string(now->tm_hour); + hour.insert(hour.begin(), 2 - hour.length(), '0'); + + auto min = to_string(now->tm_min); + min.insert(min.begin(), 2 - min.length(), '0'); + + // Calculate Win/Loss string + auto won = match.PrimaryPlayer.WonMatch(match.Team0Score, match.Team1Score); + auto winloss = won ? string("Win") : string("Loss"); + auto wl = won ? string("W") : string("L"); + + string name = nameTemplate; + ReplaceAll(name, "{MODE}", match.GameMode); + ReplaceAll(name, "{PLAYER}", match.PrimaryPlayer.Name); + ReplaceAll(name, "{UNIQUEID}", to_string(match.PrimaryPlayer.UniqueId)); + ReplaceAll(name, "{WINLOSS}", winloss); + ReplaceAll(name, "{WL}", wl); + ReplaceAll(name, "{YEAR}", year); + ReplaceAll(name, "{MONTH}", month); + ReplaceAll(name, "{DAY}", day); + ReplaceAll(name, "{HOUR}", hour); + ReplaceAll(name, "{MIN}", min); + + if (ReplaceAll(name, "{NUM}", to_string(*matchIndex))) + { + *matchIndex += 1; + } + + return name; +} + +bool SanitizeExportPath(shared_ptr exportPath, string defaultValue) +{ + // If empty use default and return OR after any below operation we return if empty + if (exportPath->empty()) + { + *exportPath = defaultValue; + return true; + } + + // Remove illegal characters for folder path + vector illegalChars{ '*', '?', '\"', '<', '>', '|' }; + bool changed = RemoveChars(exportPath, illegalChars, false); + if (exportPath->empty()) { *exportPath = defaultValue; return true; } + + // Replaces \ with / + size_t found = exportPath->find("\\"); + if (found != string::npos) + { + replace(exportPath->begin(), exportPath->end(), '\\', '/'); + changed = true; + } + + // Remove trailing slash + if ((*exportPath)[exportPath->size() - 1] == '/') + { + exportPath->pop_back(); + changed = true; + if (exportPath->empty()) { *exportPath = defaultValue; return true; } + } + + return changed; +} + +string CalculateReplayPath(string& exportDir, string& replayName) +{ + auto t = time(nullptr); + auto tm = *localtime(&t); + + // Use year-month-day-hour-min.replay for the replay filepath ex: 2019-05-21-14-21.replay + stringstream path; + path << exportDir << string("/") << replayName << " " << put_time(&tm, "%Y-%m-%d-%H-%M") << ".replay"; + + return path.str(); +} \ No newline at end of file diff --git a/Uploader/Replay.h b/Uploader/Replay.h new file mode 100644 index 0000000..197cba9 --- /dev/null +++ b/Uploader/Replay.h @@ -0,0 +1,14 @@ +#pragma once + +#include +#include "Match.h" + +using namespace std; + +bool SanitizeReplayNameTemplate(shared_ptr replayNameTemplate, string defaultValue); + +string ApplyNameTemplate(string& nameTemplate, Match& match, int* matchIndex); + +bool SanitizeExportPath(shared_ptr exportPath, string defaultValue); + +string CalculateReplayPath(string& exportDir, string& replayName); \ No newline at end of file diff --git a/Uploader/Uploader.vcxproj b/Uploader/Uploader.vcxproj new file mode 100644 index 0000000..ea0e0ac --- /dev/null +++ b/Uploader/Uploader.vcxproj @@ -0,0 +1,140 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + + + + + + + + + + + + + + + + + + + 15.0 + {7C4C5DE3-8223-4BB5-99B3-DD2124C6015E} + Uploader + 10.0.17763.0 + + + + StaticLibrary + true + v141 + MultiByte + + + StaticLibrary + false + v141 + true + MultiByte + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + + + + + + + + + + + + + + + + + + + + + Level3 + MaxSpeed + true + true + true + true + _MBCS;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + + + true + true + + + + + Level3 + Disabled + true + true + _MBCS;_CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + + + + + Level3 + Disabled + true + true + + + + + Level3 + MaxSpeed + true + true + true + true + + + true + true + + + + + + \ No newline at end of file diff --git a/Uploader/Uploader.vcxproj.filters b/Uploader/Uploader.vcxproj.filters new file mode 100644 index 0000000..25964a6 --- /dev/null +++ b/Uploader/Uploader.vcxproj.filters @@ -0,0 +1,63 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + Source Files + + + \ No newline at end of file diff --git a/Uploader/Utils.cpp b/Uploader/Utils.cpp new file mode 100644 index 0000000..a157fef --- /dev/null +++ b/Uploader/Utils.cpp @@ -0,0 +1,46 @@ +#include "Utils.h" + +using namespace std; + +bool ReplaceAll(string& str, const string& from, const string& to) { + bool replaced = false; + if (from.empty()) + return replaced; + size_t start_pos = 0; + while ((start_pos = str.find(from, start_pos)) != string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + replaced = true; + } + return replaced; +} + +bool RemoveChars(shared_ptr str, vector charsToRemove, bool changed) +{ + // Remove illegal characters for a folder path + string output; + output.reserve(str->size()); + for (size_t i = 0; i < str->size(); ++i) + { + char c = (*str)[i]; + bool found = false; + for (int j = 0; j < charsToRemove.size(); j++) + { + if (c == charsToRemove[j]) + { + found = true; + break; + } + } + if (!found) + { + output += c; + } + else + { + changed = true; + } + } + *str = output; + return changed; +} \ No newline at end of file diff --git a/Uploader/Utils.h b/Uploader/Utils.h new file mode 100644 index 0000000..2d9bc84 --- /dev/null +++ b/Uploader/Utils.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include +#include +#include + +using namespace std; + +bool ReplaceAll(string& str, const string& from, const string& to); + +bool RemoveChars(shared_ptr str, vector charsToRemove, bool changed); \ No newline at end of file diff --git a/autoreplayuploader.set b/autoreplayuploader.set index b02a23a..8d50f08 100644 --- a/autoreplayuploader.set +++ b/autoreplayuploader.set @@ -5,8 +5,11 @@ Auto replay uploader 6|Replay visibility|cl_autoreplayupload_ballchasing_visibility|public@public&private@private&unlisted@unlisted 12|Ballchasing auth key|cl_autoreplayupload_ballchasing_authkey 9|Ballchasing requires an authentication key to autoupload replays. Get one at https://ballchasing.com/upload -0|Test auth key|cl_autoreplayupload_ballchasing_testkey -7| 9|Auth key status: $cl_autoreplayupload_ballchasing_testkeyresult$ 8| +12|Replay Name Template|cl_autoreplayupload_replaynametemplate +12|Replay Name Sequence Number|cl_autoreplayupload_replaysequence +8| +1|Save all replay files to export filepath below|cl_autoreplayupload_save +12|Path to export replays to|cl_autoreplayupload_filepath 9|Note: For this to work, you do NOT have to have "save all replays" enabled! \ No newline at end of file