mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
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:
@@ -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
|
||||
|
||||
100
pumpkin-inventory/src/crafting.rs
Normal file
100
pumpkin-inventory/src/crafting.rs
Normal 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()
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
92
pumpkin-registry/src/recipe/mod.rs
Normal file
92
pumpkin-registry/src/recipe/mod.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
628
pumpkin-registry/src/recipe/read.rs
Normal file
628
pumpkin-registry/src/recipe/read.rs
Normal 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,
|
||||
}
|
||||
94
pumpkin-registry/src/recipe/recipe_formats.rs
Normal file
94
pumpkin-registry/src/recipe/recipe_formats.rs
Normal 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
|
||||
}
|
||||
}
|
||||
113
pumpkin-registry/src/tags.rs
Normal file
113
pumpkin-registry/src/tags.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
45
pumpkin/src/command/commands/cmd_craft.rs
Normal file
45
pumpkin/src/command/commands/cmd_craft.rs
Normal 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 {})
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod cmd_clear;
|
||||
pub mod cmd_craft;
|
||||
pub mod cmd_echest;
|
||||
pub mod cmd_gamemode;
|
||||
pub mod cmd_give;
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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(());
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user