+void leaderboard_found( LeaderboardFindResult_t *pCallback );
+void leaderboard_downloaded( LeaderboardScoresDownloaded_t *pCallback );
+
+static void render_tiles( v2i start, v2i end, v4f const regular_colour, v4f const selected_colour )
+{
+ v2i full_start = { 0,0 };
+ v2i full_end = { world.w, world.h };
+
+ if( !start || !end )
+ {
+ start = full_start;
+ end = full_end;
+ }
+
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour );
+
+ for( int y = start[1]; y < end[1]; y ++ )
+ {
+ for( int x = start[0]; x < end[0]; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+ int selected = world.selected == y*world.w + x;
+
+ int tile_offsets[][2] =
+ {
+ {2, 0}, {0, 3}, {0, 2}, {2, 2},
+ {1, 0}, {2, 3}, {3, 2}, {1, 3},
+ {3, 1}, {0, 1}, {1, 2}, {2, 1},
+ {1, 1}, {3, 3}, {2, 1}, {2, 1}
+ };
+
+ int uv[2] = { 3, 0 };
+
+ if( cell->state & (FLAG_CANAL|FLAG_INPUT|FLAG_OUTPUT) )
+ {
+ uv[0] = tile_offsets[ cell->config ][0];
+ uv[1] = tile_offsets[ cell->config ][1];
+ } else continue;
+
+ glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ), (float)x, (float)y, uv[0], uv[1] );
+ if( selected )
+ {
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, selected_colour );
+ draw_mesh( 0, 2 );
+ glUniform4fv( SHADER_UNIFORM( shader_tile_main, "uColour" ), 1, regular_colour );
+ }
+ else
+ draw_mesh( 0, 2 );
+ }
+ }
+}
+
+/*
+static void draw_numbers( v3f coord, int number )
+{
+ v3f pos;
+ v3_copy( coord, pos );
+ int digits[8]; int i = 0;
+
+ while( number > 0 && i < 8 )
+ {
+ digits[i ++] = number % 10;
+ number = number / 10;
+ }
+
+ for( int j = 0; j < i; j ++ )
+ {
+ glUniform3fv( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), 1, pos );
+ draw_mesh( MESH_NUMBERS_OFFSETS[digits[i-j-1]][0], MESH_NUMBERS_OFFSETS[digits[i-j-1]][1] );
+ pos[0] += pos[2] * 0.75f;
+ }
+}*/
+
+static void wbutton_run( enum e_world_button btn_name )
+{
+ struct cell_button *btn = &world.buttons[btn_name];
+
+ // Interaction
+
+ int is_hovering = (world.tile_x == world.w-1 && world.tile_y == world.h-btn_name-2)?1:0;
+ if( vg_get_button_up( "primary" ) && is_hovering )
+ {
+ // Click event
+ btn->pressed ^= 0x1;
+
+ if( btn_name == k_world_button_sim )
+ {
+ if( btn->pressed )
+ {
+ vg_success( "Starting simulation!\n" );
+
+ sfx_set_playrnd( &audio_rolls, &audio_system_balls_rolling, 0, 1 );
+
+ world.num_fishes = 0;
+ world.sim_frame = 0;
+ world.sim_start = vg_time;
+ world.sim_run = 0;
+ world.sim_speed = 2.5f;
+
+ for( int i = 0; i < world.w*world.h; i ++ )
+ world.data[ i ].state &= ~FLAG_FLIP_FLOP;
+
+ io_reset();
+ }
+ else
+ {
+ simulation_stop();
+ }
+ }
+ }
+
+ // Drawing
+ if( btn->pressed ) btn->light_target = is_hovering? 0.9f: 0.8f;
+ else btn->light_target = is_hovering? 0.2f: 0.0f;
+
+ if( vg_get_button( "primary" ) && is_hovering )
+ btn->light_target = 1.0f;
+
+ btn->light = vg_lerpf( btn->light, btn->light_target, vg_time_delta*26.0f );
+
+ glUniform4f( SHADER_UNIFORM( shader_buttons, "uOffset" ),
+ world.w-1,
+ world.h-btn_name-2,
+ (float)btn_name,
+ 3.0f
+ );
+ glUniform4f( SHADER_UNIFORM( shader_buttons, "uColour" ), 0.204f, 0.345f, 0.553f, btn->light );
+
+ draw_mesh( 0, 2 );
+}
+
+void vg_render(void)
+{
+ glViewport( 0,0, vg_window_x, vg_window_y );
+
+ glDisable( GL_DEPTH_TEST );
+ glClearColor( 0.369768f, 0.3654f, 0.42f, 1.0f );
+ glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT );
+
+ v4f const colour_default = {1.0f, 1.0f, 1.0f, 1.0f};
+ v4f const colour_selected = {0.90f, 0.92f, 1.0f, 1.0f};
+
+
+ // BACKGROUND
+ // ========================================================================================================
+ use_mesh( &world.shapes );
+
+ SHADER_USE( shader_background );
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_background, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+
+ glActiveTexture( GL_TEXTURE0 );
+ glBindTexture( GL_TEXTURE_2D, world.background_data );
+ glUniform1i( SHADER_UNIFORM( shader_background, "uTexMain" ), 0 );
+
+ glUniform3f( SHADER_UNIFORM( shader_background, "uOffset" ), -16, -16, 64 );
+ glUniform1f( SHADER_UNIFORM( shader_background, "uVariance" ), 0.02f );
+
+ glActiveTexture( GL_TEXTURE1 );
+ glBindTexture( GL_TEXTURE_2D, world.random_samples );
+ glUniform1i( SHADER_UNIFORM( shader_background, "uSamplerNoise" ), 1 );
+
+ draw_mesh( 0, 2 );
+
+ // TILESET BACKGROUND LAYER
+ // ========================================================================================================
+ use_mesh( &world.shapes );
+ SHADER_USE( shader_tile_main );
+
+ m2x2f subtransform;
+ m2x2_identity( subtransform );
+ glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform );
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_tile_main, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+ glUniform1f( SHADER_UNIFORM( shader_tile_main, "uGhost" ), 0.0f );
+ glUniform1f( SHADER_UNIFORM( shader_tile_main, "uForeground" ), 0.0f );
+
+ glEnable(GL_BLEND);
+ glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+ glBlendEquation(GL_FUNC_ADD);
+
+ // Bind textures
+ vg_tex2d_bind( &tex_tile_data, 0 );
+ glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexGlyphs" ), 0 );
+
+ vg_tex2d_bind( &tex_wood, 1 );
+ glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexWood" ), 1 );
+
+ render_tiles( NULL, NULL, colour_default, colour_default );
+
+ // MARBLES
+ // ========================================================================================================
+ SHADER_USE( shader_ball );
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_ball, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+
+ vg_tex2d_bind( &tex_ball_noise, 0 );
+ glUniform1i( SHADER_UNIFORM( shader_ball, "uTexMain" ), 0 );
+
+ if( get_wbutton( k_world_button_sim )->pressed )
+ {
+ for( int i = 0; i < world.num_fishes; i ++ )
+ {
+ struct fish *fish = &world.fishes[i];
+
+ if( fish->state == k_fish_state_dead || fish->state == k_fish_state_bg )
+ continue;
+
+ if( fish->state == k_fish_state_soon_dead && (world.frame_lerp > fish->death_time) )
+ continue;
+
+ v4f dot_colour = { 0.0f, 0.0f, 0.0f, 1.0f };
+ colour_code_v3( fish->payload, dot_colour );
+
+ glUniform3fv( SHADER_UNIFORM( shader_ball, "uColour" ), 1, dot_colour );
+ glUniform2fv( SHADER_UNIFORM( shader_ball, "uOffset" ), 1, fish->physics_co );
+ glUniform2f( SHADER_UNIFORM( shader_ball, "uTexOffset" ), (float)i * 1.2334, (float)i * -0.3579f );
+ draw_mesh( 0, 2 );
+ }
+ }
+
+ // TILESET FOREGROUND LAYER
+ // ========================================================================================================
+ SHADER_USE( shader_tile_main );
+
+ // Bind textures
+ vg_tex2d_bind( &tex_tile_data, 0 );
+ glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexGlyphs" ), 0 );
+
+ vg_tex2d_bind( &tex_wood, 1 );
+ glUniform1i( SHADER_UNIFORM( shader_tile_main, "uTexWood" ), 1 );
+
+ glUniform1f( SHADER_UNIFORM( shader_tile_main, "uForeground" ), 1.0f );
+ render_tiles( NULL, NULL, colour_default, colour_selected );
+
+ // Draw splitters
+ for( int y = 2; y < world.h-2; y ++ )
+ {
+ for( int x = 2; x < world.w-2; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+
+ if( cell->state & FLAG_CANAL )
+ {
+ if( cell->config == k_cell_type_split )
+ {
+ float rotation = cell->state & FLAG_FLIP_FLOP? vg_rad( -45.0f ): vg_rad( 45.0f );
+
+ if( cell->state & FLAG_FLIP_ROTATING )
+ {
+ if( (world.frame_lerp > curve_7_linear_section) )
+ {
+ float const rotation_speed = 0.4f;
+ if( (world.frame_lerp < 1.0f-rotation_speed) )
+ {
+ float t = world.frame_lerp - curve_7_linear_section;
+ t *= -2.0f * (1.0f/(1.0f-(curve_7_linear_section+rotation_speed)));
+ t += 1.0f;
+
+ rotation *= t;
+ }
+ else
+ rotation *= -1.0f;
+ }
+ }
+
+ m2x2_create_rotation( subtransform, rotation );
+
+ glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform );
+ glUniform4f( SHADER_UNIFORM( shader_tile_main, "uOffset" ),
+ (float)x,
+ (float)y + 0.125f,
+ cell->state & FLAG_TARGETED? 3.0f: 0.0f,
+ 0.0f
+ );
+ draw_mesh( 0, 2 );
+ }
+ }
+ }
+ }
+
+ // EDIT OVERLAY
+ // ========================================================================================================
+ if( world.selected != -1 && !(world.data[ world.selected ].state & FLAG_CANAL) && !world.id_drag_from )
+ {
+ v2i new_begin = { world.tile_x - 2, world.tile_y - 2 };
+ v2i new_end = { world.tile_x + 2, world.tile_y + 2 };
+
+ world.data[ world.selected ].state ^= FLAG_CANAL;
+ map_reclassify( new_begin, new_end, 0 );
+
+ m2x2_identity( subtransform );
+ glUniform1f( SHADER_UNIFORM( shader_tile_main, "uGhost" ), 1.0f );
+ glUniformMatrix2fv( SHADER_UNIFORM( shader_tile_main, "uSubTransform" ), 1, GL_FALSE, (float *)subtransform );
+ glUniform2fv( SHADER_UNIFORM( shader_tile_main, "uMousePos" ), 1, world.tile_pos );
+
+ render_tiles( new_begin, new_end, colour_default, colour_default );
+
+ world.data[ world.selected ].state ^= FLAG_CANAL;
+ map_reclassify( new_begin, new_end, 0 );
+ }
+
+ // BUTTONS
+ // ========================================================================================================
+ SHADER_USE( shader_buttons );
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_buttons, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+
+ vg_tex2d_bind( &tex_buttons, 0 );
+ glUniform1i( SHADER_UNIFORM( shader_buttons, "uTexMain" ), 0 );
+
+ wbutton_run( k_world_button_sim );
+ wbutton_run( k_world_button_ff );
+
+ // WIRES
+ // ========================================================================================================
+ glDisable(GL_BLEND);
+
+ SHADER_USE( shader_wire );
+ glBindVertexArray( world.wire.vao );
+
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_wire, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+ glUniform4f( SHADER_UNIFORM( shader_wire, "uColour" ), 0.2f, 0.2f, 0.2f, 1.0f );
+
+ if( world.id_drag_from )
+ {
+ glUniform1f( SHADER_UNIFORM( shader_wire, "uCurve" ), 0.4f );
+ glUniform3f( SHADER_UNIFORM( shader_wire, "uStart" ), world.drag_from_co[0], world.drag_from_co[1], 0.06f );
+ glUniform3f( SHADER_UNIFORM( shader_wire, "uEnd" ), world.drag_to_co[0], world.drag_to_co[1], 0.06f );
+ glDrawElements( GL_TRIANGLES, world.wire.em, GL_UNSIGNED_SHORT, (void*)(0) );
+ }
+
+ // Pulling animation
+ float rp_x1 = world.frame_lerp*9.0f;
+ float rp_x2 = 1.0f-rp_x1*expf(1.0f-rp_x1)* 0.36f;
+
+ for( int y = 2; y < world.h-2; y ++ )
+ {
+ for( int x = 2; x < world.w-2; x ++ )
+ {
+ struct cell *cell = pcell((v2i){x,y});
+
+ if( cell->state & FLAG_CANAL )
+ {
+ if( cell->state & FLAG_IS_TRIGGER )
+ {
+ int trigger_id = cell->links[0]?0:1;
+
+ int x2 = cell->links[trigger_id] % world.w;
+ int y2 = (cell->links[trigger_id] - x2) / world.w;
+
+ v2f startpoint;
+ v2f endpoint;
+
+ startpoint[0] = (float)x2 + (trigger_id? 0.75f: 0.25f);
+ startpoint[1] = (float)y2 + 0.25f;
+
+ endpoint[0] = x+0.5f;
+ endpoint[1] = y+0.5f;
+
+ glUniform1f( SHADER_UNIFORM( shader_wire, "uCurve" ), cell->state & FLAG_TRIGGERED? rp_x2 * 0.4f: 0.4f );
+ glUniform3f( SHADER_UNIFORM( shader_wire, "uStart" ), startpoint[0], startpoint[1], 0.04f );
+ glUniform3f( SHADER_UNIFORM( shader_wire, "uEnd" ), endpoint[0], endpoint[1], 0.04f );
+ glDrawElements( GL_TRIANGLES, world.wire.em, GL_UNSIGNED_SHORT, (void*)(0) );
+ }
+ }
+ }
+ }
+
+ // I/O ARRAYS
+ // ========================================================================================================
+
+ SHADER_USE( shader_tile_colour );
+ glUniformMatrix3fv( SHADER_UNIFORM( shader_tile_colour, "uPv" ), 1, GL_FALSE, (float *)vg_pv );
+ use_mesh( &world.shapes );
+
+ int const circle_base = 4;
+ int const filled_start = circle_base+0;
+ int const filled_count = circle_base+32;
+ int const empty_start = circle_base+32;
+ int const empty_count = circle_base+32*2;
+
+ glEnable(GL_BLEND);
+
+ for( int i = 0; i < arrlen( world.io ); i ++ )
+ {
+ struct cell_terminal *term = &world.io[ i ];
+ int is_input = pcell(term->pos)->state & FLAG_INPUT;
+
+ v4f dot_colour = { 0.0f, 0.0f, 0.0f, 1.0f };
+
+ for( int k = 0; k < term->run_count; k ++ )
+ {
+ float arr_base = is_input? 1.2f: -0.2f,
+ run_offset = (is_input? 0.2f: -0.2f) * (float)k,
+ y_position = is_input?
+ (arr_base + (float)term->pos[1] + (float)(term->run_count-1)*0.2f) - run_offset:
+ (float)term->pos[1] + arr_base + run_offset;
+
+ if( k & 0x1 )
+ {
+ glUniform4f( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1.0f, 1.0f, 1.0f, 0.1f );
+ glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ), (float)term->pos[0], y_position - 0.1f, 1.0f );
+ draw_mesh( 2, 2 );
+ }
+
+ for( int j = 0; j < term->runs[k].condition_count; j ++ )
+ {
+ glUniform3f( SHADER_UNIFORM( shader_tile_colour, "uOffset" ),
+ (float)term->pos[0] + 0.2f + 0.2f * (float)j,
+ y_position,
+ 0.1f
+ );
+
+ if( is_input )
+ {
+ colour_code_v3( term->runs[k].conditions[j], dot_colour );
+ glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1, dot_colour );
+
+ // Draw filled if tick not passed, draw empty if empty
+ if( (world.sim_frame > j && world.sim_run >= k) || world.sim_run > k )
+ draw_mesh( empty_start, empty_count );
+ else
+ draw_mesh( filled_start, filled_count );
+ }
+ else
+ {
+
+ if( term->runs[k].recv_count > j )
+ {
+ colour_code_v3( term->runs[k].recieved[j], dot_colour );
+ v3_muls( dot_colour, 0.8f, dot_colour );
+ glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1, dot_colour );
+
+ draw_mesh( filled_start, filled_count );
+ }
+
+ colour_code_v3( term->runs[k].conditions[j], dot_colour );
+ glUniform4fv( SHADER_UNIFORM( shader_tile_colour, "uColour" ), 1, dot_colour );
+
+ draw_mesh( empty_start, empty_count );
+ }
+ }
+ }
+ }
+
+ 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 );
+ */
+}
+
+static ui_colourset flcol_list_a = {
+ .main = 0xff877979,
+ .hover = 0xffa09393,
+ .active = 0xffbfb1b0
+};
+static ui_colourset flcol_list_b = {
+ .main = 0xff7c6e6e,
+ .hover = 0xffa09393,
+ .active = 0xffbfb1b0
+};
+
+static ui_colourset flcol_list_complete_a = {
+ .main = 0xff62a064,
+ .hover = 0xff8dc18f,
+ .active = 0xffb2ddb3
+};
+
+static ui_colourset flcol_list_complete_b = {
+ .main = 0xff79b37b,
+ .hover = 0xff8dc18f,
+ .active = 0xffb2ddb3
+};
+
+static ui_colourset flcol_list_locked = {
+ .main = 0xff655959,
+ .hover = 0xff655959,
+ .active = 0xff655959
+};
+
+static struct
+{
+ SteamLeaderboard_t steam_leaderboard;
+ int leaderboard_show;
+
+ struct leaderboard_player
+ {
+ // Internal
+ u64_steamid id;
+ i32 score;
+ int is_local_player;
+
+ // 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;
+}
+ui_data;
+
+void vg_ui(void)
+{
+ // UI memory
+ static int pack_selection = 0;
+ static struct pack_info
+ {
+ struct cmp_level *levels;
+ u32 level_count;
+ const char *name;
+ }
+ pack_infos[] =
+ {
+ {
+ .levels = cmp_levels_tutorials,
+ .level_count = vg_list_size(cmp_levels_tutorials),
+ .name = "Training"
+ },
+ {
+ .levels = cmp_levels_basic,
+ .level_count = vg_list_size(cmp_levels_basic),
+ .name = "Main"
+ },
+ {
+ .levels = cmp_levels_grad,
+ .level_count = vg_list_size(cmp_levels_tutorials),
+ .name = "Expert"
+ }
+ };
+
+ // UI Code
+ ui_global_ctx.cursor[0] = 0;
+ ui_global_ctx.cursor[1] = 0;
+ ui_global_ctx.cursor[2] = 256;
+
+ gui_fill_y();
+
+ ui_global_ctx.id_base = 4 << 16;
+
+ gui_new_node();
+ {
+ gui_capture_mouse( 9999 );
+ gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+
+ gui_text( "ASSIGNMENTS", 8, 0 );
+
+ ui_global_ctx.cursor[1] += 30;
+ ui_global_ctx.cursor[3] = 25;
+
+ gui_new_node();
+ {
+ ui_rect_pad( ui_global_ctx.cursor, 2 );
+ ui_global_ctx.cursor[2] = 84;
+
+ for( int i = 0; i < 3; i ++ )
+ {
+ if( i == pack_selection )
+ gui_override_colours( &flcol_list_locked );
+
+ if( gui_button( 2000 + i ) == k_button_click )
+ pack_selection = i;
+
+ ui_global_ctx.cursor[1] += 2;
+ gui_text( pack_infos[i].name, 4, 0 );
+ gui_end_right();
+
+ gui_reset_colours();
+ }
+ }
+ gui_end_down();
+
+ ui_global_ctx.cursor[3] = 500;
+
+ // DRAW LEVEL SELECTION LIST
+ {
+ struct cmp_level *levels = pack_infos[ pack_selection ].levels;
+ int count = pack_infos[ pack_selection ].level_count;
+
+ static struct ui_scrollbar sb = {
+ .bar_height = 400
+ };
+
+ ui_px view_height = ui_global_ctx.cursor[3];
+ ui_px level_height = 50;
+
+ // Level scroll view
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+ gui_set_clip( ui_global_ctx.cursor );
+
+ ui_global_ctx.cursor[2] = 14;
+ gui_align_right();
+
+ ui_px content_height = count*level_height;
+ if( content_height > view_height )
+ {
+ ui_scrollbar( &ui_global_ctx, &sb, 1 );
+ ui_global_ctx.cursor[1] -= ui_calculate_content_scroll( &sb, content_height );
+ }
+ else
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0xff807373 );
+ }
+
+ ui_global_ctx.cursor[2] = 240;
+ ui_global_ctx.cursor[3] = level_height;
+ gui_align_left();
+
+ for( int i = 0; i < count; i ++ )
+ {
+ struct cmp_level *lvl_info = &levels[i];
+
+ if( lvl_info->unlocked )
+ {
+ if( lvl_info->completed_score != 0 )
+ gui_override_colours( i&0x1? &flcol_list_complete_a: &flcol_list_complete_b );
+ else
+ gui_override_colours( i&0x1? &flcol_list_a: &flcol_list_b );
+ }
+ else
+ gui_override_colours( &flcol_list_locked );
+
+ if( lvl_info->unlocked )
+ {
+ if( gui_button( 2 + i ) == k_button_click )
+ {
+ ui_data.level_selected = &levels[i];
+ ui_data.leaderboard_show = 0;
+
+ if( pack_selection >= 1 )
+ sw_find_leaderboard( ui_data.level_selected->map_name );
+ }
+
+ ui_global_ctx.override_colour = 0xffffffff;
+ gui_text( lvl_info->title, 6, 0 );
+ ui_global_ctx.cursor[1] += 18;
+ gui_text( lvl_info->completed_score>0? "passed": "incomplete", 4, 0 );
+ }
+ else
+ {
+ gui_button( 2 + i );
+
+ ui_global_ctx.override_colour = 0xff786f6f;
+ gui_text( "???", 6, 0 );
+ ui_global_ctx.cursor[1] += 18;
+ gui_text( "locked", 4, 0 );
+ }
+
+ gui_end_down();
+ }
+
+ gui_reset_colours();
+ gui_release_clip();
+ }
+ gui_end_down();
+ }
+ }
+ gui_end_right();
+
+ // Selected level UI
+ // ============================================================
+
+ if( ui_data.level_selected )
+ {
+ ui_global_ctx.cursor[0] += 16;
+ ui_global_ctx.cursor[1] += 16;
+ ui_global_ctx.cursor[2] = 512-40;
+ ui_global_ctx.cursor[3] = 560-16;
+
+ gui_new_node();
+ {
+ gui_capture_mouse( 9999 );
+
+ gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+ ui_global_ctx.cursor[1] += 4;
+ gui_text( ui_data.level_selected->title, 6, 0 );
+
+ ui_global_ctx.cursor[1] += 30;
+ ui_rect_pad( ui_global_ctx.cursor, 8 );
+ ui_global_ctx.cursor[3] = 300;
+
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0xff655959 );
+ }
+ gui_end_down();
+
+ ui_text_use_paragraph( &ui_global_ctx );
+ ui_global_ctx.cursor[1] += 2;
+
+ gui_text( ui_data.level_selected->description, 5, 0 );
+ ui_text_use_title( &ui_global_ctx );
+
+ // Buttons at the bottom
+ ui_global_ctx.cursor[3] = 25;
+ ui_global_ctx.cursor[2] = 80;
+
+ gui_align_bottom();
+ ui_global_ctx.cursor[1] -= 8;
+
+ if( gui_button( 3000 ) == k_button_click )
+ {
+ ui_data.level_selected = NULL;
+ }
+ gui_text( "BACK", 6, k_text_alignment_center );
+ gui_end();
+
+ gui_align_right();
+ ui_global_ctx.cursor[2] = 170;
+ ui_global_ctx.cursor[0] -= 8 + 170 + 2;
+
+ {
+ gui_override_colours( &flcol_list_locked );
+ if( gui_button( 3001 ) == k_button_click )
+ vg_error( "UNIMPLEMENTED\n" );
+
+ ui_global_ctx.override_colour = 0xff888888;
+
+ gui_text( "RESTORE SOLUTION", 6, k_text_alignment_center );
+ gui_end_right();
+ ui_global_ctx.override_colour = 0xffffffff;
+ }
+
+ ui_global_ctx.cursor[0] += 2;
+ ui_global_ctx.cursor[2] = 80;
+
+ {
+ gui_override_colours( &flcol_list_complete_a );
+ if( gui_button( 3002 ) == k_button_click )
+ {
+ if( console_changelevel( 1, &ui_data.level_selected->map_name ) )
+ {
+ world.pCmpLevel = ui_data.level_selected;
+
+ ui_data.level_selected = NULL;
+ ui_data.leaderboard_show = 0;
+ }
+ }
+ gui_text( "PLAY", 6, k_text_alignment_center );
+ gui_end();
+ }
+
+ gui_reset_colours();
+ }
+ gui_end_right();
+
+ if( ui_data.leaderboard_show )
+ {
+ ui_global_ctx.cursor[0] += 16;
+ ui_global_ctx.cursor[2] = 350;
+ ui_global_ctx.cursor[3] = 25;
+
+ // If has results
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, 0xff5a4e4d );
+ gui_text( "FRIEND LEADERBOARD", 6, 0 );
+ }
+ gui_end_down();
+
+ ui_global_ctx.cursor[1] += 2;
+
+ gui_new_node();
+ {
+ ui_global_ctx.cursor[3] = 32+8;
+
+ for( int i = 0; i < ui_data.leaderboard_count; i ++ )
+ {
+ gui_new_node();
+ {
+ gui_fill_rect( ui_global_ctx.cursor, i&0x1? flcol_list_a.main: flcol_list_b.main );
+
+ ui_global_ctx.cursor[0] += 4;
+ ui_global_ctx.cursor[1] += 4;
+
+ // 1,2,3 ...
+ static const char *places[] = { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" };
+ gui_text( places[i], 7, 0 );
+ ui_global_ctx.cursor[0] += 32;
+
+ struct leaderboard_player *player = &ui_data.leaderboard_players[i];
+
+ // Players image
+ 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();
+
+ // Players name
+ gui_text( player->player_name, 7, 0 );
+
+ ui_global_ctx.cursor[2] = 50;
+ gui_align_right();
+
+ gui_text( player->score_text, 7, k_text_alignment_right );
+ }
+ gui_end_down();
+
+ ui_global_ctx.cursor[1] += 2;
+ }
+ }
+ 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, 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 );
+}
+
+// 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 ) )
+ {
+ simulation_stop();
+ return 1;
+ }
+ }
+ else
+ {
+ vg_error( "Missing argument <map_path>\n" );
+ }
+
+ return 0;
+}
+
+// START UP / SHUTDOWN
+// ===========================================================================================================
+
+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",
+ .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
+ });
+
+ // Combined quad, long quad / empty circle / filled circle mesh
+ {
+ float combined_mesh[6*8 + 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
+ };
+
+ float *circle_mesh = combined_mesh + 6*4;
+ 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 ) );
+ }
+
+ // Numbers mesh
+ {
+ init_mesh( &world.numbers,
+ MESH_NUMBERS_BUFFER,
+ vg_list_size( MESH_NUMBERS_BUFFER )
+ );
+
+ for( int i = 0; i < 10; i ++ )
+ {
+ vg_info( "offset: %u, length: %u\n", MESH_NUMBERS_OFFSETS[i][0], MESH_NUMBERS_OFFSETS[i][1] );
+ }
+ }
+
+ // 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_linear();
+ vg_tex2d_repeat();
+
+ free( data );
+ }
+
+ resource_load_main();
+
+ // Restore gamestate
+ career_local_data_init();
+ career_load();
+}
+
+void vg_free(void)
+{
+ sw_free_opengl();
+ 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 );
+ free_mesh( &world.numbers );
+
+ map_free();
+}
+
+int main( int argc, char *argv[] )
+{
+ vg_init( argc, argv, "Marble Computing" );
+ return 0;
+}