+ if( cell->state & FLAG_TARGETED )
+ {
+ for( int j = 0; j < 2; j ++ )
+ {
+ if( !cell->links[j] )
+ continue;
+
+ struct cell *other_cell = &world.data[ cell->links[ j ]];
+ struct cell_description *desc = &cell_descriptions[ other_cell->config ];
+
+ int x2 = cell->links[j] % world.w;
+ int y2 = (cell->links[j] - x2) / world.w;
+
+ v2f pts[2];
+
+ pts[0][0] = (float)cmd->pos[0] + (j? 0.75f: 0.25f);
+ pts[0][1] = (float)cmd->pos[1] + 0.25f;
+
+ pts[1][0] = x2;
+ pts[1][1] = y2;
+
+ v2_add( desc->trigger_pos, pts[1], pts[1] );
+
+ if( cell->state & FLAG_EMITTER )
+ {
+ v4f wire_colour;
+ colour_code_v3( cell->emit[j], wire_colour );
+
+ v3_muls( wire_colour, 0.8f, wire_colour );
+ wire_colour[3] = 1.0f;
+
+ glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1, wire_colour );
+ }
+ else
+ glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1,j?wire_right_colour: wire_left_colour );
+
+ for( int i = 0; i < 2; i ++ )
+ {
+ glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ),
+ pts[i][0],
+ pts[i][1],
+ 0.08f * world.st.world_transition
+ );
+ draw_mesh( filled_start, filled_count );
+ }
+ }
+ }
+ }
+
+ // SUB SPLITTER DIRECTION
+ // ========================================================================================================
+
+ /*
+ glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 0.9f, 0.35f, 0.1f, 0.75f );
+
+ for( int i = 0; i < world.tile_special_count; i ++ )
+ {
+ struct render_cmd *cmd = &world.cmd_buf_specials[i];
+ struct cell *cell = cmd->ptr;
+
+ if( cell->state & FLAG_TARGETED && cell->config == k_cell_type_split )
+ {
+ glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), cmd->pos[0], cmd->pos[1], 1.0f );
+ draw_mesh( cell->state & FLAG_FLIP_FLOP? 5: 4, 1 );
+ }
+ }
+ */
+
+ // LIGHT FLARES
+ // ========================================================================================================
+ glBlendFunc(GL_ONE, GL_ONE);
+ glBlendEquation(GL_FUNC_ADD);
+
+ SHADER_USE( shader_sprite );
+
+ vg_tex2d_bind( &tex_sprites, 0 );
+ glUniform1i( SHADER_UNIFORM( shader_sprite, "uTexMain" ), 0 );
+
+ for( int i = 0; i < world.tile_special_count; i ++ )
+ {
+ struct render_cmd *cmd = &world.cmd_buf_specials[i];
+ struct cell *cell = cmd->ptr;
+
+ if( cell->config == k_cell_type_split )
+ {
+ v2f center = { cmd->pos[0] + 0.5f, cmd->pos[1] + 0.5f };
+
+ v3f p0 = { 0.0f, 0.0f, 12.0f };
+ v3f p1 = { 0.0f, 0.0f, 12.0f };
+
+ v2_add( center, (v2f){ -0.25f, -0.25f }, p0 );
+ v2_add( center, (v2f){ 0.25f, -0.25f }, p1 );
+
+ if( cell->state & FLAG_TARGETED )
+ {
+ if( cell->state & FLAG_FLIP_FLOP )
+ render_sprite( k_sprite_flare_y, p1 );
+ else
+ render_sprite( k_sprite_flare_b, p0 );
+ }
+ else
+ render_sprite( k_sprite_flare_w, cell->state &FLAG_FLIP_FLOP? p1: p0 );
+ }
+ }
+
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+
+ glDisable(GL_BLEND);
+
+ // Draw score
+ /*
+ float const score_bright = 1.25f;
+ glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ),
+ 0.4f*score_bright, 0.39f*score_bright, 0.45f*score_bright, 1.0f );
+
+ use_mesh( &world.numbers );
+ draw_numbers( (v3f){ 2.0f, (float)world.h-1.875f, 0.3333f }, world.score );
+ */
+
+ if( !enable_bloom )
+ {
+ if( enable_vignette )
+ goto image_composite;
+
+ return;
+ }
+
+ /* Scale down image and remap colour values */
+ glViewport( 0,0,
+ vg_window_x/EFFECT_BUFFER_RATIO, vg_window_y/EFFECT_BUFFER_RATIO );
+ glBindFramebuffer( GL_FRAMEBUFFER, world.st.bloomframebuffer[0] );
+
+ SHADER_USE( shader_post_darken );
+ glActiveTexture( GL_TEXTURE0 );
+ glBindTexture( GL_TEXTURE_2D, world.st.colourbuffer );
+ glUniform1i( SHADER_UNIFORM( shader_post_darken, "uTexMain" ), 0 );
+
+ draw_mesh( 0, 2 );
+
+ /* Two pass blur */
+ v2f res_inv, blur_dir;
+ res_inv[0] = 1.0f / (float)( vg_window_x/EFFECT_BUFFER_RATIO );
+ res_inv[1] = 1.0f / (float)( vg_window_y/EFFECT_BUFFER_RATIO );
+
+ SHADER_USE( shader_post_blur );
+ glUniform1i( SHADER_UNIFORM( shader_post_blur, "uTexMain" ), 0 );
+
+ for( int i=0; i<1; i++ )
+ {
+ glBindFramebuffer( GL_FRAMEBUFFER, world.st.bloomframebuffer[1] );
+
+ v2_mul( (v2f){ 1.0f*(float)(i+1), 0.0f }, res_inv, blur_dir );
+
+ glUniform2fv( SHADER_UNIFORM(shader_post_blur,"uDir"), 1, blur_dir );
+ glActiveTexture( GL_TEXTURE0 );
+ glBindTexture( GL_TEXTURE_2D, world.st.bloomcolourbuffer[0] );
+
+ draw_mesh( 0, 2 );
+
+ v2_mul( (v2f){ 0.0f, 1.0f*(float)(i+1) }, res_inv, blur_dir );
+
+ glBindFramebuffer( GL_FRAMEBUFFER, world.st.bloomframebuffer[0] );
+ glUniform2fv( SHADER_UNIFORM(shader_post_blur,"uDir"), 1, blur_dir );
+ glBindTexture( GL_TEXTURE_2D, world.st.bloomcolourbuffer[1] );
+ draw_mesh( 0, 2 );
+ }
+
+ /* Scene composite */
+ glViewport( 0,0, vg_window_x, vg_window_y );
+
+image_composite:
+ glBindFramebuffer( GL_FRAMEBUFFER, 0 );
+
+ SHADER_USE( shader_post_comp );
+
+ glActiveTexture( GL_TEXTURE0 );
+ glBindTexture( GL_TEXTURE_2D, world.st.colourbuffer );
+ glUniform1i( SHADER_UNIFORM( shader_post_comp, "uTexMain" ), 0 );
+
+ glActiveTexture( GL_TEXTURE1 );
+ glBindTexture( GL_TEXTURE_2D, world.st.bloomcolourbuffer[0] );
+ glUniform1i( SHADER_UNIFORM( shader_post_comp, "uTexBloom" ), 1 );
+
+ glUniform2f( SHADER_UNIFORM( shader_post_comp, "uComp" ),
+ enable_bloom? 1.0f: 0.0f,
+ enable_vignette? 0.0f: 1.0f );
+
+ draw_mesh( 0, 2 );
+}
+
+void vg_ui(void)
+{
+ // Drawing world name
+ if( world.pCmpLevel )
+ {
+ gui_text( (ui_px [2]){ vg_window_x / 2, 4 }, world.pCmpLevel->title, 2, k_text_align_center );
+ gui_text( (ui_px [2]){ vg_window_x / 2, 28 }, world.pCmpLevel->description, 1, k_text_align_center );
+ }
+
+#if 0
+ if( world.st.state == k_game_state_update )
+ {
+ gui_group_id( 34 );
+
+ ui_global_ctx.cursor[2] = 458;
+ ui_global_ctx.cursor[3] = 316;
+ ui_global_ctx.cursor[0] = vg_window_x / 2 - 229;
+ ui_global_ctx.cursor[1] = vg_window_y / 2 - 158;
+
+ gui_new_node();
+ {
+ gui_capture_mouse( 200 );
+ gui_fill_rect( ui_global_ctx.cursor, 0xE8303030 );
+
+ ui_px title_pos[2];
+ title_pos[0] = ui_global_ctx.cursor[0] + 229;
+ title_pos[1] = ui_global_ctx.cursor[1] + 16;
+
+ gui_text( title_pos, "Update 1.5", 2, k_text_align_center );
+
+ gui_text( (ui_px [2]){ ui_global_ctx.cursor[0] + 16, title_pos[1] + 45 },
+ "Welcome to the first update to marble computing!"
+ "\n"
+ "New features have been added:\n"
+ "\n"
+ " - Settings menu\n"
+ " - Map skins\n"
+ " - More levels and a new block type\n"
+ " - Scores for each level\n"
+ " - Zooming and panning (mousewheel)\n"
+ "\n"
+ "There is much more in the works, such as a\n"
+ "soundtrack, and the rest of the levels for the\n"
+ "3 bit computer!\n"
+ "\n"
+ "Thank you everyone for enjoying my game :)\n",
+ 1, k_text_align_left
+ );
+
+ ui_global_ctx.cursor[2] = 100;
+ ui_global_ctx.cursor[3] = 30;
+ ui_global_ctx.cursor[0] += 229 - 50;
+ ui_global_ctx.cursor[1] += 316 - 30 - 16;
+
+ if( gui_button( 1 ) )
+ {
+ world.st.state = k_game_state_main;
+ }
+ gui_text( (ui_px [2]){ ui_global_ctx.cursor[0] + 50,
+ ui_global_ctx.cursor[1] + 4 }, "OK", 1, k_text_align_center );
+ gui_end();
+ }
+ gui_end();
+ }
+ else
+#endif
+ if( world.st.state == k_game_state_settings )
+ {
+ gui_group_id( 35 );
+
+ ui_global_ctx.cursor[2] = 225;
+ gui_fill_y();
+ gui_align_right();
+
+ gui_new_node();
+ {
+ gui_capture_mouse( 200 );
+
+ gui_fill_rect( ui_global_ctx.cursor, 0xC0202020 );
+ ui_rect_pad( ui_global_ctx.cursor, 8 );
+
+ ui_global_ctx.cursor[3] = 25;
+
+ gui_new_node();
+ {
+ gui_text( ui_global_ctx.cursor, "SETTINGS", 2, 0 );
+
+ ui_global_ctx.cursor[2] = 25;
+ gui_align_right();
+
+ if( gui_button(4) == k_button_click )
+ {
+ world.st.buttons[ k_world_button_settings ].state = 0;
+ world.st.state = k_game_state_main;
+ vg_info( "exit\n" );
+ }
+ ui_global_ctx.cursor[0] += 4;
+ ui_global_ctx.cursor[1] -= 4;
+ gui_text( ui_global_ctx.cursor, "x", 2, 0 );
+ gui_end();
+ }
+ gui_end();
+
+ // Colour scheme selection
+ ui_global_ctx.cursor[1] += 30;
+
+ gui_text( ui_global_ctx.cursor, "Colour Scheme", 1, 0 );
+ ui_global_ctx.cursor[1] += 25;
+
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[2] = 50;
+
+ for( int i = 0; i < 4; i ++ )
+ {
+ gui_new_node();
+ {
+ // Convert to RGB
+ u32 rgb = 0xff000000;
+
+ for( int j = 0; j < 3; j ++ )
+ rgb |= (u32)(colour_sets[ colour_set_id ][i][j]*255.0f) << j * 8;
+
+ gui_fill_rect( ui_global_ctx.cursor, rgb );
+ }
+ gui_end_right();
+ }
+ }
+ gui_end_down();
+
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[2] = 25;
+ if( gui_button( 0 ) == k_button_click )
+ {
+ if( colour_set_id > 0 )
+ colour_set_id --;
+ }
+ gui_text( ui_global_ctx.cursor, "<", 2, 0 );
+ gui_end_right();
+
+ ui_global_ctx.cursor[2] = 150;
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0x33ffffff );
+ gui_text(
+ (ui_px [2]){ ui_global_ctx.cursor[0] + 75, ui_global_ctx.cursor[1] + 6 },
+ (const char *[]){ "Normal", "Extra1", "Extra2" }[ colour_set_id ],
+ 1, k_text_align_center
+ );
+ }
+ gui_end_right();
+
+ ui_global_ctx.cursor[2] = 25;
+ if( gui_button( 1 ) == k_button_click )
+ {
+ if( colour_set_id < vg_list_size( colour_sets )-1 )
+ colour_set_id ++;
+ }
+ gui_text( ui_global_ctx.cursor, ">", 2, 0 );
+ gui_end_down();
+ }
+ gui_end_down();
+
+ // Theme select
+ ui_global_ctx.cursor[1] += 16;
+
+#if 0
+ gui_text( ui_global_ctx.cursor, "Tile Theme", 1, 0 );
+ ui_global_ctx.cursor[1] += 20;
+
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[2] = 25;
+ if( gui_button( 2 ) == k_button_click )
+ {
+ if( world_theme_id > 0 )
+ world_theme_id --;
+ }
+ gui_text( ui_global_ctx.cursor, "<", 2, 0 );
+ gui_end_right();
+
+ ui_global_ctx.cursor[2] = 150;
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0x33ffffff );
+ gui_text(
+ (ui_px [2]){ ui_global_ctx.cursor[0] + 75, ui_global_ctx.cursor[1] + 6 },
+ world_themes[ world_theme_id ].name, 1, k_text_align_center
+ );
+ }
+ gui_end_right();
+
+ ui_global_ctx.cursor[2] = 25;
+ if( gui_button( 3 ) == k_button_click )
+ {
+ if( world_theme_id < vg_list_size( world_themes )-1 )
+ world_theme_id ++;
+ }
+ gui_text( ui_global_ctx.cursor, ">", 2, 0 );
+ gui_end_down();
+ }
+ gui_end_down();
+#endif
+
+ gui_text( ui_global_ctx.cursor, "Graphics", 1, 0 );
+ ui_global_ctx.cursor[1] += 20;
+
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[2] = 200;
+ if( gui_button( 5 ) == k_button_click )
+ {
+ enable_bloom ^= 0x1;
+ }
+ ui_global_ctx.cursor[0] += 4;
+ ui_global_ctx.cursor[1] += 4;
+ gui_text( ui_global_ctx.cursor, enable_bloom?
+ "Bloom: ENABLED":
+ "Bloom: DISABLED", 1, 0 );
+ gui_end_down();
+ }
+ gui_end_down();
+
+ ui_global_ctx.cursor[1] += 10;
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[2] = 200;
+ if( gui_button( 6 ) == k_button_click )
+ {
+ enable_vignette ^= 0x1;
+ }
+ ui_global_ctx.cursor[0] += 4;
+ ui_global_ctx.cursor[1] += 4;
+ gui_text( ui_global_ctx.cursor, enable_vignette?
+ "Vignette: ENABLED":
+ "Vignette: DISABLED", 1, 0 );
+ gui_end_down();
+ }
+ gui_end_down();
+
+ ui_global_ctx.cursor[1] += 16;
+ gui_text( ui_global_ctx.cursor, "Music Volume", 1, 0 );
+ ui_global_ctx.cursor[1] += 20;
+
+ gui_new_node();
+ {
+ ui_px slider_start = ui_global_ctx.cursor[0];
+
+ float const bar_width = 45.0f,
+ bar_total = 200.0f,
+ bar_movement = bar_total-bar_width,
+ bar_start = bar_width * 0.5f;
+
+ ui_global_ctx.cursor[2] = bar_total;
+ ui_fill_rect( &ui_global_ctx,
+ ui_global_ctx.cursor,
+ 0xff111111 );
+
+ ui_global_ctx.cursor[2] = bar_width;
+ ui_global_ctx.cursor[0] = slider_start + music_volume * bar_movement;
+
+ int status = gui_button( 7 );
+
+ static ui_px drag_start = 0.0f;
+
+ if( status == k_button_start_click )
+ drag_start = ui_global_ctx.mouse[0];
+ else if( ui_global_ctx.capture_lock &&
+ (ui_global_ctx.capture_mouse_id == ui_group_id(&ui_global_ctx,7)))
+ {
+ ui_px drag_offset = ui_global_ctx.mouse[0] - drag_start;
+ float offset_local = (drag_start + drag_offset - slider_start - bar_start) / bar_movement;
+
+ music_volume = vg_minf( vg_maxf( offset_local, 0.0f ), 1.0f );
+ music_volume_update();
+ }
+
+ ui_global_ctx.cursor[0] += 4;
+ ui_global_ctx.cursor[1] += 4;
+
+ char volbuf[12];
+ snprintf( volbuf, 12, "%.2f", music_volume );
+ gui_text( ui_global_ctx.cursor, volbuf, 1, 0 );
+ gui_end_down();
+ }
+ gui_end_down();
+ }
+ gui_end();
+ }
+}
+
+#if STEAM_LEADERBOARDS
+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, 8 );
+ 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, 8 );
+
+ u64_steamid local_player = sw_get_steamid();
+
+ 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;
+
+ player->is_local_player = local_player == player->id? 1: 0;
+ }
+
+ 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;
+
+ // 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 );
+}
+#endif
+
+// CONSOLE COMMANDS
+// ===========================================================================================================
+
+static int console_credits( int argc, char const *argv[] )
+{
+ vg_info( "Aknowledgements:\n" );
+ vg_info( " GLFW zlib/libpng glfw.org\n" );
+ vg_info( " miniaudio MIT0 miniaud.io\n" );
+ vg_info( " QOI MIT phoboslab.org\n" );
+ vg_info( " STB library MIT nothings.org\n" );
+ return 0;
+}
+
+static int console_save_map( int argc, char const *argv[] )
+{
+ if( !world.initialzed )
+ {
+ vg_error( "Tried to save uninitialized map!\n" );
+ return 0;
+ }
+
+ char map_path[ 256 ];
+
+ strcpy( map_path, "sav/" );
+ strcat( map_path, world.map_name );
+ strcat( map_path, ".map" );
+
+ FILE *test_writer = fopen( map_path, "wb" );
+ if( test_writer )
+ {
+ vg_info( "Saving map to '%s'\n", map_path );
+ map_serialize( test_writer );
+
+ fclose( test_writer );
+ return 1;
+ }
+ else
+ {
+ vg_error( "Unable to open stream for writing\n" );
+ return 0;
+ }
+}
+
+static int console_load_map( int argc, char const *argv[] )
+{
+ char map_path[ 256 ];
+
+ if( argc >= 1 )
+ {
+ // try from saves
+ strcpy( map_path, "sav/" );
+ strcat( map_path, argv[0] );
+ strcat( map_path, ".map" );
+
+ char *text_source = vg_textasset_read( map_path );
+
+ if( !text_source )
+ {
+ strcpy( map_path, "maps/" );
+ strcat( map_path, argv[0] );
+ strcat( map_path, ".map" );
+
+ text_source = vg_textasset_read( map_path );
+ }
+
+ if( text_source )
+ {
+ vg_info( "Loading map: '%s'\n", map_path );
+ world.pCmpLevel = NULL;
+
+ if( !map_load( text_source, argv[0] ) )
+ {
+ free( text_source );
+ return 0;
+ }
+
+ free( text_source );
+ return 1;
+ }
+ else
+ {
+ vg_error( "Missing maps '%s'\n", argv[0] );
+ return 0;
+ }
+ }
+ else
+ {
+ vg_error( "Missing argument <map_path>\n" );
+ return 0;
+ }
+}
+
+static int console_changelevel( int argc, char const *argv[] )
+{
+ if( argc >= 1 )
+ {
+ // Save current level
+ console_save_map( 0, NULL );
+
+ if( console_load_map( argc, argv ) )
+ {
+ world.st.zoom = 0.0f;
+ simulation_stop();
+ return 1;
+ }
+ }
+ else
+ {
+ vg_error( "Missing argument <map_path>\n" );
+ }
+
+ return 0;
+}
+
+// START UP / SHUTDOWN
+// ===========================================================================================================
+
+#define TRANSFORM_TRI_2D( S, OX, OY, X1, Y1, X2, Y2, X3, Y3 ) \
+ X1*S+OX, Y1*S+OY, X2*S+OX, Y2*S+OY, X3*S+OX, Y3*S+OY
+
+void vg_start(void)
+{
+ // Steamworks callbacks
+ #ifdef STEAM_LEADERBOARDS
+ sw_leaderboard_found = &leaderboard_found;
+ sw_leaderboard_downloaded = &leaderboard_downloaded;
+ #endif
+
+ vg_function_push( (struct vg_cmd){
+ .name = "_map_write",
+ .function = console_save_map
+ });
+
+ vg_function_push( (struct vg_cmd){
+ .name = "_map_load",
+ .function = console_load_map
+ });
+
+ vg_function_push( (struct vg_cmd){
+ .name = "map",
+ .function = console_changelevel
+ });
+
+ vg_function_push( (struct vg_cmd){
+ .name = "credits",
+ .function = console_credits
+ });
+
+ vg_convar_push( (struct vg_convar){
+ .name = "colours",
+ .data = &colour_set_id,
+ .data_type = k_convar_dtype_i32,
+ .opt_i32 = { .min = 0, .max = 2, .clamp = 1 },
+ .persistent = 1
+ });
+
+ vg_convar_push( (struct vg_convar){
+ .name = "theme",
+ .data = &world_theme_id,
+ .data_type = k_convar_dtype_i32,
+ .opt_i32 = { .min = 0, .max = vg_list_size( world_themes )-1, .clamp = 1 },
+ .persistent = 1,
+ .update = NULL
+ });
+
+ vg_convar_push( (struct vg_convar){
+ .name = "enable_bloom",
+ .data = &enable_bloom,
+ .data_type = k_convar_dtype_i32,
+ .opt_i32 = { .min = 0, .max = 1, .clamp = 1 },
+ .persistent = 1,
+ .update = NULL
+ });
+
+ vg_convar_push( (struct vg_convar){
+ .name = "enable_vignette",
+ .data = &enable_vignette,
+ .data_type = k_convar_dtype_i32,
+ .opt_i32 = { .min = 0, .max = 1, .clamp = 1 },
+ .persistent = 1,
+ .update = NULL
+ });
+
+ vg_convar_push( (struct vg_convar){
+ .name = "music_volume",
+ .data = &music_volume,
+ .data_type = k_convar_dtype_f32,
+ .opt_f32 = { .min = 0.0f, .max = 1.0f, .clamp = 1 },
+ .persistent = 1,
+ .update = music_volume_update
+ });
+
+ // Combined quad, long quad / empty circle / filled circle mesh
+ {
+ float combined_mesh[6*6 + 32*6*3] = {
+ 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f,
+ 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.0f,
+
+ 0.0f, 0.0f, 0.0f, 0.2f, 1.0f, 0.2f,
+ 0.0f, 0.0f, 1.0f, 0.2f, 1.0f, 0.0f,
+
+ TRANSFORM_TRI_2D( 0.15f,0.05f,0.4f, 0.0f, 1.0f, 1.0f, 2.0f, 1.0f, 0.0f ),
+ TRANSFORM_TRI_2D( 0.15f,0.80f,0.4f, 0.0f, 0.0f, 0.0f, 2.0f, 1.0f, 1.0f )
+ };
+
+ float *circle_mesh = combined_mesh + 6*6;
+ int const res = 32;
+
+ for( int i = 0; i < res; i ++ )
+ {
+ v2f v0 = { sinf( ((float)i/(float)res)*VG_TAUf ), cosf( ((float)i/(float)res)*VG_TAUf ) };
+ v2f v1 = { sinf( ((float)(i+1)/(float)res)*VG_TAUf ), cosf( ((float)(i+1)/(float)res)*VG_TAUf ) };
+
+ circle_mesh[ i*6+0 ] = 0.0f;
+ circle_mesh[ i*6+1 ] = 0.0f;
+
+ v2_copy( v0, circle_mesh + 32*6 + i*12 );
+ v2_muls( v0, 0.8f, circle_mesh + 32*6 + i*12+2 );
+ v2_copy( v1, circle_mesh + 32*6 + i*12+4 );
+
+ v2_copy( v1, circle_mesh + 32*6 + i*12+6 );
+ v2_muls( v1, 0.8f, circle_mesh + 32*6 + i*12+8 );
+ v2_muls( v0, 0.8f, circle_mesh + 32*6 + i*12+10 );
+
+ v2_copy( v0, circle_mesh + i*6+4 );
+ v2_copy( v1, circle_mesh + i*6+2 );
+ v2_copy( v0, circle_mesh+i*6+4 );
+ v2_copy( v1, circle_mesh+i*6+2 );
+ }
+
+ init_mesh( &world.shapes, combined_mesh, vg_list_size( combined_mesh ) );
+ }
+
+ // Create wire mesh
+ {
+ int const num_segments = 64;
+
+ struct mesh_wire *mw = &world.wire;
+
+ v2f wire_points[ num_segments * 2 ];
+ u16 wire_indices[ 6*(num_segments-1) ];
+
+ for( int i = 0; i < num_segments; i ++ )
+ {
+ float l = (float)i / (float)(num_segments-1);
+
+ v2_copy( (v2f){ l, -0.5f }, wire_points[i*2+0] );
+ v2_copy( (v2f){ l, 0.5f }, wire_points[i*2+1] );
+
+ if( i < num_segments-1 )
+ {
+ wire_indices[ i*6+0 ] = i*2 + 0;
+ wire_indices[ i*6+1 ] = i*2 + 1;
+ wire_indices[ i*6+2 ] = i*2 + 3;
+ wire_indices[ i*6+3 ] = i*2 + 0;
+ wire_indices[ i*6+4 ] = i*2 + 3;
+ wire_indices[ i*6+5 ] = i*2 + 2;
+ }
+ }
+
+ glGenVertexArrays( 1, &mw->vao );
+ glGenBuffers( 1, &mw->vbo );
+ glGenBuffers( 1, &mw->ebo );
+ glBindVertexArray( mw->vao );
+
+ glBindBuffer( GL_ARRAY_BUFFER, mw->vbo );
+
+ glBufferData( GL_ARRAY_BUFFER, sizeof( wire_points ), wire_points, GL_STATIC_DRAW );
+ glBindVertexArray( mw->vao );
+
+ glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mw->ebo );
+ glBufferData( GL_ELEMENT_ARRAY_BUFFER, sizeof( wire_indices ), wire_indices, GL_STATIC_DRAW );
+
+ // XY
+ glVertexAttribPointer( 0, 2, GL_FLOAT, GL_FALSE, 2*sizeof(float), (void*)0 );
+ glEnableVertexAttribArray( 0 );
+
+ VG_CHECK_GL();
+
+ mw->em = vg_list_size( wire_indices );
+ }
+
+ // Create info data texture
+ {
+ glGenTextures( 1, &world.background_data );
+ glBindTexture( GL_TEXTURE_2D, world.background_data );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, 64, 64, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL );
+ vg_tex2d_nearest();
+ }
+
+ // Create random smaples texture
+ {
+ u8 *data = malloc(512*512*2);
+ for( int i = 0; i < 512*512*2; i ++ )
+ data[ i ] = rand()/(RAND_MAX/255);
+
+ glGenTextures( 1, &world.random_samples );
+ glBindTexture( GL_TEXTURE_2D, world.random_samples );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_RG, 512, 512, 0, GL_RG, GL_UNSIGNED_BYTE, data );
+ vg_tex2d_nearest();
+ vg_tex2d_repeat();
+
+ free( data );
+ }
+
+ resource_load_main();
+
+ // Init world text
+ {
+ ui_init_context( &world.st.world_text, 15000 );
+ }
+
+ // Restore gamestate
+ career_local_data_init();
+ career_load();
+
+ /* Create framebuffers */
+ glGenFramebuffers( 1, &world.st.framebuffer );
+ glBindFramebuffer( GL_FRAMEBUFFER, world.st.framebuffer );
+
+ glGenTextures( 1, &world.st.colourbuffer );
+ glBindTexture( GL_TEXTURE_2D, world.st.colourbuffer );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, vg_window_x, vg_window_y,
+ 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
+
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+ glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D,
+ world.st.colourbuffer, 0);
+
+ /* Bloom framebuffer (quater res) */
+ glGenFramebuffers( 2, world.st.bloomframebuffer );
+ glGenTextures( 2, world.st.bloomcolourbuffer );
+
+ for( int i=0; i<2; i++ )
+ {
+ glBindFramebuffer( GL_FRAMEBUFFER, world.st.bloomframebuffer[i] );
+
+ glBindTexture( GL_TEXTURE_2D, world.st.bloomcolourbuffer[i] );
+ glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB,
+ vg_window_x/EFFECT_BUFFER_RATIO, vg_window_y/EFFECT_BUFFER_RATIO,
+ 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
+ vg_tex2d_clamp();
+
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
+ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
+ glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
+ GL_TEXTURE_2D, world.st.bloomcolourbuffer[i], 0);
+ }
+}
+
+void vg_free(void)
+{
+#ifdef VG_STEAM
+ sw_free_opengl();
+#endif
+
+ console_save_map( 0, NULL );
+ career_serialize();
+
+ resource_free_main();
+
+ glDeleteTextures( 1, &world.background_data );
+ glDeleteTextures( 1, &world.random_samples );
+
+ glDeleteVertexArrays( 1, &world.wire.vao );
+ glDeleteBuffers( 1, &world.wire.vbo );
+ glDeleteBuffers( 1, &world.wire.ebo );
+
+ free_mesh( &world.shapes );
+
+ ui_context_free( &world.st.world_text );
+
+ map_free();
+}
+
+int main( int argc, char *argv[] )