commit cce08f490e10f266cd27f564e03106828bd1c456 parent ece214b017d50d058d8fa5768794a6a7a0b1f43e Author: Michael Savage <mikejsavage@gmail.com> Date: Sat Jan 21 22:42:49 +0200 Give the launcher a GUI Diffstat:
launcher/main.cc | | | 599 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------- |
diff --git a/launcher/main.cc b/launcher/main.cc @@ -5,14 +5,33 @@ #include <map> #include <set> -#include "../intrinsics.h" -#include "../log.h" -#include "../http.h" -#include "../sha2.h" -#include "../strlcpy.h" -#include "../platform_io.h" +#include "intrinsics.h" +#include "game.h" +#include "log.h" +#include "gl.h" +#include "http.h" +#include "locked.h" +#include "work_queue.h" +#include "str.h" +#include "sha2.h" +#include "strlcpy.h" +#include "platform_io.h" +#include "platform_network.h" +#include "platform_thread.h" +#include "platform_time.h" +#include "liberation.h" + +#define GLFW_INCLUDE_NONE +#include "libs/glfw/include/GLFW/glfw3.h" + +#include "libs/imgui/imgui.h" +#include "libs/imgui/imgui_impl_glfw_gl3.h" -#define HASH_SEED 1234 +#if PLATFORM_WINDOWS +#define GAME_BINARY "medfall.exe" +#else +#define GAME_BINARY "medfall" +#endif struct SHA256 { SHA256() { } @@ -47,7 +66,6 @@ static bool operator!=( const SHA256 & a, const SHA256 & b ) { } struct ManifestEntry { - const char * file_name; SHA256 checksum; u64 file_size; }; @@ -67,23 +85,27 @@ static bool operator!=( const Version & a, const Version & b ) { static bool parse_version( Version * v, const char * str ) { int fields = sscanf( str, "%u.%u.%u.%u", &v->a, &v->b, &v->c, &v->d ); if( fields != 4 ) { - *v = { }; + zero( v ); return false; } return true; } -static bool parse_manifest( std::map< std::string, ManifestEntry > & manifest, const char * data ) { +static bool parse_manifest( std::map< std::string, ManifestEntry > * manifest_ptr, const char * data ) { + std::map< std::string, ManifestEntry > & manifest = *manifest_ptr; size_t len = strlen( data ); size_t i = 0; while( true ) { - char file_name[ 128 ]; - char sha256[ 65 ]; + char file_name[ 256 ]; + char sha256[ SHA256_DIGEST_STRING_LENGTH * 2 + 1 ]; unsigned long long file_size_ull; int bytes_read; int fields = sscanf( data + i, "%127s %64[0-9a-f] %llu%n", file_name, sha256, &file_size_ull, &bytes_read ); - if( fields != 3 ) return false; + if( fields != 3 ) { + manifest.clear(); + return false; + } ManifestEntry entry; strlcpy( entry.checksum.e, sha256, sizeof( entry.checksum.e ) ); @@ -163,151 +185,488 @@ static void recursive_mkdir( } } -#define HOST "medfall.mikejsavage.co.uk" -int main() { -#if PLATFORM_WINDOWS - WSADATA wsa_data; - if( WSAStartup( MAKEWORD( 2, 2 ), &wsa_data ) != 0 ) { - FATAL( "couldn't init Winsock" ); - } -#endif +struct FileUpdate { + std::string file_name; + u64 bytes_downloaded; + bool done; - // TODO: if local verison doesn't exist treat it as 0.0.0.0 - const char * local_version_str = file_get_contents_or_empty( "version.txt" ); - std::string remote_version_str; - // TODO: if getting remote version fails wait 30s and try again - struct sockaddr_storage address; - if( !dns_first( HOST, &address ) ) { - FATAL( "couldn't resolve " HOST ); - } - if( http_get( address, HOST, "/version.txt", &remote_version_str ) != GET_OK ) { - FATAL( "couldn't download version.txt" ); + explicit FileUpdate( const std::string & n ) { + file_name = n; + bytes_downloaded = 0; + done = false; } +}; - Version local_version, remote_version; - bool has_local_version = parse_version( &local_version, local_version_str ); - // TODO: if getting remote version fails wait 30s and try again - if( !parse_version( &remote_version, remote_version_str.c_str() ) ) { - FATAL( "can't parse version: %s", remote_version_str.c_str() ); - } +enum UpdaterState { + UPDATER_DNS, + UPDATER_DNS_FAILED, + UPDATER_DNS_RETRY, - printf( "local %u.%u.%u.%u remote %u.%u.%u.%u\n", - local_version.a, local_version.b, local_version.c, local_version.d, - remote_version.a, remote_version.b, remote_version.c, remote_version.d ); + UPDATER_VERSION, + UPDATER_VERSION_FAILED, + UPDATER_VERSION_RETRY, - if( has_local_version && local_version == remote_version ) { - printf( "no need to update\n" ); - return 0; - } + UPDATER_MANIFEST, + UPDATER_MANIFEST_FAILED, + UPDATER_MANIFEST_RETRY, - const char * local_manifest_str = file_get_contents_or_empty( "manifest.txt" ); - char remote_manifest_path[ 256 ]; - sprintf( remote_manifest_path, "/%u.%u.%u.%u.txt", remote_version.a, remote_version.b, remote_version.c, remote_version.d ); - std::string remote_manifest_str; - http_get( address, HOST, remote_manifest_path, &remote_manifest_str ); + UPDATER_NEED_UPDATE, + UPDATER_DOWNLOADING, + UPDATER_INSTALLING, + + UPDATER_READY, +}; + +struct Updater { + UpdaterState state = UPDATER_DNS_RETRY; + double retry_at; + + struct sockaddr_storage address; + + Version local_version, remote_version; std::map< std::string, ManifestEntry > local_manifest, remote_manifest; - parse_manifest( local_manifest, local_manifest_str ); - parse_manifest( remote_manifest, remote_manifest_str.c_str() ); - std::vector< std::string > files_to_update; + std::vector< FileUpdate > files_to_update; std::vector< std::string > files_to_remove; - std::vector< std::string > directories_to_remove; + u64 update_size = 0; +}; + +#define DOWNLOAD_THREADS 16 +static Locked< Updater > locked_updater; +static WorkQueue download_queue; - // look for files that have changed and files that should be removed - for( const auto & kv : local_manifest ) { - const auto & it = remote_manifest.find( kv.first ); - if( it == remote_manifest.end() ) { - files_to_remove.push_back( kv.first ); +#define HOST "medfall.mikejsavage.co.uk" + +static WORK_QUEUE_CALLBACK( lookup_update_server ) { + struct sockaddr_storage address; + bool ok = dns_first( HOST, &address ); + + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + if( ok ) { + updater->address = address; + updater->state = UPDATER_VERSION_RETRY; + updater->retry_at = get_time(); + } + else { + updater->state = UPDATER_DNS_FAILED; + } +} + +static WORK_QUEUE_CALLBACK( download_remote_version ) { + struct sockaddr_storage address; + { + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + address = updater->address; + } + + std::string remote_version_str; + bool ok = http_get( address, HOST, "/version.txt", &remote_version_str ) == GET_OK; + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + if( ok ) { + ok = parse_version( &updater->remote_version, remote_version_str.c_str() ); + } + if( ok ) { + if( updater->local_version == updater->remote_version ) { + updater->state = UPDATER_READY; } - else if( it->second.checksum != kv.second.checksum ) { - files_to_update.push_back( kv.first ); - update_size += it->second.file_size; + else { + updater->state = UPDATER_MANIFEST_RETRY; + updater->retry_at = get_time(); } } + else { + updater->state = UPDATER_VERSION_FAILED; + } +} - // look for new files - for( const auto & kv : remote_manifest ) { - const auto & it = local_manifest.find( kv.first ); - if( it == local_manifest.end() ) { - files_to_update.push_back( kv.first ); - update_size += kv.second.file_size; - } +static WORK_QUEUE_CALLBACK( download_manifest ) { + struct sockaddr_storage address; + Version version; + { + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + address = updater->address; + version = updater->remote_version; } - for( size_t i = 0; i < files_to_update.size(); i++ ) { - const ManifestEntry & entry = remote_manifest[ files_to_update[ i ] ]; - const std::string update_path = "update/" + files_to_update[ i ]; + char path[ 256 ]; + snprintf( path, sizeof( path ), "/%u.%u.%u.%u.txt", version.a, version.b, version.c, version.d ); + std::string manifest_str; + bool ok = http_get( address, HOST, path, &manifest_str ) == GET_OK; + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + if( ok ) { + ok = parse_manifest( &updater->remote_manifest, manifest_str.c_str() ); + } + if( ok ) { + // look for files that have changed and files that should be removed + for( const auto & kv : updater->local_manifest ) { + const auto & it = updater->remote_manifest.find( kv.first ); + if( it == updater->remote_manifest.end() ) { + updater->files_to_remove.push_back( kv.first ); + } + else if( it->second.checksum != kv.second.checksum ) { + updater->files_to_update.push_back( FileUpdate( kv.first ) ); + updater->update_size += it->second.file_size; + } + } - // check if we already downloaded it - if( already_downloaded( update_path, entry ) ) { - printf( "already downloaded %s\n", files_to_update[ i ].c_str() ); - continue; + // look for new files + for( const auto & kv : updater->remote_manifest ) { + const auto & it = updater->local_manifest.find( kv.first ); + if( it == updater->local_manifest.end() ) { + updater->files_to_update.push_back( FileUpdate( kv.first ) ); + updater->update_size += kv.second.file_size; + } } - const std::string remote_path = std::string( "/" ) + entry.checksum.e; - printf( "downloading %s (" HOST "%s)\n", - files_to_update[ i ].c_str(), - remote_path.c_str() ); + updater->state = UPDATER_NEED_UPDATE; + } + else { + updater->state = UPDATER_MANIFEST_FAILED; + } +} - // TODO: http_get_to_file. can keep track of file size and SHA256 state as we download - std::string file_contents; - GetResult res = http_get( address, HOST, remote_path.c_str(), &file_contents ); - if( res != GET_OK ) { - FATAL( "couldn't download %s", files_to_update[ i ].c_str() ); - } +static HTTP_PROGRESS_CALLBACK( download_progress ) { + SCOPED_ACQUIRE( locked_updater ); + u64 * entry_bytes_downloaded = ( u64 * ) data; + *entry_bytes_downloaded = bytes_downloaded; +} - if( file_contents.size() != entry.file_size ) { - FATAL( "sizes don't match" ); - } +static WORK_QUEUE_CALLBACK( download_file ) { + FileUpdate * file = ( FileUpdate * ) data; - SHA256 checksum( file_contents ); - if( checksum != entry.checksum ) { - FATAL( "checksums don't match" ); - } + std::string file_name; + ManifestEntry entry; - recursive_mkdir( &directories_to_remove, update_path ); + struct sockaddr_storage address; + Version version; + { + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + address = updater->address; + version = updater->remote_version; - FILE * f = fopen( update_path.c_str(), "wb" ); - if( f == NULL ) { - FATAL( "couldn't open %s for writing", update_path.c_str() ); - } - fwrite( file_contents.c_str(), 1, file_contents.size(), f ); - fclose( f ); + file_name = file->file_name; + entry = updater->remote_manifest[ file_name ]; } - for( size_t i = 0; i < files_to_update.size(); i++ ) { - const std::string update_path = "update/" + files_to_update[ i ]; - recursive_mkdir( NULL, files_to_update[ i ].c_str() ); - rename( update_path.c_str(), files_to_update[ i ].c_str() ); + std::string body; + str< 256 > path( "/%s", entry.checksum.e ); + GetResult ok = http_get( address, HOST, path.c_str(), &body, download_progress, &file->bytes_downloaded ); + // TODO: real error handling + ASSERT( ok == GET_OK ); + + std::string local_path = "update/" + file->file_name; + recursive_mkdir( NULL, local_path.c_str() ); + FILE * f = fopen( local_path.c_str(), "wb" ); + if( f == NULL ) { + FATAL( "couldn't open %s for writing", local_path.c_str() ); } + fwrite( body.c_str(), 1, body.size(), f ); + fclose( f ); + + file->done = true; +} - std::set< std::string > already_removed; - for( size_t i = 0; i < directories_to_remove.size(); i++ ) { - size_t idx = directories_to_remove.size() - i - 1; - const std::string & dir = directories_to_remove[ idx ]; - if( already_removed.find( dir ) == already_removed.end() ) { - rmdir( dir.c_str() ); - already_removed.insert( dir ); +static WORK_QUEUE_CALLBACK( install_update ) { + std::vector< std::string > updated_files; + Version version; + std::map< std::string, ManifestEntry > manifest; + + { + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + + for( const FileUpdate & file : updater->files_to_update ) { + updated_files.push_back( file.file_name ); } + + version = updater->remote_version; + manifest = updater->remote_manifest; + } + + for( const std::string & file_name : updated_files ) { + recursive_mkdir( NULL, file_name.c_str() ); + rename( ( "update/" + file_name ).c_str(), file_name.c_str() ); + } + + // TODO: remove update directory tree + // TODO: remove game files that are no longer needed + + FILE * version_txt = fopen( "version.txt", "w" ); + fprintf( version_txt, "%u.%u.%u.%u\n", version.a, version.b, version.c, version.d ); + fclose( version_txt ); + + FILE * manifest_txt = fopen( "manifest.txt", "w" ); + for( const auto & kv : manifest ) { + fprintf( manifest_txt, "%s %s %llu\n", kv.first.c_str(), kv.second.checksum.e, kv.second.file_size ); + } + fclose( manifest_txt ); + + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + updater->state = UPDATER_READY; +} + +static void asdf() { + double now = get_time(); + + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + bool retry = now >= updater->retry_at; + switch( updater->state ) { + case UPDATER_DNS_FAILED: + updater->retry_at = now + 5; + updater->state = UPDATER_DNS_RETRY; + break; + + case UPDATER_DNS_RETRY: + if( retry ) { + updater->state = UPDATER_DNS; + workqueue_enqueue( &download_queue, lookup_update_server ); + } + break; + + case UPDATER_VERSION_FAILED: + updater->retry_at = now + 5; + updater->state = UPDATER_VERSION_RETRY; + break; + + case UPDATER_VERSION_RETRY: + if( retry ) { + updater->state = UPDATER_VERSION; + workqueue_enqueue( &download_queue, download_remote_version ); + } + break; + + case UPDATER_MANIFEST_FAILED: + updater->retry_at = now + 5; + updater->state = UPDATER_MANIFEST_RETRY; + break; + + case UPDATER_MANIFEST_RETRY: + if( retry ) { + updater->state = UPDATER_MANIFEST; + workqueue_enqueue( &download_queue, download_manifest ); + } + break; + + default: + break; + } +} + +int main() { + net_init(); + + static u8 memory[ megabytes( 512 ) ]; + MemoryArena arena; + memarena_init( &arena, memory, sizeof( memory ) ); + workqueue_init( &download_queue, &arena, DOWNLOAD_THREADS ); + + GLFWwindow * window = gl_init(); + + ImGui_ImplGlfwGL3_Init( window, true ); + + ImFont * small; + ImFont * large; + ImFont * verylarge; + + { + const u32 * data = liberation_ttf_compressed_data; + const u32 size = liberation_ttf_compressed_size; + ImGuiIO & io = ImGui::GetIO(); + io.IniFilename = NULL; + io.Fonts->AddFontFromMemoryCompressedTTF( data, size, 16.0f ); + small = io.Fonts->AddFontFromMemoryCompressedTTF( data, size, 14.0f ); + large = io.Fonts->AddFontFromMemoryCompressedTTF( data, size, 32.0f ); + verylarge = io.Fonts->AddFontFromMemoryCompressedTTF( data, size, 128.0f ); } - for( size_t i = 0; i < files_to_remove.size(); i++ ) { - printf( "removing: %s\n", files_to_remove[ i ].c_str() ); - // TODO - // unlink( files_to_remove[ i ].c_str() ); + { + ImGuiStyle & style = ImGui::GetStyle(); + style.WindowRounding = 0; + style.FramePadding = ImVec2( 8, 8 ); + style.Colors[ ImGuiCol_WindowBg ] = ImColor( 40, 51, 54 ); + style.ItemSpacing.y = 8; } { - FILE * f = fopen( "version.txt", "w" ); - fwrite( remote_version_str.c_str(), 1, remote_version_str.size(), f ); - fclose( f ); + const char * local_version_str = file_get_contents_or_empty( "version.txt" ); + const char * local_manifest_str = file_get_contents_or_empty( "manifest.txt" ); - FILE * f2 = fopen( "manifest.txt", "w" ); - fwrite( remote_manifest_str.c_str(), 1, remote_manifest_str.size(), f2 ); - fclose( f2 ); + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + updater->retry_at = get_time(); + parse_version( &updater->local_version, local_version_str ); + parse_manifest( &updater->local_manifest, local_manifest_str ); } + // char name[ 64 ] = "travis_rizzle"; + + double last_frame_time = get_time(); + while( !glfwWindowShouldClose( window ) ) + { + asdf(); + + glfwPollEvents(); + ImGui_ImplGlfwGL3_NewFrame(); + + double now = get_time(); + double dt = now - last_frame_time; + + Updater * updater = SCOPED_ACQUIRE( locked_updater ); + + ImGui::SetNextWindowPos( ImVec2() ); + ImGui::SetNextWindowSize( ImVec2( WIDTH, HEIGHT ) ); + ImGui::Begin( "", NULL, 0 + | ImGuiWindowFlags_NoTitleBar + | ImGuiWindowFlags_NoResize + | ImGuiWindowFlags_NoMove + | ImGuiWindowFlags_NoCollapse + | ImGuiWindowFlags_NoScrollbar + | ImGuiWindowFlags_NoScrollWithMouse + ); + + ImGui::PushItemWidth( 900 ); + ImGui::PushStyleColor( ImGuiCol_Text, ImColor( 245, 250, 255 ) ); + ImGui::PushFont( verylarge ); + ImGui::Text( "Medfall!!!!!!!" ); + ImGui::PopFont(); + ImGui::PopStyleColor(); + ImGui::PopItemWidth(); + + ImGui::Text( "v%u.%u.%u.%u", + updater->local_version.a, + updater->local_version.b, + updater->local_version.c, + updater->local_version.d + ); + + // ImGui::AlignFirstTextHeightToWidgets(); + // ImGui::Text( "Name:" ); + // ImGui::SameLine(); + // ImGui::PushItemWidth( 200 ); + // ImGui::InputText( "##", name, sizeof( name ), 0 + // | ImGuiInputTextFlags_AutoSelectAll + // ); + // ImGui::PopItemWidth(); + + ImGui::PushFont( large ); + if( updater->state == UPDATER_READY ) { + ImGui::PushStyleColor( ImGuiCol_Button, ImColor( 32, 128, 64 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonHovered, ImColor( 32, 192, 64 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonActive, ImColor( 32, 225, 96 ) ); + bool launch = ImGui::Button( "Run fae crabs with mortalengine", ImVec2( -1, 50 ) ); + ImGui::PopStyleColor( 3 ); + + if( launch ) { + execl( GAME_BINARY, GAME_BINARY, ( char * ) 0 ); + break; + } + } + else if( updater->state == UPDATER_NEED_UPDATE ) { + ImGui::PushStyleColor( ImGuiCol_Button, ImColor( 128, 128, 64 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonHovered, ImColor( 192, 192, 64 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonActive, ImColor( 225, 225, 96 ) ); + + str< 256 > button_text( "Update to v%u.%u.%u.%u - %.2fMb", + updater->remote_version.a, + updater->remote_version.b, + updater->remote_version.c, + updater->remote_version.d, + updater->update_size / 1024.0 / 1024.0 + ); + bool update = ImGui::Button( button_text.c_str(), ImVec2( -1, 50 ) ); + ImGui::PopStyleColor( 3 ); + + if( update ) { + updater->state = UPDATER_DOWNLOADING; + for( size_t i = 0; i < updater->files_to_update.size(); i++ ) { + workqueue_enqueue( &download_queue, download_file, &updater->files_to_update[ i ] ); + } + } + } + else if( updater->state == UPDATER_DOWNLOADING ) { + static u64 last_total_downloaded = 0; + u64 total_downloaded = 0; + bool all_done = true; + for( const FileUpdate & file : updater->files_to_update ) { + total_downloaded += file.bytes_downloaded; + if( !file.done ) { + all_done = false; + } + } + double frac = double( total_downloaded ) / double( updater->update_size ); + + static double smooth_download_speed; + static bool download_speed_set = false; + + if( download_speed_set ) { + double speed = ( total_downloaded - last_total_downloaded ) / dt; + smooth_download_speed += ( speed - smooth_download_speed ) / ( 0.5 / dt ); + } + else { + smooth_download_speed = total_downloaded * 60.0; + download_speed_set = true; + } + + ImGui::PushStyleColor( ImGuiCol_PlotHistogram, ImColor( 255, 0, 0 ) ); + str< 256 > progress_text( "%.2f/%.2fMb. %.2f%% %.2fKb/s", + total_downloaded / 1024.0 / 1024.0, + updater->update_size / 1024.0 / 1024.0, + frac * 100, + smooth_download_speed / 1024.0 + ); + ImGui::ProgressBar( frac, ImVec2( -1, 50 ), progress_text.c_str() ); + ImGui::PopStyleColor(); + + last_total_downloaded = total_downloaded; + + if( all_done ) { + updater->state = UPDATER_INSTALLING; + workqueue_enqueue( &download_queue, install_update ); + } + } + else if( updater->state == UPDATER_INSTALLING ) { + ImGui::PushStyleColor( ImGuiCol_Button, ImColor( 0, 0, 0 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonHovered, ImColor( 0, 0, 0 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonActive, ImColor( 0, 0, 0 ) ); + ImGui::Button( "Installing update...", ImVec2( -1, 50 ) ); + ImGui::PopStyleColor( 3 ); + } + else { + ImGui::PushStyleColor( ImGuiCol_Button, ImColor( 0, 0, 0 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonHovered, ImColor( 0, 0, 0 ) ); + ImGui::PushStyleColor( ImGuiCol_ButtonActive, ImColor( 0, 0, 0 ) ); + ImGui::Button( "Checking for updates...", ImVec2( -1, 50 ) ); + ImGui::PopStyleColor( 3 ); + } + + ImGui::PopFont(); + + bool exit = ImGui::Button( "i'd rather put cat photos on discord" ); + if( ImGui::IsItemHovered() ) { + ImGui::SameLine(); + ImGui::Text( "pussy" ); + } + if( exit ) break; + + + // ImGui::BeginChildFrame( 1337, ImVec2(), 0 + // | ImGuiWindowFlags_AlwaysVerticalScrollbar + // ); + // ImGui::PushFont( small ); + // for( int i = 0; i < 100; i++ ) ImGui::TextWrapped( "lots of text" ); + // ImGui::PopFont(); + // ImGui::EndChildFrame(); + + ImGui::End(); + + ImGui::Render(); + glfwSwapBuffers( window ); + + last_frame_time = now; + } + + ImGui_ImplGlfwGL3_Shutdown(); + glfwTerminate(); + return 0; }