feat: add Villager trading
currently job selection is missing
This commit is contained in:
parent
7fe705c7eb
commit
fdd2684432
1
assets/villager_data.json
Normal file
1
assets/villager_data.json
Normal file
File diff suppressed because one or more lines are too long
1
pumpkin-codegen/Cargo.lock
generated
1
pumpkin-codegen/Cargo.lock
generated
@ -1015,6 +1015,7 @@ version = "1.0.149"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -12,7 +12,7 @@ heck = "0.5"
|
|||||||
md5 = "0.8"
|
md5 = "0.8"
|
||||||
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = { version = "1.0", features = ["preserve_order"] }
|
||||||
serde_json5 = "0.2"
|
serde_json5 = "0.2"
|
||||||
|
|
||||||
rayon = "1.11.0"
|
rayon = "1.11.0"
|
||||||
|
|||||||
@ -57,6 +57,7 @@ mod tag;
|
|||||||
mod tracked_data;
|
mod tracked_data;
|
||||||
mod translations;
|
mod translations;
|
||||||
mod version;
|
mod version;
|
||||||
|
mod villager;
|
||||||
mod world_event;
|
mod world_event;
|
||||||
|
|
||||||
/// Output directory where all generated Rust source files are written.
|
/// Output directory where all generated Rust source files are written.
|
||||||
@ -101,6 +102,7 @@ pub fn main() {
|
|||||||
(entity_status::build, "entity_status.rs"),
|
(entity_status::build, "entity_status.rs"),
|
||||||
(tag::build, "tag.rs"),
|
(tag::build, "tag.rs"),
|
||||||
(noise_router::build, "noise_router.rs"),
|
(noise_router::build, "noise_router.rs"),
|
||||||
|
(villager::build, "villager.rs"),
|
||||||
(
|
(
|
||||||
flower_pot_transformations::build,
|
flower_pot_transformations::build,
|
||||||
"flower_pot_transformations.rs",
|
"flower_pot_transformations.rs",
|
||||||
|
|||||||
328
pumpkin-codegen/src/villager.rs
Normal file
328
pumpkin-codegen/src/villager.rs
Normal file
@ -0,0 +1,328 @@
|
|||||||
|
use heck::{ToPascalCase, ToShoutySnakeCase};
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct VillagerDataJson {
|
||||||
|
professions: IndexMap<String, ProfessionJson>,
|
||||||
|
types: IndexMap<String, String>,
|
||||||
|
trade_sets: IndexMap<String, TradeSetJson>,
|
||||||
|
villager_trades: IndexMap<String, TradeJson>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ProfessionJson {
|
||||||
|
name: NameJson,
|
||||||
|
requested_items: Vec<String>,
|
||||||
|
work_sound: Option<String>,
|
||||||
|
trade_sets: IndexMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct NameJson {
|
||||||
|
translate: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TradeSetJson {
|
||||||
|
trades: String, // Tag like "#minecraft:armorer/level_1"
|
||||||
|
amount: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TradeJson {
|
||||||
|
wants: TradeItemJson,
|
||||||
|
wants_b: Option<TradeItemJson>,
|
||||||
|
gives: TradeItemJson,
|
||||||
|
max_uses: Option<f32>,
|
||||||
|
xp: Option<f32>,
|
||||||
|
price_multiplier: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TradeItemJson {
|
||||||
|
id: String,
|
||||||
|
count: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build() -> TokenStream {
|
||||||
|
let data: VillagerDataJson =
|
||||||
|
serde_json::from_str(&fs::read_to_string("../assets/villager_data.json").unwrap())
|
||||||
|
.expect("Failed to parse villager_data.json");
|
||||||
|
|
||||||
|
let mut profession_variants = Vec::new();
|
||||||
|
let mut type_variants = Vec::new();
|
||||||
|
|
||||||
|
let mut work_sounds = Vec::new();
|
||||||
|
let mut requested_items = Vec::new();
|
||||||
|
let mut profession_names = Vec::new();
|
||||||
|
|
||||||
|
let mut profession_from_i32 = Vec::new();
|
||||||
|
let mut type_from_i32 = Vec::new();
|
||||||
|
|
||||||
|
let mut trade_set_data = Vec::new();
|
||||||
|
let mut generated_trade_sets = IndexMap::new();
|
||||||
|
|
||||||
|
// Helper to format a trade into TokenStream
|
||||||
|
let format_trade = |trade: &TradeJson| {
|
||||||
|
let wants_item = format_ident!(
|
||||||
|
"{}",
|
||||||
|
trade.wants.id.strip_prefix("minecraft:").unwrap_or(&trade.wants.id).to_shouty_snake_case()
|
||||||
|
);
|
||||||
|
let wants_count = trade.wants.count.unwrap_or(1.0) as i32;
|
||||||
|
let wants = quote! { VillagerTradeItem { item: &crate::item::Item::#wants_item, count: #wants_count } };
|
||||||
|
|
||||||
|
let wants_b = if let Some(b) = &trade.wants_b {
|
||||||
|
let item = format_ident!(
|
||||||
|
"{}",
|
||||||
|
b.id.strip_prefix("minecraft:").unwrap_or(&b.id).to_shouty_snake_case()
|
||||||
|
);
|
||||||
|
let count = b.count.unwrap_or(1.0) as i32;
|
||||||
|
quote! { Some(VillagerTradeItem { item: &crate::item::Item::#item, count: #count }) }
|
||||||
|
} else {
|
||||||
|
quote! { None }
|
||||||
|
};
|
||||||
|
|
||||||
|
let gives_item = format_ident!(
|
||||||
|
"{}",
|
||||||
|
trade.gives.id.strip_prefix("minecraft:").unwrap_or(&trade.gives.id).to_shouty_snake_case()
|
||||||
|
);
|
||||||
|
let gives_count = trade.gives.count.unwrap_or(1.0) as i32;
|
||||||
|
let gives = quote! { VillagerTradeItem { item: &crate::item::Item::#gives_item, count: #gives_count } };
|
||||||
|
|
||||||
|
let max_uses = trade.max_uses.unwrap_or(16.0) as i32;
|
||||||
|
let xp = trade.xp.unwrap_or(2.0) as i32;
|
||||||
|
let price_multiplier = trade.price_multiplier.unwrap_or(0.05);
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
VillagerTrade {
|
||||||
|
wants: #wants,
|
||||||
|
wants_b: #wants_b,
|
||||||
|
gives: #gives,
|
||||||
|
max_uses: #max_uses,
|
||||||
|
xp: #xp,
|
||||||
|
price_multiplier: #price_multiplier,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pre-process all trade sets mentioned in trade_sets map
|
||||||
|
for (set_key, set_data) in &data.trade_sets {
|
||||||
|
let tag = &set_data.trades;
|
||||||
|
if !tag.starts_with("#minecraft:") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tag_content = tag.strip_prefix("#minecraft:").unwrap();
|
||||||
|
let parts: Vec<&str> = tag_content.split('/').collect();
|
||||||
|
if parts.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let prof = parts[0];
|
||||||
|
let level_str = parts[1].strip_prefix("level_").unwrap_or(parts[1]);
|
||||||
|
|
||||||
|
let mut matching_trades = Vec::new();
|
||||||
|
let prefix = format!("{prof}/{level_str}/");
|
||||||
|
for (key, trade) in &data.villager_trades {
|
||||||
|
if key.starts_with(&prefix) {
|
||||||
|
matching_trades.push(format_trade(trade));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for smiths
|
||||||
|
if matching_trades.is_empty() && (prof == "armorer" || prof == "toolsmith" || prof == "weaponsmith") {
|
||||||
|
let smith_prefix = format!("smith/{level_str}/");
|
||||||
|
for (key, trade) in &data.villager_trades {
|
||||||
|
if key.starts_with(&smith_prefix) {
|
||||||
|
matching_trades.push(format_trade(trade));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matching_trades.is_empty() {
|
||||||
|
let ident_name = tag_content.replace('/', "_").to_shouty_snake_case();
|
||||||
|
let ident = format_ident!("TRADES_{}", ident_name);
|
||||||
|
trade_set_data.push(quote! {
|
||||||
|
pub const #ident: &[VillagerTrade] = &[
|
||||||
|
#(#matching_trades),*
|
||||||
|
];
|
||||||
|
});
|
||||||
|
generated_trade_sets.insert(tag.clone(), ident);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut profession_trade_sets = Vec::new();
|
||||||
|
|
||||||
|
for (i, (name, prof_data)) in data.professions.iter().enumerate() {
|
||||||
|
let ident = format_ident!("{}", name.to_pascal_case());
|
||||||
|
profession_variants.push(quote! { #ident });
|
||||||
|
|
||||||
|
let sound = if let Some(sound) = &prof_data.work_sound {
|
||||||
|
let sound_ident = format_ident!(
|
||||||
|
"{}",
|
||||||
|
sound
|
||||||
|
.strip_prefix("minecraft:")
|
||||||
|
.unwrap_or(sound)
|
||||||
|
.replace('.', "_")
|
||||||
|
.to_pascal_case()
|
||||||
|
);
|
||||||
|
quote! { Some(crate::sound::Sound::#sound_ident) }
|
||||||
|
} else {
|
||||||
|
quote! { None }
|
||||||
|
};
|
||||||
|
work_sounds.push(quote! { Self::#ident => #sound });
|
||||||
|
|
||||||
|
let items: Vec<_> = prof_data
|
||||||
|
.requested_items
|
||||||
|
.iter()
|
||||||
|
.map(|i| {
|
||||||
|
let item_ident = format_ident!(
|
||||||
|
"{}",
|
||||||
|
i.strip_prefix("minecraft:")
|
||||||
|
.unwrap_or(i)
|
||||||
|
.to_shouty_snake_case()
|
||||||
|
);
|
||||||
|
quote! { &crate::item::Item::#item_ident }
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
requested_items.push(quote! { Self::#ident => &[#(#items),*] });
|
||||||
|
|
||||||
|
let translate = &prof_data.name.translate;
|
||||||
|
profession_names.push(quote! { Self::#ident => #translate });
|
||||||
|
|
||||||
|
let i = i as i32;
|
||||||
|
profession_from_i32.push(quote! { #i => Some(Self::#ident) });
|
||||||
|
|
||||||
|
let mut level_matches = Vec::new();
|
||||||
|
for (level_str, set_key) in &prof_data.trade_sets {
|
||||||
|
let level = level_str.parse::<i32>().unwrap();
|
||||||
|
let set_key_clean = set_key.strip_prefix("minecraft:").unwrap_or(set_key);
|
||||||
|
if let Some(set) = data.trade_sets.get(set_key_clean) {
|
||||||
|
if let Some(trades_ident) = generated_trade_sets.get(&set.trades) {
|
||||||
|
let amount = set.amount as i32;
|
||||||
|
level_matches.push(quote! { #level => Some(VillagerTradeSet { trades: #trades_ident, amount: #amount }) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
profession_trade_sets.push(quote! {
|
||||||
|
Self::#ident => match level {
|
||||||
|
#(#level_matches,)*
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i, name) in data.types.keys().enumerate() {
|
||||||
|
let ident = format_ident!("{}", name.to_pascal_case());
|
||||||
|
type_variants.push(quote! { #ident });
|
||||||
|
|
||||||
|
let i = i as i32;
|
||||||
|
type_from_i32.push(quote! { #i => Some(Self::#ident) });
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub struct VillagerTradeItem {
|
||||||
|
pub item: &'static crate::item::Item,
|
||||||
|
pub count: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub struct VillagerTrade {
|
||||||
|
pub wants: VillagerTradeItem,
|
||||||
|
pub wants_b: Option<VillagerTradeItem>,
|
||||||
|
pub gives: VillagerTradeItem,
|
||||||
|
pub max_uses: i32,
|
||||||
|
pub xp: i32,
|
||||||
|
pub price_multiplier: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq)]
|
||||||
|
pub struct VillagerTradeSet {
|
||||||
|
pub trades: &'static [VillagerTrade],
|
||||||
|
pub amount: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#(#trade_set_data)*
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum VillagerProfession {
|
||||||
|
#(#profession_variants),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VillagerProfession {
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_i32(id: i32) -> Option<Self> {
|
||||||
|
match id {
|
||||||
|
#(#profession_from_i32,)*
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn work_sound(&self) -> Option<crate::sound::Sound> {
|
||||||
|
match self {
|
||||||
|
#(#work_sounds),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn requested_items(&self) -> &'static [&'static crate::item::Item] {
|
||||||
|
match self {
|
||||||
|
#(#requested_items),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn translation_key(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
#(#profession_names),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn trade_set(&self, level: i32) -> Option<VillagerTradeSet> {
|
||||||
|
match self {
|
||||||
|
#(#profession_trade_sets,)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for VillagerProfession {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_i32(value).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum VillagerType {
|
||||||
|
#(#type_variants),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VillagerType {
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_i32(id: i32) -> Option<Self> {
|
||||||
|
match id {
|
||||||
|
#(#type_from_i32,)*
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<i32> for VillagerType {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(value: i32) -> Result<Self, Self::Error> {
|
||||||
|
Self::from_i32(value).ok_or(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -42,6 +42,7 @@ default = [
|
|||||||
"recipe_remainder",
|
"recipe_remainder",
|
||||||
"chunk_gen_settings",
|
"chunk_gen_settings",
|
||||||
"carver",
|
"carver",
|
||||||
|
"villager",
|
||||||
]
|
]
|
||||||
|
|
||||||
item = []
|
item = []
|
||||||
@ -78,6 +79,7 @@ potion = ["potion_brewing"]
|
|||||||
recipe_remainder = []
|
recipe_remainder = []
|
||||||
chunk_gen_settings = []
|
chunk_gen_settings = []
|
||||||
carver = []
|
carver = []
|
||||||
|
villager = ["item", "sound"]
|
||||||
|
|
||||||
noise_parameter = []
|
noise_parameter = []
|
||||||
biome = []
|
biome = []
|
||||||
|
|||||||
4676
pumpkin-data/src/generated/villager.rs
Normal file
4676
pumpkin-data/src/generated/villager.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -294,3 +294,8 @@ pub mod chunk_gen_settings;
|
|||||||
#[rustfmt::skip]
|
#[rustfmt::skip]
|
||||||
#[path = "generated/carver.rs"]
|
#[path = "generated/carver.rs"]
|
||||||
pub mod carver;
|
pub mod carver;
|
||||||
|
|
||||||
|
#[cfg(feature = "villager")]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[path = "generated/villager.rs"]
|
||||||
|
pub mod villager;
|
||||||
|
|||||||
@ -103,6 +103,10 @@ impl ScreenHandler for BrewingScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -568,6 +568,10 @@ impl ScreenHandler for CraftingTableScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -136,6 +136,10 @@ impl ScreenHandler for FurnaceLikeScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -167,6 +167,10 @@ impl ScreenHandler for GenericContainerScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,10 @@ impl ScreenHandler for GUIScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,7 @@ mod error;
|
|||||||
pub mod furnace_like;
|
pub mod furnace_like;
|
||||||
pub mod generic_container_screen_handler;
|
pub mod generic_container_screen_handler;
|
||||||
pub mod gui_builder;
|
pub mod gui_builder;
|
||||||
|
pub mod merchant;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod screen_handler;
|
pub mod screen_handler;
|
||||||
pub mod slot;
|
pub mod slot;
|
||||||
|
|||||||
222
pumpkin-inventory/src/merchant/merchant_screen_handler.rs
Normal file
222
pumpkin-inventory/src/merchant/merchant_screen_handler.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pumpkin_data::{item_stack::ItemStack, screen::WindowType};
|
||||||
|
use pumpkin_world::inventory::Inventory;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
player::player_inventory::PlayerInventory,
|
||||||
|
screen_handler::{
|
||||||
|
InventoryPlayer, ItemStackFuture, ScreenHandler, ScreenHandlerBehaviour,
|
||||||
|
ScreenHandlerFuture,
|
||||||
|
},
|
||||||
|
slot::NormalSlot,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct MerchantScreenHandler {
|
||||||
|
pub inventory: Arc<dyn Inventory>,
|
||||||
|
behaviour: ScreenHandlerBehaviour,
|
||||||
|
selected_offer: usize,
|
||||||
|
pub offers: Vec<pumpkin_protocol::java::client::play::MerchantOffer>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MerchantScreenHandler {
|
||||||
|
pub async fn new(
|
||||||
|
sync_id: u8,
|
||||||
|
player_inventory: &Arc<PlayerInventory>,
|
||||||
|
inventory: Arc<dyn Inventory>,
|
||||||
|
offers: Vec<pumpkin_protocol::java::client::play::MerchantOffer>,
|
||||||
|
) -> Self {
|
||||||
|
let mut handler = Self {
|
||||||
|
inventory: inventory.clone(),
|
||||||
|
behaviour: ScreenHandlerBehaviour::new(sync_id, Some(WindowType::Merchant)),
|
||||||
|
selected_offer: 0,
|
||||||
|
offers,
|
||||||
|
};
|
||||||
|
|
||||||
|
inventory.on_open().await;
|
||||||
|
|
||||||
|
// Merchant specific slots: 2 input, 1 output
|
||||||
|
for i in 0..3 {
|
||||||
|
handler.add_slot(Arc::new(NormalSlot::new(inventory.clone(), i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let player_inventory: Arc<dyn Inventory> = player_inventory.clone();
|
||||||
|
handler.add_player_slots(&player_inventory);
|
||||||
|
|
||||||
|
handler
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_selected_offer(&mut self, index: usize) {
|
||||||
|
self.selected_offer = index;
|
||||||
|
self.update_result_slot().await;
|
||||||
|
self.send_content_updates().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_result_slot(&mut self) {
|
||||||
|
if self.selected_offer >= self.offers.len() {
|
||||||
|
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offer = &self.offers[self.selected_offer];
|
||||||
|
let input_a = self.inventory.get_stack(0).await;
|
||||||
|
let input_a = input_a.lock().await;
|
||||||
|
let input_b = self.inventory.get_stack(1).await;
|
||||||
|
let input_b = input_b.lock().await;
|
||||||
|
|
||||||
|
let match_a = input_a.are_items_and_components_equal(&offer.base_cost_a.0)
|
||||||
|
&& input_a.item_count >= offer.base_cost_a.0.item_count;
|
||||||
|
|
||||||
|
let match_b = offer.cost_b.as_ref().map_or_else(
|
||||||
|
|| input_b.is_empty(),
|
||||||
|
|cost_b| {
|
||||||
|
input_b.are_items_and_components_equal(&cost_b.0)
|
||||||
|
&& input_b.item_count >= cost_b.0.item_count
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if match_a && match_b {
|
||||||
|
self.inventory.set_stack(2, (*offer.output.0).clone()).await;
|
||||||
|
} else {
|
||||||
|
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenHandler for MerchantScreenHandler {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
|
&self.behaviour
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_behaviour_mut(&mut self) -> &mut ScreenHandlerBehaviour {
|
||||||
|
&mut self.behaviour
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_closed<'a>(&'a mut self, player: &'a dyn InventoryPlayer) -> ScreenHandlerFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.default_on_closed(player).await;
|
||||||
|
self.inventory.on_close().await;
|
||||||
|
// Vanilla drops items from merchant container on close
|
||||||
|
for i in 0..2 {
|
||||||
|
// Drop inputs only, output is virtual/ghost in some sense or just cleared
|
||||||
|
let stack = self.inventory.remove_stack(i).await;
|
||||||
|
if !stack.is_empty() {
|
||||||
|
player.drop_item(stack, false).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Clear output slot
|
||||||
|
self.inventory.set_stack(2, ItemStack::EMPTY.clone()).await;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quick_move<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
_player: &'a dyn InventoryPlayer,
|
||||||
|
slot_index: i32,
|
||||||
|
) -> ItemStackFuture<'a> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut stack_left = ItemStack::EMPTY.clone();
|
||||||
|
let slot = self.get_behaviour().slots[slot_index as usize].clone();
|
||||||
|
|
||||||
|
if slot.has_stack().await {
|
||||||
|
let slot_stack_lock = slot.get_stack().await;
|
||||||
|
let slot_stack_guard = slot_stack_lock.lock().await;
|
||||||
|
stack_left = slot_stack_guard.clone();
|
||||||
|
drop(slot_stack_guard);
|
||||||
|
|
||||||
|
let mut slot_stack_mut = slot_stack_lock.lock().await;
|
||||||
|
|
||||||
|
if slot_index < 3 {
|
||||||
|
// From merchant slots to player inventory
|
||||||
|
if !self
|
||||||
|
.insert_item(
|
||||||
|
&mut slot_stack_mut,
|
||||||
|
3,
|
||||||
|
self.get_behaviour().slots.len() as i32,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// From player inventory to merchant inputs (0 and 1)
|
||||||
|
if !self.insert_item(&mut slot_stack_mut, 0, 2, false).await {
|
||||||
|
return ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if slot_stack_mut.is_empty() {
|
||||||
|
drop(slot_stack_mut);
|
||||||
|
slot.set_stack(ItemStack::EMPTY.clone()).await;
|
||||||
|
} else {
|
||||||
|
drop(slot_stack_mut);
|
||||||
|
slot.mark_dirty().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack_left
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_slot_click<'a>(
|
||||||
|
&'a mut self,
|
||||||
|
slot_index: i32,
|
||||||
|
button: i32,
|
||||||
|
action_type: pumpkin_protocol::java::server::play::SlotActionType,
|
||||||
|
player: &'a dyn InventoryPlayer,
|
||||||
|
) -> ScreenHandlerFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if slot_index == 2 {
|
||||||
|
// Special handling for taking from output slot
|
||||||
|
let result_slot = self.get_behaviour().slots[2].clone();
|
||||||
|
if result_slot.has_stack().await {
|
||||||
|
let result_stack = result_slot.get_cloned_stack().await;
|
||||||
|
if !result_stack.is_empty() {
|
||||||
|
// Consume inputs
|
||||||
|
let offer = &self.offers[self.selected_offer];
|
||||||
|
|
||||||
|
let input_a = self.inventory.get_stack(0).await;
|
||||||
|
let mut input_a = input_a.lock().await;
|
||||||
|
input_a.decrement(offer.base_cost_a.0.item_count);
|
||||||
|
if input_a.is_empty() {
|
||||||
|
*input_a = ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
drop(input_a);
|
||||||
|
self.get_behaviour().slots[0].mark_dirty().await;
|
||||||
|
|
||||||
|
if let Some(cost_b) = &offer.cost_b {
|
||||||
|
let input_b = self.inventory.get_stack(1).await;
|
||||||
|
let mut input_b = input_b.lock().await;
|
||||||
|
input_b.decrement(cost_b.0.item_count);
|
||||||
|
if input_b.is_empty() {
|
||||||
|
*input_b = ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
drop(input_b);
|
||||||
|
self.get_behaviour().slots[1].mark_dirty().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Award XP
|
||||||
|
player.award_experience(offer.xp).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.internal_on_slot_click(slot_index, button, action_type, player)
|
||||||
|
.await;
|
||||||
|
if slot_index == 0 || slot_index == 1 || slot_index == 2 {
|
||||||
|
self.update_result_slot().await;
|
||||||
|
self.send_content_updates().await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1
pumpkin-inventory/src/merchant/mod.rs
Normal file
1
pumpkin-inventory/src/merchant/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod merchant_screen_handler;
|
||||||
@ -121,6 +121,10 @@ impl ScreenHandler for PlayerScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
@ -223,6 +223,9 @@ pub trait ScreenHandler: Send + Sync {
|
|||||||
/// Returns this screen handler as an Any reference.
|
/// Returns this screen handler as an Any reference.
|
||||||
fn as_any(&self) -> &dyn Any;
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
|
||||||
|
/// Returns this screen handler as a mutable Any reference.
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
|
||||||
/// Gets the sync ID for this screen handler.
|
/// Gets the sync ID for this screen handler.
|
||||||
fn sync_id(&self) -> u8 {
|
fn sync_id(&self) -> u8 {
|
||||||
self.get_behaviour().sync_id
|
self.get_behaviour().sync_id
|
||||||
|
|||||||
@ -13,6 +13,7 @@ use serde::{
|
|||||||
};
|
};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct ItemStackSerializer<'a>(pub Cow<'a, ItemStack>);
|
pub struct ItemStackSerializer<'a>(pub Cow<'a, ItemStack>);
|
||||||
|
|
||||||
fn item_component_counts(stack: &ItemStack) -> (u8, u8) {
|
fn item_component_counts(stack: &ItemStack) -> (u8, u8) {
|
||||||
|
|||||||
52
pumpkin-protocol/src/java/client/play/merchant_offers.rs
Normal file
52
pumpkin-protocol/src/java/client/play/merchant_offers.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use pumpkin_data::packet::clientbound::PLAY_MERCHANT_OFFERS;
|
||||||
|
use pumpkin_macros::java_packet;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::VarInt;
|
||||||
|
use crate::codec::item_stack_seralizer::ItemStackSerializer;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
|
pub struct MerchantOffer {
|
||||||
|
pub base_cost_a: ItemStackSerializer<'static>,
|
||||||
|
pub output: ItemStackSerializer<'static>,
|
||||||
|
pub cost_b: Option<ItemStackSerializer<'static>>,
|
||||||
|
pub is_disabled: bool,
|
||||||
|
pub uses: i32,
|
||||||
|
pub max_uses: i32,
|
||||||
|
pub xp: i32,
|
||||||
|
pub special_price: i32,
|
||||||
|
pub price_multiplier: f32,
|
||||||
|
pub demand: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[java_packet(PLAY_MERCHANT_OFFERS)]
|
||||||
|
pub struct CMerchantOffers {
|
||||||
|
pub window_id: VarInt,
|
||||||
|
pub offers: Vec<MerchantOffer>,
|
||||||
|
pub villager_level: VarInt,
|
||||||
|
pub experience: VarInt,
|
||||||
|
pub is_regular_villager: bool,
|
||||||
|
pub can_restock: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CMerchantOffers {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(
|
||||||
|
window_id: VarInt,
|
||||||
|
offers: Vec<MerchantOffer>,
|
||||||
|
villager_level: VarInt,
|
||||||
|
experience: VarInt,
|
||||||
|
is_regular_villager: bool,
|
||||||
|
can_restock: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
window_id,
|
||||||
|
offers,
|
||||||
|
villager_level,
|
||||||
|
experience,
|
||||||
|
is_regular_villager,
|
||||||
|
can_restock,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ mod keep_alive;
|
|||||||
mod level_event;
|
mod level_event;
|
||||||
mod light_update;
|
mod light_update;
|
||||||
mod login;
|
mod login;
|
||||||
|
mod merchant_offers;
|
||||||
mod multi_block_update;
|
mod multi_block_update;
|
||||||
mod open_screen;
|
mod open_screen;
|
||||||
mod open_sign_editor;
|
mod open_sign_editor;
|
||||||
@ -136,6 +137,7 @@ pub use keep_alive::*;
|
|||||||
pub use level_event::*;
|
pub use level_event::*;
|
||||||
pub use light_update::*;
|
pub use light_update::*;
|
||||||
pub use login::*;
|
pub use login::*;
|
||||||
|
pub use merchant_offers::*;
|
||||||
pub use multi_block_update::*;
|
pub use multi_block_update::*;
|
||||||
pub use open_screen::*;
|
pub use open_screen::*;
|
||||||
pub use open_sign_editor::*;
|
pub use open_sign_editor::*;
|
||||||
|
|||||||
@ -31,6 +31,7 @@ mod player_rotation;
|
|||||||
mod player_session;
|
mod player_session;
|
||||||
mod recipe_book_change_settings;
|
mod recipe_book_change_settings;
|
||||||
mod recipe_book_seen_recipe;
|
mod recipe_book_seen_recipe;
|
||||||
|
mod select_trade;
|
||||||
mod set_command_block;
|
mod set_command_block;
|
||||||
mod set_creative_slot;
|
mod set_creative_slot;
|
||||||
mod set_held_item;
|
mod set_held_item;
|
||||||
@ -72,6 +73,7 @@ pub use player_rotation::*;
|
|||||||
pub use player_session::*;
|
pub use player_session::*;
|
||||||
pub use recipe_book_change_settings::*;
|
pub use recipe_book_change_settings::*;
|
||||||
pub use recipe_book_seen_recipe::*;
|
pub use recipe_book_seen_recipe::*;
|
||||||
|
pub use select_trade::*;
|
||||||
pub use set_command_block::*;
|
pub use set_command_block::*;
|
||||||
pub use set_creative_slot::*;
|
pub use set_creative_slot::*;
|
||||||
pub use set_held_item::*;
|
pub use set_held_item::*;
|
||||||
|
|||||||
11
pumpkin-protocol/src/java/server/play/select_trade.rs
Normal file
11
pumpkin-protocol/src/java/server/play/select_trade.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use pumpkin_data::packet::serverbound::PLAY_SELECT_TRADE;
|
||||||
|
use pumpkin_macros::java_packet;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::VarInt;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[java_packet(PLAY_SELECT_TRADE)]
|
||||||
|
pub struct SSelectTrade {
|
||||||
|
pub selected_slot: VarInt,
|
||||||
|
}
|
||||||
@ -6,8 +6,10 @@ use pumpkin_data::item_stack::ItemStack;
|
|||||||
|
|
||||||
#[expect(clippy::module_inception)]
|
#[expect(clippy::module_inception)]
|
||||||
mod inventory;
|
mod inventory;
|
||||||
|
mod simple_inventory;
|
||||||
|
|
||||||
pub use inventory::*;
|
pub use inventory::*;
|
||||||
|
pub use simple_inventory::*;
|
||||||
|
|
||||||
// These are some utility functions found in Inventories.java
|
// These are some utility functions found in Inventories.java
|
||||||
pub async fn split_stack(stacks: &[Arc<Mutex<ItemStack>>], slot: usize, amount: u8) -> ItemStack {
|
pub async fn split_stack(stacks: &[Arc<Mutex<ItemStack>>], slot: usize, amount: u8) -> ItemStack {
|
||||||
|
|||||||
73
pumpkin-world/src/inventory/simple_inventory.rs
Normal file
73
pumpkin-world/src/inventory/simple_inventory.rs
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
use crate::inventory::{Clearable, Inventory, InventoryFuture, split_stack};
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
|
use std::any::Any;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
pub struct SimpleInventory {
|
||||||
|
pub stacks: Vec<Arc<Mutex<ItemStack>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimpleInventory {
|
||||||
|
pub fn new(size: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
stacks: (0..size)
|
||||||
|
.map(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone())))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clearable for SimpleInventory {
|
||||||
|
fn clear(&self) -> InventoryFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
for stack in &self.stacks {
|
||||||
|
*stack.lock().await = ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Inventory for SimpleInventory {
|
||||||
|
fn size(&self) -> usize {
|
||||||
|
self.stacks.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> InventoryFuture<'_, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
for stack in &self.stacks {
|
||||||
|
if !stack.lock().await.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_stack(&self, slot: usize) -> InventoryFuture<'_, Arc<Mutex<ItemStack>>> {
|
||||||
|
Box::pin(async move { self.stacks[slot].clone() })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_stack(&self, slot: usize) -> InventoryFuture<'_, ItemStack> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let mut stack = self.stacks[slot].lock().await;
|
||||||
|
let result = stack.clone();
|
||||||
|
*stack = ItemStack::EMPTY.clone();
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_stack_specific(&self, slot: usize, amount: u8) -> InventoryFuture<'_, ItemStack> {
|
||||||
|
Box::pin(async move { split_stack(&self.stacks, slot, amount).await })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_stack(&self, slot: usize, stack: ItemStack) -> InventoryFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
*self.stacks[slot].lock().await = stack;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,55 +0,0 @@
|
|||||||
use std::sync::{Arc, Weak};
|
|
||||||
|
|
||||||
use pumpkin_data::entity::EntityType;
|
|
||||||
|
|
||||||
use crate::entity::{
|
|
||||||
Entity, NBTStorage,
|
|
||||||
ai::goal::{
|
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
|
||||||
wander_around::WanderAroundGoal,
|
|
||||||
},
|
|
||||||
mob::{Mob, MobEntity},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct VillagerEntity {
|
|
||||||
pub mob_entity: MobEntity,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VillagerEntity {
|
|
||||||
pub async fn new(entity: Entity) -> Arc<Self> {
|
|
||||||
let mob_entity = MobEntity::new(entity);
|
|
||||||
let villager = Self { mob_entity };
|
|
||||||
let mob_arc = Arc::new(villager);
|
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
|
||||||
Arc::downgrade(&mob_arc)
|
|
||||||
};
|
|
||||||
|
|
||||||
{
|
|
||||||
let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await;
|
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
|
||||||
// Villagers have complex AI, but for now we give them basic movement
|
|
||||||
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(0.6)));
|
|
||||||
goal_selector.add_goal(
|
|
||||||
2,
|
|
||||||
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
|
|
||||||
);
|
|
||||||
goal_selector.add_goal(
|
|
||||||
3,
|
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::VILLAGER, 8.0),
|
|
||||||
);
|
|
||||||
goal_selector.add_goal(4, Box::new(RandomLookAroundGoal::default()));
|
|
||||||
};
|
|
||||||
|
|
||||||
mob_arc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NBTStorage for VillagerEntity {}
|
|
||||||
|
|
||||||
impl Mob for VillagerEntity {
|
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
|
||||||
&self.mob_entity
|
|
||||||
}
|
|
||||||
}
|
|
||||||
48
pumpkin/src/entity/passive/villager/data.rs
Normal file
48
pumpkin/src/entity/passive/villager/data.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
use pumpkin_data::item::Item;
|
||||||
|
pub use pumpkin_data::villager::{VillagerProfession, VillagerType};
|
||||||
|
use pumpkin_protocol::codec::var_int::VarInt;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
pub const BREEDING_FOOD_THRESHOLD: i32 = 12;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get_food_points(item: &Item) -> i32 {
|
||||||
|
match item.id {
|
||||||
|
id if id == Item::BREAD.id => 4,
|
||||||
|
id if id == Item::POTATO.id => 1,
|
||||||
|
id if id == Item::CARROT.id => 1,
|
||||||
|
id if id == Item::BEETROOT.id => 1,
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum GossipType {
|
||||||
|
MajorNegative = 0,
|
||||||
|
MinorNegative = 1,
|
||||||
|
MajorPositive = 2,
|
||||||
|
MinorPositive = 3,
|
||||||
|
Trading = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub struct VillagerData {
|
||||||
|
pub r#type: VillagerType,
|
||||||
|
pub profession: VillagerProfession,
|
||||||
|
pub level: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for VillagerData {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: serde::Serializer,
|
||||||
|
{
|
||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
let mut state = serializer.serialize_struct("VillagerData", 3)?;
|
||||||
|
state.serialize_field("type", &VarInt(self.r#type as i32))?;
|
||||||
|
state.serialize_field("profession", &VarInt(self.profession as i32))?;
|
||||||
|
state.serialize_field("level", &VarInt(self.level))?;
|
||||||
|
state.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
459
pumpkin/src/entity/passive/villager/mod.rs
Normal file
459
pumpkin/src/entity/passive/villager/mod.rs
Normal file
@ -0,0 +1,459 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicI32, AtomicI64, Ordering};
|
||||||
|
use std::sync::{Arc, Weak};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
|
use pumpkin_data::meta_data_type::MetaDataType;
|
||||||
|
use pumpkin_data::tracked_data::TrackedData;
|
||||||
|
use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
|
||||||
|
use pumpkin_inventory::screen_handler::{
|
||||||
|
BoxFuture, InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler,
|
||||||
|
};
|
||||||
|
use pumpkin_protocol::codec::var_int::VarInt;
|
||||||
|
use pumpkin_protocol::java::client::play::{CMerchantOffers, Metadata};
|
||||||
|
use pumpkin_util::text::TextComponent;
|
||||||
|
use pumpkin_world::inventory::SimpleInventory;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::entity::player::Player;
|
||||||
|
use crate::entity::{
|
||||||
|
Entity, EntityBase, NBTStorage,
|
||||||
|
ai::goal::{
|
||||||
|
avoid_entity::AvoidEntityGoal, look_around::RandomLookAroundGoal,
|
||||||
|
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
|
||||||
|
},
|
||||||
|
mob::{Mob, MobEntity},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod data;
|
||||||
|
pub use data::{
|
||||||
|
BREEDING_FOOD_THRESHOLD, GossipType, VillagerData, VillagerProfession, VillagerType,
|
||||||
|
get_food_points,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct VillagerEntity {
|
||||||
|
pub mob_entity: MobEntity,
|
||||||
|
pub villager_data: Mutex<VillagerData>,
|
||||||
|
pub food_level: AtomicI32,
|
||||||
|
pub xp: AtomicI32,
|
||||||
|
pub last_restock_time: AtomicI64,
|
||||||
|
pub restocks_today: AtomicI32,
|
||||||
|
pub gossips: Mutex<HashMap<Uuid, HashMap<GossipType, i32>>>,
|
||||||
|
pub inventory: Arc<Mutex<Vec<Arc<Mutex<ItemStack>>>>>,
|
||||||
|
pub merchant_inventory: Arc<SimpleInventory>,
|
||||||
|
pub offers: Mutex<Vec<pumpkin_protocol::java::client::play::MerchantOffer>>,
|
||||||
|
pub self_arc: Mutex<Option<Weak<Self>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VillagerEntity {
|
||||||
|
#[expect(clippy::too_many_lines)]
|
||||||
|
pub async fn new(entity: Entity) -> Arc<Self> {
|
||||||
|
let mob_entity = MobEntity::new(entity);
|
||||||
|
let villager_data = VillagerData {
|
||||||
|
r#type: VillagerType::Plains,
|
||||||
|
profession: VillagerProfession::None,
|
||||||
|
level: 1,
|
||||||
|
};
|
||||||
|
let inventory = Arc::new(Mutex::new(
|
||||||
|
(0..8)
|
||||||
|
.map(|_| Arc::new(Mutex::new(ItemStack::EMPTY.clone())))
|
||||||
|
.collect(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let villager = Self {
|
||||||
|
mob_entity,
|
||||||
|
villager_data: Mutex::new(villager_data),
|
||||||
|
food_level: AtomicI32::new(0),
|
||||||
|
xp: AtomicI32::new(0),
|
||||||
|
last_restock_time: AtomicI64::new(0),
|
||||||
|
restocks_today: AtomicI32::new(0),
|
||||||
|
gossips: Mutex::new(HashMap::new()),
|
||||||
|
inventory,
|
||||||
|
merchant_inventory: Arc::new(SimpleInventory::new(3)),
|
||||||
|
offers: Mutex::new(Vec::new()),
|
||||||
|
self_arc: Mutex::new(None),
|
||||||
|
};
|
||||||
|
let mob_arc = Arc::new(villager);
|
||||||
|
*mob_arc.self_arc.lock().await = Some(Arc::downgrade(&mob_arc));
|
||||||
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
|
Arc::downgrade(&mob_arc)
|
||||||
|
};
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut goal_selector = mob_arc.mob_entity.goals_selector.lock().await;
|
||||||
|
|
||||||
|
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
||||||
|
// Villagers avoid threats
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::ZOMBIE, 8.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(
|
||||||
|
&EntityType::ZOMBIE_VILLAGER,
|
||||||
|
8.0,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::HUSK, 8.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::DROWNED, 8.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::PILLAGER, 12.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(
|
||||||
|
&EntityType::VINDICATOR,
|
||||||
|
12.0,
|
||||||
|
0.5,
|
||||||
|
0.5,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::EVOKER, 12.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::RAVAGER, 12.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(AvoidEntityGoal::new(&EntityType::VEX, 12.0, 0.5, 0.5)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Basic movement and looking (Vanilla uses 0.5 speed)
|
||||||
|
goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(0.5)));
|
||||||
|
goal_selector.add_goal(
|
||||||
|
3,
|
||||||
|
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(
|
||||||
|
4,
|
||||||
|
LookAtEntityGoal::with_default(mob_weak, &EntityType::VILLAGER, 8.0),
|
||||||
|
);
|
||||||
|
goal_selector.add_goal(5, Box::new(RandomLookAroundGoal::default()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send initial metadata
|
||||||
|
mob_arc
|
||||||
|
.get_entity()
|
||||||
|
.send_meta_data(&[Metadata::new(
|
||||||
|
TrackedData::VILLAGER_DATA,
|
||||||
|
MetaDataType::VILLAGER_DATA,
|
||||||
|
villager_data,
|
||||||
|
)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
mob_arc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn count_food_points_in_inventory(&self) -> i32 {
|
||||||
|
let inventory = self.inventory.lock().await;
|
||||||
|
let mut total = 0;
|
||||||
|
for stack_mutex in inventory.iter() {
|
||||||
|
let stack = stack_mutex.lock().await;
|
||||||
|
if !stack.is_empty() {
|
||||||
|
total += get_food_points(stack.get_item()) * stack.item_count as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
total
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn eat_until_full(&self) {
|
||||||
|
if self.food_level.load(Ordering::Relaxed) >= BREEDING_FOOD_THRESHOLD {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let inventory = self.inventory.lock().await;
|
||||||
|
for stack_mutex in inventory.iter() {
|
||||||
|
let mut stack = stack_mutex.lock().await;
|
||||||
|
if !stack.is_empty() {
|
||||||
|
let points = get_food_points(stack.get_item());
|
||||||
|
if points > 0 {
|
||||||
|
while stack.item_count > 0
|
||||||
|
&& self.food_level.load(Ordering::Relaxed) < BREEDING_FOOD_THRESHOLD
|
||||||
|
{
|
||||||
|
self.food_level.fetch_add(points, Ordering::Relaxed);
|
||||||
|
stack.item_count -= 1;
|
||||||
|
}
|
||||||
|
if stack.item_count == 0 {
|
||||||
|
*stack = ItemStack::EMPTY.clone();
|
||||||
|
}
|
||||||
|
if self.food_level.load(Ordering::Relaxed) >= BREEDING_FOOD_THRESHOLD {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_villager_data(&self, data: VillagerData) {
|
||||||
|
let mut villager_data = self.villager_data.lock().await;
|
||||||
|
let old_profession = villager_data.profession;
|
||||||
|
*villager_data = data;
|
||||||
|
self.get_entity()
|
||||||
|
.send_meta_data(&[Metadata::new(
|
||||||
|
TrackedData::VILLAGER_DATA,
|
||||||
|
MetaDataType::VILLAGER_DATA,
|
||||||
|
data,
|
||||||
|
)])
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if old_profession != data.profession {
|
||||||
|
self.generate_trades(data.profession, data.level).await;
|
||||||
|
if let Some(sound) = data.profession.work_sound() {
|
||||||
|
self.get_entity().play_sound(sound).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn generate_trades(&self, profession: VillagerProfession, level: i32) {
|
||||||
|
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
|
||||||
|
use rand::seq::IndexedRandom;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
let mut offers = self.offers.lock().await;
|
||||||
|
offers.clear();
|
||||||
|
|
||||||
|
if let Some(trade_set) = profession.trade_set(level) {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let chosen_trades = trade_set.trades.sample(&mut rng, trade_set.amount as usize);
|
||||||
|
|
||||||
|
for trade in chosen_trades {
|
||||||
|
offers.push(pumpkin_protocol::java::client::play::MerchantOffer {
|
||||||
|
base_cost_a: ItemStackSerializer(Cow::Owned(ItemStack::new(
|
||||||
|
trade.wants.count as u8,
|
||||||
|
trade.wants.item,
|
||||||
|
))),
|
||||||
|
output: ItemStackSerializer(Cow::Owned(ItemStack::new(
|
||||||
|
trade.gives.count as u8,
|
||||||
|
trade.gives.item,
|
||||||
|
))),
|
||||||
|
cost_b: trade.wants_b.as_ref().map(|b| {
|
||||||
|
ItemStackSerializer(Cow::Owned(ItemStack::new(b.count as u8, b.item)))
|
||||||
|
}),
|
||||||
|
is_disabled: false,
|
||||||
|
uses: 0,
|
||||||
|
max_uses: trade.max_uses,
|
||||||
|
xp: trade.xp,
|
||||||
|
special_price: 0,
|
||||||
|
price_multiplier: trade.price_multiplier,
|
||||||
|
demand: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_unhappy(&self) {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity
|
||||||
|
.world
|
||||||
|
.load()
|
||||||
|
.send_entity_status(entity, pumpkin_data::entity::EntityStatus::VillagerAngry)
|
||||||
|
.await;
|
||||||
|
entity
|
||||||
|
.play_sound(pumpkin_data::sound::Sound::EntityVillagerNo)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_trading_screen(&self, player: &Player) {
|
||||||
|
let self_weak = self.self_arc.lock().await;
|
||||||
|
if let Some(self_arc) = self_weak.as_ref().and_then(std::sync::Weak::upgrade) {
|
||||||
|
player.open_handled_screen(&*self_arc, None).await;
|
||||||
|
|
||||||
|
let offers = self.offers.lock().await;
|
||||||
|
let villager_data = self.villager_data.lock().await;
|
||||||
|
|
||||||
|
player
|
||||||
|
.client
|
||||||
|
.enqueue_packet(&CMerchantOffers::new(
|
||||||
|
player.screen_handler_sync_id.load(Ordering::Relaxed).into(),
|
||||||
|
offers.clone(),
|
||||||
|
VarInt(villager_data.level),
|
||||||
|
VarInt(self.xp.load(Ordering::Relaxed)),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenHandlerFactory for VillagerEntity {
|
||||||
|
fn create_screen_handler<'a>(
|
||||||
|
&'a self,
|
||||||
|
sync_id: u8,
|
||||||
|
player_inventory: &'a Arc<pumpkin_inventory::player::player_inventory::PlayerInventory>,
|
||||||
|
_player: &'a dyn InventoryPlayer,
|
||||||
|
) -> BoxFuture<'a, Option<SharedScreenHandler>> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let offers = self.offers.lock().await;
|
||||||
|
let handler = MerchantScreenHandler::new(
|
||||||
|
sync_id,
|
||||||
|
player_inventory,
|
||||||
|
self.merchant_inventory.clone(),
|
||||||
|
offers.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
Some(Arc::new(Mutex::new(handler)) as SharedScreenHandler)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_display_name(&self) -> TextComponent {
|
||||||
|
// TODO: Localized name based on profession
|
||||||
|
TextComponent::text("Villager")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NBTStorage for VillagerEntity {
|
||||||
|
fn write_nbt<'a>(
|
||||||
|
&'a self,
|
||||||
|
nbt: &'a mut pumpkin_nbt::pnbt::PNbtCompound,
|
||||||
|
) -> crate::entity::NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
let data = self.villager_data.lock().await;
|
||||||
|
nbt.put_int(data.r#type as i32);
|
||||||
|
nbt.put_int(data.profession as i32);
|
||||||
|
nbt.put_int(data.level);
|
||||||
|
|
||||||
|
nbt.put_int(self.food_level.load(Ordering::Relaxed));
|
||||||
|
nbt.put_int(self.xp.load(Ordering::Relaxed));
|
||||||
|
nbt.put_long(self.last_restock_time.load(Ordering::Relaxed));
|
||||||
|
nbt.put_int(self.restocks_today.load(Ordering::Relaxed));
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
let inventory = self.inventory.lock().await;
|
||||||
|
nbt.put_int(inventory.len() as i32);
|
||||||
|
for stack_mutex in inventory.iter() {
|
||||||
|
let stack = stack_mutex.lock().await;
|
||||||
|
stack.write_item_stack_pnbt(nbt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gossips (Simplified: just save counts per UUID and type)
|
||||||
|
let gossips = self.gossips.lock().await;
|
||||||
|
nbt.put_int(gossips.len() as i32);
|
||||||
|
for (uuid, types) in gossips.iter() {
|
||||||
|
nbt.put_uuid(uuid);
|
||||||
|
nbt.put_int(types.len() as i32);
|
||||||
|
for (gtype, value) in types {
|
||||||
|
nbt.put_int(*gtype as i32);
|
||||||
|
nbt.put_int(*value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(
|
||||||
|
&'a self,
|
||||||
|
nbt: &'a mut pumpkin_nbt::pnbt::PNbtCompound,
|
||||||
|
) -> crate::entity::NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
|
let mut data = self.villager_data.lock().await;
|
||||||
|
if let Ok(t) = nbt.get_int() {
|
||||||
|
data.r#type = VillagerType::try_from(t).unwrap_or(VillagerType::Plains);
|
||||||
|
}
|
||||||
|
if let Ok(p) = nbt.get_int() {
|
||||||
|
data.profession =
|
||||||
|
VillagerProfession::try_from(p).unwrap_or(VillagerProfession::None);
|
||||||
|
}
|
||||||
|
if let Ok(l) = nbt.get_int() {
|
||||||
|
data.level = l;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(food) = nbt.get_int() {
|
||||||
|
self.food_level.store(food, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if let Ok(xp) = nbt.get_int() {
|
||||||
|
self.xp.store(xp, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if let Ok(restock) = nbt.get_long() {
|
||||||
|
self.last_restock_time.store(restock, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if let Ok(today) = nbt.get_int() {
|
||||||
|
self.restocks_today.store(today, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inventory
|
||||||
|
if let Ok(inv_len) = nbt.get_int() {
|
||||||
|
let mut inventory = self.inventory.lock().await;
|
||||||
|
inventory.clear();
|
||||||
|
for _ in 0..inv_len {
|
||||||
|
let stack =
|
||||||
|
ItemStack::read_item_stack_pnbt(nbt).unwrap_or(ItemStack::EMPTY.clone());
|
||||||
|
inventory.push(Arc::new(Mutex::new(stack)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gossips
|
||||||
|
if let Ok(gossip_len) = nbt.get_int() {
|
||||||
|
let mut gossips = self.gossips.lock().await;
|
||||||
|
gossips.clear();
|
||||||
|
for _ in 0..gossip_len {
|
||||||
|
if let Ok(uuid) = nbt.get_uuid()
|
||||||
|
&& let Ok(types_len) = nbt.get_int()
|
||||||
|
{
|
||||||
|
let mut types = HashMap::new();
|
||||||
|
for _ in 0..types_len {
|
||||||
|
if let (Ok(gtype), Ok(val)) = (nbt.get_int(), nbt.get_int()) {
|
||||||
|
let gossip_type = match gtype {
|
||||||
|
0 => GossipType::MajorNegative,
|
||||||
|
1 => GossipType::MinorNegative,
|
||||||
|
2 => GossipType::MajorPositive,
|
||||||
|
3 => GossipType::MinorPositive,
|
||||||
|
4 => GossipType::Trading,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
types.insert(gossip_type, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gossips.insert(uuid, types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mob for VillagerEntity {
|
||||||
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
|
&self.mob_entity
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_interact<'a>(
|
||||||
|
&'a self,
|
||||||
|
player: &'a crate::entity::player::Player,
|
||||||
|
_item_stack: &'a mut pumpkin_data::item_stack::ItemStack,
|
||||||
|
) -> crate::entity::EntityBaseFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if self.get_entity().age.load(Ordering::Relaxed) < 0 {
|
||||||
|
self.set_unhappy().await;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offers = self.offers.lock().await;
|
||||||
|
if offers.is_empty() {
|
||||||
|
self.set_unhappy().await;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
drop(offers);
|
||||||
|
|
||||||
|
self.open_trading_screen(player).await;
|
||||||
|
|
||||||
|
true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,8 +12,8 @@ use pumpkin_protocol::java::server::play::{
|
|||||||
SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities,
|
SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest, SPlayerAbilities,
|
||||||
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
|
SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerLoaded, SPlayerPosition,
|
||||||
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
||||||
SRecipeBookSeenRecipe, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
|
SRecipeBookSeenRecipe, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem,
|
||||||
SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
|
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn,
|
||||||
};
|
};
|
||||||
use pumpkin_protocol::packet::MultiVersionJavaPacket;
|
use pumpkin_protocol::packet::MultiVersionJavaPacket;
|
||||||
use pumpkin_protocol::{
|
use pumpkin_protocol::{
|
||||||
@ -891,6 +891,10 @@ impl JavaClient {
|
|||||||
self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?)
|
self.handle_place_recipe(player, SPlaceRecipe::read(payload, &version)?)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
id if id == SSelectTrade::to_id(version) => {
|
||||||
|
self.handle_select_trade(player, SSelectTrade::read(payload, &version)?)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
warn!("Failed to handle player packet id {}", packet.id);
|
warn!("Failed to handle player packet id {}", packet.id);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,6 +46,7 @@ use pumpkin_data::item_stack::ItemStack;
|
|||||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
use pumpkin_data::{Block, BlockDirection, BlockState, translation};
|
use pumpkin_data::{Block, BlockDirection, BlockState, translation};
|
||||||
use pumpkin_inventory::InventoryError;
|
use pumpkin_inventory::InventoryError;
|
||||||
|
use pumpkin_inventory::merchant::merchant_screen_handler::MerchantScreenHandler;
|
||||||
use pumpkin_inventory::player::player_inventory::PlayerInventory;
|
use pumpkin_inventory::player::player_inventory::PlayerInventory;
|
||||||
use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
|
use pumpkin_inventory::screen_handler::{InventoryPlayer, ScreenHandler};
|
||||||
use pumpkin_macros::send_cancellable;
|
use pumpkin_macros::send_cancellable;
|
||||||
@ -62,8 +63,8 @@ use pumpkin_protocol::java::server::play::{
|
|||||||
SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest,
|
SKeepAlive, SMoveVehicle, SPaddleBoat, SPickItemFromBlock, SPlaceRecipe, SPlayPingRequest,
|
||||||
SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition,
|
SPlayerAbilities, SPlayerAction, SPlayerCommand, SPlayerInput, SPlayerPosition,
|
||||||
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
SPlayerPositionRotation, SPlayerRotation, SPlayerSession, SRecipeBookChangeSettings,
|
||||||
SRecipeBookSeenRecipe, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem, SSetPlayerGround,
|
SRecipeBookSeenRecipe, SSelectTrade, SSetCommandBlock, SSetCreativeSlot, SSetHeldItem,
|
||||||
SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status,
|
SSetPlayerGround, SSwingArm, SUpdateSign, SUseItem, SUseItemOn, Status,
|
||||||
};
|
};
|
||||||
use pumpkin_util::math::boundingbox::BoundingBox;
|
use pumpkin_util::math::boundingbox::BoundingBox;
|
||||||
use pumpkin_util::math::vector3::Vector3;
|
use pumpkin_util::math::vector3::Vector3;
|
||||||
@ -2765,4 +2766,17 @@ impl JavaClient {
|
|||||||
self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text))
|
self.enqueue_packet(&COpenSignEditor::new(block_position, is_front_text))
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_select_trade(&self, player: &Arc<Player>, packet: SSelectTrade) {
|
||||||
|
let screen_handler = player.current_screen_handler.lock().await;
|
||||||
|
let mut screen_handler = screen_handler.lock().await;
|
||||||
|
if let Some(merchant) = screen_handler
|
||||||
|
.as_any_mut()
|
||||||
|
.downcast_mut::<MerchantScreenHandler>()
|
||||||
|
{
|
||||||
|
merchant
|
||||||
|
.set_selected_offer(packet.selected_slot.0 as usize)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -140,6 +140,10 @@ impl ScreenHandler for PluginScreenHandler {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
fn get_behaviour(&self) -> &ScreenHandlerBehaviour {
|
||||||
&self.behaviour
|
&self.behaviour
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user