Skip to main content

mcproto_types/profile/
resolvable_profile.rs

1//! Profiles that may contain partial identity data and texture overrides.
2
3use std::io::{Read, Write};
4
5use mcproto_codec::{
6    error::{CodecError, CodecKind, InvalidEncodingReason},
7    varint::{VarIntRead, VarIntWrite},
8};
9
10use crate::{
11    GameProfile, GameProfileProperties, GameProfileUsername, Identifier, PrefixedOptional,
12    ProtocolEnum, TypeCodec, TypeStructCodec, Uuid, VarInt,
13};
14
15/// The unresolved subset of a game profile.
16///
17/// Username and UUID each carry their own boolean presence marker. Properties
18/// use the same bounded representation as [`GameProfile::properties`].
19#[derive(Debug, Clone, PartialEq, Eq, Hash, TypeStructCodec)]
20#[type_struct_codec(kind = PartialProfile)]
21pub struct PartialProfile {
22    /// Optional player username, limited to 16 UTF-16 code units.
23    pub username: PrefixedOptional<GameProfileUsername>,
24    /// Optional player UUID.
25    pub uuid: PrefixedOptional<Uuid>,
26    /// Signed or unsigned profile properties, limited to 16 entries.
27    pub properties: GameProfileProperties,
28}
29
30impl Default for PartialProfile {
31    fn default() -> Self {
32        Self {
33            username: PrefixedOptional::none(),
34            uuid: PrefixedOptional::none(),
35            properties: GameProfileProperties::default(),
36        }
37    }
38}
39
40/// Player skin model used by a resolvable profile override.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, ProtocolEnum)]
42#[protocol_enum(repr = VarInt)]
43pub enum SkinModel {
44    /// The standard, wide-arm player model.
45    #[default]
46    Wide = 0,
47    /// The slim-arm player model.
48    Slim = 1,
49}
50
51/// Identity data carried by a [`ResolvableProfile`].
52///
53/// The wire discriminant is a VarInt: `0` selects [`Partial`](Self::Partial)
54/// and `1` selects [`Complete`](Self::Complete).
55#[derive(Debug, Clone, PartialEq, Eq, Hash)]
56pub enum ResolvableProfileData {
57    /// Identity data that may omit the username or UUID.
58    Partial(PartialProfile),
59    /// A fully specified [`GameProfile`].
60    Complete(GameProfile),
61}
62
63impl From<PartialProfile> for ResolvableProfileData {
64    fn from(value: PartialProfile) -> Self {
65        Self::Partial(value)
66    }
67}
68
69impl From<GameProfile> for ResolvableProfileData {
70    fn from(value: GameProfile) -> Self {
71        Self::Complete(value)
72    }
73}
74
75/// A partial or complete game profile followed by optional skin overrides.
76///
77/// The profile starts with a VarInt kind (`0` for partial, `1` for complete).
78/// Partial data contains a prefixed optional username, a prefixed optional
79/// UUID, and at most 16 properties. It is followed by optional body, cape,
80/// elytra, and model overrides. Each override carries a boolean presence
81/// marker in the packet codec.
82///
83/// # Examples
84///
85/// ```
86/// use mcproto_types::{
87///     GameProfileUsername, Identifier, PartialProfile, PrefixedOptional,
88///     ResolvableProfile, ResolvableProfileData, SkinModel, TypeCodec,
89/// };
90///
91/// let mut profile = ResolvableProfile::new(ResolvableProfileData::Partial(
92///     PartialProfile {
93///         username: PrefixedOptional::some(GameProfileUsername::new("Alex")?),
94///         ..PartialProfile::default()
95///     },
96/// ));
97/// profile.body = PrefixedOptional::some(Identifier::new("minecraft:entity/player/wide/alex")?);
98/// profile.model = PrefixedOptional::some(SkinModel::Wide);
99///
100/// let mut encoded = Vec::new();
101/// profile.encode(&mut encoded)?;
102/// let mut input = encoded.as_slice();
103/// assert_eq!(ResolvableProfile::decode(&mut input)?, profile);
104/// assert!(input.is_empty());
105/// # Ok::<(), Box<dyn std::error::Error>>(())
106/// ```
107///
108/// See the official [Resolvable Profile] protocol documentation.
109///
110/// [Resolvable Profile]: https://minecraft.wiki/w/Java_Edition_protocol/Packets#Resolvable_Profile
111#[derive(Debug, Clone, PartialEq, Eq, Hash)]
112pub struct ResolvableProfile {
113    /// Partial or complete identity data.
114    pub profile: ResolvableProfileData,
115    /// Optional override for the body texture.
116    pub body: PrefixedOptional<Identifier>,
117    /// Optional override for the cape texture.
118    pub cape: PrefixedOptional<Identifier>,
119    /// Optional override for the elytra texture.
120    pub elytra: PrefixedOptional<Identifier>,
121    /// Optional override for the player model.
122    pub model: PrefixedOptional<SkinModel>,
123}
124
125impl ResolvableProfile {
126    /// Creates a profile without texture or model overrides.
127    #[must_use]
128    pub const fn new(profile: ResolvableProfileData) -> Self {
129        Self {
130            profile,
131            body: PrefixedOptional::none(),
132            cape: PrefixedOptional::none(),
133            elytra: PrefixedOptional::none(),
134            model: PrefixedOptional::none(),
135        }
136    }
137
138    /// Creates a resolvable profile from partial identity data.
139    #[must_use]
140    pub const fn partial(profile: PartialProfile) -> Self {
141        Self::new(ResolvableProfileData::Partial(profile))
142    }
143
144    /// Creates a resolvable profile from a complete game profile.
145    #[must_use]
146    pub const fn complete(profile: GameProfile) -> Self {
147        Self::new(ResolvableProfileData::Complete(profile))
148    }
149}
150
151impl From<PartialProfile> for ResolvableProfile {
152    fn from(value: PartialProfile) -> Self {
153        Self::partial(value)
154    }
155}
156
157impl From<GameProfile> for ResolvableProfile {
158    fn from(value: GameProfile) -> Self {
159        Self::complete(value)
160    }
161}
162
163impl TypeCodec for ResolvableProfile {
164    fn encode(&self, writer: &mut impl Write) -> Result<(), CodecError> {
165        match &self.profile {
166            ResolvableProfileData::Partial(value) => {
167                writer
168                    .write_varint(0)
169                    .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
170                value
171                    .encode(writer)
172                    .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
173            }
174            ResolvableProfileData::Complete(value) => {
175                writer
176                    .write_varint(1)
177                    .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
178                value
179                    .encode(writer)
180                    .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
181            }
182        }
183
184        self.body
185            .encode(writer)
186            .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
187        self.cape
188            .encode(writer)
189            .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
190        self.elytra
191            .encode(writer)
192            .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
193        self.model
194            .encode(writer)
195            .map_err(|error| error.with_context(CodecKind::ResolvableProfile))
196    }
197
198    fn decode(reader: &mut impl Read) -> Result<Self, CodecError> {
199        let (kind, kind_size) = reader
200            .read_varint_with_size()
201            .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?;
202        let profile = match kind {
203            0 => PartialProfile::decode(reader)
204                .map(ResolvableProfileData::Partial)
205                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
206            1 => GameProfile::decode(reader)
207                .map(ResolvableProfileData::Complete)
208                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
209            value => {
210                return Err(CodecError::invalid_encoding(
211                    CodecKind::ResolvableProfile,
212                    kind_size,
213                    InvalidEncodingReason::InvalidEnumValue {
214                        value: i128::from(value),
215                    },
216                ));
217            }
218        };
219
220        Ok(Self {
221            profile,
222            body: PrefixedOptional::decode(reader)
223                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
224            cape: PrefixedOptional::decode(reader)
225                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
226            elytra: PrefixedOptional::decode(reader)
227                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
228            model: PrefixedOptional::decode(reader)
229                .map_err(|error| error.with_context(CodecKind::ResolvableProfile))?,
230        })
231    }
232}