1use serde::{Deserialize, Serialize};
2use strum_macros::{Display, EnumString, FromRepr};
3
4use crate::{Paging, endpoint};
5
6pub const URL: &str = "https://avatar.roblox.com/v1";
7
8pub type ColorId = u8;
9
10#[repr(u8)]
11#[derive(
12 Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString, FromRepr,
13)]
14pub enum AvatarType {
15 R6 = 1,
16 R15 = 2,
17}
18
19#[repr(u8)]
20#[derive(
21 Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Display, EnumString, FromRepr,
22)]
23pub enum MorphAvatarType {
24 MorphR6 = 1,
25 MorphR15 = 2,
26}
27
28#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
29#[serde(rename_all = "camelCase")]
30pub struct AvatarScales {
31 pub height: f32,
32 pub width: f32,
33 pub head: f32,
34 pub depth: f32,
35 pub proportion: f32,
36 pub body_type: f32,
37}
38
39#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
40pub struct BodyColors {
41 #[serde(rename = "headColorId")]
42 pub head: ColorId,
43 #[serde(rename = "torsoColorId")]
44 pub torso: ColorId,
45 #[serde(rename = "rightArmColorId")]
46 pub right_arm: ColorId,
47 #[serde(rename = "leftArmColorId")]
48 pub left_arm: ColorId,
49 #[serde(rename = "rightLegColorId")]
50 pub right_leg: ColorId,
51 #[serde(rename = "leftLegColorId")]
52 pub left_leg: ColorId,
53}
54
55#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
56pub struct AssetType {
57 pub id: u8,
58 pub name: String,
59}
60
61#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
62pub struct AssetMeta {
63 pub version: u8,
64
65 pub order: u16,
66 pub puffiness: Option<f32>,
67}
68
69#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
70#[serde(rename_all = "camelCase")]
71pub struct Asset {
72 pub id: u64,
73 pub name: String,
74 #[serde(rename = "assetType")]
75 pub kind: AssetType,
76 pub current_version_id: u64,
77 pub meta: Option<AssetMeta>,
78}
79
80#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
81#[serde(rename_all = "camelCase")]
82pub struct Emote {
83 #[serde(rename = "assetId")]
84 pub id: u64,
85 #[serde(rename = "assetName")]
86 pub name: String,
87 pub position: u8,
88}
89
90#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
91#[serde(rename_all = "camelCase")]
92pub struct Outfit {
93 pub id: u64,
94 pub name: String,
95 pub is_editable: bool,
96 pub outfit_type: Option<()>,
97}
98
99#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
100#[serde(rename_all = "camelCase")]
101pub struct AvatarResponse {
102 #[serde(rename = "playerAvatarType")]
103 pub kind: AvatarType,
104 pub assets: Vec<Asset>,
105 pub scales: AvatarScales,
106 pub body_colors: BodyColors,
107 pub default_pants_applied: bool,
108 pub default_shirt_applied: bool,
109 pub emotes: Vec<Emote>,
110}
111
112#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
113#[serde(rename_all = "camelCase")]
114pub struct OutfitsResponse {
115 #[serde(rename = "data")]
116 pub outfits: Vec<Outfit>,
117 pub total: u64,
118 pub filtered_count: u64,
119}
120
121#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct OutfitDetails {
124 pub id: u64,
125 pub name: String,
126 pub universe_id: u64,
127 pub assets: Vec<Asset>,
128 pub body_colors: BodyColors,
129 #[serde(rename = "scale")]
130 pub scales: AvatarScales,
131 #[serde(rename = "playerAvatarType")]
132 pub avatar_type: AvatarType,
133 pub outfit_type: String,
134 pub is_editable: bool,
135 pub moderation_status: Option<String>,
136}
137
138#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
139#[serde(rename_all = "camelCase")]
140pub struct UniverseAvatarSettings {
141 #[serde(rename = "gameAvatarType")]
142 pub avatar_type: MorphAvatarType,
143 pub avatar_body_type: String,
144 pub avatar_collision_type: String,
145 pub joint_positioning_type: String,
146
147 pub avatar_min_scales: AvatarScales,
148 pub avatar_max_scales: AvatarScales,
149 pub avatar_asset_overrides: Vec<Option<()>>,
150
151 pub message: String,
152 pub moderation_status: Option<String>,
153
154 pub allow_custom_animations: String,
155}
156
157endpoint! {
158 user_avatar(id: u64) -> AvatarResponse {
160 GET "{URL}/users/{id}/avatar";
161 }
162
163 user_currently_wearing(id: u64) -> Vec<u64> {
165 GET "{URL}/users/{id}/currently-wearing";
166 types {
167 Response {
168 ids("assetIds"): Vec<u64>,
169 }
170 }
171 map |r: Response| r.ids
172 }
173
174 avatar_set_wearing_assets(assets: Vec<u64>) -> bool {
180 POST "{URL}/avatar/set-wearing-assets";
181 types {
182 Request<'a> {
183 asset_ids("assetIds"): &'a [u64],
184 }
185 Response {
186 success: bool,
187 }
188 }
189 body_serialize {
190 &Request { asset_ids: &assets }
191 }
192 map |r: Response| r.success
193 }
194
195 avatar_set_type(kind: AvatarType) -> bool {
197 POST "{URL}/avatar/set-player-avatar-type";
198 types {
199 Request {
200 avatar_type("playerAvatarType"): AvatarType,
201 }
202 Response {
203 success: bool,
204 }
205 }
206 body_serialize {
207 &Request { avatar_type: kind }
208 }
209 map |r: Response| r.success
210 }
211
212 avatar_set_body_colors(colors: BodyColors) -> bool {
214 POST "{URL}/avatar/set-body-colors";
215 types {
216 Response {
217 success: bool,
218 }
219 }
220 body_serialize {
221 &colors
222 }
223 map |r: Response| r.success
224 }
225
226 avatar_set_scales(scales: AvatarScales) -> bool {
228 POST "{URL}/avatar/set-scales";
229 types {
230 Response {
231 success: bool,
232 }
233 }
234 body_serialize {
235 &scales
236 }
237 map |r: Response| r.success
238 }
239
240 user_outfits(
242 id: u64, paging: Paging<'_>, is_editable: Option<bool>
243 ) -> OutfitsResponse {
244 GET "{URL}/users/{id}/outfits";
245 prelude {
246 let limit = paging.limit.unwrap_or(25).to_string();
247 let cursor = paging.cursor.unwrap_or("1");
248 let is_editable = match is_editable {
249 Some(editable) => editable.to_string(),
250 None => String::new(),
251 };
252 }
253 query {
254 "page" => cursor,
255 "itemsPerPage" => &limit,
256 "isEditable" => &is_editable,
257 }
258 }
259
260 outfit_details(id: u64) -> OutfitDetails {
262 GET "{URL}/outfits/{id}/details";
263 }
264
265 remove_outfit(id: u64) -> bool {
267 POST "{URL}/outfits/{id}/delete";
268 types {
269 Response {
270 success: bool,
271 }
272 }
273 map |r: Response| r.success
274 }
275
276 universe_avatar_settings(id: u64) -> UniverseAvatarSettings {
280 GET "{URL}/users/{id}/avatar";
281 }
282}