Skip to content

Commit f0374c3

Browse files
authored
feat(pumpkin): add /tag command with entity scoreboard tags (Pumpkin-MC#2395)
Implements the vanilla /tag command (tracked in Pumpkin-MC#15) along with the entity-side storage it needs: - Adds a scoreboard_tags: Mutex<HashSet<String>> field to Entity, with add_scoreboard_tag / remove_scoreboard_tag helpers that enforce the vanilla 1024-tag cap and report whether they changed anything. - Serializes tags to/from the entity's "Tags" NBT list, matching the vanilla format so tags round-trip through world saves. - /tag <targets> add|remove <name> and /tag <targets> list, with the existing commands.tag.* translation keys and single/multiple wording.
1 parent 1f38ba8 commit f0374c3

3 files changed

Lines changed: 255 additions & 1 deletion

File tree

pumpkin/src/command/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ mod spreadplayers;
5353
mod stop;
5454
mod stopsound;
5555
mod summon;
56+
mod tag;
5657
mod teleport;
5758
mod tellraw;
5859
mod tick;
@@ -165,6 +166,7 @@ pub async fn default_dispatcher(
165166
setidletimeout::register(&mut dispatcher, registry);
166167
spreadplayers::register(&mut dispatcher, registry);
167168
stop::register(&mut dispatcher, registry);
169+
tag::register(&mut dispatcher, registry);
168170
tick::register(&mut dispatcher, registry);
169171
advancement::register(&mut dispatcher, registry);
170172
dispatcher
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
use std::collections::BTreeSet;
2+
3+
use pumpkin_data::translation;
4+
use pumpkin_util::PermissionLvl;
5+
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
6+
use pumpkin_util::text::TextComponent;
7+
8+
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
9+
use crate::command::argument_types::core::string::StringArgumentType;
10+
use crate::command::argument_types::entity::EntityArgumentType;
11+
use crate::command::context::command_context::CommandContext;
12+
use crate::command::errors::error_types::CommandErrorType;
13+
use crate::command::node::dispatcher::CommandDispatcher;
14+
use crate::command::node::{CommandExecutor, CommandExecutorResult};
15+
16+
const DESCRIPTION: &str = "Manages the scoreboard tags of entities.";
17+
18+
const PERMISSION: &str = "minecraft:command.tag";
19+
20+
const ARG_TARGETS: &str = "targets";
21+
const ARG_NAME: &str = "name";
22+
23+
const ADD_FAILED_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
24+
translation::java::COMMANDS_TAG_ADD_FAILED,
25+
translation::bedrock::COMMANDS_TAG_ADD_FAILED,
26+
);
27+
28+
const REMOVE_FAILED_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
29+
translation::java::COMMANDS_TAG_REMOVE_FAILED,
30+
translation::bedrock::COMMANDS_TAG_REMOVE_FAILED,
31+
);
32+
33+
#[derive(Clone, Copy)]
34+
enum Action {
35+
Add,
36+
Remove,
37+
}
38+
39+
struct ChangeExecutor(Action);
40+
41+
impl CommandExecutor for ChangeExecutor {
42+
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
43+
Box::pin(async move {
44+
let targets = EntityArgumentType::get_entities(context, ARG_TARGETS).await?;
45+
let tag = StringArgumentType::get(context, ARG_NAME)?.to_owned();
46+
47+
let mut changed = 0;
48+
for target in &targets {
49+
let entity = target.get_entity();
50+
let success = match self.0 {
51+
Action::Add => entity.add_scoreboard_tag(&tag).await,
52+
Action::Remove => entity.remove_scoreboard_tag(&tag).await,
53+
};
54+
if success {
55+
changed += 1;
56+
}
57+
}
58+
59+
if changed == 0 {
60+
return Err(match self.0 {
61+
Action::Add => ADD_FAILED_ERROR_TYPE.create_without_context(),
62+
Action::Remove => REMOVE_FAILED_ERROR_TYPE.create_without_context(),
63+
});
64+
}
65+
66+
let (single_key, multiple_key) = match self.0 {
67+
Action::Add => (
68+
(
69+
translation::java::COMMANDS_TAG_ADD_SUCCESS_SINGLE,
70+
translation::bedrock::COMMANDS_TAG_ADD_SUCCESS_SINGLE,
71+
),
72+
(
73+
translation::java::COMMANDS_TAG_ADD_SUCCESS_MULTIPLE,
74+
translation::bedrock::COMMANDS_TAG_ADD_SUCCESS_MULTIPLE,
75+
),
76+
),
77+
Action::Remove => (
78+
(
79+
translation::java::COMMANDS_TAG_REMOVE_SUCCESS_SINGLE,
80+
translation::bedrock::COMMANDS_TAG_REMOVE_SUCCESS_SINGLE,
81+
),
82+
(
83+
translation::java::COMMANDS_TAG_REMOVE_SUCCESS_MULTIPLE,
84+
translation::bedrock::COMMANDS_TAG_REMOVE_SUCCESS_MULTIPLE,
85+
),
86+
),
87+
};
88+
89+
let msg = if targets.len() == 1 {
90+
TextComponent::translate_cross(
91+
single_key.0,
92+
single_key.1,
93+
[
94+
TextComponent::text(tag),
95+
targets[0].get_display_name().await,
96+
],
97+
)
98+
} else {
99+
TextComponent::translate_cross(
100+
multiple_key.0,
101+
multiple_key.1,
102+
[
103+
TextComponent::text(tag),
104+
TextComponent::text(targets.len().to_string()),
105+
],
106+
)
107+
};
108+
109+
context.source.send_feedback(msg, true).await;
110+
111+
Ok(changed)
112+
})
113+
}
114+
}
115+
116+
struct ListExecutor;
117+
118+
impl CommandExecutor for ListExecutor {
119+
fn execute<'a>(&'a self, context: &'a CommandContext) -> CommandExecutorResult<'a> {
120+
Box::pin(async move {
121+
let targets = EntityArgumentType::get_entities(context, ARG_TARGETS).await?;
122+
123+
// BTreeSet keeps the output deterministic.
124+
let mut all_tags = BTreeSet::new();
125+
for target in &targets {
126+
let tags = target.get_entity().scoreboard_tags.lock().await;
127+
all_tags.extend(tags.iter().cloned());
128+
}
129+
130+
let tag_list =
131+
TextComponent::text(all_tags.iter().cloned().collect::<Vec<String>>().join(", "));
132+
133+
let msg = if targets.len() == 1 {
134+
let name = targets[0].get_display_name().await;
135+
if all_tags.is_empty() {
136+
TextComponent::translate_cross(
137+
translation::java::COMMANDS_TAG_LIST_SINGLE_EMPTY,
138+
translation::bedrock::COMMANDS_TAG_LIST_SINGLE_EMPTY,
139+
[name],
140+
)
141+
} else {
142+
TextComponent::translate_cross(
143+
translation::java::COMMANDS_TAG_LIST_SINGLE_SUCCESS,
144+
translation::bedrock::COMMANDS_TAG_LIST_SINGLE_SUCCESS,
145+
[
146+
name,
147+
TextComponent::text(all_tags.len().to_string()),
148+
tag_list,
149+
],
150+
)
151+
}
152+
} else {
153+
let count = TextComponent::text(targets.len().to_string());
154+
if all_tags.is_empty() {
155+
TextComponent::translate_cross(
156+
translation::java::COMMANDS_TAG_LIST_MULTIPLE_EMPTY,
157+
translation::bedrock::COMMANDS_TAG_LIST_MULTIPLE_EMPTY,
158+
[count],
159+
)
160+
} else {
161+
TextComponent::translate_cross(
162+
translation::java::COMMANDS_TAG_LIST_MULTIPLE_SUCCESS,
163+
translation::bedrock::COMMANDS_TAG_LIST_MULTIPLE_SUCCESS,
164+
[
165+
count,
166+
TextComponent::text(all_tags.len().to_string()),
167+
tag_list,
168+
],
169+
)
170+
}
171+
};
172+
173+
context.source.send_feedback(msg, false).await;
174+
175+
Ok(all_tags.len() as i32)
176+
})
177+
}
178+
}
179+
180+
pub fn register(dispatcher: &mut CommandDispatcher, registry: &mut PermissionRegistry) {
181+
registry.register_permission_or_panic(Permission::new(
182+
PERMISSION,
183+
DESCRIPTION,
184+
PermissionDefault::Op(PermissionLvl::Two),
185+
));
186+
187+
dispatcher.register(
188+
command("tag", DESCRIPTION).requires(PERMISSION).then(
189+
argument(ARG_TARGETS, EntityArgumentType::Entities)
190+
.then(
191+
literal("add").then(
192+
argument(ARG_NAME, StringArgumentType::SingleWord)
193+
.executes(ChangeExecutor(Action::Add)),
194+
),
195+
)
196+
.then(literal("list").executes(ListExecutor))
197+
.then(
198+
literal("remove").then(
199+
argument(ARG_NAME, StringArgumentType::SingleWord)
200+
.executes(ChangeExecutor(Action::Remove)),
201+
),
202+
),
203+
),
204+
);
205+
}

pumpkin/src/entity/mod.rs

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ use pumpkin_util::math::{
6969
use pumpkin_util::text::TextComponent;
7070
use pumpkin_util::text::hover::HoverEvent;
7171
use serde::Serialize;
72-
use std::collections::BTreeMap;
72+
use std::collections::{BTreeMap, HashSet};
7373
use std::pin::Pin;
7474
use std::sync::{
7575
Arc,
@@ -105,6 +105,9 @@ pub mod vehicle;
105105
mod combat;
106106
pub mod predicate;
107107

108+
/// The maximum number of scoreboard tags an entity can carry, matching Vanilla.
109+
pub const MAX_SCOREBOARD_TAGS: usize = 1024;
110+
108111
/// Returns the [`EntityStatus`] that should be broadcast when the given
109112
/// equipment slot breaks.
110113
#[must_use]
@@ -800,6 +803,9 @@ pub struct Entity {
800803
pub custom_name: ArcSwap<Option<TextComponent>>,
801804
/// Indicates whether the entity's custom name is visible
802805
pub custom_name_visible: AtomicBool,
806+
/// Scoreboard tags attached to this entity, managed with `/tag`.
807+
/// Vanilla allows at most [`MAX_SCOREBOARD_TAGS`] tags per entity.
808+
pub scoreboard_tags: Mutex<HashSet<String>>,
803809
/// The data send in the Entity Spawn packet
804810
pub data: AtomicI32,
805811
/// Stores entity boolean flags (on fire, sneaking, invisible, glowing, etc.)
@@ -933,6 +939,7 @@ impl Entity {
933939
portal_manager: Mutex::new(None),
934940
custom_name: ArcSwap::new(Arc::new(None)),
935941
custom_name_visible: AtomicBool::new(false),
942+
scoreboard_tags: Mutex::new(HashSet::new()),
936943
no_clip: AtomicBool::new(false),
937944
movement_multiplier: AtomicCell::new(Vector3::default()),
938945
velocity_dirty: AtomicBool::new(true),
@@ -1017,6 +1024,22 @@ impl Entity {
10171024
self.age.store(age, Relaxed);
10181025
}
10191026

1027+
/// Adds a scoreboard tag to this entity.
1028+
///
1029+
/// Returns `false` if the entity already has the tag or already carries
1030+
/// [`MAX_SCOREBOARD_TAGS`] tags.
1031+
pub async fn add_scoreboard_tag(&self, tag: &str) -> bool {
1032+
let mut tags = self.scoreboard_tags.lock().await;
1033+
tags.len() < MAX_SCOREBOARD_TAGS && tags.insert(tag.to_owned())
1034+
}
1035+
1036+
/// Removes a scoreboard tag from this entity.
1037+
///
1038+
/// Returns `false` if the entity did not have the tag.
1039+
pub async fn remove_scoreboard_tag(&self, tag: &str) -> bool {
1040+
self.scoreboard_tags.lock().await.remove(tag)
1041+
}
1042+
10201043
/// Sets a custom name for the entity, typically used with nametags
10211044
pub fn set_custom_name(&self, name: TextComponent) {
10221045
self.custom_name.store(Arc::new(Some(name.clone())));
@@ -3386,6 +3409,18 @@ impl NBTStorage for Entity {
33863409
}
33873410
nbt.put_bool("CustomNameVisible", self.custom_name_visible.load(Relaxed));
33883411

3412+
let tags = self.scoreboard_tags.lock().await;
3413+
if !tags.is_empty() {
3414+
nbt.put(
3415+
"Tags",
3416+
NbtTag::List(
3417+
tags.iter()
3418+
.map(|tag| NbtTag::String(tag.as_str().into()))
3419+
.collect(),
3420+
),
3421+
);
3422+
}
3423+
33893424
// todo more...
33903425
})
33913426
}
@@ -3433,6 +3468,18 @@ impl NBTStorage for Entity {
34333468
}
34343469
self.custom_name_visible
34353470
.store(nbt.get_bool("CustomNameVisible").unwrap_or(false), Relaxed);
3471+
3472+
if let Some(tag_list) = nbt.get_list("Tags") {
3473+
let mut tags = self.scoreboard_tags.lock().await;
3474+
tags.clear();
3475+
tags.extend(
3476+
tag_list
3477+
.iter()
3478+
.filter_map(|tag| tag.extract_string().map(str::to_owned))
3479+
.take(MAX_SCOREBOARD_TAGS),
3480+
);
3481+
}
3482+
34363483
// todo more...
34373484
})
34383485
}

0 commit comments

Comments
 (0)