steamworks/
ugc.rs

1use super::*;
2
3use std::error;
4use std::ffi::{CStr, CString};
5use std::fmt;
6use std::marker;
7use std::mem;
8use std::os::raw::c_char;
9use std::path::Path;
10
11pub const RESULTS_PER_PAGE: u32 = sys::kNumUGCResultsPerPage as u32;
12
13pub struct UGC {
14    pub(crate) ugc: *mut sys::ISteamUGC,
15    pub(crate) inner: Arc<Inner>,
16}
17
18// TODO: should come from sys, but I don't think its generated.
19#[allow(non_upper_case_globals)]
20const UGCQueryHandleInvalid: u64 = 0xffffffffffffffff;
21
22/// Worshop item ID
23#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25pub struct PublishedFileId(pub u64);
26impl From<u64> for PublishedFileId {
27    fn from(id: u64) -> Self {
28        PublishedFileId(id)
29    }
30}
31
32/// Workshop item types to search for
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum UGCType {
35    Items,
36    ItemsMtx,
37    ItemsReadyToUse,
38    Collections,
39    Artwork,
40    Videos,
41    Screenshots,
42    AllGuides,
43    WebGuides,
44    IntegratedGuides,
45    UsableInGame,
46    ControllerBindings,
47    GameManagedItems,
48    All,
49}
50impl Into<sys::EUGCMatchingUGCType> for UGCType {
51    fn into(self) -> sys::EUGCMatchingUGCType {
52        match self {
53            UGCType::Items => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Items,
54            UGCType::ItemsMtx => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Items_Mtx,
55            UGCType::ItemsReadyToUse => {
56                sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Items_ReadyToUse
57            }
58            UGCType::Collections => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Collections,
59            UGCType::Artwork => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Artwork,
60            UGCType::Videos => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Videos,
61            UGCType::Screenshots => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_Screenshots,
62            UGCType::AllGuides => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_AllGuides,
63            UGCType::WebGuides => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_WebGuides,
64            UGCType::IntegratedGuides => {
65                sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_IntegratedGuides
66            }
67            UGCType::UsableInGame => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_UsableInGame,
68            UGCType::ControllerBindings => {
69                sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_ControllerBindings
70            }
71            UGCType::GameManagedItems => {
72                sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_GameManagedItems
73            }
74            UGCType::All => sys::EUGCMatchingUGCType::k_EUGCMatchingUGCType_All,
75        }
76    }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum UGCQueryType {
81    RankedByVote,
82    RankedByPublicationDate,
83    AcceptedForGameRankedByAcceptanceDate,
84    RankedByTrend,
85    FavoritedByFriendsRankedByPublicationDate,
86    CreatedByFriendsRankedByPublicationDate,
87    RankedByNumTimesReported,
88    CreatedByFollowedUsersRankedByPublicationDate,
89    NotYetRated,
90    RankedByTotalVotesAsc,
91    RankedByVotesUp,
92    RankedByTextSearch,
93    RankedByTotalUniqueSubscriptions,
94    RankedByPlaytimeTrend,
95    RankedByTotalPlaytime,
96    RankedByAveragePlaytimeTrend,
97    RankedByLifetimeAveragePlaytime,
98    RankedByPlaytimeSessionsTrend,
99    RankedByLifetimePlaytimeSessions,
100    RankedByLastUpdatedDate,
101}
102impl Into<sys::EUGCQuery> for UGCQueryType {
103    fn into(self) -> sys::EUGCQuery {
104        match self {
105            UGCQueryType::RankedByVote => sys::EUGCQuery::k_EUGCQuery_RankedByVote,
106            UGCQueryType::RankedByPublicationDate => {
107                sys::EUGCQuery::k_EUGCQuery_RankedByPublicationDate
108            }
109            UGCQueryType::AcceptedForGameRankedByAcceptanceDate => {
110                sys::EUGCQuery::k_EUGCQuery_AcceptedForGameRankedByAcceptanceDate
111            }
112            UGCQueryType::RankedByTrend => sys::EUGCQuery::k_EUGCQuery_RankedByTrend,
113            UGCQueryType::FavoritedByFriendsRankedByPublicationDate => {
114                sys::EUGCQuery::k_EUGCQuery_FavoritedByFriendsRankedByPublicationDate
115            }
116            UGCQueryType::CreatedByFriendsRankedByPublicationDate => {
117                sys::EUGCQuery::k_EUGCQuery_CreatedByFriendsRankedByPublicationDate
118            }
119            UGCQueryType::RankedByNumTimesReported => {
120                sys::EUGCQuery::k_EUGCQuery_RankedByNumTimesReported
121            }
122            UGCQueryType::CreatedByFollowedUsersRankedByPublicationDate => {
123                sys::EUGCQuery::k_EUGCQuery_CreatedByFollowedUsersRankedByPublicationDate
124            }
125            UGCQueryType::NotYetRated => sys::EUGCQuery::k_EUGCQuery_NotYetRated,
126            UGCQueryType::RankedByTotalVotesAsc => {
127                sys::EUGCQuery::k_EUGCQuery_RankedByTotalVotesAsc
128            }
129            UGCQueryType::RankedByVotesUp => sys::EUGCQuery::k_EUGCQuery_RankedByVotesUp,
130            UGCQueryType::RankedByTextSearch => sys::EUGCQuery::k_EUGCQuery_RankedByTextSearch,
131            UGCQueryType::RankedByTotalUniqueSubscriptions => {
132                sys::EUGCQuery::k_EUGCQuery_RankedByTotalUniqueSubscriptions
133            }
134            UGCQueryType::RankedByPlaytimeTrend => {
135                sys::EUGCQuery::k_EUGCQuery_RankedByPlaytimeTrend
136            }
137            UGCQueryType::RankedByTotalPlaytime => {
138                sys::EUGCQuery::k_EUGCQuery_RankedByTotalPlaytime
139            }
140            UGCQueryType::RankedByAveragePlaytimeTrend => {
141                sys::EUGCQuery::k_EUGCQuery_RankedByAveragePlaytimeTrend
142            }
143            UGCQueryType::RankedByLifetimeAveragePlaytime => {
144                sys::EUGCQuery::k_EUGCQuery_RankedByLifetimeAveragePlaytime
145            }
146            UGCQueryType::RankedByPlaytimeSessionsTrend => {
147                sys::EUGCQuery::k_EUGCQuery_RankedByPlaytimeSessionsTrend
148            }
149            UGCQueryType::RankedByLifetimePlaytimeSessions => {
150                sys::EUGCQuery::k_EUGCQuery_RankedByLifetimePlaytimeSessions
151            }
152            UGCQueryType::RankedByLastUpdatedDate => {
153                sys::EUGCQuery::k_EUGCQuery_RankedByLastUpdatedDate
154            }
155        }
156    }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum FileType {
161    Community,
162    Microtransaction,
163    Collection,
164    Art,
165    Video,
166    Screenshot,
167    Game,
168    Software,
169    Concept,
170    WebGuide,
171    IntegratedGuide,
172    Merch,
173    ControllerBinding,
174    SteamworksAccessInvite,
175    SteamVideo,
176    GameManagedItem,
177}
178
179impl Into<sys::EWorkshopFileType> for FileType {
180    fn into(self) -> sys::EWorkshopFileType {
181        match self {
182            FileType::Community => sys::EWorkshopFileType::k_EWorkshopFileTypeCommunity,
183            FileType::Microtransaction => {
184                sys::EWorkshopFileType::k_EWorkshopFileTypeMicrotransaction
185            }
186            FileType::Collection => sys::EWorkshopFileType::k_EWorkshopFileTypeCollection,
187            FileType::Art => sys::EWorkshopFileType::k_EWorkshopFileTypeArt,
188            FileType::Video => sys::EWorkshopFileType::k_EWorkshopFileTypeVideo,
189            FileType::Screenshot => sys::EWorkshopFileType::k_EWorkshopFileTypeScreenshot,
190            FileType::Game => sys::EWorkshopFileType::k_EWorkshopFileTypeGame,
191            FileType::Software => sys::EWorkshopFileType::k_EWorkshopFileTypeSoftware,
192            FileType::Concept => sys::EWorkshopFileType::k_EWorkshopFileTypeConcept,
193            FileType::WebGuide => sys::EWorkshopFileType::k_EWorkshopFileTypeWebGuide,
194            FileType::IntegratedGuide => sys::EWorkshopFileType::k_EWorkshopFileTypeIntegratedGuide,
195            FileType::Merch => sys::EWorkshopFileType::k_EWorkshopFileTypeMerch,
196            FileType::ControllerBinding => {
197                sys::EWorkshopFileType::k_EWorkshopFileTypeControllerBinding
198            }
199            FileType::SteamworksAccessInvite => {
200                sys::EWorkshopFileType::k_EWorkshopFileTypeSteamworksAccessInvite
201            }
202            FileType::SteamVideo => sys::EWorkshopFileType::k_EWorkshopFileTypeSteamVideo,
203            FileType::GameManagedItem => sys::EWorkshopFileType::k_EWorkshopFileTypeGameManagedItem,
204        }
205    }
206}
207impl From<sys::EWorkshopFileType> for FileType {
208    fn from(file_type: sys::EWorkshopFileType) -> FileType {
209        match file_type {
210            sys::EWorkshopFileType::k_EWorkshopFileTypeCommunity => FileType::Community,
211            sys::EWorkshopFileType::k_EWorkshopFileTypeMicrotransaction => {
212                FileType::Microtransaction
213            }
214            sys::EWorkshopFileType::k_EWorkshopFileTypeCollection => FileType::Collection,
215            sys::EWorkshopFileType::k_EWorkshopFileTypeArt => FileType::Art,
216            sys::EWorkshopFileType::k_EWorkshopFileTypeVideo => FileType::Video,
217            sys::EWorkshopFileType::k_EWorkshopFileTypeScreenshot => FileType::Screenshot,
218            sys::EWorkshopFileType::k_EWorkshopFileTypeGame => FileType::Game,
219            sys::EWorkshopFileType::k_EWorkshopFileTypeSoftware => FileType::Software,
220            sys::EWorkshopFileType::k_EWorkshopFileTypeConcept => FileType::Concept,
221            sys::EWorkshopFileType::k_EWorkshopFileTypeWebGuide => FileType::WebGuide,
222            sys::EWorkshopFileType::k_EWorkshopFileTypeIntegratedGuide => FileType::IntegratedGuide,
223            sys::EWorkshopFileType::k_EWorkshopFileTypeMerch => FileType::Merch,
224            sys::EWorkshopFileType::k_EWorkshopFileTypeControllerBinding => {
225                FileType::ControllerBinding
226            }
227            sys::EWorkshopFileType::k_EWorkshopFileTypeSteamworksAccessInvite => {
228                FileType::SteamworksAccessInvite
229            }
230            sys::EWorkshopFileType::k_EWorkshopFileTypeSteamVideo => FileType::SteamVideo,
231            sys::EWorkshopFileType::k_EWorkshopFileTypeGameManagedItem => FileType::GameManagedItem,
232            _ => unreachable!(),
233        }
234    }
235}
236
237/// AppID filter for queries.
238/// The "consumer" app is the app that the content is for.
239/// The "creator" app is a separate editor to create the content in, if applicable.
240#[derive(Debug, Clone, Copy, PartialEq, Eq)]
241pub enum AppIDs {
242    CreatorAppId(AppId),
243    ConsumerAppId(AppId),
244    Both { creator: AppId, consumer: AppId },
245}
246
247impl AppIDs {
248    pub fn creator_app_id(&self) -> Option<AppId> {
249        match self {
250            AppIDs::CreatorAppId(v) => Some(*v),
251            AppIDs::ConsumerAppId(_) => None,
252            AppIDs::Both { creator, .. } => Some(*creator),
253        }
254    }
255    pub fn consumer_app_id(&self) -> Option<AppId> {
256        match self {
257            AppIDs::CreatorAppId(_) => None,
258            AppIDs::ConsumerAppId(v) => Some(*v),
259            AppIDs::Both { consumer, .. } => Some(*consumer),
260        }
261    }
262}
263
264/// Query result sorting
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum UserListOrder {
267    CreationOrderAsc,
268    CreationOrderDesc,
269    TitleAsc,
270    LastUpdatedDesc,
271    SubscriptionDateDesc,
272    VoteScoreDesc,
273    ForModeration,
274}
275
276impl Into<sys::EUserUGCListSortOrder> for UserListOrder {
277    fn into(self) -> sys::EUserUGCListSortOrder {
278        match self {
279            UserListOrder::CreationOrderAsc => {
280                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_CreationOrderAsc
281            }
282            UserListOrder::CreationOrderDesc => {
283                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_CreationOrderDesc
284            }
285            UserListOrder::TitleAsc => sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_TitleAsc,
286            UserListOrder::LastUpdatedDesc => {
287                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_LastUpdatedDesc
288            }
289            UserListOrder::SubscriptionDateDesc => {
290                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_SubscriptionDateDesc
291            }
292            UserListOrder::VoteScoreDesc => {
293                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_VoteScoreDesc
294            }
295            UserListOrder::ForModeration => {
296                sys::EUserUGCListSortOrder::k_EUserUGCListSortOrder_ForModeration
297            }
298        }
299    }
300}
301
302/// Available user-specific lists.
303/// Certain ones are only available to the currently logged in user.
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum UserList {
306    /// Files user has published
307    Published,
308    /// Files user has voted on
309    VotedOn,
310    /// Files user has voted up (current user only)
311    VotedUp,
312    /// Files user has voted down (current user only)
313    VotedDown,
314    /// Deprecated
315    #[deprecated(note = "Deprecated in Steam API")]
316    WillVoteLater,
317    /// Files user has favorited
318    Favorited,
319    /// Files user has subscribed to (current user only)
320    Subscribed,
321    /// Files user has spent in-game time with
322    UsedOrPlayed,
323    /// Files user is following updates for
324    Followed,
325}
326impl Into<sys::EUserUGCList> for UserList {
327    #[allow(deprecated)]
328    fn into(self) -> sys::EUserUGCList {
329        match self {
330            UserList::Published => sys::EUserUGCList::k_EUserUGCList_Published,
331            UserList::VotedOn => sys::EUserUGCList::k_EUserUGCList_VotedOn,
332            UserList::VotedUp => sys::EUserUGCList::k_EUserUGCList_VotedUp,
333            UserList::VotedDown => sys::EUserUGCList::k_EUserUGCList_VotedDown,
334            UserList::WillVoteLater => sys::EUserUGCList::k_EUserUGCList_WillVoteLater,
335            UserList::Favorited => sys::EUserUGCList::k_EUserUGCList_Favorited,
336            UserList::Subscribed => sys::EUserUGCList::k_EUserUGCList_Subscribed,
337            UserList::UsedOrPlayed => sys::EUserUGCList::k_EUserUGCList_UsedOrPlayed,
338            UserList::Followed => sys::EUserUGCList::k_EUserUGCList_Followed,
339        }
340    }
341}
342
343/// Available published item statistic types.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum UGCStatisticType {
346    /// The number of subscriptions.
347    Subscriptions,
348    /// The number of favorites.
349    Favorites,
350    /// The number of followers.
351    Followers,
352    /// The number of unique subscriptions.
353    UniqueSubscriptions,
354    /// The number of unique favorites.
355    UniqueFavorites,
356    /// The number of unique followers.
357    UniqueFollowers,
358    /// The number of unique views the item has on its Steam Workshop page.
359    UniqueWebsiteViews,
360    /// The number of times the item has been reported.
361    Reports,
362    /// The total number of seconds this item has been used across all players.
363    SecondsPlayed,
364    /// The total number of play sessions this item has been used in.
365    PlaytimeSessions,
366    /// The number of comments on the items steam has on its Steam Workshop page.
367    Comments,
368    /// The number of seconds this item has been used over the given time period.
369    SecondsPlayedDuringTimePeriod,
370    /// The number of sessions this item has been used in over the given time period.
371    PlaytimeSessionsDuringTimePeriod,
372}
373impl Into<sys::EItemStatistic> for UGCStatisticType {
374    fn into(self) -> sys::EItemStatistic {
375        match self {
376            UGCStatisticType::Subscriptions => {
377                sys::EItemStatistic::k_EItemStatistic_NumSubscriptions
378            }
379            UGCStatisticType::Favorites => sys::EItemStatistic::k_EItemStatistic_NumFavorites,
380            UGCStatisticType::Followers => sys::EItemStatistic::k_EItemStatistic_NumFollowers,
381            UGCStatisticType::UniqueSubscriptions => {
382                sys::EItemStatistic::k_EItemStatistic_NumUniqueSubscriptions
383            }
384            UGCStatisticType::UniqueFavorites => {
385                sys::EItemStatistic::k_EItemStatistic_NumUniqueFavorites
386            }
387            UGCStatisticType::UniqueFollowers => {
388                sys::EItemStatistic::k_EItemStatistic_NumUniqueFollowers
389            }
390            UGCStatisticType::UniqueWebsiteViews => {
391                sys::EItemStatistic::k_EItemStatistic_NumUniqueWebsiteViews
392            }
393            UGCStatisticType::Reports => sys::EItemStatistic::k_EItemStatistic_ReportScore,
394            UGCStatisticType::SecondsPlayed => {
395                sys::EItemStatistic::k_EItemStatistic_NumSecondsPlayed
396            }
397            UGCStatisticType::PlaytimeSessions => {
398                sys::EItemStatistic::k_EItemStatistic_NumPlaytimeSessions
399            }
400            UGCStatisticType::Comments => sys::EItemStatistic::k_EItemStatistic_NumComments,
401            UGCStatisticType::SecondsPlayedDuringTimePeriod => {
402                sys::EItemStatistic::k_EItemStatistic_NumSecondsPlayedDuringTimePeriod
403            }
404            UGCStatisticType::PlaytimeSessionsDuringTimePeriod => {
405                sys::EItemStatistic::k_EItemStatistic_NumPlaytimeSessionsDuringTimePeriod
406            }
407        }
408    }
409}
410
411bitflags! {
412    #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)]
413    pub struct ItemState: u32 {
414        const NONE = 0;
415        const SUBSCRIBED = 1;
416        const LEGACY_ITEM = 2;
417        const INSTALLED = 4;
418        const NEEDS_UPDATE = 8;
419        const DOWNLOADING = 16;
420        const DOWNLOAD_PENDING = 32;
421    }
422}
423
424/// Users can control what user-generated content they want to see under the Mature Content Filtering section in their preferences.
425/// This filtering is done automatically by Steam servers, but first, user-generated content must be tagged appropriately.
426/// Developers can use AddContentDescriptor and RemoveContentDescriptor calls to manage content descriptors a piece of UGC has.
427/// These can be retrieved from the result of a query via GetQueryUGCContentDescriptors.
428pub enum UGCContentDescriptorID {
429    /// Some Nudity or Sexual Content: Contains content that has some nudity or sexual themes, but not as the primary focus.
430    NudityOrSexualContent = 1,
431    /// Frequent Violence or Gore: Contains content that features extreme violence or gore.
432    FrequentViolenceOrGore = 2,
433    /// Adult Only Sexual Content: Contains content that is sexually explicit or graphic and is intended for adults only. Users must affirm that they are at least eighteen years old before they can view content with this content descriptor.
434    AdultOnlySexualContent = 3,
435    /// Frequent Nudity or Sexual Content: Contains content that primarily features nudity or sexual themes. Users must affirm that they are at least eighteen years old before they can view content with this content descriptor.
436    GratuitousSexualContent = 4,
437    /// General Mature Content: Contains mature topics that may not be appropriate for all audiences.
438    AnyMatureContent = 5,
439}
440impl Into<sys::EUGCContentDescriptorID> for UGCContentDescriptorID {
441    fn into(self) -> sys::EUGCContentDescriptorID {
442        match self {
443            UGCContentDescriptorID::NudityOrSexualContent => {
444                sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_NudityOrSexualContent
445            }
446            UGCContentDescriptorID::FrequentViolenceOrGore => {
447                sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_FrequentViolenceOrGore
448            }
449            UGCContentDescriptorID::AdultOnlySexualContent => {
450                sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_AdultOnlySexualContent
451            }
452            UGCContentDescriptorID::GratuitousSexualContent => {
453                sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_GratuitousSexualContent
454            }
455            UGCContentDescriptorID::AnyMatureContent => {
456                sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_AnyMatureContent
457            }
458        }
459    }
460}
461impl From<sys::EUGCContentDescriptorID> for UGCContentDescriptorID {
462    fn from(content_descriptor_id: sys::EUGCContentDescriptorID) -> UGCContentDescriptorID {
463        match content_descriptor_id {
464            sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_NudityOrSexualContent => {
465                UGCContentDescriptorID::NudityOrSexualContent
466            }
467            sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_FrequentViolenceOrGore => {
468                UGCContentDescriptorID::FrequentViolenceOrGore
469            }
470            sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_AdultOnlySexualContent => {
471                UGCContentDescriptorID::AdultOnlySexualContent
472            }
473            sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_GratuitousSexualContent => {
474                UGCContentDescriptorID::GratuitousSexualContent
475            }
476            sys::EUGCContentDescriptorID::k_EUGCContentDescriptor_AnyMatureContent => {
477                UGCContentDescriptorID::AnyMatureContent
478            }
479            _ => unreachable!(),
480        }
481    }
482}
483
484#[derive(Clone, Debug)]
485#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
486pub struct DownloadItemResult {
487    pub app_id: AppId,
488    pub published_file_id: PublishedFileId,
489    pub error: Option<SteamError>,
490}
491
492impl_callback!(cb: DownloadItemResult_t => DownloadItemResult {
493    Self {
494        app_id: AppId(cb.m_unAppID),
495        published_file_id: PublishedFileId(cb.m_nPublishedFileId),
496        error: match cb.m_eResult {
497            sys::EResult::k_EResultOK => None,
498            error => Some(error.into()),
499        },
500    }
501});
502
503#[derive(Clone, Debug)]
504#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
505pub struct InstallInfo {
506    pub folder: String,
507    pub size_on_disk: u64,
508    pub timestamp: u32,
509}
510
511impl UGC {
512    /// Suspends or resumes all workshop downloads
513    pub fn suspend_downloads(&self, suspend: bool) {
514        unsafe {
515            sys::SteamAPI_ISteamUGC_SuspendDownloads(self.ugc, suspend);
516        }
517    }
518
519    /// Creates a workshop item
520    pub fn create_item<F>(&self, app_id: AppId, file_type: FileType, cb: F)
521    where
522        F: FnOnce(Result<(PublishedFileId, bool), SteamError>) + 'static + Send,
523    {
524        unsafe {
525            let api_call = sys::SteamAPI_ISteamUGC_CreateItem(self.ugc, app_id.0, file_type.into());
526            register_call_result::<sys::CreateItemResult_t, _>(
527                &self.inner,
528                api_call,
529                move |v, io_error| {
530                    cb(if io_error {
531                        Err(SteamError::IOFailure)
532                    } else {
533                        crate::to_steam_result(v.m_eResult).map(|_| {
534                            (
535                                PublishedFileId(v.m_nPublishedFileId),
536                                v.m_bUserNeedsToAcceptWorkshopLegalAgreement,
537                            )
538                        })
539                    })
540                },
541            );
542        }
543    }
544
545    /// Starts an item update process
546    #[must_use]
547    pub fn start_item_update(&self, app_id: AppId, file_id: PublishedFileId) -> UpdateHandle {
548        unsafe {
549            let handle = sys::SteamAPI_ISteamUGC_StartItemUpdate(self.ugc, app_id.0, file_id.0);
550            UpdateHandle {
551                ugc: self.ugc,
552                inner: self.inner.clone(),
553
554                handle,
555            }
556        }
557    }
558
559    /// Subscribes to a workshop item
560    pub fn subscribe_item<F>(&self, published_file_id: PublishedFileId, cb: F)
561    where
562        F: FnOnce(Result<(), SteamError>) + 'static + Send,
563    {
564        unsafe {
565            let api_call = sys::SteamAPI_ISteamUGC_SubscribeItem(self.ugc, published_file_id.0);
566            register_call_result::<sys::RemoteStorageSubscribePublishedFileResult_t, _>(
567                &self.inner,
568                api_call,
569                move |v, io_error| {
570                    cb(if io_error {
571                        Err(SteamError::IOFailure)
572                    } else {
573                        crate::to_steam_result(v.m_eResult)
574                    })
575                },
576            );
577        }
578    }
579
580    pub fn unsubscribe_item<F>(&self, published_file_id: PublishedFileId, cb: F)
581    where
582        F: FnOnce(Result<(), SteamError>) + 'static + Send,
583    {
584        unsafe {
585            let api_call = sys::SteamAPI_ISteamUGC_UnsubscribeItem(self.ugc, published_file_id.0);
586            register_call_result::<sys::RemoteStorageUnsubscribePublishedFileResult_t, _>(
587                &self.inner,
588                api_call,
589                move |v, io_error| {
590                    cb(if io_error {
591                        Err(SteamError::IOFailure)
592                    } else {
593                        crate::to_steam_result(v.m_eResult)
594                    })
595                },
596            );
597        }
598    }
599
600    /// Gets the publisher file IDs of all currently subscribed items.
601    ///
602    /// Set `include_locally_disabled` to `true` to include items that are
603    /// locally disabled.
604    pub fn subscribed_items(&self, include_locally_disabled: bool) -> Vec<PublishedFileId> {
605        unsafe {
606            let count =
607                sys::SteamAPI_ISteamUGC_GetNumSubscribedItems(self.ugc, include_locally_disabled);
608            let mut data: Vec<sys::PublishedFileId_t> = vec![0; count as usize];
609            let gotten_count = sys::SteamAPI_ISteamUGC_GetSubscribedItems(
610                self.ugc,
611                data.as_mut_ptr(),
612                count,
613                include_locally_disabled,
614            );
615            debug_assert!(count == gotten_count);
616            data.into_iter().map(|v| PublishedFileId(v)).collect()
617        }
618    }
619
620    pub fn item_state(&self, item: PublishedFileId) -> ItemState {
621        unsafe {
622            let state = sys::SteamAPI_ISteamUGC_GetItemState(self.ugc, item.0);
623            ItemState::from_bits_truncate(state)
624        }
625    }
626
627    pub fn item_download_info(&self, item: PublishedFileId) -> Option<(u64, u64)> {
628        unsafe {
629            let mut current = 0u64;
630            let mut total = 0u64;
631            if sys::SteamAPI_ISteamUGC_GetItemDownloadInfo(
632                self.ugc,
633                item.0,
634                &mut current,
635                &mut total,
636            ) {
637                Some((current, total))
638            } else {
639                None
640            }
641        }
642    }
643
644    pub fn item_install_info(&self, item: PublishedFileId) -> Option<InstallInfo> {
645        unsafe {
646            let mut size_on_disk = 0u64;
647            let mut folder = [0 as c_char; 4096];
648            let mut timestamp = 0u32;
649            if sys::SteamAPI_ISteamUGC_GetItemInstallInfo(
650                self.ugc,
651                item.0,
652                &mut size_on_disk,
653                folder.as_mut_ptr(),
654                folder.len() as _,
655                &mut timestamp,
656            ) {
657                Some(InstallInfo {
658                    folder: CStr::from_ptr(folder.as_ptr())
659                        .to_string_lossy()
660                        .into_owned(),
661                    size_on_disk,
662                    timestamp,
663                })
664            } else {
665                None
666            }
667        }
668    }
669
670    pub fn download_item(&self, item: PublishedFileId, high_priority: bool) -> bool {
671        unsafe { sys::SteamAPI_ISteamUGC_DownloadItem(self.ugc, item.0, high_priority) }
672    }
673
674    /// Queries a paged list of all workshop items.
675    pub fn query_all(
676        &self,
677        query_type: UGCQueryType,
678        item_type: UGCType,
679        appids: AppIDs,
680        page: u32,
681    ) -> Result<QueryHandle, CreateQueryError> {
682        // Call the external function with the correct parameters
683        let handle = unsafe {
684            sys::SteamAPI_ISteamUGC_CreateQueryAllUGCRequestPage(
685                self.ugc,
686                query_type.into(),
687                item_type.into(),
688                appids.creator_app_id().unwrap_or(AppId(0)).0,
689                appids.consumer_app_id().unwrap_or(AppId(0)).0,
690                page,
691            )
692        };
693
694        // Check for an invalid handle
695        if handle == UGCQueryHandleInvalid {
696            return Err(CreateQueryError);
697        }
698
699        // Return a new AllQuery instance
700        Ok(QueryHandle {
701            ugc: self.ugc,
702            inner: Arc::clone(&self.inner),
703            handle: Some(handle),
704        })
705    }
706
707    /// Queries a list of workshop itmes, related to a user in some way (Ex. user's subscriptions, favorites, upvoted, ...)
708    pub fn query_user(
709        &self,
710        account: AccountId,
711        list_type: UserList,
712        item_type: UGCType,
713        sort_order: UserListOrder,
714        appids: AppIDs,
715        page: u32,
716    ) -> Result<QueryHandle, CreateQueryError> {
717        let res = unsafe {
718            sys::SteamAPI_ISteamUGC_CreateQueryUserUGCRequest(
719                self.ugc,
720                account.0,
721                list_type.into(),
722                item_type.into(),
723                sort_order.into(),
724                appids.creator_app_id().unwrap_or(AppId(0)).0,
725                appids.consumer_app_id().unwrap_or(AppId(0)).0,
726                page,
727            )
728        };
729
730        if res == UGCQueryHandleInvalid {
731            return Err(CreateQueryError);
732        }
733
734        Ok(QueryHandle {
735            ugc: self.ugc,
736            inner: Arc::clone(&self.inner),
737            handle: Some(res),
738        })
739    }
740
741    pub fn query_items(
742        &self,
743        mut items: Vec<PublishedFileId>,
744    ) -> Result<QueryHandle, CreateQueryError> {
745        debug_assert!(items.len() > 0);
746
747        let res = unsafe {
748            sys::SteamAPI_ISteamUGC_CreateQueryUGCDetailsRequest(
749                self.ugc,
750                items.as_mut_ptr() as _,
751                items.len() as _,
752            )
753        };
754
755        if res == UGCQueryHandleInvalid {
756            return Err(CreateQueryError);
757        }
758
759        Ok(QueryHandle {
760            ugc: self.ugc,
761            inner: Arc::clone(&self.inner),
762            handle: Some(res),
763        })
764    }
765
766    pub fn query_item(&self, item: PublishedFileId) -> Result<QueryHandle, CreateQueryError> {
767        let mut items = vec![item];
768
769        let res = unsafe {
770            sys::SteamAPI_ISteamUGC_CreateQueryUGCDetailsRequest(
771                self.ugc,
772                items.as_mut_ptr() as _,
773                1 as _,
774            )
775        };
776
777        if res == UGCQueryHandleInvalid {
778            return Err(CreateQueryError);
779        }
780
781        Ok(QueryHandle {
782            ugc: self.ugc,
783            inner: Arc::clone(&self.inner),
784            handle: Some(res),
785        })
786    }
787
788    /// **DELETES** the item from the Steam Workshop.
789    pub fn delete_item<F>(&self, published_file_id: PublishedFileId, cb: F)
790    where
791        F: FnOnce(Result<(), SteamError>) + 'static + Send,
792    {
793        unsafe {
794            let api_call = sys::SteamAPI_ISteamUGC_DeleteItem(self.ugc, published_file_id.0);
795            register_call_result::<sys::DownloadItemResult_t, _>(
796                &self.inner,
797                api_call,
798                move |v, io_error| {
799                    cb(if io_error {
800                        Err(SteamError::IOFailure)
801                    } else if v.m_eResult != sys::EResult::k_EResultNone
802                        && v.m_eResult != sys::EResult::k_EResultOK
803                    {
804                        Err(v.m_eResult.into())
805                    } else {
806                        Ok(())
807                    })
808                },
809            );
810        }
811    }
812}
813
814impl UGC {
815    /// Initialize this UGC interface for a Steam game server.
816    ///
817    /// You should pass in the Workshop depot, you can find this on SteamDB. It's usually just the app ID.
818    ///
819    /// The folder is a path to the directory where you wish for this game server to store UGC content.
820    ///
821    /// `true` upon success; otherwise, `false` if the calling user is not a game server or if the workshop is currently updating its content.
822    pub fn init_for_game_server(&self, workshop_depot: sys::DepotId_t, folder: &str) -> bool {
823        unsafe {
824            let folder = CString::new(folder).unwrap();
825            sys::SteamAPI_ISteamUGC_BInitWorkshopForGameServer(
826                self.ugc,
827                workshop_depot,
828                folder.as_ptr(),
829            )
830        }
831    }
832}
833
834/// A handle to update a published item
835pub struct UpdateHandle {
836    ugc: *mut sys::ISteamUGC,
837    inner: Arc<Inner>,
838
839    handle: sys::UGCUpdateHandle_t,
840}
841
842impl UpdateHandle {
843    #[must_use]
844    pub fn title(self, title: &str) -> Self {
845        unsafe {
846            let title = CString::new(title).unwrap();
847            assert!(sys::SteamAPI_ISteamUGC_SetItemTitle(
848                self.ugc,
849                self.handle,
850                title.as_ptr()
851            ));
852        }
853        self
854    }
855
856    #[must_use]
857    pub fn description(self, description: &str) -> Self {
858        unsafe {
859            let description = CString::new(description).unwrap();
860            assert!(sys::SteamAPI_ISteamUGC_SetItemDescription(
861                self.ugc,
862                self.handle,
863                description.as_ptr()
864            ));
865        }
866        self
867    }
868
869    #[must_use]
870    pub fn preview_path(self, path: &Path) -> Self {
871        unsafe {
872            let path = path.canonicalize().unwrap();
873            let preview_path = CString::new(&*path.to_string_lossy()).unwrap();
874            assert!(sys::SteamAPI_ISteamUGC_SetItemPreview(
875                self.ugc,
876                self.handle,
877                preview_path.as_ptr()
878            ));
879        }
880        self
881    }
882
883    #[must_use]
884    pub fn content_path(self, path: &Path) -> Self {
885        unsafe {
886            let path = path.canonicalize().unwrap();
887            let content_path = CString::new(&*path.to_string_lossy()).unwrap();
888            assert!(sys::SteamAPI_ISteamUGC_SetItemContent(
889                self.ugc,
890                self.handle,
891                content_path.as_ptr()
892            ));
893        }
894        self
895    }
896
897    #[must_use]
898    pub fn metadata(self, metadata: &str) -> Self {
899        unsafe {
900            let metadata = CString::new(metadata).unwrap();
901            assert!(sys::SteamAPI_ISteamUGC_SetItemMetadata(
902                self.ugc,
903                self.handle,
904                metadata.as_ptr()
905            ));
906        }
907        self
908    }
909
910    pub fn visibility(self, visibility: remote_storage::PublishedFileVisibility) -> Self {
911        unsafe {
912            assert!(sys::SteamAPI_ISteamUGC_SetItemVisibility(
913                self.ugc,
914                self.handle,
915                visibility.into()
916            ));
917        }
918        self
919    }
920
921    pub fn tags<S: AsRef<str>>(self, tags: Vec<S>, allow_admin_tags: bool) -> Self {
922        unsafe {
923            let mut tags = SteamParamStringArray::new(&tags);
924            assert!(sys::SteamAPI_ISteamUGC_SetItemTags(
925                self.ugc,
926                self.handle,
927                &tags.as_raw(),
928                allow_admin_tags
929            ));
930        }
931        self
932    }
933
934    pub fn add_key_value_tag(self, key: &str, value: &str) -> Self {
935        unsafe {
936            let key = CString::new(key).unwrap();
937            let value = CString::new(value).unwrap();
938            assert!(sys::SteamAPI_ISteamUGC_AddItemKeyValueTag(
939                self.ugc,
940                self.handle,
941                key.as_ptr(),
942                value.as_ptr()
943            ));
944        }
945        self
946    }
947
948    pub fn remove_key_value_tag(self, key: &str) -> Self {
949        unsafe {
950            let key = CString::new(key).unwrap();
951            assert!(sys::SteamAPI_ISteamUGC_RemoveItemKeyValueTags(
952                self.ugc,
953                self.handle,
954                key.as_ptr()
955            ));
956        }
957        self
958    }
959
960    pub fn add_content_descriptor(self, desc_id: UGCContentDescriptorID) -> Self {
961        unsafe {
962            assert!(sys::SteamAPI_ISteamUGC_AddContentDescriptor(
963                self.ugc,
964                self.handle,
965                desc_id.into(),
966            ));
967        }
968        self
969    }
970
971    pub fn remove_content_descriptor(self, desc_id: UGCContentDescriptorID) -> Self {
972        unsafe {
973            assert!(sys::SteamAPI_ISteamUGC_RemoveContentDescriptor(
974                self.ugc,
975                self.handle,
976                desc_id.into()
977            ));
978        }
979        self
980    }
981
982    pub fn remove_all_key_value_tags(self) -> Self {
983        unsafe {
984            assert!(sys::SteamAPI_ISteamUGC_RemoveAllItemKeyValueTags(
985                self.ugc,
986                self.handle
987            ));
988        }
989        self
990    }
991
992    pub fn submit<F>(self, change_note: Option<&str>, cb: F) -> UpdateWatchHandle
993    where
994        F: FnOnce(Result<(PublishedFileId, bool), SteamError>) + 'static + Send,
995    {
996        use std::ptr;
997        unsafe {
998            let change_note = change_note.and_then(|v| CString::new(v).ok());
999            let note = change_note.as_ref().map_or(ptr::null(), |v| v.as_ptr());
1000            let api_call = sys::SteamAPI_ISteamUGC_SubmitItemUpdate(self.ugc, self.handle, note);
1001            register_call_result::<sys::SubmitItemUpdateResult_t, _>(
1002                &self.inner,
1003                api_call,
1004                move |v, io_error| {
1005                    cb(if io_error {
1006                        Err(SteamError::IOFailure)
1007                    } else if v.m_eResult != sys::EResult::k_EResultOK {
1008                        Err(v.m_eResult.into())
1009                    } else {
1010                        Ok((
1011                            PublishedFileId(v.m_nPublishedFileId),
1012                            v.m_bUserNeedsToAcceptWorkshopLegalAgreement,
1013                        ))
1014                    })
1015                },
1016            );
1017        }
1018        UpdateWatchHandle {
1019            ugc: self.ugc,
1020            _inner: self.inner,
1021            handle: self.handle,
1022        }
1023    }
1024}
1025
1026/// A handle to watch an update of a published item
1027pub struct UpdateWatchHandle {
1028    ugc: *mut sys::ISteamUGC,
1029    _inner: Arc<Inner>,
1030
1031    handle: sys::UGCUpdateHandle_t,
1032}
1033
1034unsafe impl Send for UpdateWatchHandle {}
1035unsafe impl Sync for UpdateWatchHandle {}
1036
1037impl UpdateWatchHandle {
1038    pub fn progress(&self) -> (UpdateStatus, u64, u64) {
1039        unsafe {
1040            let mut progress = 0;
1041            let mut total = 0;
1042            let status = sys::SteamAPI_ISteamUGC_GetItemUpdateProgress(
1043                self.ugc,
1044                self.handle,
1045                &mut progress,
1046                &mut total,
1047            );
1048            let status = match status {
1049                sys::EItemUpdateStatus::k_EItemUpdateStatusInvalid => UpdateStatus::Invalid,
1050                sys::EItemUpdateStatus::k_EItemUpdateStatusPreparingConfig => {
1051                    UpdateStatus::PreparingConfig
1052                }
1053                sys::EItemUpdateStatus::k_EItemUpdateStatusPreparingContent => {
1054                    UpdateStatus::PreparingContent
1055                }
1056                sys::EItemUpdateStatus::k_EItemUpdateStatusUploadingContent => {
1057                    UpdateStatus::UploadingContent
1058                }
1059                sys::EItemUpdateStatus::k_EItemUpdateStatusUploadingPreviewFile => {
1060                    UpdateStatus::UploadingPreviewFile
1061                }
1062                sys::EItemUpdateStatus::k_EItemUpdateStatusCommittingChanges => {
1063                    UpdateStatus::CommittingChanges
1064                }
1065                _ => unreachable!(),
1066            };
1067            (status, progress, total)
1068        }
1069    }
1070}
1071
1072#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1073pub enum UpdateStatus {
1074    Invalid,
1075    PreparingConfig,
1076    PreparingContent,
1077    UploadingContent,
1078    UploadingPreviewFile,
1079    CommittingChanges,
1080}
1081
1082/// Query handle, to allow for more filtering.
1083pub struct QueryHandle {
1084    ugc: *mut sys::ISteamUGC,
1085    inner: Arc<Inner>,
1086
1087    // Note: this is always filled except in `fetch`, where it must be taken
1088    // to prevent the handle from being dropped when this query is dropped.
1089    handle: Option<sys::UGCQueryHandle_t>,
1090}
1091
1092impl Drop for QueryHandle {
1093    fn drop(&mut self) {
1094        if let Some(handle) = self.handle.as_mut() {
1095            unsafe {
1096                sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(self.ugc, *handle);
1097            }
1098        }
1099    }
1100}
1101
1102impl QueryHandle {
1103    /// Excludes items with a specific tag.
1104    ///
1105    /// Panics if `tag` could not be converted to a `CString`.
1106    pub fn exclude_tag(self, tag: &str) -> Self {
1107        let cstr = CString::new(tag)
1108            .expect("String passed to exclude_tag could not be converted to a c string");
1109        let ok = unsafe {
1110            sys::SteamAPI_ISteamUGC_AddExcludedTag(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1111        };
1112        debug_assert!(ok);
1113        self
1114    }
1115
1116    /// Only include items with a specific tag.
1117    ///
1118    /// Panics if `tag` could not be converted to a `CString`.
1119    pub fn require_tag(self, tag: &str) -> Self {
1120        let cstr = CString::new(tag)
1121            .expect("String passed to require_tag could not be converted to a c string");
1122        let ok = unsafe {
1123            sys::SteamAPI_ISteamUGC_AddRequiredTag(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1124        };
1125        debug_assert!(ok);
1126        self
1127    }
1128
1129    /// Sets how to match tags added by `require_tag`. If `true`, then any tag may match. If `false`, all required tags must match.
1130    pub fn any_required(self, any: bool) -> Self {
1131        let ok =
1132            unsafe { sys::SteamAPI_ISteamUGC_SetMatchAnyTag(self.ugc, self.handle.unwrap(), any) };
1133        debug_assert!(ok);
1134        self
1135    }
1136
1137    /// Sets the language to return the title and description in for the items on a pending UGC Query.
1138    ///
1139    /// Defaults to "english"
1140    pub fn language(self, language: &str) -> Self {
1141        let cstr = CString::new(language)
1142            .expect("String passed to language could not be converted to a c string");
1143        let ok = unsafe {
1144            sys::SteamAPI_ISteamUGC_SetLanguage(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1145        };
1146        debug_assert!(ok);
1147        self
1148    }
1149
1150    /// Sets whether results will be returned from the cache for the specific period of time on a pending UGC Query.
1151    ///
1152    /// Age is in seconds.
1153    pub fn allow_cached_response(self, max_age_s: u32) -> Self {
1154        let ok = unsafe {
1155            sys::SteamAPI_ISteamUGC_SetAllowCachedResponse(
1156                self.ugc,
1157                self.handle.unwrap(),
1158                max_age_s,
1159            )
1160        };
1161        debug_assert!(ok);
1162        self
1163    }
1164
1165    /// Include the full description in results
1166    pub fn include_long_desc(self, include: bool) -> Self {
1167        let ok = unsafe {
1168            sys::SteamAPI_ISteamUGC_SetReturnLongDescription(
1169                self.ugc,
1170                self.handle.unwrap(),
1171                include,
1172            )
1173        };
1174        debug_assert!(ok);
1175        self
1176    }
1177
1178    /// Include children in results
1179    pub fn include_children(self, include: bool) -> Self {
1180        let ok = unsafe {
1181            sys::SteamAPI_ISteamUGC_SetReturnChildren(self.ugc, self.handle.unwrap(), include)
1182        };
1183        debug_assert!(ok);
1184        self
1185    }
1186
1187    /// Include metadata in results
1188    pub fn include_metadata(self, include: bool) -> Self {
1189        let ok = unsafe {
1190            sys::SteamAPI_ISteamUGC_SetReturnMetadata(self.ugc, self.handle.unwrap(), include)
1191        };
1192        debug_assert!(ok);
1193        self
1194    }
1195
1196    /// Include additional previews in results
1197    pub fn include_additional_previews(self, include: bool) -> Self {
1198        let ok = unsafe {
1199            sys::SteamAPI_ISteamUGC_SetReturnAdditionalPreviews(
1200                self.ugc,
1201                self.handle.unwrap(),
1202                include,
1203            )
1204        };
1205        debug_assert!(ok);
1206        self
1207    }
1208
1209    /// Include key value tags in results
1210    pub fn include_key_value_tags(self, include: bool) -> Self {
1211        let ok = unsafe {
1212            sys::SteamAPI_ISteamUGC_SetReturnKeyValueTags(self.ugc, self.handle.unwrap(), include)
1213        };
1214        debug_assert!(ok);
1215        self
1216    }
1217
1218    /// Adds a tag that must be present on all returned items.
1219    pub fn add_required_tag(self, tag: &str) -> Self {
1220        let cstr = CString::new(tag).unwrap();
1221        let ok = unsafe {
1222            sys::SteamAPI_ISteamUGC_AddRequiredTag(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1223        };
1224        debug_assert!(ok);
1225        self
1226    }
1227
1228    /// Adds a tag that must not be present on any returned items.
1229    pub fn add_excluded_tag(self, tag: &str) -> Self {
1230        let cstr = CString::new(tag).unwrap();
1231        let ok = unsafe {
1232            sys::SteamAPI_ISteamUGC_AddExcludedTag(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1233        };
1234        debug_assert!(ok);
1235        self
1236    }
1237
1238    /// Sets whether to only return the IDs of the items.
1239    pub fn set_return_only_ids(self, return_only_ids: bool) -> Self {
1240        let ok = unsafe {
1241            sys::SteamAPI_ISteamUGC_SetReturnOnlyIDs(
1242                self.ugc,
1243                self.handle.unwrap(),
1244                return_only_ids,
1245            )
1246        };
1247        debug_assert!(ok);
1248        self
1249    }
1250
1251    /// Sets whether to return key value tags with the items.
1252    pub fn set_return_key_value_tags(self, return_kv_tags: bool) -> Self {
1253        let ok = unsafe {
1254            sys::SteamAPI_ISteamUGC_SetReturnKeyValueTags(
1255                self.ugc,
1256                self.handle.unwrap(),
1257                return_kv_tags,
1258            )
1259        };
1260        debug_assert!(ok);
1261        self
1262    }
1263
1264    /// Sets whether to return the full description of the items.
1265    pub fn set_return_long_description(self, return_long_desc: bool) -> Self {
1266        let ok = unsafe {
1267            sys::SteamAPI_ISteamUGC_SetReturnLongDescription(
1268                self.ugc,
1269                self.handle.unwrap(),
1270                return_long_desc,
1271            )
1272        };
1273        debug_assert!(ok);
1274        self
1275    }
1276
1277    /// Sets whether to return metadata with the items.
1278    pub fn set_return_metadata(self, return_metadata: bool) -> Self {
1279        let ok = unsafe {
1280            sys::SteamAPI_ISteamUGC_SetReturnMetadata(
1281                self.ugc,
1282                self.handle.unwrap(),
1283                return_metadata,
1284            )
1285        };
1286        debug_assert!(ok);
1287        self
1288    }
1289
1290    /// Sets whether to return children with the items.
1291    pub fn set_return_children(self, return_children: bool) -> Self {
1292        let ok = unsafe {
1293            sys::SteamAPI_ISteamUGC_SetReturnChildren(
1294                self.ugc,
1295                self.handle.unwrap(),
1296                return_children,
1297            )
1298        };
1299        debug_assert!(ok);
1300        self
1301    }
1302
1303    /// Sets whether to return additional previews with the items.
1304    pub fn set_return_additional_previews(self, return_additional_previews: bool) -> Self {
1305        let ok = unsafe {
1306            sys::SteamAPI_ISteamUGC_SetReturnAdditionalPreviews(
1307                self.ugc,
1308                self.handle.unwrap(),
1309                return_additional_previews,
1310            )
1311        };
1312        debug_assert!(ok);
1313        self
1314    }
1315
1316    /// Sets whether to only return the total number of items.
1317    pub fn set_return_total_only(self, return_total_only: bool) -> Self {
1318        let ok = unsafe {
1319            sys::SteamAPI_ISteamUGC_SetReturnTotalOnly(
1320                self.ugc,
1321                self.handle.unwrap(),
1322                return_total_only,
1323            )
1324        };
1325        debug_assert!(ok);
1326        self
1327    }
1328
1329    /// Sets the language to return the title and description in.
1330    pub fn set_language(self, language: &str) -> Self {
1331        let cstr = CString::new(language).unwrap();
1332        let ok = unsafe {
1333            sys::SteamAPI_ISteamUGC_SetLanguage(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1334        };
1335        debug_assert!(ok);
1336        self
1337    }
1338
1339    /// Sets whether results will be returned from the cache.
1340    pub fn set_allow_cached_response(self, max_age_seconds: u32) -> Self {
1341        let ok = unsafe {
1342            sys::SteamAPI_ISteamUGC_SetAllowCachedResponse(
1343                self.ugc,
1344                self.handle.unwrap(),
1345                max_age_seconds,
1346            )
1347        };
1348        debug_assert!(ok);
1349        self
1350    }
1351
1352    /// Sets a filter for the cloud file name.
1353    pub fn set_cloud_file_name_filter(self, file_name: &str) -> Self {
1354        let cstr = CString::new(file_name).unwrap();
1355        let ok = unsafe {
1356            sys::SteamAPI_ISteamUGC_SetCloudFileNameFilter(
1357                self.ugc,
1358                self.handle.unwrap(),
1359                cstr.as_ptr(),
1360            )
1361        };
1362        debug_assert!(ok);
1363        self
1364    }
1365
1366    /// Sets whether any of the required tags are sufficient for an item to be returned.
1367    pub fn set_match_any_tag(self, match_any_tag: bool) -> Self {
1368        let ok = unsafe {
1369            sys::SteamAPI_ISteamUGC_SetMatchAnyTag(self.ugc, self.handle.unwrap(), match_any_tag)
1370        };
1371        debug_assert!(ok);
1372        self
1373    }
1374
1375    /// Sets the full-text search string.
1376    pub fn set_search_text(self, search_text: &str) -> Self {
1377        let cstr = CString::new(search_text).unwrap();
1378        let ok = unsafe {
1379            sys::SteamAPI_ISteamUGC_SetSearchText(self.ugc, self.handle.unwrap(), cstr.as_ptr())
1380        };
1381        debug_assert!(ok);
1382        self
1383    }
1384
1385    /// Sets the number of days to consider for trending items.
1386    pub fn set_ranked_by_trend_days(self, days: u32) -> Self {
1387        let ok = unsafe {
1388            sys::SteamAPI_ISteamUGC_SetRankedByTrendDays(self.ugc, self.handle.unwrap(), days)
1389        };
1390        debug_assert!(ok);
1391        self
1392    }
1393
1394    /// Adds a required key-value tag that must be present on all returned items.
1395    pub fn add_required_key_value_tag(self, key: &str, value: &str) -> Self {
1396        let key_cstr = CString::new(key).unwrap();
1397        let value_cstr = CString::new(value).unwrap();
1398        let ok = unsafe {
1399            sys::SteamAPI_ISteamUGC_AddRequiredKeyValueTag(
1400                self.ugc,
1401                self.handle.unwrap(),
1402                key_cstr.as_ptr(),
1403                value_cstr.as_ptr(),
1404            )
1405        };
1406        debug_assert!(ok);
1407        self
1408    }
1409
1410    /// Sends the query to Steam and calls the provided callback with the results when completed.
1411    pub fn fetch<F>(mut self, cb: F)
1412    where
1413        F: for<'a> FnOnce(Result<QueryResults<'a>, SteamError>) + 'static + Send,
1414    {
1415        let ugc = self.ugc;
1416        let inner = Arc::clone(&self.inner);
1417        let handle = self.handle.take().unwrap();
1418        mem::drop(self);
1419
1420        unsafe {
1421            let api_call = sys::SteamAPI_ISteamUGC_SendQueryUGCRequest(ugc, handle);
1422            register_call_result::<sys::SteamUGCQueryCompleted_t, _>(
1423                &inner,
1424                api_call,
1425                move |v, io_error| {
1426                    let ugc = sys::SteamAPI_SteamUGC_v021();
1427                    if io_error {
1428                        sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(ugc, handle);
1429                        cb(Err(SteamError::IOFailure));
1430                        return;
1431                    } else if v.m_eResult != sys::EResult::k_EResultOK {
1432                        sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(ugc, handle);
1433                        cb(Err(v.m_eResult.into()));
1434                        return;
1435                    }
1436
1437                    let result = QueryResults {
1438                        ugc,
1439                        handle,
1440                        num_results_returned: v.m_unNumResultsReturned,
1441                        num_results_total: v.m_unTotalMatchingResults,
1442                        was_cached: v.m_bCachedData,
1443                        _phantom: Default::default(),
1444                    };
1445                    cb(Ok(result));
1446                },
1447            );
1448        }
1449    }
1450
1451    /// Runs the query, only fetching the total number of results.
1452    pub fn fetch_total<F>(self, cb: F)
1453    where
1454        F: Fn(Result<u32, SteamError>) + 'static + Send,
1455    {
1456        unsafe {
1457            let ok =
1458                sys::SteamAPI_ISteamUGC_SetReturnTotalOnly(self.ugc, self.handle.unwrap(), true);
1459            debug_assert!(ok);
1460        }
1461
1462        self.fetch(move |res| cb(res.map(|qr| qr.total_results())))
1463    }
1464
1465    /// Runs the query, only fetching the IDs.
1466    pub fn fetch_ids<F>(self, cb: F)
1467    where
1468        F: Fn(Result<Vec<PublishedFileId>, SteamError>) + 'static + Send,
1469    {
1470        unsafe {
1471            let ok = sys::SteamAPI_ISteamUGC_SetReturnOnlyIDs(self.ugc, self.handle.unwrap(), true);
1472            debug_assert!(ok);
1473        }
1474
1475        self.fetch(move |res| {
1476            cb(res.map(|qr| {
1477                qr.iter()
1478                    .filter_map(|v| v.map(|v| PublishedFileId(v.published_file_id.0)))
1479                    .collect::<Vec<_>>()
1480            }))
1481        })
1482    }
1483}
1484
1485/// Query results
1486pub struct QueryResults<'a> {
1487    ugc: *mut sys::ISteamUGC,
1488    handle: sys::UGCQueryHandle_t,
1489    num_results_returned: u32,
1490    num_results_total: u32,
1491    was_cached: bool,
1492    _phantom: marker::PhantomData<&'a sys::ISteamUGC>,
1493}
1494impl<'a> Drop for QueryResults<'a> {
1495    fn drop(&mut self) {
1496        unsafe {
1497            sys::SteamAPI_ISteamUGC_ReleaseQueryUGCRequest(self.ugc, self.handle);
1498        }
1499    }
1500}
1501impl<'a> QueryResults<'a> {
1502    /// Were these results retreived from a cache?
1503    pub fn was_cached(&self) -> bool {
1504        self.was_cached
1505    }
1506
1507    /// Gets the total number of results in this query, not just the current page
1508    pub fn total_results(&self) -> u32 {
1509        self.num_results_total
1510    }
1511
1512    /// Gets the number of results in this page.
1513    pub fn returned_results(&self) -> u32 {
1514        self.num_results_returned
1515    }
1516
1517    /// Gets the preview URL of the published file at the specified index.
1518    pub fn preview_url(&self, index: u32) -> Option<String> {
1519        let mut url = [0 as c_char; 4096];
1520
1521        let ok = unsafe {
1522            sys::SteamAPI_ISteamUGC_GetQueryUGCPreviewURL(
1523                self.ugc,
1524                self.handle,
1525                index,
1526                url.as_mut_ptr(),
1527                url.len() as _,
1528            )
1529        };
1530
1531        if ok {
1532            Some(unsafe { CStr::from_ptr(url.as_ptr()).to_string_lossy().into_owned() })
1533        } else {
1534            None
1535        }
1536    }
1537
1538    /// Gets a UGC statistic about the published file at the specified index.
1539    pub fn statistic(&self, index: u32, stat_type: UGCStatisticType) -> Option<u64> {
1540        let mut value = 0u64;
1541
1542        let ok = unsafe {
1543            sys::SteamAPI_ISteamUGC_GetQueryUGCStatistic(
1544                self.ugc,
1545                self.handle,
1546                index,
1547                stat_type.into(),
1548                &mut value,
1549            )
1550        };
1551
1552        debug_assert!(ok);
1553
1554        if ok {
1555            Some(value)
1556        } else {
1557            None
1558        }
1559    }
1560
1561    /// Gets UGCContentDescriptors of the published file at the specified index.
1562    pub fn content_descriptor(&self, index: u32) -> Vec<UGCContentDescriptorID> {
1563        let mut descriptors: [sys::EUGCContentDescriptorID; 10] = unsafe { std::mem::zeroed() };
1564        let max_entries = descriptors.len() as std::ffi::c_uint;
1565
1566        let num_descriptors = unsafe {
1567            sys::SteamAPI_ISteamUGC_GetQueryUGCContentDescriptors(
1568                self.ugc,
1569                self.handle,
1570                index,
1571                descriptors.as_mut_ptr(),
1572                max_entries,
1573            )
1574        } as usize;
1575
1576        Vec::from(&descriptors[..num_descriptors])
1577            .iter()
1578            .map(|&x| x.into())
1579            .collect()
1580    }
1581
1582    /// Gets a result.
1583    ///
1584    /// Returns None if index was out of bounds.
1585    pub fn get(&self, index: u32) -> Option<QueryResult> {
1586        if index >= self.num_results_returned {
1587            return None;
1588        }
1589
1590        unsafe {
1591            let mut raw_details: sys::SteamUGCDetails_t = mem::zeroed();
1592            let ok = sys::SteamAPI_ISteamUGC_GetQueryUGCResult(
1593                self.ugc,
1594                self.handle,
1595                index,
1596                &mut raw_details,
1597            );
1598            debug_assert!(ok);
1599
1600            if raw_details.m_eResult != sys::EResult::k_EResultOK {
1601                return None;
1602            }
1603
1604            let tags = CStr::from_ptr(raw_details.m_rgchTags.as_ptr())
1605                .to_string_lossy()
1606                .split(',')
1607                .map(|s| String::from(s))
1608                .collect::<Vec<_>>();
1609
1610            Some(QueryResult {
1611                published_file_id: PublishedFileId(raw_details.m_nPublishedFileId),
1612                creator_app_id: if raw_details.m_nCreatorAppID != 0 {
1613                    Some(AppId(raw_details.m_nCreatorAppID))
1614                } else {
1615                    None
1616                },
1617                consumer_app_id: if raw_details.m_nConsumerAppID != 0 {
1618                    Some(AppId(raw_details.m_nConsumerAppID))
1619                } else {
1620                    None
1621                },
1622                title: CStr::from_ptr(raw_details.m_rgchTitle.as_ptr())
1623                    .to_string_lossy()
1624                    .into_owned(),
1625                description: CStr::from_ptr(raw_details.m_rgchDescription.as_ptr())
1626                    .to_string_lossy()
1627                    .into_owned(),
1628                owner: SteamId(raw_details.m_ulSteamIDOwner),
1629                time_created: raw_details.m_rtimeCreated,
1630                time_updated: raw_details.m_rtimeUpdated,
1631                time_added_to_user_list: raw_details.m_rtimeAddedToUserList,
1632                visibility: raw_details.m_eVisibility.into(),
1633                banned: raw_details.m_bBanned,
1634                accepted_for_use: raw_details.m_bAcceptedForUse,
1635                url: CStr::from_ptr(raw_details.m_rgchURL.as_ptr())
1636                    .to_string_lossy()
1637                    .into_owned(),
1638                num_upvotes: raw_details.m_unVotesUp,
1639                num_downvotes: raw_details.m_unVotesDown,
1640                score: raw_details.m_flScore,
1641                num_children: raw_details.m_unNumChildren,
1642                tags,
1643                tags_truncated: raw_details.m_bTagsTruncated,
1644                file_name: CStr::from_ptr(raw_details.m_pchFileName.as_ptr())
1645                    .to_string_lossy()
1646                    .into_owned(),
1647                file_type: raw_details.m_eFileType.into(),
1648                file_size: raw_details.m_nFileSize.max(0) as u32,
1649            })
1650        }
1651    }
1652
1653    /// Returns an iterator that runs over all the fetched results
1654    pub fn iter<'b>(&'b self) -> impl Iterator<Item = Option<QueryResult>> + 'b {
1655        (0..self.returned_results()).map(move |i| self.get(i))
1656    }
1657
1658    /// Returns the given index's children as a list of PublishedFileId.
1659    ///
1660    /// You must call `include_children(true)` before fetching the query for this to work.
1661    ///
1662    /// Returns None if the index was out of bounds.
1663    pub fn get_children(&self, index: u32) -> Option<Vec<PublishedFileId>> {
1664        let num_children = self.get(index)?.num_children;
1665        let mut children: Vec<sys::PublishedFileId_t> = vec![0; num_children as usize];
1666
1667        let ok = unsafe {
1668            sys::SteamAPI_ISteamUGC_GetQueryUGCChildren(
1669                self.ugc,
1670                self.handle,
1671                index,
1672                children.as_mut_ptr(),
1673                num_children,
1674            )
1675        };
1676
1677        if ok {
1678            Some(children.into_iter().map(Into::into).collect())
1679        } else {
1680            None
1681        }
1682    }
1683
1684    /// Returns the number of key value tags associated with the item at the specified index.
1685    pub fn key_value_tags(&self, index: u32) -> u32 {
1686        unsafe { sys::SteamAPI_ISteamUGC_GetQueryUGCNumKeyValueTags(self.ugc, self.handle, index) }
1687    }
1688
1689    /// Gets the key value pair of a specified key value tag associated with the item at the specified index.
1690    pub fn get_key_value_tag(&self, index: u32, kv_tag_index: u32) -> Option<(String, String)> {
1691        let mut key = [0 as c_char; 256];
1692        let mut value = [0 as c_char; 256];
1693
1694        let ok = unsafe {
1695            sys::SteamAPI_ISteamUGC_GetQueryUGCKeyValueTag(
1696                self.ugc,
1697                self.handle,
1698                index,
1699                kv_tag_index,
1700                key.as_mut_ptr(),
1701                256,
1702                value.as_mut_ptr(),
1703                256,
1704            )
1705        };
1706
1707        if ok {
1708            Some(unsafe {
1709                (
1710                    CStr::from_ptr(key.as_ptr()).to_string_lossy().into_owned(),
1711                    CStr::from_ptr(value.as_ptr())
1712                        .to_string_lossy()
1713                        .into_owned(),
1714                )
1715            })
1716        } else {
1717            None
1718        }
1719    }
1720
1721    /// Gets the developer-set metadata associated with the item at the specified index.
1722    ///
1723    /// This is returned as a vector of raw bytes.
1724    pub fn get_metadata(&self, index: u32) -> Option<Vec<u8>> {
1725        let mut metadata = [0 as c_char; sys::k_cchDeveloperMetadataMax as usize];
1726
1727        let ok = unsafe {
1728            sys::SteamAPI_ISteamUGC_GetQueryUGCMetadata(
1729                self.ugc,
1730                self.handle,
1731                index,
1732                metadata.as_mut_ptr(),
1733                sys::k_cchDeveloperMetadataMax,
1734            )
1735        };
1736
1737        if ok {
1738            let metadata = unsafe { CStr::from_ptr(metadata.as_ptr()).to_bytes() };
1739            if metadata.is_empty() {
1740                None
1741            } else {
1742                Some(metadata.to_vec())
1743            }
1744        } else {
1745            None
1746        }
1747    }
1748}
1749
1750/// Query result
1751#[derive(Debug, Clone)]
1752pub struct QueryResult {
1753    pub published_file_id: PublishedFileId,
1754    pub creator_app_id: Option<AppId>,
1755    pub consumer_app_id: Option<AppId>,
1756    pub title: String,
1757    pub description: String,
1758    pub owner: SteamId,
1759    /// Time created in unix epoch seconds format
1760    pub time_created: u32,
1761    /// Time updated in unix epoch seconds format
1762    pub time_updated: u32,
1763    /// Time when the user added the published item to their list (not always applicable), provided in Unix epoch format (time since Jan 1st, 1970).
1764    pub time_added_to_user_list: u32,
1765    pub visibility: PublishedFileVisibility,
1766    pub banned: bool,
1767    pub accepted_for_use: bool,
1768    pub tags: Vec<String>,
1769    pub tags_truncated: bool,
1770    /// Original file name of the workshop item. Used in old games, like Total War: Shogun 2.
1771    pub file_name: String,
1772    pub file_type: FileType,
1773    pub file_size: u32,
1774
1775    pub url: String,
1776    pub num_upvotes: u32,
1777    pub num_downvotes: u32,
1778    /// The bayesian average for up votes / total votes, between \[0,1\].
1779    pub score: f32,
1780    pub num_children: u32,
1781    // TODO: Add missing fields as needed
1782}
1783
1784#[derive(Debug, Clone, Copy)]
1785pub struct CreateQueryError;
1786impl fmt::Display for CreateQueryError {
1787    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1788        write!(f, "Could not create workshop query")
1789    }
1790}
1791impl error::Error for CreateQueryError {}