Skip to content

Commit

Permalink
Custom skin example (#462)
Browse files Browse the repository at this point in the history
# Objective

This PR aims to make it a little bit easier to create player entities
with custom skins. This is common for things like NPCs.

closes #460

# Solution

- Added a new example.
- Added `skin` and `set_skin` methods to `Properties`.
  • Loading branch information
dyc3 authored Sep 19, 2023
1 parent 06e1417 commit 623392b
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 0 deletions.
26 changes: 26 additions & 0 deletions crates/valence_server/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,32 @@ impl Properties {
pub fn textures_mut(&mut self) -> Option<&mut Property> {
self.0.iter_mut().find(|p| p.name == "textures")
}

/// Returns the value of the "textures" property. It's a base64-encoded
/// JSON string that contains the skin and cape URLs.
pub fn skin(&self) -> Option<&str> {
self.textures().map(|p| p.value.as_str())
}

/// Sets the value of the "textures" property, or adds it if it does not
/// exist. Can be used for custom skins on player entities.
///
/// `signature` is the Yggdrasil signature for the texture data. It is
/// required if you want the skin to show up on vanilla Notchian
/// clients. You can't sign skins yourself, so you'll have to get it from
/// Mojang.
pub fn set_skin(&mut self, skin: impl Into<String>, signature: impl Into<String>) {
if let Some(prop) = self.textures_mut() {
prop.value = skin.into();
prop.signature = Some(signature.into());
} else {
self.0.push(Property {
name: "textures".to_owned(),
value: skin.into(),
signature: Some(signature.into()),
});
}
}
}

#[derive(Clone, PartialEq, Eq, Debug)]
Expand Down
109 changes: 109 additions & 0 deletions examples/custom_npc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use valence::entity::player::PlayerEntityBundle;
use valence::player_list::{DisplayName, Listed, PlayerListEntryBundle};
use valence::prelude::*;
use valence::text::IntoText;

const SPAWN_Y: i32 = 64;

pub fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(
Update,
(
init_clients,
despawn_disconnected_clients,
apply_custom_skin,
),
)
.run();
}

fn setup(
mut commands: Commands,
server: Res<Server>,
dimensions: Res<DimensionTypeRegistry>,
biomes: Res<BiomeRegistry>,
) {
let mut layer = LayerBundle::new(ident!("overworld"), &dimensions, &biomes, &server);

for z in -5..5 {
for x in -5..5 {
layer.chunk.insert_chunk([x, z], UnloadedChunk::new());
}
}

for z in -25..25 {
for x in -25..25 {
layer
.chunk
.set_block([x, SPAWN_Y, z], BlockState::GRASS_BLOCK);
}
}

let layer_id = commands.spawn(layer).id();

let npc_id = UniqueId::default();

commands.spawn(PlayerEntityBundle {
layer: EntityLayerId(layer_id),
uuid: npc_id,
position: Position::new((0.0, SPAWN_Y as f64 + 1.0, 6.0)),
look: Look::new(180.0, 0.0),
head_yaw: HeadYaw(180.0),
..Default::default()
});

// In order for the player entity to be visible to other players, there must
// be an entry in the player list.
commands.spawn(PlayerListEntryBundle {
uuid: npc_id,
username: Username("Alice".into()),
// adjusts the appearance of the name in the player list only
display_name: DisplayName("Alice".color(Color::RED).into()),
// makes the fake player not appear in the player list
listed: Listed(false),
..Default::default()
});
}

fn init_clients(
mut clients: Query<
(
&mut EntityLayerId,
&mut VisibleChunkLayer,
&mut VisibleEntityLayers,
&mut Position,
&mut GameMode,
),
Added<Client>,
>,
layers: Query<Entity, (With<ChunkLayer>, With<EntityLayer>)>,
) {
for (
mut layer_id,
mut visible_chunk_layer,
mut visible_entity_layers,
mut pos,
mut game_mode,
) in &mut clients
{
let layer = layers.single();

layer_id.0 = layer;
visible_chunk_layer.0 = layer;
visible_entity_layers.0.insert(layer);
pos.set([0.0, 65.0, 0.0]);
*game_mode = GameMode::Creative;
}
}

fn apply_custom_skin(mut query: Query<&mut Properties, (Added<Properties>, Without<Client>)>) {
for mut props in query.iter_mut() {
props.set_skin(
"ewogICJ0aW1lc3RhbXAiIDogMTY5MTcwNjU3MzE1NiwKICAicHJvZmlsZUlkIiA6ICJlODgyNzRlYjNmNTE0ZDYwYmMxYWQ5NTQ4MTIxODMwMyIsCiAgInByb2ZpbGVOYW1lIiA6ICJBbmltYWxUaGVHYW1lciIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8xZGUyYzgzZjhmNGZiMzgwNjlmNTVmNTJlNGY4ZWU1ZjA4NjcyMjllYWQ5MWI3ZTc5ZGVmNzU0YjcwZWE5NDMzIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0=",
"k/g8JTYB0A5O+h8+XSdw3QFEVHnzomDsGl6eubV/sE396yAL7E4qCT24r3Uv88YYforuET1BXG0GBOewcij3uMajm+mc/P7v+0+C+NSS9g5dpSs2e9MdeGZBgDEr1kTnXzQmayZUvLGitW23GuRDHdVHx76JZpxBk3q0VsjgncNs6UVZwfYNCaUGZZx38bqG5FXGxE0MfFHKiJawKwWRaoAbHjrfsByLipIKUhssUF3pt+HPWbgaOD2rO0EOLBrGzvEnu9oeLPH4tqdlvurjGrdpM4wKCmS3j8K91OBTABciVR9xt0fRnhbL4JoZuLK+iefNXx8nBCVEOm9sNk4pXHNWZvKEkqMb3jvpxuYHsSZPm0IdN+74FEmjHy0sY/7+ZG/h/IUHs4CyrPAtR/rqON6MG8nVVBxUq4kWV+2Xj+U+O02gQUVFqMM77AqArRsPIkeFIgVQ6+WvBZYXuRe1Ryo6qwjmYGc4AeTZTtvafzv8vfAMFfJJmT69nkTTDO5hAtDTUnCd86nNFQ3qijdO9CW7OFDyysb9M0a1O7pQ7Nu10rkNwY+6uTfKoATtT80+RoMzvKwcIAG4cY+PR5jhsKP+sf+AEymovD+cPVnLOuZQ6bAyKW6yjf9Xd0vyirCgNaU1CGmDE1mihGK2kC0fm11RaoDbyKvMcLKAq+OFos0=",
);
}
}

0 comments on commit 623392b

Please sign in to comment.