using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Threading.Tasks; using Steamworks.Data; using QueryType = Steamworks.Ugc.Query; namespace Steamworks.Ugc { public struct Editor { public PublishedFileId FileId { get; private set; } bool creatingNew; WorkshopFileType creatingType; AppId consumerAppId; internal Editor( WorkshopFileType filetype ) : this() { this.creatingNew = true; this.creatingType = filetype; } public Editor( PublishedFileId fileId ) : this() { this.FileId = fileId; } /// /// Create a Normal Workshop item that can be subscribed to /// public static Editor NewCommunityFile => new Editor( WorkshopFileType.Community ); /// /// Create a Collection /// Add items using Item.AddDependency() /// public static Editor NewCollection => new Editor( WorkshopFileType.Collection ); /// /// Workshop item that is meant to be voted on for the purpose of selling in-game /// public static Editor NewMicrotransactionFile => new Editor( WorkshopFileType.Microtransaction ); /// /// Workshop item that is meant to be managed by the game. It is queryable by the API, but isn't visible on the web browser. /// public static Editor NewGameManagedFile => new Editor(WorkshopFileType.GameManagedItem); public Editor ForAppId( AppId id ) { this.consumerAppId = id; return this; } public string? Title { get; private set; } public Editor WithTitle( string t ) { this.Title = t; return this; } public string? Description { get; private set; } public Editor WithDescription( string t ) { this.Description = t; return this; } string? MetaData; public Editor WithMetaData( string t ) { this.MetaData = t; return this; } string? ChangeLog; public Editor WithChangeLog( string t ) { this.ChangeLog = t; return this; } string? Language; public Editor InLanguage( string t ) { this.Language = t; return this; } public string? PreviewFile { get; private set; } public Editor WithPreviewFile( string? t ) { this.PreviewFile = t; return this; } public System.IO.DirectoryInfo? ContentFolder { get; private set; } public Editor WithContent( System.IO.DirectoryInfo t ) { this.ContentFolder = t; return this; } public Editor WithContent( string folderName ) { return WithContent( new System.IO.DirectoryInfo( folderName ) ); } public Visibility? Visibility; public Editor WithVisibility(Visibility visibility) { Visibility = visibility; return this; } public List? Tags { get; private set; } Dictionary>? keyValueTags; HashSet? keyValueTagsToRemove; public Editor WithTag( string tag ) { if ( Tags == null ) Tags = new List(); Tags.Add( tag ); return this; } public Editor WithTags(IEnumerable tags) { if (Tags == null) Tags = new List(); Tags.AddRange(tags); return this; } public Editor WithoutTag(string tag) { if (Tags != null && Tags.Contains(tag)) Tags.Remove(tag); return this; } /// /// Adds a key-value tag pair to an item. /// Keys can map to multiple different values (1-to-many relationship). /// Key names are restricted to alpha-numeric characters and the '_' character. /// Both keys and values cannot exceed 255 characters in length. Key-value tags are searchable by exact match only. /// To replace all values associated to one key use RemoveKeyValueTags then AddKeyValueTag. /// public Editor AddKeyValueTag(string key, string value) { if (keyValueTags == null) keyValueTags = new Dictionary>(); if ( keyValueTags.TryGetValue( key, out var list ) ) list.Add( value ); else keyValueTags[key] = new List() { value }; return this; } /// /// Removes a key and all values associated to it. /// You can remove up to 100 keys per item update. /// If you need remove more tags than that you'll need to make subsequent item updates. /// public Editor RemoveKeyValueTags(string key) { if (keyValueTagsToRemove == null) keyValueTagsToRemove = new HashSet(); keyValueTagsToRemove.Add(key); return this; } public async Task SubmitAsync( IProgress? progress = null, Action? onItemCreated = null ) { var result = default( PublishResult ); if (SteamUGC.Internal is null) { return result; } progress?.Report( 0 ); if ( consumerAppId == 0 ) consumerAppId = SteamClient.AppId; // // Checks // if ( ContentFolder != null ) { if ( !System.IO.Directory.Exists( ContentFolder.FullName ) ) throw new System.Exception( $"UgcEditor - Content Folder doesn't exist ({ContentFolder.FullName})" ); if ( !ContentFolder.EnumerateFiles( "*", System.IO.SearchOption.AllDirectories ).Any() ) throw new System.Exception( $"UgcEditor - Content Folder is empty" ); } // // Item Create // if ( creatingNew ) { result.Result = Steamworks.Result.Fail; var created = await SteamUGC.Internal.CreateItem( consumerAppId, creatingType ); if ( !created.HasValue ) return result; result.Result = created.Value.Result; if ( result.Result != Steamworks.Result.OK ) return result; FileId = created.Value.PublishedFileId; result.NeedsWorkshopAgreement = created.Value.UserNeedsToAcceptWorkshopLegalAgreement; result.FileId = FileId; if ( onItemCreated != null ) onItemCreated( result ); } result.FileId = FileId; // // Item Update // { var handle = SteamUGC.Internal.StartItemUpdate( consumerAppId, FileId); if ( handle == 0xffffffffffffffff ) return result; if ( Title != null ) SteamUGC.Internal.SetItemTitle( handle, Title ); if ( Description != null ) SteamUGC.Internal.SetItemDescription( handle, Description ); if ( MetaData != null ) SteamUGC.Internal.SetItemMetadata( handle, MetaData ); if ( Language != null ) SteamUGC.Internal.SetItemUpdateLanguage( handle, Language ); if ( ContentFolder != null ) SteamUGC.Internal.SetItemContent( handle, ContentFolder.FullName ); if ( PreviewFile != null ) SteamUGC.Internal.SetItemPreview( handle, PreviewFile ); if ( Visibility.HasValue ) SteamUGC.Internal.SetItemVisibility( handle, (RemoteStoragePublishedFileVisibility)Visibility.Value ); if ( Tags != null && Tags.Count > 0 ) { using ( var a = SteamParamStringArray.From( Tags.ToArray() ) ) { var val = a.Value; SteamUGC.Internal.SetItemTags( handle, ref val ); } } if ( keyValueTagsToRemove != null) { foreach ( var key in keyValueTagsToRemove ) SteamUGC.Internal.RemoveItemKeyValueTags( handle, key ); } if ( keyValueTags != null ) { foreach ( var keyWithValues in keyValueTags ) { var key = keyWithValues.Key; foreach ( var value in keyWithValues.Value ) SteamUGC.Internal.AddItemKeyValueTag( handle, key, value ); } } result.Result = Steamworks.Result.Fail; if ( ChangeLog == null ) ChangeLog = ""; var updating = SteamUGC.Internal.SubmitItemUpdate( handle, ChangeLog ); while ( !updating.IsCompleted ) { if ( progress != null ) { ulong total = 0; ulong processed = 0; var r = SteamUGC.Internal.GetItemUpdateProgress( handle, ref processed, ref total ); switch ( r ) { case ItemUpdateStatus.PreparingConfig: { progress?.Report( 0.1f ); break; } case ItemUpdateStatus.PreparingContent: { progress?.Report( 0.2f ); break; } case ItemUpdateStatus.UploadingContent: { var uploaded = total > 0 ? ((float)processed / (float)total) : 0.0f; progress?.Report( 0.2f + uploaded * 0.6f ); break; } case ItemUpdateStatus.UploadingPreviewFile: { progress?.Report( 0.8f ); break; } case ItemUpdateStatus.CommittingChanges: { progress?.Report( 1 ); break; } } } await Task.Delay( 1000 / 60 ); } progress?.Report( 1 ); var updated = updating.GetResult(); if ( !updated.HasValue ) return result; result.Result = updated.Value.Result; if ( result.Result != Steamworks.Result.OK ) return result; result.NeedsWorkshopAgreement = updated.Value.UserNeedsToAcceptWorkshopLegalAgreement; result.FileId = FileId; } return result; } } public struct PublishResult { public bool Success => Result == Steamworks.Result.OK; public Steamworks.Result Result; public PublishedFileId FileId; /// /// https://partner.steamgames.com/doc/features/workshop/implementation#Legal /// public bool NeedsWorkshopAgreement; } }