valence-rs / valence

A Rust framework for building Minecraft servers.
http://valence.rs/
MIT License
2.77k stars 142 forks source link

Entity Equipment #662

Open maxomatic458 opened 1 week ago

maxomatic458 commented 1 week ago

Describe the problem related to your feature request.

Valence does not sync player equipment (held items, armor, etc)

What solution would you like?

a Equipment plugin which exposes a Equipment component.

What alternative(s) have you considered?

this could also be implemented in the inventory crate, although in my opinion it would be better to implement equipment as a seperate Plugin.

Additional context

related #254

I believe an Equipment Component could be implemented like this

#[derive(Debug, Default, Clone, Component)]
pub struct Equipment {
    main_hand: ItemStack,
    off_hand: ItemStack,
    boots: ItemStack,
    leggings: ItemStack,
    chestplate: ItemStack,
    helmet: ItemStack,

    /// Contains a set bit for each modified slot in `slots`.
    #[doc(hidden)]
    pub changed: u8,
}

impl Equipment {
    pub const SLOT_COUNT: u8 = 5;

    pub const MAIN_HAND_SLOT: u8 = 0;
    pub const OFF_HAND_SLOT: u8 = 1;
    pub const BOOTS_SLOT: u8 = 2;
    pub const LEGGINGS_SLOT: u8 = 3;
    pub const CHESTPLATE_SLOT: u8 = 4;
    pub const HELMET_SLOT: u8 = 5;

    pub fn main_hand(&self) -> &ItemStack {
        self.main_hand;
    }

    pub fn set_main_hand(&self) {
         ...
     }
     ...
}

that would make it pretty easy to use.

I also think this should be as seperate as possible from valence_inventory (so people using this crate would have to implement custom logic to update equipment based on inventory changes, but that should not be to hard)

we would have to use Inventory to get the HeldItem

thats the packet https://wiki.vg/index.php?title=Protocol&oldid=18375#Set_Equipment

Edit: since we would have to use setters/getters (so we garuantee that updated the changed field) we might as well store the actual items in a list instead of the fields.

Edit 2: My idea was that valence would only provide this component, and an updater system. So you would have to implement custom logic if you want to lets say set your equipment when you put on armor (as i do think there are use cases where you dont want to bind it to the inventory)

Any thoughts on that?

lukashermansson commented 1 week ago

Edit 2: My idea was that valence would only provide this component, and an updater system. So you would have to implement custom logic if you want to lets say set your equipment when you put on armor (as i do think there are use cases where you dont want to bind it to the inventory)

This seems like a good thing, you might not always want the equipment and player inventories to be in sync. One caveat that I think should be noted is that the vanilla client does not seem make this distinction on its own for its own player, if it has the slots filled where equipment is healed it renders its own client as having those items equipped, and the server sends no equipmentUpdate packets, (maybe the client should never see the equipment update packets for themselves as the Notchan server does not seem to send these so i have no idea what happens if you do send these to the client themselves)

maxomatic458 commented 1 week ago

im currently working on implementing this

and im looking for a way to send the equipment of an entity as soon as the player "renders" (or requests the data) from the server. Any idea what to look for?

lukashermansson commented 1 week ago

I would have a look on how we handle entity changes and updates, there is currently one system to handle most of the entity related packets: https://github.com/valence-rs/valence/blob/76fb18f185913a9279862a3084787b2afeb514f2/crates/valence_entity/src/query.rs https://github.com/valence-rs/valence/blob/main/crates/valence_server/src/layer/entity.rs#L497

the systems above shows how it works when an entity is loaded by the client when an entity exists and we should begin rendering the entity but the client was not aware of it before (like when they join, move layers etc) the init_logic packets for all shown entities are constructed to initialize the entity as shown here: https://github.com/valence-rs/valence/blob/main/crates/valence_server/src/client.rs#L982

One approach would just be to also add a Equipment component to the systems here for initializing and updating (would be a relatively simple change in the existing systems), (I guess it could be argued that Equipment is a thing that is tied to entities). If you want it to be its own freestanding plugin i think you would have to copy the relevant parts of update_view_and_layers in the client and perform similar updates to when an existing entity is first shown. (but make sure to schedule it after the the client update_view_and_layers as its in there the entities might get initialized, and we cant set equipment before the client is aware of the entities)

I have not really considered what benefits/downsides it is to have this as its own plugin myself, I think there might be performance differences between the two. Maybe someone else than me has a better idea if a change such as this deserves its own crate/plugin (as opposed to living in the valance_entity crate and systems). Regardless I think that's the rough idea on how it could be achieved, no matter what approach you think is appropriate.

maxomatic458 commented 1 week ago

thanks!

i believe it would probably be the best to have a seperate valence_equipment crate that would expose that plugin (which can be activated by default like inventory)

maxomatic458 commented 1 week ago

@dyc3 would you be ok with having equipment logic in a seperate (but activated by default) plugin?

maxomatic458 commented 1 week ago

i believe we could maybe emit a event here: https://github.com/valence-rs/valence/blob/main/crates/valence_server/src/client.rs#L982 which we can then be use in the equipment plugin (could probably also be useful for other things)

maxomatic458 commented 1 week ago

something like this could work:

/// This event will be emitted when a entity is unloaded for a client (e.g when moving out of range of the entity).
#[derive(Debug, Clone, PartialEq, Event)]
pub struct UnloadEntityForClientEvent {
    /// The client to unload the entity for.
    pub client: Entity,
    /// The MC Entity ID of the entity that will be unloaded.
    pub entity_unloaded: EntityId,
}

/// This event will be emitted when a entity is loaded for a client (e.g when moving into range of the entity).
#[derive(Debug, Clone, PartialEq, Event)]
pub struct LoadEntityForClientEvent {
    /// The client to load the entity for.
    pub client: Entity,
    /// The MC Entity ID of the entity that will be loaded.
    pub entity_loaded: EntityId,
}

pub(crate) fn update_view_and_layers(
    ...
    mut unload_entity_writer: EventWriter<UnloadEntityForClientEvent>,
    mut load_entity_writer: EventWriter<LoadEntityForClientEvent>,
) {
    // Wrap the events in this, so we only need one channel.
    enum ChannelEvent {
        UnloadEntity(UnloadEntityForClientEvent),
        LoadEntity(LoadEntityForClientEvent),
    }

    let (tx, rx) = crossbeam_channel::unbounded();

    (clients).par_iter_mut().for_each(
         // in here, send the events to our channel
         ...

    );

    // Send the events using the bevy event writers
    for event in rx.try_iter() {
        match event {
            ChannelEvent::UnloadEntity(event) => { 
                unload_entity_writer.send(event); 
            },
            ChannelEvent::LoadEntity(event) => { 
                load_entity_writer.send(event); 
            },
        };
    }    
}

I believe this would also be generally useful outside of equipment context.