7 #include "ent_skateshop.h"
9 #include "vg/vg_steam_ugc.h"
13 char description
[512];
16 struct player_board board_model
;
18 world_instance
*view_world
;
19 ent_swspreview
*ptr_ent
;
29 enum workshop_form_status
{
30 k_workshop_form_hidden
,
31 k_workshop_form_editing
,
32 k_workshop_form_submitting
,
33 k_workshop_form_failed
36 const char *failure_reason
;
37 PublishedFileId_t publishing_file_id
;
44 struct workshop_package_info
{
46 char abs_preview_image
[ 1024 ];
47 char abs_content_folder
[ 1024 ];
48 char abs_content_file
[ 1024 ];
51 VG_STATIC
void on_workshop_update_result( void *data
, void *user
)
53 vg_info( "Recieved workshop update result\n" );
54 SubmitItemUpdateResult_t
*result
= data
;
56 if( result
->m_bUserNeedsToAcceptWorkshopLegalAgreement
){
57 vg_warn( "Workshop agreement currently not accepted\n" );
60 if( result
->m_eResult
== k_EResultOK
){
61 vg_success( "Successfully uploaded workshop file\n" );
64 vg_error( "Error with the submitted file (%d)\n", result
->m_eResult
);
68 VG_STATIC
void workshop_form_async_package_complete( void *data
, u32 size
)
70 struct workshop_package_info
*info
= data
;
72 workshop_form
.status
= k_workshop_form_failed
;
73 workshop_form
.failure_reason
= "Packaging workshop folder failed.";
77 ISteamUGC
*hSteamUGC
= SteamAPI_SteamUGC();
78 UGCUpdateHandle_t handle
79 = SteamAPI_ISteamUGC_StartItemUpdate( hSteamUGC
, SKATERIFT_APPID
,
80 workshop_form
.publishing_file_id
);
82 /* TODO: Handle failure cases for these */
83 vg_info( "Setting title\n" );
84 SteamAPI_ISteamUGC_SetItemTitle( hSteamUGC
, handle
, workshop_form
.title
);
86 vg_info( "Setting description\n" );
87 SteamAPI_ISteamUGC_SetItemDescription( hSteamUGC
, handle
,
88 workshop_form
.description
);
89 vg_info( "Setting preview image\n" );
90 SteamAPI_ISteamUGC_SetItemPreview( hSteamUGC
,
91 handle
, info
->abs_preview_image
);
92 vg_info( "Setting item content\n" );
93 SteamAPI_ISteamUGC_SetItemContent( hSteamUGC
, handle
,
94 info
->abs_content_folder
);
95 vg_info( "Setting visibility\n" );
96 SteamAPI_ISteamUGC_SetItemVisibility( hSteamUGC
, handle
,
97 k_ERemoteStoragePublishedFileVisibilityPublic
);
99 vg_info( "Submitting updates\n" );
100 vg_steam_async_call
*call
= vg_alloc_async_steam_api_call();
101 call
->userdata
= NULL
;
102 call
->p_handler
= on_workshop_update_result
;
103 call
->id
= SteamAPI_ISteamUGC_SubmitItemUpdate( hSteamUGC
, handle
, "" );
106 VG_STATIC
void workshop_package_thread( void *data
)
108 vg_info( "Packaging workshop content folder\n" );
110 vg_async_item
*call
= vg_async_alloc( sizeof(struct workshop_package_info
) );
111 struct workshop_package_info
*info
= call
->payload
;
113 /* build content folder path */
114 snprintf( info
->abs_content_folder
, 1024, "%smodels/boards/workshop/%lu",
115 vg
.base_path
, (u64
)workshop_form
.publishing_file_id
);
117 /* build content file path */
118 snprintf( info
->abs_content_file
, 1024, "%s/%s",
119 info
->abs_content_folder
,
120 vg_path_filename( workshop_form
.model_path
) );
122 /* build workshop preview file path */
123 snprintf( info
->abs_preview_image
, 1024, "%sworkshop_preview.jpg",
127 if( !vg_mkdir( info
->abs_content_folder
) ){
129 vg_async_dispatch( call
, workshop_form_async_package_complete
);
133 vg_linear_clear( vg_mem
.scratch
);
134 if( !vg_file_copy( workshop_form
.model_path
, info
->abs_content_file
,
137 vg_async_dispatch( call
, workshop_form_async_package_complete
);
142 vg_async_dispatch( call
, workshop_form_async_package_complete
);
145 VG_STATIC
void on_workshop_createitem( void *data
, void *user
)
147 CreateItemResult_t
*result
= data
;
149 if( result
->m_eResult
== k_EResultOK
){
150 vg_info( "Created workshop file with id: %lu\n",
151 result
->m_nPublishedFileId
);
154 if( result
->m_bUserNeedsToAcceptWorkshopLegalAgreement
){
155 vg_warn( "Workshop agreement currently not accepted\n" );
158 workshop_form
.publishing_file_id
= result
->m_nPublishedFileId
;
159 vg_loader_start( workshop_package_thread
, NULL
);
162 const char *error
= NULL
;
163 switch( result
->m_eResult
){
164 case k_EResultInsufficientPrivilege
:
165 error
= "Your account currently is restricted from uploading content "
166 "due to a hub ban, account lock, or community ban. You need to "
167 "contact Steam Support to resolve the issue.\n";
169 case k_EResultBanned
:
170 error
= "You do not have permission to upload content to this hub "
171 "because you have an active VAC or Game ban.";
173 case k_EResultTimeout
:
174 error
= "Timeout: The operation took longer than expected, please try "
177 case k_EResultNotLoggedOn
:
178 error
= "You are currently not logged into Steam.";
180 case k_EResultServiceUnavailable
:
181 error
= "The workshop server is having issues, please try again.";
183 case k_EResultInvalidParam
:
184 error
= "One of the submission fields contains something not being "
185 "accepted by that field.";
187 case k_EResultAccessDenied
:
188 error
= "There was a problem trying to save the title and description. "
189 "Access was denied.";
191 case k_EResultLimitExceeded
:
192 error
= "You have exceeded your Steam Cloud quota. If you wish to "
193 "upload this file, you must remove some published items.";
195 case k_EResultFileNotFound
:
196 error
= "The uploaded file could not be found.";
198 case k_EResultDuplicateRequest
:
199 error
= "The file was already successfully uploaded.";
201 case k_EResultDuplicateName
:
202 error
= "You already have a Steam Workshop item with that name.";
204 case k_EResultServiceReadOnly
:
205 error
= "Due to a recent password or email change, you are not allowed "
206 "to upload new content. Usually this restriction will expire in"
207 " 5 days, but can last up to 30 days if the account has been "
208 "inactive recently.";
210 default: workshop_form
.failure_reason
= "Unknown failure";
213 vg_error( "ISteamUGC_CreateItem() failed(%d): '%s' \n",
214 result
->m_eResult
, error
);
215 workshop_form
.failure_reason
= error
;
216 workshop_form
.status
= k_workshop_form_failed
;
220 VG_STATIC
void workshop_form_async_submit_complete( void *payload
, u32 size
)
222 vg_steam_async_call
*call
= vg_alloc_async_steam_api_call();
223 call
->userdata
= NULL
;
224 call
->p_handler
= on_workshop_createitem
;
226 ISteamUGC
*hSteamUGC
= SteamAPI_SteamUGC();
227 call
->id
= SteamAPI_ISteamUGC_CreateItem( hSteamUGC
, SKATERIFT_APPID
,
228 k_EWorkshopFileTypeCommunity
);
231 VG_STATIC
void workshop_form_sync_download_image( void *payload
, u32 size
)
234 render_fb_get_current_res( gpipeline
.fb_workshop_preview
, &w
, &h
);
235 vg_linear_clear( vg_mem
.scratch
);
236 workshop_form
.img_buffer
= vg_linear_alloc( vg_mem
.scratch
, w
*h
*3 );
238 vg_info( "read framebuffer: glReadPixels( %dx%d )\n", w
,h
);
240 glBindFramebuffer( GL_READ_FRAMEBUFFER
, gpipeline
.fb_workshop_preview
->fb
);
241 glReadBuffer( GL_COLOR_ATTACHMENT0
);
242 glReadPixels( 0,0, w
,h
, GL_RGB
, GL_UNSIGNED_BYTE
, workshop_form
.img_buffer
);
244 workshop_form
.img_w
= w
;
245 workshop_form
.img_h
= h
;
248 VG_STATIC
void workshop_form_submit_thread( void *data
)
250 vg_async_call( workshop_form_sync_download_image
, NULL
, 0 );
253 int w
= workshop_form
.img_w
,
254 h
= workshop_form
.img_h
;
256 vg_info( "writing: workshop_preview.jpg (%dx%d @90%%)\n", w
,h
);
257 stbi_flip_vertically_on_write(1);
258 stbi_write_jpg( "workshop_preview.jpg", w
,h
, 3,
259 workshop_form
.img_buffer
, 90 );
261 vg_async_call( workshop_form_async_submit_complete
, NULL
, 0 );
264 VG_STATIC
void workshop_form_async_complete( void *payload
, u32 size
)
266 v2_zero( workshop_form
.view_angles
);
267 v3_zero( workshop_form
.view_offset
);
268 workshop_form
.view_dist
= 1.0f
;
269 workshop_form
.status
= k_workshop_form_editing
;
270 workshop_form
.view_changed
= 1;
273 VG_STATIC
void workshop_form_load_thread( void *data
)
275 vg_info( "workshop_form_load_thread()\n" );
276 player_board_load( &workshop_form
.board_model
, workshop_form
.model_path
);
277 vg_success( "loaded\n" );
279 vg_async_call( workshop_form_async_complete
, NULL
, 0 );
282 VG_STATIC
void workshop_start_submission( const char *path
)
284 if( workshop_form
.status
!= k_workshop_form_hidden
){
285 vg_error( "Workshop form is already open\n" );
289 workshop_form
.view_world
= get_active_world();
291 if( mdl_arrcount( &workshop_form
.view_world
->ent_swspreview
) ){
292 workshop_form
.ptr_ent
=
293 mdl_arritm( &workshop_form
.view_world
->ent_swspreview
, 0 );
295 vg_strncpy( path
, workshop_form
.model_path
, 128,
296 k_strncpy_always_add_null
);
297 workshop_form
.description
[0] = '\0';
298 workshop_form
.title
[0] = '\0';
300 vg_loader_start( workshop_form_load_thread
, NULL
);
303 vg_error( "There is no ent_swspreview in the level. "
304 "Cannot publish here\n" );
308 VG_STATIC
int workshop_submit_command( int argc
, const char *argv
[] )
311 workshop_start_submission( argv
[0] );
314 vg_error( "usage: workshop_submit <path>\n" );
319 VG_STATIC
void workshop_init(void)
321 vg_console_reg_cmd( "workshop_submit", workshop_submit_command
, NULL
);
324 VG_STATIC
void workshop_form_gui(void)
326 if( workshop_form
.status
== k_workshop_form_hidden
) return;
329 ui_rect screen
= { 0, 0, vg
.window_x
, vg
.window_y
};
330 ui_rect window
= { 0, 0, 1000, 700 };
331 ui_rect_center( screen
, window
);
332 vg_ui
.wants_mouse
= 1;
334 ui_fill( window
, ui_colour( k_ui_bg
+1 ) );
335 ui_outline( window
, 1, ui_colour( k_ui_bg
+7 ) );
337 ui_rect title
, panel
;
338 ui_split_px( window
, k_ui_axis_h
, 28, 0, title
, panel
);
339 ui_fill( title
, ui_colour( k_ui_bg
+7 ) );
340 ui_text( title
, "Workshop tool", 1, k_ui_align_middle_center
,
341 ui_colourcont(k_ui_bg
+7) );
343 if( workshop_form
.status
== k_workshop_form_submitting
){
344 ui_text( panel
, "Submitting... status: ...\n", 1,
345 k_ui_align_middle_center
, 0 );
349 /* re draw board preview if need to */
350 if( workshop_form
.view_changed
){
351 render_fb_bind( gpipeline
.fb_workshop_preview
, 0 );
353 glClearColor( 0.0f
, 0.0f
, 0.3f
, 1.0f
);
354 glClear( GL_COLOR_BUFFER_BIT
|GL_DEPTH_BUFFER_BIT
);
355 glEnable( GL_DEPTH_TEST
);
356 glDisable( GL_BLEND
);
358 ent_swspreview
*swsprev
= workshop_form
.ptr_ent
;
359 world_instance
*world
= workshop_form
.view_world
;
361 ent_camera
*ref
= mdl_arritm( &world
->ent_camera
,
362 mdl_entity_id_id(swsprev
->id_camera
) );
363 ent_marker
*display
= mdl_arritm( &world
->ent_marker
,
364 mdl_entity_id_id(swsprev
->id_display
) ),
365 *display1
= mdl_arritm( &world
->ent_marker
,
366 mdl_entity_id_id(swsprev
->id_display1
) );
369 v3_add( display
->transform
.co
, display1
->transform
.co
, baseco
);
370 v3_muls( baseco
, 0.5f
, baseco
);
374 v3_sub( display
->transform
.co
, ref
->transform
.co
, basevector
);
375 float dist
= v3_length( basevector
);
378 player_vector_angles( baseangles
, basevector
, 1.0f
, 0.0f
);
380 v2_add( workshop_form
.view_angles
, baseangles
, cam
.angles
);
381 cam
.angles
[2] = 0.0f
;
383 float sX
= sinf( cam
.angles
[0] ),
384 cX
= cosf( cam
.angles
[0] ),
385 sY
= sinf( cam
.angles
[1] ),
386 cY
= cosf( cam
.angles
[1] );
388 v3f offset
= { -sX
* cY
, sY
, cX
* cY
};
390 v3_muladds( display
->transform
.co
, offset
,
391 dist
*workshop_form
.view_dist
, cam
.pos
);
393 cam
.pos
[0] += -sX
*workshop_form
.view_offset
[2];
394 cam
.pos
[2] += cX
*workshop_form
.view_offset
[2];
395 cam
.pos
[0] += cX
*workshop_form
.view_offset
[0];
396 cam
.pos
[2] += sX
*workshop_form
.view_offset
[0];
397 cam
.pos
[1] += workshop_form
.view_offset
[1];
403 camera_update_transform( &cam
);
404 camera_update_view( &cam
);
405 camera_update_projection( &cam
);
406 camera_finalize( &cam
);
409 mdl_transform_m4x3( &display
->transform
, mmdl
);
410 mdl_transform_m4x3( &display1
->transform
, mmdl1
);
412 /* force update this for nice shadows. its usually set in the world
413 * pre-render step, but that includes timer stuff
415 struct player_board
*board
= &workshop_form
.board_model
;
416 struct ub_world_lighting
*ubo
= &world
->ub_lighting
;
418 v3_copy((v3f
){0.0f
,0.1f
, board
->truck_positions
[0][2]}, vp0
);
419 v3_copy((v3f
){0.0f
,0.1f
, board
->truck_positions
[1][2]}, vp1
);
420 m4x3_mulv( mmdl1
, vp0
, ubo
->g_board_0
);
421 m4x3_mulv( mmdl1
, vp1
, ubo
->g_board_1
);
422 glBindBuffer( GL_UNIFORM_BUFFER
, world
->ubo_lighting
);
423 glBufferSubData( GL_UNIFORM_BUFFER
, 0,
424 sizeof(struct ub_world_lighting
), &world
->ub_lighting
);
426 render_world( world
, &cam
, 1 );
427 render_board( &cam
, world
, board
, mmdl
, k_board_shader_entity
);
428 render_board( &cam
, world
, board
, mmdl1
, k_board_shader_entity
);
430 glBindFramebuffer( GL_FRAMEBUFFER
, 0 );
431 glViewport( 0,0, vg
.window_x
, vg
.window_y
);
433 workshop_form
.view_changed
= 0;
436 struct workshop_form
*form
= &workshop_form
;
438 ui_rect sidebar
, content
;
439 ui_split_ratio( panel
, k_ui_axis_v
, 0.3f
, 1, sidebar
, content
);
442 ui_fill( sidebar
, ui_colour( k_ui_bg
+2 ) );
444 ui_split_px( sidebar
, k_ui_axis_h
, 28, 0, title
, sidebar
);
445 ui_text( title
, "Your submissions", 1, k_ui_align_middle_center
, 0 );
448 ui_split_px( sidebar
, k_ui_axis_h
, 28, 0, controls
, sidebar
);
449 ui_fill( controls
, ui_colour( k_ui_bg
+1 ) );
450 ui_outline( controls
, -1, ui_colour( k_ui_bg
+4 ) );
453 ui_split_ratio( controls
, k_ui_axis_v
, 0.25f
, 0, info
, controls
);
454 ui_text( info
, "page 0/2", 1, k_ui_align_middle_center
, 0 );
456 ui_rect btn_left
, btn_right
;
457 ui_split_ratio( controls
, k_ui_axis_v
, 0.5f
, 2, btn_left
, btn_right
);
459 if( ui_button_text( btn_left
, "prev", 1 ) ){
463 if( ui_button_text( btn_right
, "next", 1 ) ){
464 vg_info( "right\n" );
468 ui_rect_pad( content
, 8 );
471 ui_split_px( content
, k_ui_axis_h
, 300, 0, image_plane
, content
);
472 ui_fill( image_plane
, ui_colour( k_ui_bg
+0 ) );
475 ui_fit_item( image_plane
, (ui_px
[2]){ 3, 2 }, img_box
);
476 ui_image( img_box
, gpipeline
.fb_workshop_preview
->attachments
[0].id
);
479 int hover
= ui_inside_rect( img_box
, vg_ui
.mouse
),
480 target
= ui_inside_rect( img_box
, vg_ui
.mouse_click
);
482 if( ui_click_down(UI_MOUSE_MIDDLE
) && target
){
483 v3_copy( workshop_form
.view_offset
,
484 workshop_form
.view_offset_begin
);
486 else if( ui_click_down(UI_MOUSE_LEFT
) && target
){
487 v2_copy( workshop_form
.view_angles
,
488 workshop_form
.view_angles_begin
);
491 if( ui_clicking(UI_MOUSE_MIDDLE
) && target
){
492 v2f delta
= { vg_ui
.mouse
[0]-vg_ui
.mouse_click
[0],
493 vg_ui
.mouse
[1]-vg_ui
.mouse_click
[1] };
495 float *begin
= workshop_form
.view_offset_begin
,
496 *offset
= workshop_form
.view_offset
;
497 offset
[0] = vg_clampf( begin
[0]-delta
[0]*0.002f
, -1.0f
, 1.0f
);
498 offset
[2] = vg_clampf( begin
[2]-delta
[1]*0.002f
, -1.0f
, 1.0f
);
499 workshop_form
.view_changed
= 1;
501 else if( ui_clicking(UI_MOUSE_LEFT
) && target
){
502 v2f delta
= { vg_ui
.mouse
[0]-vg_ui
.mouse_click
[0],
503 vg_ui
.mouse
[1]-vg_ui
.mouse_click
[1] };
506 v2_muladds( workshop_form
.view_angles_begin
, delta
, 0.002f
, angles
);
508 float limit
= VG_PIf
*0.2f
;
510 angles
[0] = vg_clampf( angles
[0], -limit
, limit
);
511 angles
[1] = vg_clampf( angles
[1], -limit
, limit
);
513 v2_copy( angles
, workshop_form
.view_angles
);
514 workshop_form
.view_changed
= 1;
517 if( !ui_clicking(UI_MOUSE_LEFT
) && hover
){
518 float zoom
= workshop_form
.view_dist
;
519 zoom
+= vg
.mouse_wheel
[1] * -0.07f
;
520 zoom
= vg_clampf( zoom
, 0.4f
, 2.0f
);
522 if( zoom
!= workshop_form
.view_dist
){
523 workshop_form
.view_changed
= 1;
524 workshop_form
.view_dist
= zoom
;
531 ui_split_px( content
, k_ui_axis_h
, 8, 0, null
, content
);
532 ui_split_px( content
, k_ui_axis_h
, 28, 0, file_label
, content
);
533 ui_text( file_label
, form
->model_path
, 1, k_ui_align_middle_center
,
534 ui_colour( k_ui_fg
+4 ) );
537 ui_rect title_entry
, label
;
538 ui_split_px( content
, k_ui_axis_h
, 8, 0, null
, content
);
539 ui_split_px( content
, k_ui_axis_h
, 28, 0, title_entry
, content
);
541 const char *str_title
= "Title:", *str_desc
= "Description:";
542 ui_split_px( title_entry
, k_ui_axis_v
,
543 ui_text_line_width(str_title
)+8, 0, label
, title_entry
);
545 ui_text( label
, str_title
, 1, k_ui_align_middle_left
, 0 );
546 ui_textbox( title_entry
, form
->title
, vg_list_size(form
->title
), 0 );
548 /* description box */
550 ui_split_px( content
, k_ui_axis_h
, 8, 0, null
, content
);
551 ui_split_px( content
, k_ui_axis_h
, 28*4, 0, desc_entry
, content
);
552 ui_split_px( desc_entry
, k_ui_axis_v
,
553 ui_text_line_width(str_desc
)+8, 0, label
, desc_entry
);
554 ui_text( label
, str_desc
, 1, k_ui_align_middle_left
, 0 );
555 ui_textbox( desc_entry
, form
->description
,
556 vg_list_size(form
->description
),
557 UI_TEXTBOX_MULTILINE
|UI_TEXTBOX_WRAP
);
560 ui_rect submission_row
;
561 ui_split_px( content
, k_ui_axis_h
, 8, 0, null
, content
);
562 ui_split_px( content
, k_ui_axis_h
, content
[3]-32-8, 0, content
,
565 ui_rect submission_center
;
566 rect_copy( submission_row
, submission_center
);
567 submission_center
[2] = 256;
568 ui_rect_center( submission_row
, submission_center
);
570 ui_split_ratio( submission_center
, k_ui_axis_v
, 0.5f
, 8,
571 btn_left
, btn_right
);
573 if( ui_button_text( btn_left
, "Publish", 1 ) ){
574 workshop_form
.status
= k_workshop_form_submitting
;
575 vg_loader_start( workshop_form_submit_thread
, NULL
);
577 if( ui_button_text( btn_right
, "Cancel", 1 ) ){
582 const char *disclaimer_text
=
583 "By submitting this item, you agree to the workshop terms of service";
585 ui_rect disclaimer_row
, inner
, link
;
586 ui_split_px( content
, k_ui_axis_h
, 8, 0, null
, content
);
587 ui_split_px( content
, k_ui_axis_h
, content
[3]-32, 0, content
,
590 ui_px btn_width
= 32;
592 rect_copy( disclaimer_row
, inner
);
593 inner
[2] = ui_text_line_width( disclaimer_text
) + btn_width
+8;
595 ui_rect_center( disclaimer_row
, inner
);
596 ui_split_px( inner
, k_ui_axis_v
, inner
[2]-btn_width
, 0, label
, btn_right
);
597 ui_rect_pad( btn_right
, 2 );
599 if( ui_button_text( btn_right
, "\x91", 2 ) ){
600 vg_info( "Open link\n" );
603 ui_text( label
, disclaimer_text
, 1, k_ui_align_middle_left
,
604 ui_colour( k_ui_fg
+4 ) );
609 #endif /* WORKSHOP_C */