Add Shaped and Unshaped crafting support, as well as deserializing the entire recipe format (#165)

* add cleaner registry parsing

* start adding recipes

* add tags, and some crafting things

* Fix echest command

* add partly working crafting interface

* add generated assets

* fix container commands and clippy lints

* fix issues with resolving recursive item tags for crafting

* add flatten3x3 function

* add shapeless crafting

* add tests to and fix recipe flattening function

* refactor recipe system a bit

* add rayon to speed up initial loading of recipes

* almost working after rebase

* add deserialization for new crafting type "transmute"

* make crafting work again

* make shapeless recipes not require all permutations

* fixed visual bug with when taking out ingredients for crafting

* add support for new tags system
This commit is contained in:
Edvin Bryntesson
2024-11-12 12:57:14 +01:00
committed by GitHub
parent 80bfee705c
commit 8cde02e625
20 changed files with 1458 additions and 141 deletions

View File

@@ -6,9 +6,11 @@ edition.workspace = true
[dependencies]
# For items
pumpkin-world = { path = "../pumpkin-world" }
pumpkin-registry = {path = "../pumpkin-registry"}
pumpkin-macros = { path = "../pumpkin-macros" }
log.workspace = true
rayon.workspace = true
itertools.workspace = true
crossbeam.workspace = true
tokio.workspace = true

View File

@@ -0,0 +1,100 @@
use itertools::Itertools;
use pumpkin_registry::{
flatten_3x3, get_tag_values, IngredientSlot, IngredientType, RecipeResult, TagCategory, RECIPES,
};
use pumpkin_world::item::item_registry::get_item;
use pumpkin_world::item::ItemStack;
use rayon::prelude::*;
#[inline(always)]
fn check_ingredient_type(ingredient_type: &IngredientType, input: ItemStack) -> bool {
match ingredient_type {
IngredientType::Tag(tag) => {
let items = match get_tag_values(TagCategory::Item, tag) {
Some(items) => items,
None => return false,
};
items
.iter()
.any(|tag| check_ingredient_type(&tag.to_ingredient_type(), input))
}
IngredientType::Item(item) => get_item(item).is_some_and(|item| item.id == input.item_id),
}
}
pub fn check_if_matches_crafting(input: [[Option<ItemStack>; 3]; 3]) -> Option<ItemStack> {
let input = flatten_3x3(input);
RECIPES
.par_iter()
.find_any(|recipe| {
let patterns = recipe.pattern();
if patterns
.iter()
.flatten()
.flatten()
.all(|slot| slot.is_none())
{
false
} else if recipe.recipe_type.is_shapeless() {
shapeless_crafting_match(input, recipe.pattern())
} else {
patterns.par_iter().any(|pattern| {
pattern.iter().enumerate().all(|(i, row)| {
row.iter()
.enumerate()
.all(|(j, item)| match (item, input[i][j]) {
(Some(item), Some(input)) => ingredient_slot_check(item, input),
(None, None) => true,
(Some(_), None) | (None, Some(_)) => false,
})
})
})
}
})
.map(|recipe| match recipe.result() {
RecipeResult::Single { id, .. } => Some(ItemStack {
item_id: get_item(id).unwrap().id,
item_count: 1,
}),
RecipeResult::Many { id, count, .. } => Some(ItemStack {
item_id: get_item(id).unwrap().id,
item_count: *count,
}),
RecipeResult::Special => None,
})?
}
fn ingredient_slot_check(recipe_item: &IngredientSlot, input: ItemStack) -> bool {
match recipe_item {
IngredientSlot::Single(ingredient) => check_ingredient_type(ingredient, input),
IngredientSlot::Many(ingredients) => ingredients
.iter()
.any(|ingredient| check_ingredient_type(ingredient, input)),
}
}
fn shapeless_crafting_match(
input: [[Option<ItemStack>; 3]; 3],
pattern: &[[[Option<IngredientSlot>; 3]; 3]],
) -> bool {
let mut pattern = pattern
.iter()
.flatten()
.flatten()
.flatten()
.cloned()
.collect_vec();
for item in input.into_iter().flatten().flatten() {
if let Some(index) = pattern.iter().enumerate().find_map(|(i, recipe_item)| {
if ingredient_slot_check(recipe_item, item) {
Some(i)
} else {
None
}
}) {
pattern.remove(index);
} else {
return false;
}
}
pattern.is_empty()
}

View File

@@ -5,6 +5,7 @@ use pumpkin_macros::screen;
use pumpkin_world::item::ItemStack;
pub mod container_click;
mod crafting;
pub mod drag_handler;
mod error;
mod open_container;
@@ -12,7 +13,7 @@ pub mod player;
pub mod window_property;
pub use error::InventoryError;
pub use open_container::OpenContainer;
pub use open_container::*;
/// https://wiki.vg/Inventory
#[derive(Debug, FromPrimitive, Clone, Copy, Eq, PartialEq)]
@@ -67,12 +68,26 @@ pub trait Container: Sync + Send {
carried_item: &mut Option<ItemStack>,
slot: usize,
mouse_click: MouseClick,
taking_crafted: bool,
) -> Result<(), InventoryError> {
let mut all_slots = self.all_slots();
if slot > all_slots.len() {
Err(InventoryError::InvalidSlot)?
}
if taking_crafted {
match (all_slots[slot].as_mut(), carried_item.as_mut()) {
(Some(s1), Some(s2)) => {
if s1.item_id == s2.item_id {
handle_item_change(all_slots[slot], carried_item, mouse_click);
}
}
(Some(_), None) => handle_item_change(all_slots[slot], carried_item, mouse_click),
(None, None) | (None, Some(_)) => (),
}
return Ok(());
}
handle_item_change(carried_item, all_slots[slot], mouse_click);
Ok(())
}
@@ -91,6 +106,26 @@ pub trait Container: Sync + Send {
fn internal_pumpkin_id(&self) -> u64 {
0
}
fn craft(&mut self) -> bool {
false
}
fn crafting_output_slot(&self) -> Option<usize> {
None
}
fn slot_in_crafting_input_slots(&self, _slot: &usize) -> bool {
false
}
fn crafted_item_slot(&self) -> Option<ItemStack> {
self.all_slots_ref()
.get(self.crafting_output_slot()?)?
.copied()
}
fn recipe_used(&mut self) {}
}
pub fn handle_item_take(
@@ -236,4 +271,36 @@ impl<'a> Container for OptionallyCombinedContainer<'a, 'a> {
None => self.inventory.all_slots_ref(),
}
}
fn craft(&mut self) -> bool {
match &mut self.container {
Some(container) => container.craft(),
None => self.inventory.craft(),
}
}
fn crafting_output_slot(&self) -> Option<usize> {
match &self.container {
Some(container) => container.crafting_output_slot(),
None => self.inventory.crafting_output_slot(),
}
}
fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool {
match &self.container {
Some(container) => {
// We don't have to worry about length due to inventory crafting slots being inaccessible
// while inside container interfaces
container.slot_in_crafting_input_slots(slot)
}
None => self.inventory.slot_in_crafting_input_slots(slot),
}
}
fn recipe_used(&mut self) {
match &mut self.container {
Some(container) => container.recipe_used(),
None => self.inventory.recipe_used(),
}
}
}

View File

@@ -1,3 +1,4 @@
use crate::crafting::check_if_matches_crafting;
use crate::{Container, WindowType};
use pumpkin_world::item::ItemStack;
use std::sync::Arc;
@@ -35,10 +36,10 @@ impl OpenContainer {
}
}
pub fn empty(player_id: i32) -> Self {
pub fn new_empty_container<C: Container + Default + 'static>(player_id: i32) -> Self {
Self {
players: vec![player_id],
container: Arc::new(Mutex::new(Box::new(Chest::new()))),
container: Arc::new(Mutex::new(Box::new(C::default()))),
}
}
@@ -46,8 +47,8 @@ impl OpenContainer {
self.players.clone()
}
}
struct Chest([Option<ItemStack>; 27]);
#[derive(Default)]
pub struct Chest([Option<ItemStack>; 27]);
impl Chest {
pub fn new() -> Self {
@@ -70,3 +71,71 @@ impl Container for Chest {
self.0.iter().map(|slot| slot.as_ref()).collect()
}
}
#[derive(Default)]
pub struct CraftingTable {
input: [[Option<ItemStack>; 3]; 3],
output: Option<ItemStack>,
}
impl Container for CraftingTable {
fn window_type(&self) -> &'static WindowType {
&WindowType::CraftingTable
}
fn window_name(&self) -> &'static str {
"Crafting Table"
}
fn all_slots(&mut self) -> Vec<&mut Option<ItemStack>> {
let slots = vec![&mut self.output];
let slots = slots
.into_iter()
.chain(self.input.iter_mut().flatten())
.collect();
slots
}
fn all_slots_ref(&self) -> Vec<Option<&ItemStack>> {
let slots = vec![self.output.as_ref()];
let slots = slots
.into_iter()
.chain(self.input.iter().flatten().map(|i| i.as_ref()))
.collect();
slots
}
fn craft(&mut self) -> bool {
let old_output = self.output;
self.output = check_if_matches_crafting(self.input);
old_output != self.output
|| self.input.iter().flatten().any(|s| s.is_some())
|| self.output.is_some()
}
fn crafting_output_slot(&self) -> Option<usize> {
Some(0)
}
fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool {
(1..10).contains(slot)
}
fn recipe_used(&mut self) {
self.input.iter_mut().flatten().for_each(|slot| {
if let Some(item) = slot {
if item.item_count > 1 {
item.item_count -= 1;
} else {
*slot = None;
}
}
})
}
fn all_combinable_slots(&self) -> Vec<Option<&ItemStack>> {
self.input.iter().flatten().map(|s| s.as_ref()).collect()
}
fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option<ItemStack>> {
self.input.iter_mut().flatten().collect()
}
}

View File

@@ -1,9 +1,8 @@
use std::slice::IterMut;
use std::sync::atomic::AtomicU32;
use crate::container_click::MouseClick;
use crate::crafting::check_if_matches_crafting;
use crate::{handle_item_change, Container, InventoryError, WindowType};
use pumpkin_world::item::ItemStack;
use std::slice::IterMut;
pub struct PlayerInventory {
// Main Inventory + Hotbar
@@ -14,7 +13,7 @@ pub struct PlayerInventory {
offhand: Option<ItemStack>,
// current selected slot in hotbar
selected: usize,
pub state_id: AtomicU32,
pub state_id: u32,
// Notchian server wraps this value at 100, we can just keep it as a u8 that automatically wraps
pub total_opened_containers: i32,
}
@@ -37,7 +36,7 @@ impl PlayerInventory {
offhand: None,
// TODO: What when player spawns in with an different index ?
selected: 0,
state_id: AtomicU32::new(0),
state_id: 0,
total_opened_containers: 2,
}
}
@@ -54,29 +53,23 @@ impl PlayerInventory {
/// Useful functionality for plugins in the future.
pub fn set_slot(
&mut self,
slot: u16,
slot: usize,
item: Option<ItemStack>,
item_allowed_override: bool,
) -> Result<(), InventoryError> {
if !(0..=45).contains(&slot) {
return Err(InventoryError::InvalidSlot);
}
match item_allowed_override {
true => {
*self.all_slots()[slot as usize] = item;
if item_allowed_override {
if !(0..=45).contains(&slot) {
Err(InventoryError::InvalidSlot)?
}
false => {
let slot = slot as usize;
let slot_condition = self.slot_condition(slot)?;
if let Some(item) = item {
if slot_condition(&item) {
self.all_slots()[slot] = &mut Some(item);
}
}
*self.all_slots()[slot] = item;
return Ok(());
}
let slot_condition = self.slot_condition(slot)?;
if let Some(item) = item {
if slot_condition(&item) {
*self.all_slots()[slot] = Some(item);
}
}
Ok(())
}
#[allow(clippy::type_complexity)]
@@ -168,19 +161,36 @@ impl Container for PlayerInventory {
carried_slot: &mut Option<ItemStack>,
slot: usize,
mouse_click: MouseClick,
invert: bool,
) -> Result<(), InventoryError> {
let slot_condition = self.slot_condition(slot)?;
let item_slot = self.get_slot(slot)?;
if let Some(item) = carried_slot {
if slot_condition(item) {
if invert {
handle_item_change(item_slot, carried_slot, mouse_click);
return Ok(());
}
handle_item_change(carried_slot, item_slot, mouse_click);
}
} else {
if invert {
handle_item_change(item_slot, carried_slot, mouse_click);
return Ok(());
}
handle_item_change(carried_slot, item_slot, mouse_click)
}
Ok(())
}
fn crafting_output_slot(&self) -> Option<usize> {
Some(0)
}
fn slot_in_crafting_input_slots(&self, slot: &usize) -> bool {
(1..=4).contains(slot)
}
fn all_slots(&mut self) -> Vec<&mut Option<ItemStack>> {
self.slots_mut()
}
@@ -196,4 +206,14 @@ impl Container for PlayerInventory {
fn all_combinable_slots_mut(&mut self) -> Vec<&mut Option<ItemStack>> {
self.items.iter_mut().collect()
}
fn craft(&mut self) -> bool {
let v1 = [self.crafting[0], self.crafting[1], None];
let v2 = [self.crafting[2], self.crafting[3], None];
let v3 = [None; 3];
let together = [v1, v2, v3];
self.crafting_output = check_if_matches_crafting(together);
self.crafting.iter().any(|s| s.is_some())
}
}

View File

@@ -10,8 +10,12 @@ pumpkin-core = { path = "../pumpkin-core" }
serde.workspace = true
serde_json.workspace = true
rayon.workspace = true
num-traits.workspace = true
num-derive.workspace = true
# nbt
fastnbt = { git = "https://github.com/owengage/fastnbt.git" }
itertools.workspace = true

View File

@@ -11,7 +11,11 @@ use instrument::Instrument;
use jukebox_song::JukeboxSong;
use paint::Painting;
use pumpkin_protocol::client::config::RegistryEntry;
pub use recipe::{
flatten_3x3, IngredientSlot, IngredientType, Recipe, RecipeResult, RecipeType, RECIPES,
};
use serde::{Deserialize, Serialize};
pub use tags::{get_tag_values, TagCategory, TagType};
use trim_material::TrimMaterial;
use trim_pattern::TrimPattern;
use wolf::WolfVariant;
@@ -25,6 +29,8 @@ mod enchantment;
mod instrument;
mod jukebox_song;
mod paint;
mod recipe;
mod tags;
mod trim_material;
mod trim_pattern;
mod wolf;

View File

@@ -0,0 +1,92 @@
mod read;
mod recipe_formats;
pub use read::{
ingredients::IngredientSlot, ingredients::IngredientType, Recipe, RecipeResult, RecipeType,
};
use std::sync::LazyLock;
pub fn flatten_3x3<T: Clone>(input: [[Option<T>; 3]; 3]) -> [[Option<T>; 3]; 3] {
let mut final_output = [const { [const { None }; 3] }; 3];
let mut row_alignment = 0;
let mut column_alignment = 2;
for (i, row) in input.iter().enumerate() {
let mut row_values = [false; 3];
for (i, item) in row.iter().enumerate().take(column_alignment + 1) {
if item.is_some() {
row_values[i] = true;
if i < column_alignment {
column_alignment = i;
}
}
}
if i == row_alignment && row_values.iter().all(|val| !val) {
row_alignment += 1;
}
}
for (i, row) in &mut final_output.iter_mut().enumerate() {
let input_row = input.get(i + row_alignment);
for (j, item) in row.iter_mut().enumerate() {
let val = input_row.and_then(|val| val.get(j + column_alignment));
*item = match val {
None => None,
Some(None) => None,
Some(Some(val)) => Some(val.clone()),
}
}
}
final_output
}
pub static RECIPES: LazyLock<Vec<Recipe>> =
LazyLock::new(|| serde_json::from_str(include_str!("../../../assets/recipes.json")).unwrap());
#[cfg(test)]
mod test {
use super::flatten_3x3;
use crate::RECIPES;
#[test]
fn row_flatten() {
let input = [[None; 3], [None; 3], [Some(()), Some(()), Some(())]];
let out = [[Some(()), Some(()), Some(())], [None; 3], [None; 3]];
assert_eq!(flatten_3x3(input), out);
}
#[test]
fn column_flatten() {
let one_row_right = [None, None, Some(())];
let one_row_left = [Some(()), None, None];
let input = [one_row_right, one_row_right, one_row_right];
let output = [one_row_left, one_row_left, one_row_left];
assert_eq!(flatten_3x3(input), output)
}
#[test]
fn full_flatten() {
let input_1 = [[None; 3], [None; 3], [None, None, Some(())]];
let output_1 = [[Some(()), None, None], [None; 3], [None; 3]];
let input_2 = [
[None; 3],
[None, None, Some(())],
[None, Some(()), Some(())],
];
let output_2 = [
[None, Some(()), None],
[Some(()), Some(()), None],
[None; 3],
];
let input_3 = [[Some(()), None, None], [Some(()), None, None], [None; 3]];
assert_eq!(flatten_3x3(input_1), output_1);
assert_eq!(flatten_3x3(input_2), output_2);
assert_eq!(flatten_3x3(input_3), input_3);
}
#[test]
// This makes sure that all recipes are able deserialized properly
fn check_parsing() {
assert!(!RECIPES.is_empty())
}
}

View File

@@ -0,0 +1,628 @@
use crate::flatten_3x3;
use crate::recipe::read::ingredients::{IngredientSlot, Ingredients};
use crate::recipe::read::SpecialCraftingType::{
ArmorDye, BannerDuplicate, BookCloning, Firework, RepairItem, ShieldDecoration,
ShulkerboxColoring, SuspiciousStew, TippedArrow,
};
use crate::recipe::recipe_formats::{ShapedCrafting, ShapelessCrafting};
use serde::de::{Error, MapAccess, Visitor};
use serde::{de, Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::Formatter;
use std::str::FromStr;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RecipeType {
Blasting,
CampfireCooking,
Crafting(CraftingType),
Smelting,
Smithing(SmithingType),
Smoking,
StoneCutting,
}
impl RecipeType {
pub const fn is_shapeless(&self) -> bool {
// I have not checked exactly which ones require shape and which don't!
!matches!(self, Self::Crafting(CraftingType::Shaped))
}
}
impl FromStr for RecipeType {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use CraftingType::*;
use FireworkCrafting::*;
use MapCrafting::*;
use RecipeType::*;
let s = s.trim_start_matches("minecraft:");
match s {
"blasting" => Ok(Blasting),
"campfire_cooking" => Ok(CampfireCooking),
"crafting_shaped" => Ok(Crafting(Shaped)),
"crafting_shapeless" => Ok(Crafting(Shapeless)),
"crafting_special_bookcloning" => Ok(Crafting(Special(BookCloning))),
"crafting_special_repairitem" => Ok(Crafting(Special(RepairItem))),
"crafting_special_armordye" => Ok(Crafting(Special(ArmorDye))),
"crafting_special_firework_rocket" => Ok(Crafting(Special(Firework(Rocket)))),
"crafting_special_firework_star" => Ok(Crafting(Special(Firework(Star)))),
"crafting_special_firework_star_fade" => Ok(Crafting(Special(Firework(StarFade)))),
"crafting_special_suspiciousstew" => Ok(Crafting(Special(SuspiciousStew))),
"crafting_special_mapextending" => {
Ok(Crafting(Special(SpecialCraftingType::Map(Extending))))
}
"crafting_special_mapcloning" => {
Ok(Crafting(Special(SpecialCraftingType::Map(Cloning))))
}
"crafting_special_shulkerboxcoloring" => Ok(Crafting(Special(ShulkerboxColoring))),
"crafting_special_bannerduplicate" => Ok(Crafting(Special(BannerDuplicate))),
"crafting_special_shielddecoration" => Ok(Crafting(Special(ShieldDecoration))),
"crafting_special_tippedarrow" => Ok(Crafting(Special(TippedArrow))),
"crafting_decorated_pot" => Ok(Crafting(DecoratedPot)),
"crafting_transmute" => Ok(Crafting(Transmute)),
"smelting" => Ok(Smelting),
"smithing" => Ok(Smithing(SmithingType::Normal)),
"smithing_trim" => Ok(Smithing(SmithingType::Trim)),
"smithing_transform" => Ok(Smithing(SmithingType::Transform)),
"smoking" => Ok(Smoking),
"stonecutting" => Ok(StoneCutting),
_ => Err(format!("Could not find recipe with id: \"{s}\"")),
}
}
}
pub mod ingredients {
use serde::de::{Error, SeqAccess, Visitor};
use serde::{de, Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::Formatter;
use std::hash::Hash;
#[derive(Clone, PartialEq, Debug, Eq, Hash)]
pub enum IngredientType {
Item(String),
Tag(String),
}
impl IngredientType {
pub fn to_all_types(&self, item_tags: &HashMap<String, Vec<String>>) -> Vec<String> {
match &self {
IngredientType::Tag(tag) => item_tags.get(tag).unwrap().clone(),
IngredientType::Item(s) => vec![s.to_string()],
}
}
}
struct IngredientTypeVisitor;
impl<'de> Visitor<'de> for IngredientTypeVisitor {
type Value = IngredientType;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid item type")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
match v.strip_prefix('#') {
Some(tag) => Ok(IngredientType::Tag(tag.to_string())),
None => Ok(IngredientType::Item(v.to_string())),
}
}
}
impl<'de> Deserialize<'de> for IngredientType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(IngredientTypeVisitor)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum IngredientSlot {
Single(IngredientType),
Many(Vec<IngredientType>),
}
impl PartialEq<IngredientType> for IngredientSlot {
fn eq(&self, other: &IngredientType) -> bool {
match self {
IngredientSlot::Single(ingredient) => other == ingredient,
IngredientSlot::Many(ingredients) => ingredients.contains(other),
}
}
}
impl<'de> Deserialize<'de> for IngredientSlot {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct SlotTypeVisitor;
impl<'de> Visitor<'de> for SlotTypeVisitor {
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid ingredient slot")
}
type Value = IngredientSlot;
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(IngredientSlot::Single(IngredientTypeVisitor.visit_str(v)?))
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut ingredients: Vec<IngredientType> = vec![];
while let Some(element) = seq.next_element()? {
ingredients.push(element)
}
if ingredients.len() == 1 {
Ok(IngredientSlot::Single(ingredients[0].clone()))
} else {
Ok(IngredientSlot::Many(ingredients))
}
}
}
deserializer.deserialize_any(SlotTypeVisitor)
}
}
pub struct Ingredients(pub Vec<IngredientSlot>);
impl<'de> Deserialize<'de> for Ingredients {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct IngredientsVisitor;
impl<'de> Visitor<'de> for IngredientsVisitor {
type Value = Ingredients;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid ingredients")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
let mut ingredients = vec![];
while let Some(element) = seq.next_element()? {
ingredients.push(element)
}
Ok(Ingredients(ingredients))
}
}
deserializer.deserialize_seq(IngredientsVisitor)
}
}
}
#[derive(Debug)]
pub enum RecipeResult {
Many {
count: u8,
id: String,
// TODO
components: Option<serde_json::Value>,
},
Single {
id: String,
// TODO
components: Option<serde_json::Value>,
},
Special,
}
impl RecipeResult {
pub fn id(&self) -> &str {
match self {
Self::Many { id, .. } | Self::Single { id, .. } => id,
Self::Special => "minecraft:air",
}
}
}
impl<'de> Deserialize<'de> for RecipeResult {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Fields {
Count,
Id,
Components,
}
struct ResultVisitor;
impl<'de> Visitor<'de> for ResultVisitor {
type Value = RecipeResult;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid recipe result")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut id: Option<&str> = None;
let mut count: Option<u8> = None;
let mut components: Option<serde_json::Value> = None;
while let Some(key) = map.next_key()? {
match key {
Fields::Id => visit_option(&mut map, &mut id, "id")?,
Fields::Count => visit_option(&mut map, &mut count, "count")?,
Fields::Components => {
visit_option(&mut map, &mut components, "components")?
}
}
}
let id = id
.ok_or_else(|| de::Error::missing_field("id"))?
.to_string();
if let Some(count) = count {
Ok(RecipeResult::Many {
id,
count,
components,
})
} else {
Ok(RecipeResult::Single { id, components })
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(RecipeResult::Single {
id: v.to_string(),
components: None,
})
}
}
// Evaluate putting type constraint on RecipeResult, because only Crafting Transmute can call visit_str
deserializer.deserialize_any(ResultVisitor)
}
}
pub struct RecipeKeys(pub(super) HashMap<char, IngredientSlot>);
impl<'de> Deserialize<'de> for RecipeKeys {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct KeyVisitor;
impl<'de> Visitor<'de> for KeyVisitor {
type Value = HashMap<char, IngredientSlot>;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "existing key inside recipe")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut return_map = HashMap::new();
while let Some(next) = map.next_key()? {
let test: &str = next;
let c: char = test.chars().next().unwrap();
let ingredient_type: IngredientSlot = map.next_value()?;
return_map.insert(c, ingredient_type);
}
Ok(return_map)
}
}
deserializer.deserialize_map(KeyVisitor).map(Self)
}
}
impl<'de> Deserialize<'de> for Recipe {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Fields {
Type,
Category,
Group,
Key,
Pattern,
Result,
Ingredients,
Ingredient,
// Only exists sometimes, at least on Shaped crafting
#[serde(rename = "show_notification")]
ShowNotification,
// Armor
Addition,
Base,
Template,
// Smelting
CookingTime,
Experience,
// Transmute
Input,
Material,
}
struct RecipeVisitor;
impl<'de> Visitor<'de> for RecipeVisitor {
type Value = Recipe;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid recipe")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let mut recipe_type: Option<&str> = None;
let mut category: Option<&str> = None;
let mut group: Option<&str> = None;
let mut keys: Option<RecipeKeys> = None;
let mut pattern: Option<Vec<&str>> = None;
let mut result: Option<RecipeResult> = None;
let mut ingredients: Option<Ingredients> = None;
let mut ingredient: Option<IngredientSlot> = None;
let mut addition: Option<IngredientSlot> = None;
let mut base: Option<IngredientSlot> = None;
let mut template: Option<IngredientSlot> = None;
let mut cookingtime: Option<u16> = None;
let mut experience: Option<f32> = None;
let mut show_notification: Option<bool> = None;
let mut transmute_input: Option<IngredientSlot> = None;
let mut transmute_material: Option<IngredientSlot> = None;
while let Some(key) = map.next_key()? {
(match key {
Fields::Type => visit_option(&mut map, &mut recipe_type, "type"),
Fields::Group => visit_option(&mut map, &mut group, "group"),
Fields::Category => visit_option(&mut map, &mut category, "group"),
Fields::Key => visit_option(&mut map, &mut keys, "key"),
Fields::Pattern => visit_option(&mut map, &mut pattern, "pattern"),
Fields::Result => visit_option(&mut map, &mut result, "result"),
Fields::Ingredients => {
visit_option(&mut map, &mut ingredients, "ingredients")
}
Fields::Ingredient => visit_option(&mut map, &mut ingredient, "ingredient"),
Fields::Addition => visit_option(&mut map, &mut addition, "addition"),
Fields::Base => visit_option(&mut map, &mut base, "base"),
Fields::Template => visit_option(&mut map, &mut template, "template"),
Fields::CookingTime => {
visit_option(&mut map, &mut cookingtime, "cookingtime")
}
Fields::Experience => visit_option(&mut map, &mut experience, "experience"),
Fields::ShowNotification => {
visit_option(&mut map, &mut show_notification, "show_notification")
}
Fields::Input => visit_option(&mut map, &mut transmute_input, "input"),
Fields::Material => {
visit_option(&mut map, &mut transmute_material, "material")
}
})?
}
let recipe_type: RecipeType = recipe_type
.ok_or_else(|| de::Error::missing_field("type"))?
.parse()
.unwrap();
let result = match recipe_type {
RecipeType::Crafting(CraftingType::Special(_))
| RecipeType::Crafting(CraftingType::DecoratedPot)
| RecipeType::Smithing(_) => RecipeResult::Special,
_ => result.ok_or_else(|| de::Error::missing_field("result"))?,
};
match recipe_type {
RecipeType::Crafting(CraftingType::Shaped) => {
let mut rows = [[None; 3], [None; 3], [None; 3]];
pattern
.ok_or_else(|| de::Error::missing_field("pattern"))?
.into_iter()
.map(|s| {
let mut chars = [None; 3];
s.chars()
.enumerate()
.for_each(|(i, char)| chars[i] = Some(char));
chars
})
.enumerate()
.for_each(|(i, row)| rows[i] = row);
let keys = keys.ok_or_else(|| de::Error::missing_field("keys"))?;
Ok(Recipe::from(ShapedCrafting::new(keys, rows, result)))
}
RecipeType::Crafting(CraftingType::Shapeless) => {
let ingredients =
ingredients.ok_or_else(|| de::Error::missing_field("ingredients"))?;
Ok(Recipe::from(ShapelessCrafting::new(ingredients.0, result)))
}
RecipeType::Crafting(CraftingType::Special(_)) => Ok(Recipe::from(Test {
recipe_type,
result,
})),
RecipeType::Crafting(CraftingType::DecoratedPot) => Ok(Recipe::from(Test {
recipe_type,
result,
})),
RecipeType::Crafting(CraftingType::Transmute) => {
let _input =
transmute_input.ok_or_else(|| de::Error::missing_field("input"))?;
// Maybe also has material
Ok(Recipe::from(Test {
recipe_type,
result,
}))
}
RecipeType::Smithing(_) => Ok(Recipe::from(Test {
recipe_type,
result: RecipeResult::Special,
})),
_ => Ok(Recipe::from(Test {
recipe_type,
result,
})),
}
}
}
const FIELDS: &[&str] = &[
"type",
"category",
"group",
"key",
"pattern",
"result",
"ingredients",
"ingredient",
"addition",
"base",
"template",
"cookingtime",
"experience",
"show_notification",
"input",
"material",
];
deserializer.deserialize_struct("Recipe", FIELDS, RecipeVisitor)
}
}
#[inline(always)]
fn visit_option<'de, T: Deserialize<'de>, Map: MapAccess<'de>>(
map: &mut Map,
option: &mut Option<T>,
field: &'static str,
) -> Result<(), Map::Error> {
match option {
Some(_) => Err(<Map as MapAccess>::Error::duplicate_field(field)),
None => {
*option = Some(map.next_value()?);
Ok(())
}
}
}
pub struct Recipe {
pub recipe_type: RecipeType,
pattern: Vec<[[Option<IngredientSlot>; 3]; 3]>,
result: RecipeResult,
}
impl Recipe {
pub fn pattern(&self) -> &[[[Option<IngredientSlot>; 3]; 3]] {
&self.pattern
}
pub fn result(&self) -> &RecipeResult {
&self.result
}
pub fn implemented(&self) -> bool {
match self.recipe_type {
RecipeType::Crafting(crafting_type) => {
matches!(
crafting_type,
CraftingType::Shapeless | CraftingType::Shaped
)
}
_ => false,
}
}
}
struct Test {
recipe_type: RecipeType,
result: RecipeResult,
}
impl RecipeTrait for Test {
fn recipe_type(&self) -> RecipeType {
self.recipe_type
}
fn pattern(&self) -> Vec<[[Option<IngredientSlot>; 3]; 3]> {
vec![[const { [const { None }; 3] }; 3]]
}
fn result(self) -> RecipeResult {
self.result
}
}
impl<T: RecipeTrait> From<T> for Recipe {
fn from(recipe_type: T) -> Self {
recipe_type.to_recipe()
}
}
pub trait RecipeTrait: Sized {
fn recipe_type(&self) -> RecipeType;
fn pattern(&self) -> Vec<[[Option<IngredientSlot>; 3]; 3]>;
fn result(self) -> RecipeResult;
fn to_recipe(self) -> Recipe {
Recipe {
recipe_type: self.recipe_type(),
pattern: self.pattern().into_iter().map(flatten_3x3).collect(),
result: self.result(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CraftingType {
Shapeless,
Shaped,
Special(SpecialCraftingType),
DecoratedPot,
Transmute,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SpecialCraftingType {
BookCloning,
RepairItem,
ArmorDye,
Firework(FireworkCrafting),
SuspiciousStew,
ShulkerboxColoring,
Map(MapCrafting),
BannerDuplicate,
ShieldDecoration,
TippedArrow,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FireworkCrafting {
Rocket,
Star,
StarFade,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum MapCrafting {
Extending,
Cloning,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SmithingType {
Normal,
Trim,
Transform,
}

View File

@@ -0,0 +1,94 @@
use super::super::recipe::RecipeType;
use super::read::{
ingredients::IngredientSlot, CraftingType, RecipeKeys, RecipeResult, RecipeTrait,
};
use itertools::Itertools;
pub struct ShapedCrafting {
keys: RecipeKeys,
pattern: [[Option<char>; 3]; 3],
output: RecipeResult,
}
impl RecipeKeys {
fn pattern_to_thing(
&self,
pattern: [[Option<char>; 3]; 3],
) -> [[Option<IngredientSlot>; 3]; 3] {
pattern
.map(|row| row.map(|maybe_char| maybe_char.and_then(|char| self.0.get(&char).cloned())))
}
}
impl ShapedCrafting {
pub fn new(keys: RecipeKeys, pattern: [[Option<char>; 3]; 3], output: RecipeResult) -> Self {
Self {
keys,
pattern,
output,
}
}
}
impl RecipeTrait for ShapedCrafting {
fn recipe_type(&self) -> RecipeType {
RecipeType::Crafting(CraftingType::Shaped)
}
fn pattern(&self) -> Vec<[[Option<IngredientSlot>; 3]; 3]> {
vec![self.keys.pattern_to_thing(self.pattern)]
}
fn result(self) -> RecipeResult {
self.output
}
}
pub struct ShapelessCrafting {
ingredients: Vec<IngredientSlot>,
output: RecipeResult,
}
impl ShapelessCrafting {
pub(crate) fn new(ingredients: Vec<IngredientSlot>, output: RecipeResult) -> Self {
Self {
ingredients,
output,
}
}
}
impl RecipeTrait for ShapelessCrafting {
fn recipe_type(&self) -> RecipeType {
RecipeType::Crafting(CraftingType::Shapeless)
}
// Iterating over all permutations is cheaper than resolving and iterating over all tags when trying to check if recipe
// is correct. Otherwise, we would have to backtrack and check for each item in the recipe input, which tags they are inside,
// and then sort those permutations
fn pattern(&self) -> Vec<[[std::option::Option<IngredientSlot>; 3]; 3]> {
vec![
self.ingredients.clone(), //.permutations(self.ingredients.len())
]
.into_iter()
.map(|thing| {
let mut v1 = [const { None }; 3];
let mut v2 = [const { None }; 3];
let mut v3 = [const { None }; 3];
for (i, thing) in thing.into_iter().enumerate() {
if i < 3 {
v1[i] = Some(thing)
} else if i < 6 {
v2[i - 3] = Some(thing)
} else {
v3[i - 6] = Some(thing)
}
}
[v1, v2, v3]
})
.collect_vec()
}
fn result(self) -> RecipeResult {
self.output
}
}

View File

@@ -0,0 +1,113 @@
use serde::de::{Error, Visitor};
use serde::{Deserialize, Deserializer};
use std::collections::HashMap;
use std::fmt::Formatter;
use std::sync::LazyLock;
use crate::IngredientType;
#[derive(Deserialize, Eq, PartialEq, Hash)]
pub enum TagCategory {
#[serde(rename = "minecraft:instrument")]
Instrument,
#[serde(rename = "minecraft:worldgen/biome")]
WorldGenBiome,
#[serde(rename = "minecraft:point_of_interest_type")]
PointOfInterest,
#[serde(rename = "minecraft:entity_type")]
Entity,
#[serde(rename = "minecraft:damage_type")]
DamageType,
#[serde(rename = "minecraft:banner_pattern")]
BannerPattern,
#[serde(rename = "minecraft:block")]
Block,
#[serde(rename = "minecraft:fluid")]
Fluid,
#[serde(rename = "minecraft:enchantment")]
Enchantment,
#[serde(rename = "minecraft:cat_variant")]
Cat,
#[serde(rename = "minecraft:painting_variant")]
Painting,
#[serde(rename = "minecraft:item")]
Item,
#[serde(rename = "minecraft:game_event")]
GameEvent,
}
pub static TAGS: LazyLock<HashMap<TagCategory, HashMap<String, Vec<TagType>>>> =
LazyLock::new(|| {
let mut map = HashMap::new();
let tags_str = include_str!("../../assets/tags.json");
let tags: Vec<TagCollection> =
serde_json::from_str(tags_str).expect("Valid tag collections");
for tag in tags {
map.insert(tag.name, tag.values);
}
map
});
pub fn get_tag_values(tag_category: TagCategory, tag: &str) -> Option<&Vec<TagType>> {
TAGS.get(&tag_category)
.expect("Should deserialize all tag categories")
.get(tag)
}
#[derive(Deserialize)]
pub struct TagCollection {
name: TagCategory,
#[serde(flatten)]
values: HashMap<String, Vec<TagType>>,
}
#[derive(Clone)]
pub enum TagType {
Item(String),
Tag(String),
}
impl TagType {
pub fn to_ingredient_type(&self) -> IngredientType {
match self {
TagType::Tag(tag) => IngredientType::Tag(tag.to_string()),
TagType::Item(item) => IngredientType::Item(item.to_string()),
}
}
}
impl<'de> Deserialize<'de> for TagType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct TagVisitor;
impl<'de> Visitor<'de> for TagVisitor {
type Value = TagType;
fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
write!(formatter, "valid tag")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: Error,
{
match v.strip_prefix('#') {
Some(v) => Ok(TagType::Tag(v.to_string())),
None => Ok(TagType::Item(v.to_string())),
}
}
}
deserializer.deserialize_str(TagVisitor)
}
}
#[cfg(test)]
mod test {
use crate::tags::TAGS;
#[test]
// This test assures that all tags that exist are loaded into the tags registry
fn load_tags() {
assert!(!TAGS.is_empty())
}
}

View File

@@ -19,33 +19,31 @@ use pumpkin_protocol::VarInt;
use pumpkin_world::item::ItemStack;
use std::sync::Arc;
#[expect(unused)]
impl Player {
pub async fn open_container(&self, server: &Server, window_type: WindowType) {
let inventory = self.inventory.lock().await;
inventory
.state_id
.store(0, std::sync::atomic::Ordering::Relaxed);
let total_opened_containers = inventory.total_opened_containers;
let container = self.get_open_container(server);
let container = container.as_ref().map(|container| container.lock());
// TODO
let window_title = match container {
Some(container) => container.await.window_name(),
None => inventory.window_name(),
let mut inventory = self.inventory.lock().await;
inventory.state_id = 0;
inventory.total_opened_containers += 1;
let mut container = self.get_open_container(server).await;
let mut container = match container.as_mut() {
Some(container) => Some(container.lock().await),
None => None,
};
let window_title = container.as_ref().map_or_else(
|| inventory.window_name(),
|container| container.window_name(),
);
let title = TextComponent::text(window_title);
self.client
.send_packet(&COpenScreen::new(
total_opened_containers.into(),
inventory.total_opened_containers.into(),
VarInt(window_type as i32),
title,
))
.await;
drop(inventory);
// self.set_container_content(container.as_deref_mut());
self.set_container_content(container.as_deref_mut()).await;
}
pub async fn set_container_content(&self, container: Option<&mut Box<dyn Container>>) {
@@ -67,12 +65,10 @@ impl Player {
.map_or_else(Slot::empty, std::convert::Into::into);
// Gets the previous value
let i = inventory
.state_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
inventory.state_id += 1;
let packet = CSetContainerContent::new(
total_opened_containers.into(),
((i + 1) as i32).into(),
(inventory.state_id as i32).into(),
&slots,
&carried_item,
);
@@ -109,24 +105,24 @@ impl Player {
server: &Arc<Server>,
packet: SClickContainer,
) -> Result<(), InventoryError> {
let opened_container = self.get_open_container(server);
let opened_container = opened_container.as_ref().map(|container| container.lock());
let opened_container = self.get_open_container(server).await;
let mut opened_container = match opened_container.as_ref() {
Some(container) => Some(container.lock().await),
None => None,
};
let drag_handler = &server.drag_handler;
let state_id = self
.inventory
.lock()
.await
.state_id
.load(std::sync::atomic::Ordering::Relaxed);
let state_id = self.inventory.lock().await.state_id;
// This is just checking for regular desync, client hasn't done anything malicious
if state_id != packet.state_id.0 as u32 {
// self.set_container_content(opened_container.as_deref_mut());
self.set_container_content(opened_container.as_deref_mut())
.await;
return Ok(());
}
if opened_container.is_some() {
if packet.window_id.0 != self.inventory.lock().await.total_opened_containers {
let total_containers = self.inventory.lock().await.total_opened_containers;
if packet.window_id.0 != total_containers {
return Err(InventoryError::ClosedContainerInteract(self.entity_id()));
}
} else if packet.window_id.0 != 0 {
@@ -134,7 +130,6 @@ impl Player {
}
let click = Click::new(
// TODO: This is very bad
packet
.mode
.0
@@ -143,60 +138,54 @@ impl Player {
packet.button,
packet.slot,
)?;
let (crafted_item, crafted_item_slot) = {
let mut inventory = self.inventory.lock().await;
let combined =
OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut());
(
combined.crafted_item_slot(),
combined.crafting_output_slot(),
)
};
let crafted_is_picked = crafted_item.is_some()
&& match click.slot {
container_click::Slot::Normal(slot) => {
crafted_item_slot.is_some_and(|crafted_slot| crafted_slot == slot)
}
container_click::Slot::OutsideInventory => false,
};
let mut update_whole_container = false;
match click.click_type {
ClickType::MouseClick(mouse_click) => {
// self.mouse_click(opened_container.as_deref_mut(), mouse_click, click.slot).await
todo!()
let click_slot = click.slot;
self.match_click_behaviour(
opened_container.as_deref_mut(),
click,
drag_handler,
&mut update_whole_container,
crafted_is_picked,
)
.await?;
// Checks for if crafted item has been taken
{
let mut inventory = self.inventory.lock().await;
let mut combined =
OptionallyCombinedContainer::new(&mut inventory, opened_container.as_deref_mut());
if combined.crafted_item_slot().is_none() && crafted_item.is_some() {
combined.recipe_used();
}
ClickType::ShiftClick => {
// self.shift_mouse_click(opened_container.as_deref_mut(), click.slot).await
todo!()
if combined.craft() {
drop(inventory);
self.set_container_content(opened_container.as_deref_mut())
.await;
}
ClickType::KeyClick(key_click) => {
todo!()
// container_click::Slot::Normal(slot) => {
// self.number_button_pressed(opened_container.as_deref_mut(), key_click, slot).await
// }
// container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket),
}
ClickType::CreativePickItem => {
// if let container_click::Slot::Normal(slot) = click.slot {
// self.creative_pick_item(opened_container.as_deref_mut(), slot).await
// } else {
// Err(InventoryError::InvalidPacket)
// }
todo!()
}
ClickType::DoubleClick => {
update_whole_container = true;
// if let container_click::Slot::Normal(slot) = click.slot {
// self.double_click(opened_container.as_deref_mut(), slot)
// } else {
// Err(InventoryError::InvalidPacket)
// }
todo!()
}
ClickType::MouseDrag { drag_state } => {
if drag_state == MouseDragState::End {
update_whole_container = true;
}
todo!()
// self.mouse_drag(drag_handler, opened_container.as_deref_mut(), drag_state)
}
ClickType::DropType(_drop_type) => {
log::debug!("todo");
Ok(())
}
}?;
if let Some(opened_container) = opened_container {
}
if let Some(mut opened_container) = opened_container {
if update_whole_container {
drop(opened_container);
self.send_whole_container_change(server).await?;
} else if let container_click::Slot::Normal(slot_index) = click.slot {
} else if let container_click::Slot::Normal(slot_index) = click_slot {
let mut inventory = self.inventory.lock().await;
let mut opened_container = opened_container.await;
let combined_container =
OptionallyCombinedContainer::new(&mut inventory, Some(&mut opened_container));
if let Some(slot) = combined_container.get_slot_excluding_inventory(slot_index) {
@@ -210,19 +199,87 @@ impl Player {
Ok(())
}
async fn match_click_behaviour(
&self,
opened_container: Option<&mut Box<dyn Container>>,
click: Click,
drag_handler: &DragHandler,
update_whole_container: &mut bool,
using_crafting_slot: bool,
) -> Result<(), InventoryError> {
match click.click_type {
ClickType::MouseClick(mouse_click) => {
self.mouse_click(
opened_container,
mouse_click,
click.slot,
using_crafting_slot,
)
.await
}
ClickType::ShiftClick => {
self.shift_mouse_click(opened_container, click.slot, using_crafting_slot)
.await
}
ClickType::KeyClick(key_click) => match click.slot {
container_click::Slot::Normal(slot) => {
self.number_button_pressed(
opened_container,
key_click,
slot,
using_crafting_slot,
)
.await
}
container_click::Slot::OutsideInventory => Err(InventoryError::InvalidPacket),
},
ClickType::CreativePickItem => {
if let container_click::Slot::Normal(slot) = click.slot {
self.creative_pick_item(opened_container, slot).await
} else {
Err(InventoryError::InvalidPacket)
}
}
ClickType::DoubleClick => {
*update_whole_container = true;
if let container_click::Slot::Normal(slot) = click.slot {
self.double_click(opened_container, slot).await
} else {
Err(InventoryError::InvalidPacket)
}
}
ClickType::MouseDrag { drag_state } => {
if drag_state == MouseDragState::End {
*update_whole_container = true;
}
self.mouse_drag(drag_handler, opened_container, drag_state)
.await
}
ClickType::DropType(_drop_type) => {
log::debug!("todo");
Ok(())
}
}
}
async fn mouse_click(
&self,
opened_container: Option<&mut Box<dyn Container>>,
mouse_click: MouseClick,
slot: container_click::Slot,
taking_crafted: bool,
) -> Result<(), InventoryError> {
let mut inventory = self.inventory.lock().await;
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
match slot {
container_click::Slot::Normal(slot) => {
let mut carried_item = self.carried_item.load();
let res = container.handle_item_change(&mut carried_item, slot, mouse_click);
let res = container.handle_item_change(
&mut carried_item,
slot,
mouse_click,
taking_crafted,
);
self.carried_item.store(carried_item);
res
}
@@ -234,6 +291,7 @@ impl Player {
&self,
opened_container: Option<&mut Box<dyn Container>>,
slot: container_click::Slot,
taking_crafted: bool,
) -> Result<(), InventoryError> {
let mut inventory = self.inventory.lock().await;
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
@@ -267,7 +325,12 @@ impl Player {
};
if let Some(slot) = slots {
let mut item_slot = container.all_slots()[slot].map(|i| i);
container.handle_item_change(&mut item_slot, slot, MouseClick::Left)?;
container.handle_item_change(
&mut item_slot,
slot,
MouseClick::Left,
taking_crafted,
)?;
*container.all_slots()[slot] = item_slot;
}
}
@@ -282,6 +345,7 @@ impl Player {
opened_container: Option<&mut Box<dyn Container>>,
key_click: KeyClick,
slot: usize,
taking_crafted: bool,
) -> Result<(), InventoryError> {
let changing_slot = match key_click {
KeyClick::Slot(slot) => slot,
@@ -291,7 +355,12 @@ impl Player {
let mut changing_item_slot = inventory.get_slot(changing_slot as usize)?.to_owned();
let mut container = OptionallyCombinedContainer::new(&mut inventory, opened_container);
container.handle_item_change(&mut changing_item_slot, slot, MouseClick::Left)?;
container.handle_item_change(
&mut changing_item_slot,
slot,
MouseClick::Left,
taking_crafted,
)?;
*inventory.get_slot(changing_slot as usize)? = changing_item_slot;
Ok(())
}
@@ -436,16 +505,14 @@ impl Player {
slot: Slot,
) -> Result<(), InventoryError> {
for player in self.get_current_players_in_container(server).await {
let inventory = player.inventory.lock().await;
let mut inventory = player.inventory.lock().await;
let total_opened_containers = inventory.total_opened_containers;
// Returns previous value
let i = inventory
.state_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
inventory.state_id += 1;
let packet = CSetContainerSlot::new(
total_opened_containers as i8,
(i + 1) as i32,
(inventory.state_id) as i32,
slot_index,
&slot,
);
@@ -458,18 +525,23 @@ impl Player {
let players = self.get_current_players_in_container(server).await;
for player in players {
let container = player.get_open_container(server);
let container = container.as_ref().map(|v| v.lock());
// player.set_container_content(container.as_deref_mut());
let container = player.get_open_container(server).await;
let mut container = match container.as_ref() {
Some(container) => Some(container.lock().await),
None => None,
};
player.set_container_content(container.as_deref_mut()).await;
}
Ok(())
}
pub fn get_open_container(
pub async fn get_open_container(
&self,
server: &Server,
) -> Option<Arc<tokio::sync::Mutex<Box<dyn Container>>>> {
// self.open_container .load().map_or_else(|| None, |id| server.try_get_container(self.entity_id(), id).await)
None
match self.open_container.load() {
Some(id) => server.try_get_container(self.entity_id(), id).await,
None => None,
}
}
}

View File

@@ -234,7 +234,11 @@ impl Player {
.await;
}
pub async fn handle_chat_command(self: &Arc<Self>, server: &Server, command: SChatCommand) {
pub async fn handle_chat_command(
self: &Arc<Self>,
server: &Arc<Server>,
command: SChatCommand,
) {
let dispatcher = server.command_dispatcher.clone();
dispatcher
.handle_command(
@@ -615,15 +619,15 @@ impl Player {
if self.gamemode.load() != GameMode::Creative {
return Err(InventoryError::PermissionError);
}
let valid_slot = packet.slot >= 1 && packet.slot <= 45;
let valid_slot = packet.slot >= 0 && packet.slot <= 45;
if valid_slot {
self.inventory.lock().await.set_slot(
packet.slot as u16,
packet.slot as usize,
packet.clicked_item.to_item(),
true,
)?;
};
// TODO: The Item was droped per drag and drop,
// TODO: The Item was dropped per drag and drop,
Ok(())
}
@@ -636,11 +640,9 @@ impl Player {
return;
};
// window_id 0 represents both 9x1 Generic AND inventory here
self.inventory
.lock()
.await
.state_id
.store(0, std::sync::atomic::Ordering::Relaxed);
let mut inventory = self.inventory.lock().await;
inventory.state_id = 0;
let open_container = self.open_container.load();
if let Some(id) = open_container {
let mut open_containers = server.open_containers.write().await;

View File

@@ -0,0 +1,45 @@
use async_trait::async_trait;
use pumpkin_inventory::{CraftingTable, OpenContainer, WindowType};
use crate::command::{
args::ConsumedArgs, tree::CommandTree, CommandExecutor, CommandSender, InvalidTreeError,
};
const NAMES: [&str; 1] = ["craft"];
const DESCRIPTION: &str = "Open a crafting table";
struct CraftingTableExecutor {}
#[async_trait]
impl CommandExecutor for CraftingTableExecutor {
async fn execute<'a>(
&self,
sender: &mut CommandSender<'a>,
server: &crate::server::Server,
_args: &ConsumedArgs<'a>,
) -> Result<(), InvalidTreeError> {
if let Some(player) = sender.as_player() {
let entity_id = player.entity_id();
player.open_container.store(Some(1));
{
let mut open_containers = server.open_containers.write().await;
if let Some(ender_chest) = open_containers.get_mut(&1) {
ender_chest.add_player(entity_id);
} else {
let open_container =
OpenContainer::new_empty_container::<CraftingTable>(entity_id);
open_containers.insert(1, open_container);
}
}
player
.open_container(server, WindowType::CraftingTable)
.await;
}
Ok(())
}
}
pub fn init_command_tree<'a>() -> CommandTree<'a> {
CommandTree::new(NAMES, DESCRIPTION).execute(&CraftingTableExecutor {})
}

View File

@@ -1,5 +1,5 @@
use async_trait::async_trait;
use pumpkin_inventory::OpenContainer;
use pumpkin_inventory::{Chest, OpenContainer};
use crate::command::{
args::ConsumedArgs, tree::CommandTree, CommandExecutor, CommandSender, InvalidTreeError,
@@ -28,7 +28,7 @@ impl CommandExecutor for EchestExecutor {
if let Some(ender_chest) = open_containers.get_mut(&0) {
ender_chest.add_player(entity_id);
} else {
let open_container = OpenContainer::empty(entity_id);
let open_container = OpenContainer::new_empty_container::<Chest>(entity_id);
open_containers.insert(0, open_container);
}
}

View File

@@ -1,4 +1,5 @@
pub mod cmd_clear;
pub mod cmd_craft;
pub mod cmd_echest;
pub mod cmd_gamemode;
pub mod cmd_give;

View File

@@ -3,8 +3,8 @@ use std::sync::Arc;
use args::ConsumedArgs;
use async_trait::async_trait;
use commands::{
cmd_clear, cmd_echest, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill, cmd_list,
cmd_pumpkin, cmd_say, cmd_stop, cmd_teleport, cmd_worldborder,
cmd_clear, cmd_craft, cmd_echest, cmd_gamemode, cmd_give, cmd_help, cmd_kick, cmd_kill,
cmd_list, cmd_pumpkin, cmd_say, cmd_stop, cmd_teleport, cmd_worldborder,
};
use dispatcher::InvalidTreeError;
use pumpkin_core::math::vector3::Vector3;
@@ -78,6 +78,7 @@ pub fn default_dispatcher<'a>() -> Arc<CommandDispatcher<'a>> {
dispatcher.register(cmd_stop::init_command_tree());
dispatcher.register(cmd_help::init_command_tree());
dispatcher.register(cmd_echest::init_command_tree());
dispatcher.register(cmd_craft::init_command_tree());
dispatcher.register(cmd_kill::init_command_tree());
dispatcher.register(cmd_kick::init_command_tree());
dispatcher.register(cmd_worldborder::init_command_tree());

View File

@@ -43,7 +43,7 @@ use pumpkin_protocol::{
use tokio::sync::{Mutex, Notify};
use tokio::task::JoinHandle;
use pumpkin_protocol::server::play::SKeepAlive;
use pumpkin_protocol::server::play::{SClickContainer, SKeepAlive};
use pumpkin_world::{
cylindrical_chunk_iterator::Cylindrical,
item::{item_registry::Item, ItemStack},
@@ -832,6 +832,10 @@ impl Player {
self.handle_player_command(SPlayerCommand::read(bytebuf)?)
.await;
}
SClickContainer::PACKET_ID => {
self.handle_click_container(server, SClickContainer::read(bytebuf)?)
.await?;
}
SSetHeldItem::PACKET_ID => {
self.handle_set_held_item(SSetHeldItem::read(bytebuf)?)
.await;
@@ -865,12 +869,10 @@ impl Player {
let slot = (&*inventory.get_slot(slot_index)?).into();
// Returns previous value
let i = inventory
.state_id
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
inventory.state_id += 1;
let packet = CSetContainerSlot::new(
PlayerInventory::CONTAINER_ID,
(i + 1) as i32,
inventory.state_id as i32,
slot_index,
&slot,
);

View File

@@ -60,7 +60,7 @@ impl RCONClient {
}
/// Returns if client is closed or not
pub async fn handle(&mut self, server: &Server, password: &str) -> bool {
pub async fn handle(&mut self, server: &Arc<Server>, password: &str) -> bool {
if !self.closed {
match self.read_bytes().await {
// Stream closed, so we can't reply, so we just close everything.
@@ -80,7 +80,7 @@ impl RCONClient {
self.closed
}
async fn poll(&mut self, server: &Server, password: &str) -> Result<(), PacketError> {
async fn poll(&mut self, server: &Arc<Server>, password: &str) -> Result<(), PacketError> {
let Some(packet) = self.receive_packet().await? else {
return Ok(());
};

View File

@@ -18,8 +18,7 @@ use std::{
},
time::Duration,
};
use tokio::sync::Mutex;
use tokio::sync::RwLock;
use tokio::sync::{Mutex, RwLock};
use crate::client::EncryptionError;
use crate::{
@@ -49,8 +48,8 @@ pub struct Server {
pub worlds: Vec<Arc<World>>,
/// Caches game registries for efficient access.
pub cached_registry: Vec<Registry>,
pub open_containers: RwLock<HashMap<u64, OpenContainer>>,
/// Tracks open containers used for item interactions.
pub open_containers: RwLock<HashMap<u64, OpenContainer>>,
pub drag_handler: DragHandler,
/// Assigns unique IDs to entities.
entity_id: AtomicI32,