leaderboard have picture
authorhgn <hgodden00@gmail.com>
Mon, 6 Dec 2021 22:34:24 +0000 (22:34 +0000)
committerhgn <hgodden00@gmail.com>
Mon, 6 Dec 2021 22:34:24 +0000 (22:34 +0000)
fishladder.c
fishladder_resources.h
textures/ascii.png
textures/unkown.png [new file with mode: 0644]
vg/vg_console.h
vg/vg_steamworks.h
vg/vg_ui.h

index 7f47fdb54a49c96a1a5e9cfe8a913f88ef590861..3b2c09f599c903741d874ebb04e1e6f087d45b8a 100644 (file)
@@ -356,13 +356,16 @@ struct world
        int num_fishes;
        
        char map_name[64];
-       struct career_level *ptr_career_level;
+       //struct career_level *ptr_career_level;
+       struct cmp_level *pCmpLevel;
        
        u32 score;
        u32 completed;
        u32 time;
 } world = {};
 
+void leaderboard_set_score( struct cmp_level *cmp_level, u32 score );
+
 static void map_free(void)
 {      
        arrfree( world.data );
@@ -806,7 +809,7 @@ static int console_load_map( int argc, char const *argv[] )
                if( text_source )
                {
                        vg_info( "Loading map: '%s'\n", map_path );
-                       world.ptr_career_level = NULL;
+                       world.pCmpLevel = NULL;
                        
                        if( !map_load( text_source, argv[0] ) )
                        {
@@ -814,17 +817,7 @@ static int console_load_map( int argc, char const *argv[] )
                                return 0;
                        }
                        
-                       free( text_source );
-                       
-                       for( int i = 0; i < vg_list_size( level_pack_1 ); i ++ )
-                       {
-                               if( !strcmp( level_pack_1[i], argv[0] ) )
-                               {
-                                       world.ptr_career_level = career.levels + i;
-                                       break;
-                               }
-                       }
-                       
+                       free( text_source );    
                        return 1;
                }
                else
@@ -875,10 +868,13 @@ static int console_changelevel( int argc, char const *argv[] )
 }
 
 void leaderboard_found( LeaderboardFindResult_t *pCallback );
+void leaderboard_downloaded( LeaderboardScoresDownloaded_t *pCallback );
+
 void vg_start(void)
 {
        // Steamworks callbacks
        sw_leaderboard_found = &leaderboard_found;
+       sw_leaderboard_downloaded = &leaderboard_downloaded;
 
        vg_function_push( (struct vg_cmd){
                .name = "_map_write",
@@ -1038,6 +1034,7 @@ void vg_start(void)
 
 void vg_free(void)
 {
+       sw_free_opengl();
        console_save_map( 0, NULL );
        career_serialize();
 
@@ -1760,11 +1757,12 @@ void vg_update(void)
                                }
                                
                                // Copy into career data
-                               if( world.ptr_career_level )
+                               if( world.pCmpLevel )
                                {
-                                       world.ptr_career_level->score = world.score;
-                                       world.ptr_career_level->time = world.time;
-                                       world.ptr_career_level->completed = world.completed;
+                                       if( world.score > world.pCmpLevel->completed_score )
+                                               leaderboard_set_score( world.pCmpLevel, world.score );
+                                       
+                                       world.pCmpLevel->completed_score = world.score;
                                }
                                
                                simulation_stop(); // TODO: Async?
@@ -2278,7 +2276,30 @@ static ui_colourset flcol_list_locked = {
 static struct
 {
        SteamLeaderboard_t steam_leaderboard;
-       int leaderboard_matches;
+       int leaderboard_show;
+       
+       struct leaderboard_player
+       {
+               // Internal
+               u64_steamid id;
+               i32 score;
+               
+               // To be displayed
+               char score_text[ 16 ];
+               char player_name[ 48 ];
+               GLuint texture;                 // Not dynamic
+       }
+       leaderboard_players[10];
+       int leaderboard_count;
+       
+       struct
+       {
+               struct cmp_level *level;
+               
+               i32 score;
+               int is_waiting;
+       }
+       upload_request;
        
        struct cmp_level *level_selected;
 } 
@@ -2412,7 +2433,7 @@ void vg_ui(void)
                                                if( gui_button( 2 + i ) == k_button_click )
                                                {
                                                        ui_data.level_selected = &levels[i];
-                                                       ui_data.leaderboard_matches = 0;
+                                                       ui_data.leaderboard_show = 0;
                                                        sw_find_leaderboard( ui_data.level_selected->map_name );
                                                }
                                                
@@ -2511,8 +2532,10 @@ void vg_ui(void)
                                if( gui_button( 3002 ) == k_button_click )
                                {
                                        console_changelevel( 1, &ui_data.level_selected->map_name );
+                                       world.pCmpLevel = ui_data.level_selected;
+
                                        ui_data.level_selected = NULL;
-                                       ui_data.leaderboard_matches = 0;
+                                       ui_data.leaderboard_show = 0;
                                }
                                gui_text( "Play", 6, 0 );
                                gui_end();
@@ -2522,7 +2545,7 @@ void vg_ui(void)
                }
                gui_end_right();
                
-               if( ui_data.leaderboard_matches )
+               if( ui_data.leaderboard_show )
                {
                        ui_global_ctx.cursor[0] += 16;
                        ui_global_ctx.cursor[3] = 250;
@@ -2531,21 +2554,144 @@ void vg_ui(void)
                        gui_new_node();
                        {
                                gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+                               
+                               ui_rect_pad( ui_global_ctx.cursor, 8 );
+                               
+                               gui_new_node();
+                               {
+                                       ui_global_ctx.cursor[3] = 32+8;
+                                       
+                                       for( int i = 0; i < ui_data.leaderboard_count; i ++ )
+                                       {
+                                               gui_new_node();
+                                               {
+                                                       struct leaderboard_player *player = &ui_data.leaderboard_players[i];
+                                                       
+                                                       ui_global_ctx.cursor[0] += 4;
+                                                       ui_global_ctx.cursor[1] += 4;                                                   
+                                                       ui_global_ctx.cursor[2] = 32;
+                                                       ui_global_ctx.cursor[3] = 32;
+                                                       
+                                                       gui_new_node();
+                                                       {
+                                                               gui_push_image( ui_global_ctx.cursor, player->texture );
+                                                       }
+                                                       gui_end_right();
+                                                       
+                                                       gui_text( player->player_name, 6, 0 );
+                                                       
+                                                       ui_global_ctx.cursor[2] = 50;
+                                                       gui_align_right();
+                                                       
+                                                       gui_text( player->score_text, 6, 0 );
+                                               }
+                                               gui_end_down();
+                                               
+                                               ui_global_ctx.cursor[1] += 2;
+                                               
+                                       }
+                               }
+                               gui_end();
                        }
                        gui_end();
                }
        }
 }
 
+void leaderboard_dispatch_score(void)
+{
+       sw_upload_leaderboard_score( 
+               ui_data.upload_request.level->steam_leaderboard, 
+               k_ELeaderboardUploadScoreMethodKeepBest,
+               ui_data.upload_request.score,
+               NULL,
+               0
+       );
+       
+       ui_data.upload_request.is_waiting = 0;
+       
+       vg_success( "Dispatched leaderboard score\n" );
+}
+
 void leaderboard_found( LeaderboardFindResult_t *pCallback )
 {
        if( !pCallback->m_bLeaderboardFound )
+       {
                vg_error( "Leaderboard could not be found\n" );
+               ui_data.steam_leaderboard = 0;
+       }
+       else
+       {
+               const char *recieved_name = sw_get_leaderboard_name( pCallback->m_hSteamLeaderboard );
+               
+               // Update UI state and request entries if this callback found the current UI level
+               if( ui_data.level_selected )
+               {
+                       if( !strcmp( recieved_name, ui_data.level_selected->map_name ) )
+                       {
+                               sw_download_leaderboard_entries( pCallback->m_hSteamLeaderboard, k_ELeaderboardDataRequestFriends, 0, 10 );
+                               ui_data.level_selected->steam_leaderboard = pCallback->m_hSteamLeaderboard;
+                       }
+               }
+               
+               // Dispatch the waiting request if there was one
+               if( ui_data.upload_request.is_waiting )
+               {
+                       if( !strcmp( recieved_name, ui_data.upload_request.level->map_name ) )
+                       {
+                               ui_data.upload_request.level->steam_leaderboard = pCallback->m_hSteamLeaderboard;
+                               leaderboard_dispatch_score();
+                       }
+               }
+       }
+}
+
+void leaderboard_downloaded( LeaderboardScoresDownloaded_t *pCallback )
+{
+       // Update UI if this leaderboard matches what we currently have in view
+       if( ui_data.level_selected->steam_leaderboard == pCallback->m_hSteamLeaderboard )
+       {
+               vg_info( "Recieved %d entries\n", pCallback->m_cEntryCount );
+               ui_data.leaderboard_count = VG_MIN( pCallback->m_cEntryCount, 10 );
+               
+               for( int i = 0; i < ui_data.leaderboard_count; i ++ )
+               {
+                       LeaderboardEntry_t entry;
+                       sw_get_downloaded_entry( pCallback->m_hSteamLeaderboardEntries, i, &entry, NULL, 0 );
+                       
+                       struct leaderboard_player *player = &ui_data.leaderboard_players[i];
+
+                       player->id = entry.m_steamIDUser.m_unAll64Bits;
+                       strncpy( player->player_name, sw_get_friend_persona_name( player->id ), vg_list_size( player->player_name )-1 );
+                       player->score = entry.m_nScore;
+                       
+                       snprintf( player->score_text, vg_list_size(player->score_text), "%d", player->score );
+                       player->texture = sw_get_player_image( player->id );
+                       
+                       if( player->texture == 0 )
+                               player->texture = tex_unkown.name;
+               }
+               
+               if( ui_data.leaderboard_count )
+                       ui_data.leaderboard_show = 1;
+               else
+                       ui_data.leaderboard_show = 0;
+       }
+       else vg_warn( "Downloaded leaderboard does not match requested!\n" );
+}
+
+void leaderboard_set_score( struct cmp_level *cmp_level, u32 score )
+{
+       if( ui_data.upload_request.is_waiting )
+               vg_warn( "You are uploading leaderboard entries too quickly!\n" );
+               
+       ui_data.upload_request.level = cmp_level;
+       ui_data.upload_request.score = score;
+       ui_data.upload_request.is_waiting = 1;
        
-       ui_data.steam_leaderboard = pCallback->m_hSteamLeaderboard;
-       ui_data.leaderboard_matches = 0;
-       
-       if( ui_data.level_selected )
-               if( !strcmp( sw_get_leaderboard_name( ui_data.steam_leaderboard ), ui_data.level_selected->map_name ) )
-                       ui_data.leaderboard_matches = 1;
+       // If leaderboard ID has been downloaded already then just immediately dispatch this
+       if( cmp_level->steam_leaderboard )
+               leaderboard_dispatch_score();
+       else
+               sw_find_leaderboard( cmp_level->map_name );
 }
index 9a030d1ced25f550122958bf471d8e7964172144..fca9152da0e1fc9b032a856b8bfbaecaecd634c3 100644 (file)
@@ -7,8 +7,9 @@ vg_tex2d tex_wood =                     { .path = "textures/wood.qoi" };
 vg_tex2d tex_background =      { .path = "textures/background.qoi" };
 vg_tex2d tex_ball_noise =  { .path = "textures/bnoise.qoi" };
 vg_tex2d tex_monofur   =  { .path = "textures/ascii.qoi", .flags = VG_TEXTURE_NO_MIP };
+vg_tex2d tex_unkown            =  { .path = "textures/unkown.qoi" };
 
-vg_tex2d *texture_list[] = { &tex_tile_detail, &tex_tile_data, &tex_wood, &tex_background, &tex_ball_noise, &tex_monofur };
+vg_tex2d *texture_list[] = { &tex_tile_detail, &tex_tile_data, &tex_wood, &tex_background, &tex_ball_noise, &tex_monofur, &tex_unkown };
 
 // AUDIO
 // ===========================================================================================================
@@ -571,6 +572,8 @@ struct cmp_level
        int linked_unlocks;     // When unlocked, unlock this many levels additionally
        
        int serial_id;
+       
+       SteamLeaderboard_t steam_leaderboard;
 };
 
 struct cmp_level cmp_levels_tutorials[] = 
index daa341f0e57e51e96eea6c8b59054f04c6a39304..78884ae9e23954866f758c2e10aee7932eddcf6d 100644 (file)
Binary files a/textures/ascii.png and b/textures/ascii.png differ
diff --git a/textures/unkown.png b/textures/unkown.png
new file mode 100644 (file)
index 0000000..b26c041
Binary files /dev/null and b/textures/unkown.png differ
index 48f1f2ec985b1228400f528d7d44dcfb0c5b52bd..c10abd653fb1cb3fc23f611ddaef545c459895fd 100644 (file)
@@ -80,7 +80,7 @@ static void vg_console_draw( void )
                
                for( int i = 0; i < vg_console.len; i ++ )
                {
-                       ui_text( &ui_global_ctx, vg_console.lines[ptr], vg_console.scale, 0 );
+                       ui_text( &ui_global_ctx, vg_console.lines[ptr], vg_console.scale*2, 0 );
                        ui_global_ctx.cursor[1] -= 8*vg_console.scale;
                
                        ptr --;
@@ -97,7 +97,7 @@ static void vg_console_draw( void )
        {
                ui_fill_rect( &ui_global_ctx, ui_global_ctx.cursor, 0x77333333 );
                
-               ui_text( &ui_global_ctx, vg_console.input, vg_console.scale, 0 );
+               ui_text( &ui_global_ctx, vg_console.input, vg_console.scale*2, 0 );
                
                int start = VG_MIN( vg_console.cursor_pos, vg_console.cursor_user ),
                         end   = VG_MAX( vg_console.cursor_pos, vg_console.cursor_user );
index b14e9eaf63decf3916e97911584b291499a39941..c0baba54bcca3dea04b42f4e9e0a411c913f4bdc 100644 (file)
@@ -547,6 +547,14 @@ int                SteamAPI_ISteamUserStats_SetAchievement( ISteamUserStats *self, const char
 
 ESteamAPICallFailure SteamAPI_ISteamUtils_GetAPICallFailureReason( ISteamUtils* self, SteamAPICall_t hSteamAPICall );
 
+// Friends
+char                                   *SteamAPI_ISteamFriends_GetPersonaName( ISteamFriends *self );
+const char                     *SteamAPI_ISteamFriends_GetFriendPersonaName( ISteamFriends *self, u64_steamid steamIDFriend );
+u64_steamid             SteamAPI_ISteamUser_GetSteamID( ISteamUser *self );
+int                                     SteamAPI_ISteamFriends_GetSmallFriendAvatar( ISteamFriends *self, u64_steamid steamIDFriend ); // 32x32
+int    SteamAPI_ISteamUtils_GetImageSize( ISteamUtils *self, int iImage, u32 *pnWidth, u32 *pnHeight );
+int    SteamAPI_ISteamUtils_GetImageRGBA( ISteamUtils *self, int iImage, u8 *pubDest, int nDestBufferSize );
+
 // Leaderboards
 SteamAPICall_t SteamAPI_ISteamUserStats_FindOrCreateLeaderboard( ISteamUserStats* self, const char * pchLeaderboardName, ELeaderboardSortMethod eLeaderboardSortMethod, ELeaderboardDisplayType eLeaderboardDisplayType );
 SteamAPICall_t SteamAPI_ISteamUserStats_FindLeaderboard( ISteamUserStats* self, const char * pchLeaderboardName );
@@ -560,6 +568,18 @@ int SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry( ISteamUserStats* sel
 SteamAPICall_t SteamAPI_ISteamUserStats_UploadLeaderboardScore( ISteamUserStats* self, SteamLeaderboard_t hSteamLeaderboard, ELeaderboardUploadScoreMethod eLeaderboardUploadScoreMethod, i32 nScore, const i32 * pScoreDetails, int cScoreDetailsCount );
 SteamAPICall_t SteamAPI_ISteamUserStats_AttachLeaderboardUGC( ISteamUserStats* self, SteamLeaderboard_t hSteamLeaderboard, UGCHandle_t hUGC );
 
+#define sw_get_image_size(...) SteamAPI_ISteamUtils_GetImageSize( steam_api_classes.utils, __VA_ARGS__ )
+#define sw_get_image_rgba(...) SteamAPI_ISteamUtils_GetImageRGBA( steam_api_classes.utils, __VA_ARGS__ )
+#define sw_get_small_friend_avatar(...) SteamAPI_ISteamFriends_GetSmallFriendAvatar( steam_api_classes.friends, __VA_ARGS__ )
+#define sw_get_steamid() SteamAPI_ISteamUser_GetSteamID( steam_api_classes.friends )
+#define sw_get_friend_persona_name(...) SteamAPI_ISteamFriends_GetFriendPersonaName( steam_api_classes.friends, __VA_ARGS__ )
+#define sw_get_persona_name() SteamAPI_ISteamFriends_GetPersonaName( steam_api_classes.friends )
+#define sw_find_leaderboard(...) SteamAPI_ISteamUserStats_FindLeaderboard( steam_api_classes.stats, __VA_ARGS__ );
+#define sw_get_leaderboard_name(...) SteamAPI_ISteamUserStats_GetLeaderboardName( steam_api_classes.stats, __VA_ARGS__ )
+#define sw_download_leaderboard_entries(...) SteamAPI_ISteamUserStats_DownloadLeaderboardEntries( steam_api_classes.stats, __VA_ARGS__ )
+#define sw_get_downloaded_entry(...) SteamAPI_ISteamUserStats_GetDownloadedLeaderboardEntry( steam_api_classes.stats, __VA_ARGS__ )
+#define sw_upload_leaderboard_score(...) SteamAPI_ISteamUserStats_UploadLeaderboardScore( steam_api_classes.stats, __VA_ARGS__ )
+
 HSteamPipe SteamAPI_GetHSteamPipe();
 HSteamUser SteamAPI_GetHSteamUser();
 
@@ -573,8 +593,115 @@ struct
        
        HSteamPipe                      pipe;
        
+       struct cached_player
+       {
+               u64_steamid id;
+               GLuint          avatar_texture;         // tex_unkown.name
+               
+               struct cached_player *l, *r;
+       }
+       cached_players[20];
+       
+       struct cached_player *cache_head, *cache_tail;
+       
+       u32 cache_count;
+       
 } steam_api_classes;
 
+static void _sw_cache_push( struct cached_player *player )
+{
+       player->l = NULL;
+       player->r = steam_api_classes.cache_head;
+       if( steam_api_classes.cache_head ) steam_api_classes.cache_head->l = player;
+       if( !steam_api_classes.cache_tail ) steam_api_classes.cache_tail = player;
+       steam_api_classes.cache_head = player;
+       steam_api_classes.cache_count ++;
+}
+
+static void _sw_cache_evict( struct cached_player *player )
+{
+       if( player == steam_api_classes.cache_tail ) steam_api_classes.cache_tail = player->l;
+       if( player == steam_api_classes.cache_head ) steam_api_classes.cache_head = player->r;
+       if( player->l ) player->l->r = player->r;
+       if( player->r ) player->r->l = player->l;
+       steam_api_classes.cache_count --;
+}
+
+static void _sw_access_cache( struct cached_player *player )
+{
+       _sw_cache_evict( player );
+       _sw_cache_push( player );
+}
+
+static GLuint sw_get_player_image( u64_steamid usr )
+{
+       // Look for player in cache
+       for( int i = 0; i < steam_api_classes.cache_count; i ++ )
+       {
+               struct cached_player *player = &steam_api_classes.cached_players[i];
+               
+               if( player->id == usr )
+               {
+                       _sw_access_cache( player );
+                       return player->avatar_texture;
+               }
+       }
+       
+       struct cached_player *dest;
+       
+       if( steam_api_classes.cache_count == vg_list_size( steam_api_classes.cached_players ) )
+       {
+               dest = steam_api_classes.cache_tail;
+               _sw_access_cache( dest );
+               
+               // Delete previous before creating a new one
+               glDeleteTextures( 1, &dest->avatar_texture );
+       }
+       else
+       {
+               dest = &steam_api_classes.cached_players[ steam_api_classes.cache_count ];
+               _sw_cache_push( dest );
+       }
+       
+       dest->id = usr;
+       dest->avatar_texture = 0;
+       
+       // Upload new image
+       u32 x = 32, y = 32;
+       int steam_image;
+       
+       steam_image = sw_get_small_friend_avatar( usr );
+       if( !steam_image )
+               return 0;
+               
+       if( !sw_get_image_size( steam_image, &x, &y ) )
+               return 0;
+               
+       u8 * img_buf = (u8 *)malloc( x * y * 4 );
+       
+       if( !sw_get_image_rgba(steam_image, img_buf, x * y * 4) )
+       {
+               free( img_buf );
+               return 0;
+       }
+       
+       glGenTextures( 1, &dest->avatar_texture );
+       glBindTexture( GL_TEXTURE_2D, dest->avatar_texture );
+       
+       glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, x, y, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_buf );
+       glGenerateMipmap( GL_TEXTURE_2D );
+       
+       glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
+       glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
+       
+       glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
+       glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+       
+       free( img_buf );
+       
+       return dest->avatar_texture;
+}
+
 ISteamFriends          *SteamAPI_SteamFriends_v017();
 ISteamUser                     *SteamAPI_SteamUser_v021();
 ISteamUserStats        *SteamAPI_SteamUserStats_v012();
@@ -586,6 +713,14 @@ static void sw_exit(void)
        SteamAPI_Shutdown();
 }
 
+// Needs to be manually called by client unfortunately
+static void sw_free_opengl(void)
+{
+       for( int i = 0; i < steam_api_classes.cache_count; i ++ )
+               if( steam_api_classes.cached_players[i].avatar_texture )
+                       glDeleteTextures( 1, &steam_api_classes.cached_players[i].avatar_texture );
+}
+
 static int sw_init(void)
 {
        #if defined(VALVE_CALLBACK_PACK_SMALL)
@@ -638,6 +773,7 @@ static int sw_init(void)
 
 
 void (*sw_leaderboard_found)( LeaderboardFindResult_t *pCallback );
+void (*sw_leaderboard_downloaded)( LeaderboardScoresDownloaded_t *pCallback );
 
 static void sw_event_loop(void)
 {
@@ -674,6 +810,9 @@ static void sw_event_loop(void)
                                        case SW_CBID_LeaderboardFindResult:
                                                if( sw_leaderboard_found ) sw_leaderboard_found( (LeaderboardFindResult_t*)pTmpCallResult );
                                        break;
+                                       case SW_CBID_LeaderboardScoresDownloaded:
+                                               if( sw_leaderboard_downloaded ) sw_leaderboard_downloaded( (LeaderboardScoresDownloaded_t*)pTmpCallResult );
+                                       break;
                                        default:break;
                                }
                        } 
@@ -729,13 +868,3 @@ static void sw_set_achievement( const char *vg_ach_name )
                vg_success( "Achievement set: '%s'\n", vg_ach_name );
        }
 }
-
-static void sw_find_leaderboard( const char *name )
-{
-       SteamAPI_ISteamUserStats_FindLeaderboard( steam_api_classes.stats, name );
-}
-
-static const char *sw_get_leaderboard_name( SteamLeaderboard_t hSteamLeaderboard )
-{
-       return SteamAPI_ISteamUserStats_GetLeaderboardName( steam_api_classes.stats, hSteamLeaderboard );
-}
index 03647392a27ebe8cfce24e7f82b891197720066f..3e4ae5e3717d272c90af5d9832368b5f5bb032c5 100644 (file)
@@ -39,7 +39,7 @@ SHADER_DEFINE( shader_ui,
                "float clip_blend = step( aWsp.x, aClip.z ) * step( aWsp.y, aClip.w ) * step( aClip.x, aWsp.x ) * step( aClip.y, aWsp.y );"
        
                "vec4 glyph = texture( uTexGlyphs, aTexCoords );"
-               "FragColor = aColour * vec4( 1.0, 1.0, 1.0, glyph.r * clip_blend );"
+               "FragColor = vec4( aColour.rgb * glyph.rgb, aColour.a*glyph.a*clip_blend );"
        "}"
        ,
        UNIFORMS({ "uPv", "uTexGlyphs" })
@@ -120,41 +120,20 @@ struct ui_ctx
        
        ui_colourset *colours_main;
        ui_colourset *colours_current;
+       
+       GLuint vao;
+       GLuint vbo;
+       GLuint ebo;
+       
+       struct ui_image
+       {
+               ui_rect rc;
+               GLuint image;
+       }
+       images[16];
+       int image_count;
 };
 
-// Shortnames
-#define gui_draw(...) ui_draw( &ui_global_ctx, __VA_ARGS__)
-#define gui_current(...) ui_current( &ui_global_ctx, __VA_ARGS__)
-#define gui_new_node() ui_new_node( &ui_global_ctx )
-#define gui_hasmouse(...) ui_hasmouse( &ui_global_ctx, __VA_ARGS__)
-#define gui_end() ui_end( &ui_global_ctx )
-#define gui_end_down() ui_end_down( &ui_global_ctx )
-#define gui_end_right() ui_end_right( &ui_global_ctx )
-#define gui_fill_y() ui_fill_y( &ui_global_ctx)
-#define gui_fill_x() ui_fill_x( &ui_global_ctx)
-#define gui_align_bottom() ui_align_bottom( &ui_global_ctx )
-#define gui_align_right() ui_align_right( &ui_global_ctx )
-#define gui_align_top() ui_align_top( &ui_global_ctx )
-#define gui_align_left() ui_align_left( &ui_global_ctx )
-#define gui_clamp_rect(...) ui_clamp_rect( &ui_global_ctx, __VA_ARGS__)
-#define gui_group_id(...) ui_group_id( &ui_global_ctx, __VA_ARGS__)
-#define gui_capture_mouse(...) ui_capture_mouse( &ui_global_ctx, __VA_ARGS__)
-#define gui_set_clip(...) ui_set_clip( &ui_global_ctx, __VA_ARGS__)
-#define gui_release_clip() ui_release_clip( &ui_global_ctx )
-#define gui_fill_rect_uv(...) ui_fill_rect_uv( &ui_global_ctx, __VA_ARGS__)
-#define gui_fill_rect(...) ui_fill_rect( &ui_global_ctx, __VA_ARGS__)
-#define gui_text(...) ui_text( &ui_global_ctx, __VA_ARGS__)
-#define gui_begin(...) ui_begin( &ui_global_ctx, __VA_ARGS__)
-#define gui_resolve(...) ui_resolve( &ui_global_ctx, __VA_ARGS__)
-#define gui_set_mouse(...) ui_set_mouse( &ui_global_ctx, __VA_ARGS__)
-#define gui_button(...) ui_button( &ui_global_ctx, __VA_ARGS__)
-#define gui_window(...) ui_window( &ui_global_ctx, __VA_ARGS__)
-#define gui_want_mouse() ui_want_mouse( &ui_global_ctx )
-
-#define gui_scrollbar(...) ui_scrollbar( &ui_global_ctx, __VA_ARGS__)
-#define gui_override_colours(...) ui_override_colours( &ui_global_ctx, __VA_ARGS__)
-#define gui_reset_colours(...) ui_reset_colours( &ui_global_ctx )
-
 // Globals
 // ===========================================================================================================
 
@@ -162,12 +141,6 @@ struct ui_ctx
 int ui_glyph_override = 0;
 ui_px ui_glyph_spacing_x = 6;
 GLuint ui_glyph_texture = 0;
-GLuint ui_vao;
-GLuint ui_vbo;
-GLuint ui_ebo;
-
-#define UI_BUFFER_SIZE 30000
-#define UI_INDEX_SIZE 20000
 
 ui_colourset ui_default_colours = {
        .main = 0xff00ff00,
@@ -176,7 +149,6 @@ ui_colourset ui_default_colours = {
 };
 ui_ctx ui_global_ctx = { 
        .padding = 8, 
-       .colours_current = &ui_default_colours, 
        .colours_main = &ui_default_colours
 };
 
@@ -184,6 +156,64 @@ ui_ctx ui_global_ctx = {
 // Initialization
 // ===========================================================================================================
 
+static void ui_reset_colours( ui_ctx *ctx );
+static void ui_init_context( ui_ctx *ctx, int index_buffer_size )
+{
+       ui_reset_colours( ctx );
+       
+       u32 vertex_buffer_size = (index_buffer_size+(index_buffer_size/2));
+       
+       // Generate the buffer we are gonna be drawing to
+       {
+               glGenVertexArrays(1, &ctx->vao);
+               glGenBuffers( 1, &ctx->vbo );
+               glGenBuffers( 1, &ctx->ebo );
+               glBindVertexArray( ctx->vao );
+               
+               glBindBuffer( GL_ARRAY_BUFFER, ctx->vbo );
+               
+               glBufferData( GL_ARRAY_BUFFER, vertex_buffer_size * sizeof( struct ui_vert ), NULL, GL_DYNAMIC_DRAW );
+               glBindVertexArray( ctx->vao );
+               
+               glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, ctx->ebo );
+               glBufferData( GL_ELEMENT_ARRAY_BUFFER, index_buffer_size * sizeof( u16 ), NULL, GL_DYNAMIC_DRAW );
+               
+               u32 const stride = sizeof( struct ui_vert );
+               
+               // XY
+               glVertexAttribPointer( 0, 2, GL_SHORT, GL_FALSE, stride, (void *)offsetof( struct ui_vert, co ) );
+               glEnableVertexAttribArray( 0 );
+               
+               // UV
+               glVertexAttribPointer( 1, 2, GL_UNSIGNED_BYTE, GL_FALSE, stride, (void *)offsetof( struct ui_vert, uv ) );
+               glEnableVertexAttribArray( 1 );
+               
+               // COLOUR
+               glVertexAttribPointer( 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, stride, (void *)offsetof( struct ui_vert, colour ) );
+               glEnableVertexAttribArray( 2 );
+               
+               // CLIPPING
+               glVertexAttribPointer( 3, 4, GL_SHORT, GL_FALSE, stride, (void *)offsetof( struct ui_vert, clip ) );
+               glEnableVertexAttribArray( 3 );
+       }
+       
+       // Initialize default context
+       {
+               ctx->verts = (struct ui_vert *)malloc( vertex_buffer_size * sizeof(struct ui_vert) );
+               ctx->indices = (u16*)malloc( index_buffer_size * sizeof(u16) );
+       }
+}
+
+static void ui_context_free( ui_ctx *ctx )
+{
+       glDeleteVertexArrays( 1, &ctx->vao );
+       glDeleteBuffers( 1, &ctx->vbo );
+       glDeleteBuffers( 1, &ctx->ebo );
+       
+       free( ctx->verts );
+       free( ctx->indices );
+}
+
 static void ui_override_font( GLuint new_tex, ui_px space_x )
 {
        if( ui_glyph_texture )
@@ -233,47 +263,8 @@ static void ui_default_init(void)
        }
        
        // Setup OpenGL memory
-       {
-               SHADER_INIT( shader_ui );
-               
-               // Generate the buffer we are gonna be drawing to
-               glGenVertexArrays(1, &ui_vao);
-               glGenBuffers( 1, &ui_vbo );
-               glGenBuffers( 1, &ui_ebo );
-               glBindVertexArray( ui_vao );
-               
-               glBindBuffer( GL_ARRAY_BUFFER, ui_vbo );
-               
-               glBufferData( GL_ARRAY_BUFFER, UI_BUFFER_SIZE * sizeof( struct ui_vert ), NULL, GL_DYNAMIC_DRAW );
-               glBindVertexArray( ui_vao );
-               
-               glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, ui_ebo );
-               glBufferData( GL_ELEMENT_ARRAY_BUFFER, UI_INDEX_SIZE * sizeof( u16 ), NULL, GL_DYNAMIC_DRAW );
-               
-               u32 const stride = sizeof( struct ui_vert );
-               
-               // XY
-               glVertexAttribPointer( 0, 2, GL_SHORT, GL_FALSE, stride, (void *)offsetof( struct ui_vert, co ) );
-               glEnableVertexAttribArray( 0 );
-               
-               // UV
-               glVertexAttribPointer( 1, 2, GL_UNSIGNED_BYTE, GL_FALSE, stride, (void *)offsetof( struct ui_vert, uv ) );
-               glEnableVertexAttribArray( 1 );
-               
-               // COLOUR
-               glVertexAttribPointer( 2, 4, GL_UNSIGNED_BYTE, GL_TRUE, stride, (void *)offsetof( struct ui_vert, colour ) );
-               glEnableVertexAttribArray( 2 );
-               
-               // CLIPPING
-               glVertexAttribPointer( 3, 4, GL_SHORT, GL_FALSE, stride, (void *)offsetof( struct ui_vert, clip ) );
-               glEnableVertexAttribArray( 3 );
-       }
-       
-       // Initialize default context
-       {
-               ui_global_ctx.verts = (struct ui_vert *)malloc( UI_BUFFER_SIZE * sizeof(struct ui_vert) );
-               ui_global_ctx.indices = (u16*)malloc( UI_INDEX_SIZE * sizeof(u16) );
-       }
+       SHADER_INIT( shader_ui );
+       ui_init_context( &ui_global_ctx, 20000 );
 }
 
 static void ui_default_free(void)
@@ -281,22 +272,24 @@ static void ui_default_free(void)
        if( !ui_glyph_override )        
                glDeleteTextures( 1, &ui_glyph_texture );
        
-       glDeleteVertexArrays( 1, &ui_vao );
-       glDeleteBuffers( 1, &ui_vbo );
-       glDeleteBuffers( 1, &ui_ebo );
-       
-       free( ui_global_ctx.verts );
-       free( ui_global_ctx.indices );
+       ui_context_free( &ui_global_ctx );
 }
 
+static struct ui_vert *ui_fill_rect_uv( ui_ctx *ctx, ui_rect rect, u32 colour, ui_px uv[4] );
 static void ui_draw( ui_ctx *ctx, m3x3f view_override )
 {
-       glBindVertexArray( ui_vao );
+       u32 num_indices_normal = ctx->num_indices;
        
-       glBindBuffer( GL_ARRAY_BUFFER, ui_vbo );
+       // Append images to back of buffer
+       for( int i = 0; i < ctx->image_count; i ++ )
+               ui_fill_rect_uv( ctx, ctx->images[i].rc, 0xffffffff, (ui_px[4]){0,0,128,128} );
+
+       glBindVertexArray( ctx->vao );
+       
+       glBindBuffer( GL_ARRAY_BUFFER, ctx->vbo );
        glBufferSubData( GL_ARRAY_BUFFER, 0, ctx->num_verts * sizeof( struct ui_vert ), ctx->verts );
        
-       glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, ui_ebo );
+       glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, ctx->ebo );
        glBufferSubData( GL_ELEMENT_ARRAY_BUFFER, 0, ctx->num_indices * sizeof( u16 ), ctx->indices );
        
        glEnable(GL_BLEND);
@@ -321,9 +314,16 @@ static void ui_draw( ui_ctx *ctx, m3x3f view_override )
        glBindTexture( GL_TEXTURE_2D, ui_glyph_texture );
        glUniform1i( SHADER_UNIFORM( shader_ui, "uTexGlyphs" ), 0 );
        
-       glDrawElements( GL_TRIANGLES, ctx->num_indices, GL_UNSIGNED_SHORT, (void*)(0) );
+       glDrawElements( GL_TRIANGLES, num_indices_normal, GL_UNSIGNED_SHORT, (void*)(0) );
        
-       //vg_info( "Verts: %u, Indices: %u\n", ctx->num_verts, ctx->num_indices );
+       // Draw image elements
+       for( int i = 0; i < ctx->image_count; i ++ )
+       {
+               struct ui_image *img = &ctx->images[i];
+               
+               glBindTexture( GL_TEXTURE_2D, img->image );
+               glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (void*)( (num_indices_normal + 6*i)*sizeof(u16) ) );
+       }
        
        glDisable(GL_BLEND);
 }
@@ -679,6 +679,8 @@ static void ui_begin( ui_ctx *ctx, ui_px res_x, ui_px res_y )
        
        if( ctx->click_state == 0 )
                ctx->capture_mouse_id = 0;
+       
+       ctx->image_count = 0;
 }
 
 static void ui_resolve( ui_ctx *ctx )
@@ -898,3 +900,43 @@ static void ui_reset_colours( ui_ctx *ctx )
        ctx->colours_current = ctx->colours_main;
        ctx->override_colour = 0xffffffff;
 }
+
+static void ui_push_image( ui_ctx *ctx, ui_rect rc, GLuint image )
+{
+       struct ui_image *img = &ctx->images[ ctx->image_count ++ ];
+       ui_rect_copy( rc, img->rc );
+       img->image = image;
+}
+
+// Shortnames
+#define gui_draw(...) ui_draw( &ui_global_ctx, __VA_ARGS__)
+#define gui_current(...) ui_current( &ui_global_ctx, __VA_ARGS__)
+#define gui_new_node() ui_new_node( &ui_global_ctx )
+#define gui_hasmouse(...) ui_hasmouse( &ui_global_ctx, __VA_ARGS__)
+#define gui_end() ui_end( &ui_global_ctx )
+#define gui_end_down() ui_end_down( &ui_global_ctx )
+#define gui_end_right() ui_end_right( &ui_global_ctx )
+#define gui_fill_y() ui_fill_y( &ui_global_ctx)
+#define gui_fill_x() ui_fill_x( &ui_global_ctx)
+#define gui_align_bottom() ui_align_bottom( &ui_global_ctx )
+#define gui_align_right() ui_align_right( &ui_global_ctx )
+#define gui_align_top() ui_align_top( &ui_global_ctx )
+#define gui_align_left() ui_align_left( &ui_global_ctx )
+#define gui_clamp_rect(...) ui_clamp_rect( &ui_global_ctx, __VA_ARGS__)
+#define gui_group_id(...) ui_group_id( &ui_global_ctx, __VA_ARGS__)
+#define gui_capture_mouse(...) ui_capture_mouse( &ui_global_ctx, __VA_ARGS__)
+#define gui_set_clip(...) ui_set_clip( &ui_global_ctx, __VA_ARGS__)
+#define gui_release_clip() ui_release_clip( &ui_global_ctx )
+#define gui_fill_rect_uv(...) ui_fill_rect_uv( &ui_global_ctx, __VA_ARGS__)
+#define gui_fill_rect(...) ui_fill_rect( &ui_global_ctx, __VA_ARGS__)
+#define gui_text(...) ui_text( &ui_global_ctx, __VA_ARGS__)
+#define gui_begin(...) ui_begin( &ui_global_ctx, __VA_ARGS__)
+#define gui_resolve(...) ui_resolve( &ui_global_ctx, __VA_ARGS__)
+#define gui_set_mouse(...) ui_set_mouse( &ui_global_ctx, __VA_ARGS__)
+#define gui_button(...) ui_button( &ui_global_ctx, __VA_ARGS__)
+#define gui_window(...) ui_window( &ui_global_ctx, __VA_ARGS__)
+#define gui_want_mouse() ui_want_mouse( &ui_global_ctx )
+#define gui_push_image(...) ui_push_image( &ui_global_ctx, __VA_ARGS__ )
+#define gui_scrollbar(...) ui_scrollbar( &ui_global_ctx, __VA_ARGS__)
+#define gui_override_colours(...) ui_override_colours( &ui_global_ctx, __VA_ARGS__)
+#define gui_reset_colours(...) ui_reset_colours( &ui_global_ctx )