Alexander Medvedev 2026-09-02 13:24:23 +02:00
parent 2be3e8be88
commit d1cfc0d181
41 changed files with 2754 additions and 542 deletions

1
.gitignore vendored
View File

@ -157,3 +157,4 @@ version_history.json
world_nether/
world_the_end/
patchbukkit/
minecraft-sources*/

View File

@ -352,18 +352,33 @@ impl DataComponentImpl for CustomModelDataImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct TooltipDisplayImpl;
impl TooltipDisplayImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for TooltipDisplayImpl {
default_impl!(TooltipDisplay);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct CreativeSlotLockImpl;
impl CreativeSlotLockImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for CreativeSlotLockImpl {
default_impl!(CreativeSlotLock);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct EnchantmentGlintOverrideImpl;
impl EnchantmentGlintOverrideImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for EnchantmentGlintOverrideImpl {
default_impl!(EnchantmentGlintOverride);
}
@ -431,18 +446,33 @@ impl DataComponentImpl for BaseColorImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct InstrumentImpl;
impl InstrumentImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for InstrumentImpl {
default_impl!(Instrument);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct ProvidesTrimMaterialImpl;
impl ProvidesTrimMaterialImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for ProvidesTrimMaterialImpl {
default_impl!(ProvidesTrimMaterial);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct ProvidesBannerPatternsImpl;
impl ProvidesBannerPatternsImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for ProvidesBannerPatternsImpl {
default_impl!(ProvidesBannerPatterns);
}
@ -495,6 +525,11 @@ impl DataComponentImpl for BannerPatternsImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct PotDecorationsImpl;
impl PotDecorationsImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for PotDecorationsImpl {
default_impl!(PotDecorations);
}
@ -522,6 +557,11 @@ impl DataComponentImpl for LockImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BreakSoundImpl;
impl BreakSoundImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for BreakSoundImpl {
default_impl!(BreakSound);
}

View File

@ -26,12 +26,22 @@ impl DataComponentImpl for BlockEntityDataImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct EntityDataImpl;
impl EntityDataImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for EntityDataImpl {
default_impl!(EntityData);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BucketEntityDataImpl;
impl BucketEntityDataImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for BucketEntityDataImpl {
default_impl!(BucketEntityData);
}
@ -148,6 +158,11 @@ impl DataComponentImpl for BlockStateImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BeesImpl;
impl BeesImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for BeesImpl {
default_impl!(Bees);
}
@ -177,6 +192,11 @@ impl DataComponentImpl for ContainerLootImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct SulfurCubeContentImpl;
impl SulfurCubeContentImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for SulfurCubeContentImpl {
default_impl!(SulfurCubeContent);
}

View File

@ -90,6 +90,11 @@ impl DataComponentImpl for WrittenBookContentImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct DebugStickStateImpl;
impl DebugStickStateImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for DebugStickStateImpl {
default_impl!(DebugStickState);
}

View File

@ -48,6 +48,13 @@ impl Hash for Modifier {
pub struct AttributeModifiersImpl {
pub attribute_modifiers: Cow<'static, [Modifier]>,
}
impl AttributeModifiersImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self {
attribute_modifiers: Cow::Borrowed(&[]),
})
}
}
impl DataComponentImpl for AttributeModifiersImpl {
default_impl!(AttributeModifiers);
}
@ -742,12 +749,22 @@ impl DataComponentImpl for GliderImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct DeathProtectionImpl;
impl DeathProtectionImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for DeathProtectionImpl {
default_impl!(DeathProtection);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct BlocksAttacksImpl;
impl BlocksAttacksImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for BlocksAttacksImpl {
default_impl!(BlocksAttacks);
}
@ -1037,6 +1054,11 @@ impl DataComponentImpl for SwingAnimationImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct AdditionalTradeCostImpl;
impl AdditionalTradeCostImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for AdditionalTradeCostImpl {
default_impl!(AdditionalTradeCost);
}

View File

@ -384,12 +384,22 @@ impl Hash for ConsumableImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct UseEffectsImpl;
impl UseEffectsImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for UseEffectsImpl {
default_impl!(UseEffects);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct UseRemainderImpl;
impl UseRemainderImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for UseRemainderImpl {
default_impl!(UseRemainder);
}

View File

@ -652,7 +652,41 @@ pub fn read_data(id: DataComponent, data: &NbtTag) -> Option<Box<dyn DataCompone
DataComponent::SwingAnimation => Some(SwingAnimationImpl::read_data(data)?.to_dyn()),
DataComponent::Rarity => Some(RarityImpl::read_data(data)?.to_dyn()),
DataComponent::BannerPatterns => Some(BannerPatternsImpl::read_data(data)?.to_dyn()),
_ => None,
DataComponent::UseEffects => Some(UseEffectsImpl::read_data(data)?.to_dyn()),
DataComponent::UseRemainder => Some(UseRemainderImpl::read_data(data)?.to_dyn()),
DataComponent::Weapon => Some(WeaponImpl::read_data(data)?.to_dyn()),
DataComponent::TooltipDisplay => Some(TooltipDisplayImpl::read_data(data)?.to_dyn()),
DataComponent::CreativeSlotLock => Some(CreativeSlotLockImpl::read_data(data)?.to_dyn()),
DataComponent::EnchantmentGlintOverride => {
Some(EnchantmentGlintOverrideImpl::read_data(data)?.to_dyn())
}
DataComponent::DeathProtection => Some(DeathProtectionImpl::read_data(data)?.to_dyn()),
DataComponent::BlocksAttacks => Some(BlocksAttacksImpl::read_data(data)?.to_dyn()),
DataComponent::AdditionalTradeCost => {
Some(AdditionalTradeCostImpl::read_data(data)?.to_dyn())
}
DataComponent::Dye => Some(DyeImpl::read_data(data)?.to_dyn()),
DataComponent::MapColor => Some(MapColorImpl::read_data(data)?.to_dyn()),
DataComponent::MapDecorations => Some(MapDecorationsImpl::read_data(data)?.to_dyn()),
DataComponent::DebugStickState => Some(DebugStickStateImpl::read_data(data)?.to_dyn()),
DataComponent::EntityData => Some(EntityDataImpl::read_data(data)?.to_dyn()),
DataComponent::BucketEntityData => Some(BucketEntityDataImpl::read_data(data)?.to_dyn()),
DataComponent::Instrument => Some(InstrumentImpl::read_data(data)?.to_dyn()),
DataComponent::ProvidesTrimMaterial => {
Some(ProvidesTrimMaterialImpl::read_data(data)?.to_dyn())
}
DataComponent::JukeboxPlayable => Some(JukeboxPlayableImpl::read_data(data)?.to_dyn()),
DataComponent::ProvidesBannerPatterns => {
Some(ProvidesBannerPatternsImpl::read_data(data)?.to_dyn())
}
DataComponent::Recipes => Some(RecipesImpl::read_data(data)?.to_dyn()),
DataComponent::PotDecorations => Some(PotDecorationsImpl::read_data(data)?.to_dyn()),
DataComponent::Bees => Some(BeesImpl::read_data(data)?.to_dyn()),
DataComponent::SulfurCubeContent => Some(SulfurCubeContentImpl::read_data(data)?.to_dyn()),
DataComponent::BreakSound => Some(BreakSoundImpl::read_data(data)?.to_dyn()),
DataComponent::AttributeModifiers => {
Some(AttributeModifiersImpl::read_data(data)?.to_dyn())
}
}
}

View File

@ -6,6 +6,11 @@ use pumpkin_nbt::tag::NbtTag;
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct DyeImpl;
impl DyeImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for DyeImpl {
default_impl!(Dye);
}
@ -31,6 +36,11 @@ impl DataComponentImpl for DyedColorImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct MapColorImpl;
impl MapColorImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for MapColorImpl {
default_impl!(MapColor);
}
@ -56,6 +66,11 @@ impl DataComponentImpl for MapIdImpl {
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct MapDecorationsImpl;
impl MapDecorationsImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for MapDecorationsImpl {
default_impl!(MapDecorations);
}
@ -590,12 +605,28 @@ impl DataComponentImpl for ProfileImpl {
pub struct JukeboxPlayableImpl {
pub song: &'static str,
}
impl JukeboxPlayableImpl {
pub fn read_data(data: &NbtTag) -> Option<Self> {
let compound = data.extract_compound()?;
let song = compound.get_string("song")?;
let static_song = crate::jukebox_song::JukeboxSong::from_name(
song.strip_prefix("minecraft:").unwrap_or(song),
)
.map_or("", |s| s.to_name());
Some(Self { song: static_song })
}
}
impl DataComponentImpl for JukeboxPlayableImpl {
default_impl!(JukeboxPlayable);
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub struct RecipesImpl;
impl RecipesImpl {
pub const fn read_data(_data: &NbtTag) -> Option<Self> {
Some(Self)
}
}
impl DataComponentImpl for RecipesImpl {
default_impl!(Recipes);
}

File diff suppressed because it is too large Load Diff

View File

@ -49,8 +49,17 @@ impl GameProfileResult {
///
/// Instead, call this method *before* using `write()`/`read()` on a lock.
pub fn resolve(&self, source: &CommandSource) -> Result<Vec<GameProfile>, CommandSyntaxError> {
let players = match self {
Self::Selector(selector) => selector.find_players(source),
match self {
Self::Selector(selector) => {
let players = selector.find_players(source)?;
if players.is_empty() {
return Err(
crate::command::argument_types::entity::NO_PLAYERS_ERROR_TYPE
.create_without_context(),
);
}
Ok(players.iter().map(|p| p.gameprofile.clone()).collect())
}
Self::Name(name) => {
let server = source.server();
if let Some(player) = server.get_player_by_name(name) {
@ -72,7 +81,7 @@ impl GameProfileResult {
}
if server.advanced_config.networking.java.online_mode {
return match lookup_profile_by_name_blocking(
match lookup_profile_by_name_blocking(
name,
&server.advanced_config.networking.java.authentication,
) {
@ -86,7 +95,7 @@ impl GameProfileResult {
Ok(vec![Self::profile_from_uuid_name(uuid, resolved_name)])
}
_ => Err(Self::unknown_player_syntax_error()),
};
}
} else if let Ok(uuid) = offline_uuid(name) {
let profile = Self::profile_from_uuid_name(uuid, name.clone());
server
@ -95,10 +104,10 @@ impl GameProfileResult {
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.upsert(profile.id, profile.name.clone());
return Ok(vec![profile]);
Ok(vec![profile])
} else {
Err(Self::unknown_player_syntax_error())
}
return Err(Self::unknown_player_syntax_error());
}
Self::Uuid(uuid) => {
let server = source.server();
@ -121,11 +130,9 @@ impl GameProfileResult {
return Ok(vec![profile]);
}
return Err(Self::unknown_player_syntax_error());
Err(Self::unknown_player_syntax_error())
}
}
}?;
Ok(players.iter().map(|p| &p.gameprofile).cloned().collect())
}
fn resolve_known_profile_by_name(server: &Server, name: &str) -> Option<GameProfile> {

View File

@ -28,7 +28,6 @@ const ARG_TARGETS: &str = "targets";
const ARG_ADVANCEMENT: &str = "advancement";
const ARG_CRITERION: &str = "criterion";
#[allow(unused)]
const ERROR_CRITERION_NOT_FOUND: CommandErrorType<2> = CommandErrorType::new(
translation::java::COMMANDS_ADVANCEMENT_CRITERIONNOTFOUND,
translation::java::COMMANDS_ADVANCEMENT_CRITERIONNOTFOUND,
@ -112,7 +111,7 @@ impl Action {
true
}
Self::Revoke => {
if !progress.is_done() {
if !progress.has_progress() {
return false;
}
let criteria: Vec<Arc<str>> = progress.get_completed_criteria().collect();
@ -306,11 +305,16 @@ fn perform(
advancements: &[&'static Advancement],
show_advancement: bool,
) -> Result<i32, CommandSyntaxError> {
let mut i = 0;
let mut advancement_count = 0;
let mut player_count = 0;
for player in targets {
i += action.perform(player, advancements, show_advancement);
let changed = action.perform(player, advancements, show_advancement);
if changed > 0 {
player_count += 1;
}
if i == 0 {
advancement_count += changed;
}
if advancement_count == 0 {
return if let [first_advancement] = advancements[..] {
if let [first_player] = targets {
Err(match action {
@ -362,7 +366,7 @@ fn perform(
format!("{}.one.to.many.success", action.get_key()),
[
first_advancement.name(),
TextComponent::text(targets.len().to_string()),
TextComponent::text(player_count.to_string()),
],
)
}
@ -370,7 +374,7 @@ fn perform(
TextComponent::translate(
format!("{}.many.to.one.success", action.get_key()),
[
TextComponent::text(advancements.len().to_string()),
TextComponent::text(advancement_count.to_string()),
first.get_display_name(),
],
)
@ -378,13 +382,13 @@ fn perform(
TextComponent::translate(
format!("{}.many.to.many.success", action.get_key()),
[
TextComponent::text(advancements.len().to_string()),
TextComponent::text(targets.len().to_string()),
TextComponent::text(advancement_count.to_string()),
TextComponent::text(player_count.to_string()),
],
)
};
context.send_feedback(translate, true);
Ok(i)
Ok(advancement_count)
}
/// Performs an action (grant or revoke) on a specific advancement criterion for multiple players.
@ -414,12 +418,21 @@ pub fn perform_criterion(
advancement: &'static Advancement,
criterion: &str,
) -> Result<i32, CommandSyntaxError> {
if advancement.criteria.contains(&criterion) {
if !advancement.criteria.contains(&criterion) {
return Err(
ERROR_CRITERION_NOT_FOUND.create_without_context_args_slice(&[
advancement.name(),
TextComponent::text(criterion.to_owned()),
]),
);
}
let count = targets
.iter()
.map(|player| action.perform_criterion(player, advancement, criterion))
.filter(|&success| success)
.count() as i32;
if count == 0 {
if let [first_player] = targets {
Err(match action {
@ -458,21 +471,13 @@ pub fn perform_criterion(
[
TextComponent::text(criterion.to_owned()),
advancement.name(),
TextComponent::text(targets.len().to_string()),
TextComponent::text(count.to_string()),
],
)
};
context.send_feedback(translate, true);
Ok(count)
}
} else {
Err(
ERROR_CRITERION_NOT_FOUND.create_without_context_args_slice(&[
advancement.name(),
TextComponent::text(criterion.to_owned()),
]),
)
}
}
/// use for when a criterion is specified to on grant/revoke this criterion
@ -537,17 +542,19 @@ impl SuggestionProvider for CriterionSuggestionProvider {
/// executor to grant/revoke every advancement to specified players
struct EveryAdvancementExecutor {
action: Action,
show_advancement: bool,
}
impl CommandExecutor for EveryAdvancementExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let action = self.action;
let show_advancement = self.show_advancement;
perform(
&context.source,
&EntityArgumentType::get_players(context, ARG_TARGETS)?,
action,
&Advancement::get_advancements_list(),
false,
show_advancement,
)
}
}
@ -607,10 +614,10 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
}),
),
)
.then(
literal("everything")
.executes(EveryAdvancementExecutor { action: $action }),
),
.then(literal("everything").executes(EveryAdvancementExecutor {
action: $action,
show_advancement: matches!($action, Action::Revoke),
})),
)
};
}

View File

@ -80,7 +80,7 @@ struct BanExecutor {
impl CommandExecutor for BanExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let targets = GameProfileArgumentType::get(context, "player")?;
let targets = GameProfileArgumentType::get(context, "targets")?;
let reason = if self.has_reason {
Some(StringArgumentType::get(context, "reason")?.to_string())
} else {
@ -111,7 +111,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
dispatcher.register(
command("ban", DESCRIPTION).requires(PERMISSION).then(
argument("player", GameProfileArgumentType)
argument("targets", GameProfileArgumentType)
.executes(BanExecutor { has_reason: false })
.then(
argument("reason", StringArgumentType::GreedyPhrase)

View File

@ -14,6 +14,7 @@ use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::data::SaveJSONConfiguration;
use crate::data::banlist_serializer::BannedIpEntry;
use crate::entity::EntityBase;
use crate::net::DisconnectReason;
use crate::server::Server;
@ -80,6 +81,26 @@ fn ban_ip(context: &CommandContext, target: &str, reason: Option<String>) -> Com
.cloned()
.collect();
let kick_count = players_to_kick.len();
if !players_to_kick.is_empty() {
let player_names = players_to_kick
.iter()
.map(|p| p.get_display_name())
.reduce(|acc, name| {
TextComponent::text(format!("{}, {}", acc.get_text(), name.get_text()))
})
.unwrap_or_else(TextComponent::empty);
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_BANIP_INFO,
translation::java::COMMANDS_BANIP_INFO,
[TextComponent::text(kick_count.to_string()), player_names],
),
true,
);
}
for player in players_to_kick {
let kick_msg = TextComponent::translate_cross(
translation::java::MULTIPLAYER_DISCONNECT_IP_BANNED,
@ -89,7 +110,7 @@ fn ban_ip(context: &CommandContext, target: &str, reason: Option<String>) -> Com
player.kick(DisconnectReason::Kicked, &kick_msg);
}
Ok(1)
Ok(kick_count as i32)
}
struct BanIpExecutor {
@ -98,7 +119,7 @@ struct BanIpExecutor {
impl CommandExecutor for BanIpExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let target = StringArgumentType::get(context, "ip")?;
let target = StringArgumentType::get(context, "target")?;
let reason = if self.has_reason {
Some(StringArgumentType::get(context, "reason")?.to_string())
} else {
@ -117,7 +138,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
));
let cmd = command("ban-ip", DESCRIPTION).requires(PERMISSION).then(
argument("ip", StringArgumentType::SingleWord)
argument("target", StringArgumentType::SingleWord)
.executes(BanIpExecutor { has_reason: false })
.then(
argument("reason", StringArgumentType::GreedyPhrase)

View File

@ -278,7 +278,7 @@ impl CommandExecutor for GetExecutor {
TextComponent::text(bossbar.max.to_string()),
],
),
false,
true,
);
Ok(bossbar.max)
}
@ -298,7 +298,7 @@ impl CommandExecutor for GetExecutor {
translation::bedrock::COMMANDS_BOSSBAR_GET_PLAYERS_NONE,
[bossbar_prefix(bossbar.bossbar_data.title, namespace)],
),
false,
true,
);
} else {
context.source.send_feedback(
@ -315,7 +315,7 @@ impl CommandExecutor for GetExecutor {
TextComponent::text(online_players.join(", ")),
],
),
false,
true,
);
}
Ok(count)
@ -330,7 +330,7 @@ impl CommandExecutor for GetExecutor {
TextComponent::text(bossbar.value.to_string()),
],
),
false,
true,
);
Ok(bossbar.value)
}
@ -355,7 +355,7 @@ impl CommandExecutor for GetExecutor {
namespace,
)],
),
false,
true,
);
Ok(bossbar.visible as i32)
}
@ -509,7 +509,7 @@ impl CommandExecutor for SetExecutor {
true,
);
Ok(0)
Ok(max_value)
}
CommandValueSet::Name => {
let name = ComponentArgumentType::get(context, "name")?;

View File

@ -85,7 +85,7 @@ impl CommandExecutor for DamageEntityExecutor {
let source = match self.0 {
EntityMode::WithSource | EntityMode::WithSourceAndCause => {
Some(EntityArgumentType::get_entity(context, "source")?)
Some(EntityArgumentType::get_entity(context, "entity")?)
}
EntityMode::Basic | EntityMode::WithType => None,
};
@ -133,7 +133,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
)
.then(
literal("by").then(
argument("source", EntityArgumentType::Entity)
argument("entity", EntityArgumentType::Entity)
.executes(DamageEntityExecutor(EntityMode::WithSource))
.then(literal("from").then(
argument("cause", EntityArgumentType::Entity).executes(

View File

@ -8,6 +8,8 @@ use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
use crate::command::suggestion::suggestions::SuggestionsBuilder;
use crate::data::SaveJSONConfiguration;
const DESCRIPTION: &str = "Revokes operator status from a player.";
@ -52,18 +54,35 @@ impl CommandExecutor for DeopExecutor {
}
}
if succeeded_deops > 0 {
config.save();
}
if succeeded_deops == 0 {
Err(ERROR_DEOP_FAILED.create_without_context())
} else {
config.save();
drop(config);
crate::command::commands::whitelist::kick_non_whitelisted_players(server);
Ok(succeeded_deops)
}
}
}
struct DeopSuggestionProvider;
impl SuggestionProvider for DeopSuggestionProvider {
fn suggest(
&self,
context: &CommandContext,
mut builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let ops = context.server().data.operator_config.read().unwrap();
for op in &ops.ops {
builder = builder.suggest(op.name.clone());
}
builder.build()
}
}
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
registry.register_permission_or_panic(Permission::new(
PERMISSION,
@ -72,8 +91,10 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
));
dispatcher.register(
command("deop", DESCRIPTION)
.requires(PERMISSION)
.then(argument("targets", GameProfileArgumentType).executes(DeopExecutor)),
command("deop", DESCRIPTION).requires(PERMISSION).then(
argument("targets", GameProfileArgumentType)
.suggests(DeopSuggestionProvider)
.executes(DeopExecutor),
),
);
}

View File

@ -134,7 +134,7 @@ impl ExperienceExecutor {
target.add_experience_points(amount);
} else {
let current_lvl = target.experience_level.load(Ordering::Relaxed);
if amount > experience::points_in_level(current_lvl) {
if amount >= experience::points_in_level(current_lvl) {
return false;
}
target.set_experience_points(amount);
@ -147,7 +147,7 @@ impl ExperienceExecutor {
impl CommandExecutor for ExperienceExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let targets = EntityArgumentType::get_players(context, "targets")?;
let targets = EntityArgumentType::get_players(context, "target")?;
if self.mode == Mode::Query {
let target = targets[0].clone();
@ -163,7 +163,7 @@ impl CommandExecutor for ExperienceExecutor {
}
}
if successes == 0 {
if self.mode == Mode::Set && successes == 0 {
return Err(ERROR_SET_POINTS_INVALID.create_without_context());
}
@ -171,7 +171,7 @@ impl CommandExecutor for ExperienceExecutor {
let msg = self.get_success_message(amount, &targets, first_name);
context.source.send_feedback(msg, true);
Ok(successes)
Ok(targets.len() as i32)
}
}
@ -183,50 +183,50 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
));
let add_node = literal("add").then(
argument("targets", EntityArgumentType::Players).then(
argument("amount", IntegerArgumentType::with_min(0))
argument("target", EntityArgumentType::Players).then(
argument("amount", IntegerArgumentType::any())
.executes(ExperienceExecutor {
mode: Mode::Add,
exp_type: ExpType::Points,
})
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Add,
exp_type: ExpType::Levels,
}))
.then(literal("points").executes(ExperienceExecutor {
mode: Mode::Add,
exp_type: ExpType::Points,
}))
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Add,
exp_type: ExpType::Levels,
})),
),
);
let set_node = literal("set").then(
argument("targets", EntityArgumentType::Players).then(
argument("target", EntityArgumentType::Players).then(
argument("amount", IntegerArgumentType::with_min(0))
.executes(ExperienceExecutor {
mode: Mode::Set,
exp_type: ExpType::Points,
})
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Set,
exp_type: ExpType::Levels,
}))
.then(literal("points").executes(ExperienceExecutor {
mode: Mode::Set,
exp_type: ExpType::Points,
}))
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Set,
exp_type: ExpType::Levels,
})),
),
);
let query_node = literal("query").then(
argument("targets", EntityArgumentType::Player)
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Query,
exp_type: ExpType::Levels,
}))
argument("target", EntityArgumentType::Player)
.then(literal("points").executes(ExperienceExecutor {
mode: Mode::Query,
exp_type: ExpType::Points,
}))
.then(literal("levels").executes(ExperienceExecutor {
mode: Mode::Query,
exp_type: ExpType::Levels,
})),
);

View File

@ -1,4 +1,4 @@
use crate::command::argument_builder::{ArgumentBuilder, argument, command};
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_types::coordinates::block_pos::{
BlockPosArgumentType, OUT_OF_BOUNDS_ERROR_TYPE,
};
@ -67,7 +67,7 @@ impl CommandExecutor for FillBiomeExecutor {
let target_biome = ResourceKeyArgument::get_biome(context, "biome")?;
let replace_biome = if self.has_replace {
Some(ResourceKeyArgument::get_biome(context, "replace_biome")?)
Some(ResourceKeyArgument::get_biome(context, "filter")?)
} else {
None
};
@ -126,11 +126,19 @@ impl CommandExecutor for FillBiomeExecutor {
let msg = TextComponent::translate_cross(
translation::java::COMMANDS_FILLBIOME_SUCCESS_COUNT,
translation::java::COMMANDS_FILLBIOME_SUCCESS_COUNT,
[TextComponent::text(changed_count.to_string())],
[
TextComponent::text(changed_count.to_string()),
TextComponent::text(min_x.to_string()),
TextComponent::text(min_y.to_string()),
TextComponent::text(min_z.to_string()),
TextComponent::text(max_x.to_string()),
TextComponent::text(max_y.to_string()),
TextComponent::text(max_z.to_string()),
],
);
context.source.send_feedback(msg, true);
Ok(1)
Ok(changed_count)
}
}
@ -147,11 +155,13 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
argument("biome", ResourceKeyArgument(BIOME_REGISTRY))
.executes(FillBiomeExecutor { has_replace: false })
.then(
argument("replace_biome", ResourceKeyArgument(BIOME_REGISTRY))
literal("replace").then(
argument("filter", ResourceKeyArgument(BIOME_REGISTRY))
.executes(FillBiomeExecutor { has_replace: true }),
),
),
),
),
);
dispatcher.register(builder);

View File

@ -10,7 +10,6 @@ use pumpkin_util::PermissionLvl;
use pumpkin_util::math::vector2::Vector2;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;
use pumpkin_util::text::color::{Color, NamedColor};
const DESCRIPTION: &str = "Constantly load chunks in the world.";
const PERMISSION: &str = "minecraft:command.forceload";
@ -86,13 +85,15 @@ impl CommandExecutor for ForceloadAddExecutor {
world.update_active_chunks();
let dimension_name = world.dimension.minecraft_name.to_string();
let text = if total_chunks == 1 {
TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_ADDED_SINGLE,
translation::java::COMMANDS_FORCELOAD_ADDED_SINGLE,
[
TextComponent::text(min_x.to_string()),
TextComponent::text(min_z.to_string()),
TextComponent::text(format!("[{min_x}, {min_z}]")),
TextComponent::text(dimension_name),
],
)
} else {
@ -101,16 +102,13 @@ impl CommandExecutor for ForceloadAddExecutor {
translation::java::COMMANDS_FORCELOAD_ADDED_MULTIPLE,
[
TextComponent::text(total_chunks.to_string()),
TextComponent::text(min_x.to_string()),
TextComponent::text(min_z.to_string()),
TextComponent::text(max_x.to_string()),
TextComponent::text(max_z.to_string()),
TextComponent::text(dimension_name),
TextComponent::text(format!("[{min_x}, {min_z}]")),
TextComponent::text(format!("[{max_x}, {max_z}]")),
],
)
};
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Green)), false);
context.source.send_feedback(text, true);
Ok(total_chunks)
}
@ -170,13 +168,15 @@ impl CommandExecutor for ForceloadRemoveExecutor {
world.update_active_chunks();
let dimension_name = world.dimension.minecraft_name.to_string();
let text = if total_chunks == 1 {
TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_REMOVED_SINGLE,
translation::java::COMMANDS_FORCELOAD_REMOVED_SINGLE,
[
TextComponent::text(min_x.to_string()),
TextComponent::text(min_z.to_string()),
TextComponent::text(format!("[{min_x}, {min_z}]")),
TextComponent::text(dimension_name),
],
)
} else {
@ -185,16 +185,13 @@ impl CommandExecutor for ForceloadRemoveExecutor {
translation::java::COMMANDS_FORCELOAD_REMOVED_MULTIPLE,
[
TextComponent::text(total_chunks.to_string()),
TextComponent::text(min_x.to_string()),
TextComponent::text(min_z.to_string()),
TextComponent::text(max_x.to_string()),
TextComponent::text(max_z.to_string()),
TextComponent::text(dimension_name),
TextComponent::text(format!("[{min_x}, {min_z}]")),
TextComponent::text(format!("[{max_x}, {max_z}]")),
],
)
};
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Green)), false);
context.source.send_feedback(text, true);
Ok(total_chunks)
}
@ -222,14 +219,14 @@ impl CommandExecutor for ForceloadRemoveAllExecutor {
world.update_active_chunks();
let dimension_name = world.dimension.minecraft_name.to_string();
let text = TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_REMOVED_ALL,
translation::java::COMMANDS_FORCELOAD_REMOVED_ALL,
[],
[TextComponent::text(dimension_name)],
);
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Green)), false);
context.source.send_feedback(text, true);
Ok(removed_count as i32)
}
@ -262,30 +259,28 @@ impl CommandExecutor for ForceloadQueryExecutor {
forced.contains(&chunk_pos)
};
let dimension_name = world.dimension.minecraft_name.to_string();
if is_forced {
let text = TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_QUERY_SUCCESS,
translation::java::COMMANDS_FORCELOAD_QUERY_SUCCESS,
[
TextComponent::text(chunk_pos.x.to_string()),
TextComponent::text(chunk_pos.y.to_string()),
TextComponent::text(format!("[{}, {}]", chunk_pos.x, chunk_pos.y)),
TextComponent::text(dimension_name),
],
);
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Green)), false);
context.source.send_feedback(text, false);
} else {
let text = TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_QUERY_FAILURE,
translation::java::COMMANDS_FORCELOAD_QUERY_FAILURE,
[
TextComponent::text(chunk_pos.x.to_string()),
TextComponent::text(chunk_pos.y.to_string()),
TextComponent::text(format!("[{}, {}]", chunk_pos.x, chunk_pos.y)),
TextComponent::text(dimension_name),
],
);
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Red)), false);
context.source.send_error(text);
}
let all_forced = {
@ -303,29 +298,34 @@ impl CommandExecutor for ForceloadQueryExecutor {
let text = TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_ADDED_NONE,
translation::java::COMMANDS_FORCELOAD_ADDED_NONE,
[],
[TextComponent::text(
world.dimension.minecraft_name.to_string(),
)],
);
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Gray)), false);
context.source.send_error(text);
} else {
let list_str = all_forced.join(", ");
let text = if all_forced.len() == 1 {
TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_LIST_SINGLE,
translation::java::COMMANDS_FORCELOAD_LIST_SINGLE,
[TextComponent::text(list_str)],
[
TextComponent::text(world.dimension.minecraft_name.to_string()),
TextComponent::text(list_str),
],
)
} else {
TextComponent::translate_cross(
translation::java::COMMANDS_FORCELOAD_LIST_MULTIPLE,
translation::java::COMMANDS_FORCELOAD_LIST_MULTIPLE,
[TextComponent::text(list_str)],
[
TextComponent::text(all_forced.len().to_string()),
TextComponent::text(world.dimension.minecraft_name.to_string()),
TextComponent::text(list_str),
],
)
};
context
.source
.send_feedback(text.color(Color::Named(NamedColor::Gray)), false);
context.source.send_feedback(text, false);
}
Ok(i32::from(is_forced))

View File

@ -290,21 +290,15 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
PermissionDefault::Op(PermissionLvl::Two),
));
let block_node = literal("block").then(
argument("pos", BlockPosArgumentType).then(
argument("slot", SlotArgumentType)
.then(block_item_node())
.then(literal("with").then(block_item_node())),
),
);
let block_node = literal("block")
.then(argument("pos", BlockPosArgumentType).then(
argument("slot", SlotArgumentType).then(literal("with").then(block_item_node())),
));
let entity_node = literal("entity").then(
argument("targets", EntityArgumentType::Entities).then(
argument("slot", SlotArgumentType)
.then(entity_item_node())
.then(literal("with").then(entity_item_node())),
),
);
let entity_node =
literal("entity").then(argument("targets", EntityArgumentType::Entities).then(
argument("slot", SlotArgumentType).then(literal("with").then(entity_item_node())),
));
dispatcher.register(
command("item", DESCRIPTION)

View File

@ -69,7 +69,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
PermissionDefault::Op(PermissionLvl::Four),
PermissionDefault::Op(PermissionLvl::Two),
));
dispatcher.register(

View File

@ -31,7 +31,7 @@ struct PardonIpExecutor;
impl CommandExecutor for PardonIpExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let target = StringArgumentType::get(context, "ip")?;
let target = StringArgumentType::get(context, "target")?;
let ip = IpAddr::from_str(target)
.map_err(|_| ERROR_PARDONIP_INVALID.create_without_context())?;
@ -77,7 +77,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
let cmd = command("pardon-ip", DESCRIPTION)
.requires(PERMISSION)
.then(argument("ip", StringArgumentType::SingleWord).executes(PardonIpExecutor));
.then(argument("target", StringArgumentType::SingleWord).executes(PardonIpExecutor));
dispatcher.register_with_aliases(cmd, &["pardonip"]);
}

View File

@ -357,7 +357,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
PermissionDefault::Op(PermissionLvl::Two),
PermissionDefault::Op(PermissionLvl::Three),
));
dispatcher.register(

View File

@ -1,22 +1,31 @@
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;
use crate::command::argument_builder::{ArgumentBuilder, argument, command, literal};
use crate::command::argument_builder::{
ArgumentBuilder, LiteralArgumentBuilder, argument, command, literal,
};
use crate::command::argument_types::core::bool::BoolArgumentType;
use crate::command::argument_types::core::integer::IntegerArgumentType;
use crate::command::argument_types::identifier::IdentifierArgumentType;
use crate::command::argument_types::range::IntRangeArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
use crate::command::suggestion::suggestions::SuggestionsBuilder;
const DESCRIPTION: &str = "Draws a random value.";
const PERMISSION: &str = "minecraft:command.random";
const PERMISSION_RESET: &str = "minecraft:command.random.reset";
const ARG_RANGE: &str = "range";
/// The largest allowed range span, matching Vanilla (`i32::MAX - 1`).
const MAX_RANGE_SPAN: i64 = (i32::MAX - 1) as i64;
const ARG_SEQUENCE: &str = "sequence";
const ARG_SEED: &str = "seed";
const ARG_INCLUDE_WORLD_SEED: &str = "includeWorldSeed";
const ARG_INCLUDE_SEQUENCE_ID: &str = "includeSequenceId";
const RANGE_TOO_LARGE_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_LARGE,
@ -28,10 +37,25 @@ const RANGE_TOO_SMALL_ERROR_TYPE: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_RANDOM_ERROR_RANGE_TOO_SMALL,
);
struct RandomSequenceSuggestionProvider;
impl SuggestionProvider for RandomSequenceSuggestionProvider {
fn suggest(
&self,
context: &CommandContext,
mut builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let sequences = context.server().random_sequences.lock().unwrap();
for key in sequences.get_sequence_keys() {
builder = builder.suggest(key);
}
builder.build()
}
}
struct RandomExecutor {
/// Whether the result is announced to every player (`roll`) instead of
/// only the command source (`value`).
roll: bool,
has_sequence: bool,
}
impl CommandExecutor for RandomExecutor {
@ -44,11 +68,20 @@ impl CommandExecutor for RandomExecutor {
if span == 0 {
return Err(RANGE_TOO_SMALL_ERROR_TYPE.create_without_context());
}
if span >= MAX_RANGE_SPAN {
if span >= 2_147_483_647 {
return Err(RANGE_TOO_LARGE_ERROR_TYPE.create_without_context());
}
let result = rand::random_range(min..=max);
let value = if self.has_sequence {
let sequence = IdentifierArgumentType::get(context, ARG_SEQUENCE)?;
let world_seed = context.server().level_info.load().world_gen_settings.seed;
let mut sequences = context.server().random_sequences.lock().unwrap();
sequences
.get_or_create(&sequence, world_seed)
.random_between_inclusive(min, max)
} else {
rand::random_range(min..=max)
};
if self.roll {
let msg = TextComponent::translate_cross(
@ -56,7 +89,7 @@ impl CommandExecutor for RandomExecutor {
translation::java::COMMANDS_RANDOM_ROLL,
[
context.source.display_name.clone(),
TextComponent::text(result.to_string()),
TextComponent::text(value.to_string()),
TextComponent::text(min.to_string()),
TextComponent::text(max.to_string()),
],
@ -64,7 +97,6 @@ impl CommandExecutor for RandomExecutor {
for player in context.server().get_all_players() {
player.send_system_message(&msg);
}
// Also show the roll on the console (or another non-player source).
if context.source.player_or_none().is_none() {
context.source.send_message(msg);
}
@ -73,32 +105,209 @@ impl CommandExecutor for RandomExecutor {
TextComponent::translate_cross(
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
translation::java::COMMANDS_RANDOM_SAMPLE_SUCCESS,
[TextComponent::text(result.to_string())],
[TextComponent::text(value.to_string())],
),
false,
);
}
Ok(result)
Ok(value)
}
}
struct ResetSequenceExecutor {
has_seed: bool,
has_include_world_seed: bool,
has_include_sequence_id: bool,
}
impl CommandExecutor for ResetSequenceExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let sequence = IdentifierArgumentType::get(context, ARG_SEQUENCE)?;
let world_seed = context.server().level_info.load().world_gen_settings.seed;
let mut sequences = context.server().random_sequences.lock().unwrap();
if self.has_seed {
let seed = IntegerArgumentType::get(context, ARG_SEED)?;
let include_world_seed = if self.has_include_world_seed {
BoolArgumentType::get(context, ARG_INCLUDE_WORLD_SEED)?
} else {
true
};
let include_sequence_id = if self.has_include_sequence_id {
BoolArgumentType::get(context, ARG_INCLUDE_SEQUENCE_ID)?
} else {
true
};
sequences.reset_with_options(
&sequence,
world_seed,
seed,
include_world_seed,
include_sequence_id,
);
} else {
sequences.reset(&sequence, world_seed);
}
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_RANDOM_RESET_SUCCESS,
translation::java::COMMANDS_RANDOM_RESET_SUCCESS,
[TextComponent::text(sequence.to_string())],
),
false,
);
Ok(1)
}
}
struct ResetAllSequencesExecutor {
has_seed: bool,
has_include_world_seed: bool,
has_include_sequence_id: bool,
}
impl CommandExecutor for ResetAllSequencesExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let mut sequences = context.server().random_sequences.lock().unwrap();
if self.has_seed {
let seed = IntegerArgumentType::get(context, ARG_SEED)?;
let include_world_seed = if self.has_include_world_seed {
BoolArgumentType::get(context, ARG_INCLUDE_WORLD_SEED)?
} else {
true
};
let include_sequence_id = if self.has_include_sequence_id {
BoolArgumentType::get(context, ARG_INCLUDE_SEQUENCE_ID)?
} else {
true
};
sequences.set_seed_defaults(seed, include_world_seed, include_sequence_id);
}
let count = sequences.clear() as i32;
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_RANDOM_RESET_ALL_SUCCESS,
translation::java::COMMANDS_RANDOM_RESET_ALL_SUCCESS,
[TextComponent::text(count.to_string())],
),
false,
);
Ok(count)
}
}
fn draw_random_value_tree(name: &'static str, announce: bool) -> LiteralArgumentBuilder {
literal(name).then(
argument(ARG_RANGE, IntRangeArgumentType)
.executes(RandomExecutor {
roll: announce,
has_sequence: false,
})
.then(
argument(ARG_SEQUENCE, IdentifierArgumentType)
.suggests(RandomSequenceSuggestionProvider)
.requires(PERMISSION_RESET)
.executes(RandomExecutor {
roll: announce,
has_sequence: true,
}),
),
)
}
pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistry) {
registry.register_permission_or_panic(Permission::new(
PERMISSION,
DESCRIPTION,
// Vanilla allows every player to use `/random value` and `/random roll`.
PermissionDefault::Allow,
));
registry.register_permission_or_panic(Permission::new(
PERMISSION_RESET,
DESCRIPTION,
PermissionDefault::Op(PermissionLvl::Two),
));
dispatcher.register(
command("random", DESCRIPTION)
.requires(PERMISSION)
.then(literal("value").then(
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: false }),
))
.then(literal("roll").then(
argument(ARG_RANGE, IntRangeArgumentType).executes(RandomExecutor { roll: true }),
)),
.then(draw_random_value_tree("value", false))
.then(draw_random_value_tree("roll", true))
.then(
literal("reset")
.requires(PERMISSION_RESET)
.then(
literal("*")
.executes(ResetAllSequencesExecutor {
has_seed: false,
has_include_world_seed: false,
has_include_sequence_id: false,
})
.then(
argument(ARG_SEED, IntegerArgumentType::any())
.executes(ResetAllSequencesExecutor {
has_seed: true,
has_include_world_seed: false,
has_include_sequence_id: false,
})
.then(
argument(ARG_INCLUDE_WORLD_SEED, BoolArgumentType)
.executes(ResetAllSequencesExecutor {
has_seed: true,
has_include_world_seed: true,
has_include_sequence_id: false,
})
.then(
argument(ARG_INCLUDE_SEQUENCE_ID, BoolArgumentType)
.executes(ResetAllSequencesExecutor {
has_seed: true,
has_include_world_seed: true,
has_include_sequence_id: true,
}),
),
),
),
)
.then(
argument(ARG_SEQUENCE, IdentifierArgumentType)
.suggests(RandomSequenceSuggestionProvider)
.executes(ResetSequenceExecutor {
has_seed: false,
has_include_world_seed: false,
has_include_sequence_id: false,
})
.then(
argument(ARG_SEED, IntegerArgumentType::any())
.executes(ResetSequenceExecutor {
has_seed: true,
has_include_world_seed: false,
has_include_sequence_id: false,
})
.then(
argument(ARG_INCLUDE_WORLD_SEED, BoolArgumentType)
.executes(ResetSequenceExecutor {
has_seed: true,
has_include_world_seed: true,
has_include_sequence_id: false,
})
.then(
argument(ARG_INCLUDE_SEQUENCE_ID, BoolArgumentType)
.executes(ResetSequenceExecutor {
has_seed: true,
has_include_world_seed: true,
has_include_sequence_id: true,
}),
),
),
),
),
),
);
}

View File

@ -33,10 +33,6 @@ static ERROR_NOT_RIDING: CommandErrorType<1> = CommandErrorType::new(
translation::java::COMMANDS_RIDE_NOT_RIDING,
"Not riding anything",
);
static ERROR_GENERIC: CommandErrorType<2> = CommandErrorType::new(
translation::java::COMMANDS_RIDE_MOUNT_FAILURE_GENERIC,
"Failed to mount",
);
#[allow(clippy::assigning_clones)]
fn is_riding_recursive(entity: &dyn EntityBase, possible_vehicle: &dyn EntityBase) -> bool {
@ -64,35 +60,9 @@ struct RideMountExecutor;
impl CommandExecutor for RideMountExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let targets = EntityArgumentType::get_entities(context, "target")?;
let target = EntityArgumentType::get_entity(context, "target")?;
let vehicle = EntityArgumentType::get_entity(context, "vehicle")?;
if vehicle.get_player().is_some() {
return Err(ERROR_CANT_RIDE_PLAYERS.create_without_context());
}
let vehicle_world = vehicle.get_entity().world.load();
let mut success_count = 0;
let mut last_error = None;
for target in &targets {
let target_world = target.get_entity().world.load();
if target_world.dimension.minecraft_name != vehicle_world.dimension.minecraft_name {
last_error = Some(ERROR_WRONG_DIMENSION.create_without_context());
continue;
}
if target.get_entity().entity_id == vehicle.get_entity().entity_id {
last_error = Some(ERROR_LOOP.create_without_context());
continue;
}
if is_riding_recursive(target.as_ref(), vehicle.as_ref()) {
last_error = Some(ERROR_LOOP.create_without_context());
continue;
}
let current_vehicle = target
.get_entity()
.vehicle
@ -100,23 +70,29 @@ impl CommandExecutor for RideMountExecutor {
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(ref curr_veh) = current_vehicle {
if curr_veh.get_entity().entity_id == vehicle.get_entity().entity_id {
last_error = Some(ERROR_ALREADY_RIDING.create_without_context(
target.get_display_name(),
vehicle.get_display_name(),
));
continue;
return Err(ERROR_ALREADY_RIDING
.create_without_context(target.get_display_name(), curr_veh.get_display_name()));
}
// Dismount first
curr_veh
.get_entity()
.remove_passenger_sync(target.get_entity().entity_id);
if vehicle.get_player().is_some() {
return Err(ERROR_CANT_RIDE_PLAYERS.create_without_context());
}
if target.get_entity().entity_id == vehicle.get_entity().entity_id
|| is_riding_recursive(target.as_ref(), vehicle.as_ref())
{
return Err(ERROR_LOOP.create_without_context());
}
let target_world = target.get_entity().world.load();
let vehicle_world = vehicle.get_entity().world.load();
if target_world.dimension.minecraft_name != vehicle_world.dimension.minecraft_name {
return Err(ERROR_WRONG_DIMENSION.create_without_context());
}
vehicle
.get_entity()
.add_passenger(vehicle.clone(), target.clone());
success_count += 1;
let msg = TextComponent::translate_cross(
translation::java::COMMANDS_RIDE_MOUNT_SUCCESS,
@ -124,19 +100,8 @@ impl CommandExecutor for RideMountExecutor {
[target.get_display_name(), vehicle.get_display_name()],
);
context.source.send_feedback(msg, true);
}
if success_count == 0 {
if let Some(err) = last_error {
return Err(err);
}
return Err(ERROR_GENERIC.create_without_context(
targets[0].get_display_name(),
vehicle.get_display_name(),
));
}
Ok(success_count)
Ok(1)
}
}
@ -144,43 +109,30 @@ struct RideDismountExecutor;
impl CommandExecutor for RideDismountExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let targets = EntityArgumentType::get_entities(context, "target")?;
let target = EntityArgumentType::get_entity(context, "target")?;
let mut success_count = 0;
let mut last_error = None;
for target in &targets {
let current_vehicle = target
.get_entity()
.vehicle
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(vehicle) = current_vehicle {
current_vehicle.map_or_else(
|| Err(ERROR_NOT_RIDING.create_without_context(target.get_display_name())),
|vehicle| {
let target_id = target.get_entity().entity_id;
vehicle.get_entity().remove_passenger_sync(target_id);
success_count += 1;
let msg = TextComponent::translate_cross(
translation::java::COMMANDS_RIDE_DISMOUNT_SUCCESS,
translation::java::COMMANDS_RIDE_DISMOUNT_SUCCESS,
[target.get_display_name()],
[target.get_display_name(), vehicle.get_display_name()],
);
context.source.send_feedback(msg, true);
} else {
last_error =
Some(ERROR_NOT_RIDING.create_without_context(target.get_display_name()));
}
}
if success_count == 0 {
if let Some(err) = last_error {
return Err(err);
}
return Err(ERROR_NOT_RIDING.create_without_context(targets[0].get_display_name()));
}
Ok(success_count)
Ok(1)
},
)
}
}
@ -193,7 +145,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
let builder =
command("ride", DESCRIPTION).requires(PERMISSION).then(
argument("target", EntityArgumentType::Entities)
argument("target", EntityArgumentType::Entity)
.then(literal("mount").then(
argument("vehicle", EntityArgumentType::Entity).executes(RideMountExecutor),
))

View File

@ -78,10 +78,10 @@ impl CommandExecutor for RotateFacingLocationExecutor {
let facing_pos =
Vec3ArgumentType::get_coordinates(context, "facingLocation")?.resolve(&context.source);
let entity = target.get_entity();
let eye_height = entity.get_eye_height();
let pos = entity.pos.load();
let looking_from = Vector3::new(pos.x, pos.y + eye_height, pos.z);
let looking_from = context
.source
.entity_anchor
.position_at_entity(target.get_entity());
let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &facing_pos);
@ -106,10 +106,10 @@ impl CommandExecutor for RotateFacingEntityExecutor {
EntityAnchor::Feet
};
let target_entity = target.get_entity();
let eye_height = target_entity.get_eye_height();
let pos = target_entity.pos.load();
let looking_from = Vector3::new(pos.x, pos.y + eye_height, pos.z);
let looking_from = context
.source
.entity_anchor
.position_at_entity(target.get_entity());
let looking_towards = anchor.position_at_entity(facing_entity.get_entity());

View File

@ -4,7 +4,7 @@ use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry
use pumpkin_util::text::TextComponent;
use tracing::error;
use crate::command::argument_builder::{ArgumentBuilder, command};
use crate::command::argument_builder::{ArgumentBuilder, command, literal};
use crate::command::context::command_context::CommandContext;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
@ -63,6 +63,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
dispatcher.register(
command("save-all", DESCRIPTION)
.requires(PERMISSION)
.executes(SaveAllExecutor),
.executes(SaveAllExecutor)
.then(literal("flush").executes(SaveAllExecutor)),
);
}

View File

@ -1,6 +1,5 @@
use std::sync::Arc;
use pumpkin_data::dimension::Dimension;
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::math::position::BlockPos;
@ -8,10 +7,9 @@ use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry
use pumpkin_util::text::TextComponent;
use crate::command::argument_builder::{ArgumentBuilder, argument, command};
use crate::command::argument_types::coordinates::angle::AngleArgumentType;
use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType;
use crate::command::argument_types::coordinates::rotation::RotationArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::plugin::world::spawn_change::SpawnChangeEvent;
@ -19,43 +17,33 @@ use crate::plugin::world::spawn_change::SpawnChangeEvent;
const DESCRIPTION: &str = "Sets the world spawn point.";
const PERMISSION: &str = "minecraft:command.setworldspawn";
const ERROR_NOT_OVERWORLD: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD,
translation::java::COMMANDS_SETWORLDSPAWN_FAILURE_NOT_OVERWORLD,
);
enum SetWorldSpawnMode {
SelfPos,
PosOnly,
PosAndAngle,
PosAndRotation,
}
struct SetWorldSpawnExecutor(SetWorldSpawnMode);
impl CommandExecutor for SetWorldSpawnExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let (pos, angle) = match self.0 {
let (pos, (pitch, yaw)) = match self.0 {
SetWorldSpawnMode::SelfPos => {
let block_pos = BlockPos::floored_v(context.source.position);
(block_pos, 0.0)
(block_pos, (0.0, 0.0))
}
SetWorldSpawnMode::PosOnly => {
let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?;
(block_pos, 0.0)
(block_pos, (0.0, 0.0))
}
SetWorldSpawnMode::PosAndAngle => {
SetWorldSpawnMode::PosAndRotation => {
let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?;
let angle = AngleArgumentType::get(context, "angle")?.get_angle(&context.source);
(block_pos, angle)
let rot = RotationArgumentType::get(context, "rotation")?.rotation(&context.source);
(block_pos, (rot.x, rot.y))
}
};
let world = context.source.world();
if world.dimension != Dimension::OVERWORLD && world.dimension != Dimension::OVERWORLD_CAVES
{
return Err(ERROR_NOT_OVERWORLD.create_without_context());
}
let server = context.source.server();
let current_info = server.level_info.load();
let previous_position = BlockPos::new(
@ -66,8 +54,8 @@ impl CommandExecutor for SetWorldSpawnExecutor {
let new_position = pos;
let previous_yaw = current_info.spawn_yaw;
let previous_pitch = current_info.spawn_pitch;
let new_yaw = angle;
let new_pitch = 0.0;
let new_yaw = yaw;
let new_pitch = pitch;
let mut event = SpawnChangeEvent::new(
world.clone(),
previous_position,
@ -128,8 +116,8 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
argument("pos", BlockPosArgumentType)
.executes(SetWorldSpawnExecutor(SetWorldSpawnMode::PosOnly))
.then(
argument("angle", AngleArgumentType)
.executes(SetWorldSpawnExecutor(SetWorldSpawnMode::PosAndAngle)),
argument("rotation", RotationArgumentType)
.executes(SetWorldSpawnExecutor(SetWorldSpawnMode::PosAndRotation)),
),
),
);

View File

@ -1,11 +1,12 @@
use pumpkin_data::translation;
use pumpkin_util::PermissionLvl;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::permission::{Permission, PermissionDefault, PermissionRegistry};
use pumpkin_util::text::TextComponent;
use crate::command::argument_builder::{ArgumentBuilder, argument, command};
use crate::command::argument_types::coordinates::angle::AngleArgumentType;
use crate::command::argument_types::coordinates::block_pos::BlockPosArgumentType;
use crate::command::argument_types::coordinates::rotation::RotationArgumentType;
use crate::command::argument_types::entity::EntityArgumentType;
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
@ -24,7 +25,7 @@ enum SpawnpointMode {
SelfDefault,
TargetsDefault,
TargetsPos,
TargetsPosAngle,
TargetsPosRotation,
}
struct SpawnpointExecutor(SpawnpointMode);
@ -43,37 +44,60 @@ impl CommandExecutor for SpawnpointExecutor {
_ => EntityArgumentType::get_players(context, "targets")?,
};
for target in &targets {
let (pos, yaw) = match self.0 {
let (pos, (pitch, yaw)) = match self.0 {
SpawnpointMode::SelfDefault | SpawnpointMode::TargetsDefault => {
let block_pos = target.position().to_block_pos();
let yaw = target.living_entity.entity.yaw.load();
(block_pos, yaw)
let block_pos = BlockPos::floored_v(context.source.position);
(block_pos, (0.0, 0.0))
}
SpawnpointMode::TargetsPos => {
let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?;
let yaw = target.living_entity.entity.yaw.load();
(block_pos, yaw)
(block_pos, (0.0, 0.0))
}
SpawnpointMode::TargetsPosAngle => {
SpawnpointMode::TargetsPosRotation => {
let block_pos = BlockPosArgumentType::get_block_pos(context, "pos")?;
let yaw = AngleArgumentType::get(context, "angle")?.get_angle(&context.source);
(block_pos, yaw)
let rot = RotationArgumentType::get(context, "rotation")?.rotation(&context.source);
(block_pos, (rot.x, rot.y))
}
};
let dimension = target.world().dimension.clone();
target.set_respawn_point(dimension, pos, yaw, 0.0, true);
let world = context.source.world();
let dimension = world.dimension.clone();
let dimension_name = dimension.minecraft_name.to_string();
for target in &targets {
target.set_respawn_point(dimension.clone(), pos, yaw, pitch, true);
}
if targets.len() == 1 {
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE,
translation::bedrock::COMMANDS_SPAWNPOINT_SUCCESS_SINGLE,
[
TextComponent::text(target.gameprofile.name.clone()),
TextComponent::text(pos.0.x.to_string()),
TextComponent::text(pos.0.y.to_string()),
TextComponent::text(pos.0.z.to_string()),
TextComponent::text(yaw.to_string()),
TextComponent::text(pitch.to_string()),
TextComponent::text(dimension_name),
TextComponent::text(targets[0].gameprofile.name.clone()),
],
),
true,
);
} else {
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_SPAWNPOINT_SUCCESS_MULTIPLE,
translation::java::COMMANDS_SPAWNPOINT_SUCCESS_MULTIPLE,
[
TextComponent::text(pos.0.x.to_string()),
TextComponent::text(pos.0.y.to_string()),
TextComponent::text(pos.0.z.to_string()),
TextComponent::text(yaw.to_string()),
TextComponent::text(pitch.to_string()),
TextComponent::text(dimension_name),
TextComponent::text(targets.len().to_string()),
],
),
true,
@ -102,8 +126,9 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
argument("pos", BlockPosArgumentType)
.executes(SpawnpointExecutor(SpawnpointMode::TargetsPos))
.then(
argument("angle", AngleArgumentType)
.executes(SpawnpointExecutor(SpawnpointMode::TargetsPosAngle)),
argument("rotation", RotationArgumentType).executes(
SpawnpointExecutor(SpawnpointMode::TargetsPosRotation),
),
),
),
),

View File

@ -78,18 +78,16 @@ impl CommandExecutor for SpectateTargetExecutor {
let target_entity = target.get_entity();
let target_world = target_entity.world.load_full();
let players = if self.is_self {
let player = context
let player = if self.is_self {
context
.source
.output
.as_player()
.ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?;
vec![player]
.ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?
} else {
EntityArgumentType::get_players(context, "player")?
EntityArgumentType::get_player(context, "player")?
};
for player in &players {
if player.gamemode.load() != GameMode::Spectator {
let display_name = player.get_display_name();
return Err(ERROR_NOT_SPECTATOR.create_without_context(display_name));
@ -104,20 +102,14 @@ impl CommandExecutor for SpectateTargetExecutor {
let target_name = target.get_display_name();
return Err(ERROR_CANNOT_SPECTATE.create_without_context(target_name));
}
}
let mut succeeded = 0;
for player in &players {
let target_id = target_entity.entity_id;
player.camera_target_id.store(Some(target_id));
let pos = target_entity.pos.load();
let yaw = target_entity.yaw.load();
let pitch = target_entity.pitch.load();
let player_world = player.world();
player.try_send_client_packet(&CSetCamera::new(target_id.into()));
player.teleport(pos, Some(yaw), Some(pitch), player_world);
succeeded += 1;
}
let target_name = target.get_display_name();
context.source.send_feedback(
@ -129,7 +121,7 @@ impl CommandExecutor for SpectateTargetExecutor {
false,
);
Ok(succeeded)
Ok(1)
}
}
@ -148,7 +140,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
argument("target", EntityArgumentType::Entity)
.executes(SpectateTargetExecutor { is_self: true })
.then(
argument("player", EntityArgumentType::Players)
argument("player", EntityArgumentType::Player)
.executes(SpectateTargetExecutor { is_self: false }),
),
),

View File

@ -112,7 +112,7 @@ impl CommandExecutor for QueryExecutor {
),
true,
);
Ok((elapsed_seconds * scale).floor() as i32)
Ok((elapsed_seconds * scale) as i32)
}
}

View File

@ -21,11 +21,14 @@ use pumpkin_util::text::color::NamedColor;
const DESCRIPTION: &str = "Manages teams.";
const PERMISSION: &str = "minecraft:command.team";
const ARG_TEAM_NAME: &str = "name";
const ARG_TEAM_NAME: &str = "team";
const ARG_DISPLAY_NAME: &str = "displayName";
const ARG_TEAM: &str = "team";
const ARG_MEMBERS: &str = "members";
const ARG_VALUE: &str = "value";
const ARG_PREFIX: &str = "prefix";
const ARG_SUFFIX: &str = "suffix";
const ARG_ALLOWED: &str = "allowed";
const DUPLICATE_TEAM_ERROR: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_TEAM_ADD_DUPLICATE,
@ -543,7 +546,7 @@ struct TeamModifyDisplayNameExecutor;
impl CommandExecutor for TeamModifyDisplayNameExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let new_name_str = StringArgumentType::get(context, ARG_VALUE)?;
let new_name_str = StringArgumentType::get(context, ARG_DISPLAY_NAME)?;
let world = context.world().clone();
let team_name_owned = team_name.to_string();
@ -588,7 +591,7 @@ struct TeamModifyPrefixExecutor;
impl CommandExecutor for TeamModifyPrefixExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let new_prefix_str = StringArgumentType::get(context, ARG_VALUE)?;
let new_prefix_str = StringArgumentType::get(context, ARG_PREFIX)?;
let world = context.world().clone();
let team_name_owned = team_name.to_string();
@ -629,7 +632,7 @@ struct TeamModifySuffixExecutor;
impl CommandExecutor for TeamModifySuffixExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let new_suffix_str = StringArgumentType::get(context, ARG_VALUE)?;
let new_suffix_str = StringArgumentType::get(context, ARG_SUFFIX)?;
let world = context.world().clone();
let team_name_owned = team_name.to_string();
@ -670,7 +673,7 @@ struct TeamModifyFriendlyFireExecutor;
impl CommandExecutor for TeamModifyFriendlyFireExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let value = BoolArgumentType::get(context, ARG_VALUE)?;
let value = BoolArgumentType::get(context, ARG_ALLOWED)?;
let world = context.world().clone();
let team_name_owned = team_name.to_string();
@ -724,7 +727,7 @@ struct TeamModifySeeFriendlyInvisiblesExecutor;
impl CommandExecutor for TeamModifySeeFriendlyInvisiblesExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let team_name = TeamArgumentType::get(context, ARG_TEAM)?;
let value = BoolArgumentType::get(context, ARG_VALUE)?;
let value = BoolArgumentType::get(context, ARG_ALLOWED)?;
let world = context.world().clone();
let team_name_owned = team_name.to_string();
@ -976,9 +979,7 @@ fn join_branch() -> LiteralArgumentBuilder {
}
fn leave_branch() -> LiteralArgumentBuilder {
literal("leave")
.executes(TeamLeaveExecutor { has_members: false })
.then(
literal("leave").then(
argument(ARG_MEMBERS, EntityArgumentType::Entities)
.executes(TeamLeaveExecutor { has_members: true }),
)
@ -1004,28 +1005,28 @@ fn modify_branch() -> LiteralArgumentBuilder {
)
.then(
literal("displayName").then(
argument(ARG_VALUE, StringArgumentType::GreedyPhrase)
argument(ARG_DISPLAY_NAME, StringArgumentType::GreedyPhrase)
.executes(TeamModifyDisplayNameExecutor),
),
)
.then(
literal("prefix").then(
argument(ARG_VALUE, StringArgumentType::GreedyPhrase)
argument(ARG_PREFIX, StringArgumentType::GreedyPhrase)
.executes(TeamModifyPrefixExecutor),
),
)
.then(
literal("suffix").then(
argument(ARG_VALUE, StringArgumentType::GreedyPhrase)
argument(ARG_SUFFIX, StringArgumentType::GreedyPhrase)
.executes(TeamModifySuffixExecutor),
),
)
.then(literal("friendlyFire").then(
argument(ARG_VALUE, BoolArgumentType).executes(TeamModifyFriendlyFireExecutor),
argument(ARG_ALLOWED, BoolArgumentType).executes(TeamModifyFriendlyFireExecutor),
))
.then(
literal("seeFriendlyInvisibles").then(
argument(ARG_VALUE, BoolArgumentType)
argument(ARG_ALLOWED, BoolArgumentType)
.executes(TeamModifySeeFriendlyInvisiblesExecutor),
),
)

View File

@ -48,6 +48,7 @@ impl CommandExecutor for TeamMsgCommandExecutor {
let message_component = TextComponent::text(message_text);
let online_players = world.players.load();
let mut recipients = 0;
for player in online_players.iter() {
if team.players.contains(&player.gameprofile.name) {
@ -74,10 +75,11 @@ impl CommandExecutor for TeamMsgCommandExecutor {
};
player.send_system_message(&msg);
recipients += 1;
}
}
Ok(1)
Ok(recipients)
}
}

View File

@ -11,6 +11,7 @@ use crate::command::argument_builder::{ArgumentBuilder, argument, command, liter
use crate::command::argument_types::coordinates::rotation::RotationArgumentType;
use crate::command::argument_types::coordinates::vec3::Vec3ArgumentType;
use crate::command::argument_types::entity::EntityArgumentType;
use crate::command::argument_types::entity_anchor::{EntityAnchor, EntityAnchorArgumentType};
use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
@ -21,11 +22,6 @@ use crate::world::World;
const DESCRIPTION: &str = "Teleports entities, including players.";
const PERMISSION: &str = "minecraft:command.teleport";
const ERROR_NOT_PLAYER: CommandErrorType<0> = CommandErrorType::new(
translation::java::PERMISSIONS_REQUIRES_PLAYER,
translation::java::PERMISSIONS_REQUIRES_PLAYER,
);
const ERROR_INVALID_POSITION: CommandErrorType<0> = CommandErrorType::new(
translation::java::COMMANDS_TELEPORT_INVALIDPOSITION,
translation::java::COMMANDS_TELEPORT_INVALIDPOSITION,
@ -62,28 +58,24 @@ struct SelfToPosExecutor;
impl CommandExecutor for SelfToPosExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let player = context
.source
.output
.as_player()
.ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?;
let entity = context.source.entity_or_err()?;
let pos = Vec3ArgumentType::get_coordinates(context, "location")?.resolve(&context.source);
if !World::is_valid(BlockPos(pos.floor_to_i32())) {
return Err(ERROR_INVALID_POSITION.create_without_context());
}
let yaw = player.get_entity().yaw.load();
let pitch = player.get_entity().pitch.load();
let player_world = player.world();
player.teleport(pos, Some(yaw), Some(pitch), player_world);
let yaw = entity.get_entity().yaw.load();
let pitch = entity.get_entity().pitch.load();
let world = context.source.world();
entity.teleport(pos, Some(yaw), Some(pitch), world.clone());
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_TELEPORT_SUCCESS_LOCATION_SINGLE,
translation::bedrock::COMMANDS_TP_SUCCESS_COORDINATES,
[
player.get_display_name(),
entity.get_display_name(),
TextComponent::text(pos.x.to_string()),
TextComponent::text(pos.y.to_string()),
TextComponent::text(pos.z.to_string()),
@ -100,11 +92,7 @@ struct SelfToEntityExecutor;
impl CommandExecutor for SelfToEntityExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let player = context
.source
.output
.as_player()
.ok_or_else(|| ERROR_NOT_PLAYER.create_without_context())?;
let entity = context.source.entity_or_err()?;
let destination = EntityArgumentType::get_entity(context, "destination")?;
let destination_entity = destination.get_entity();
@ -117,14 +105,14 @@ impl CommandExecutor for SelfToEntityExecutor {
return Err(ERROR_INVALID_POSITION.create_without_context());
}
player.teleport(pos, Some(yaw), Some(pitch), world);
entity.teleport(pos, Some(yaw), Some(pitch), world);
context.source.send_feedback(
TextComponent::translate_cross(
translation::java::COMMANDS_TELEPORT_SUCCESS_ENTITY_SINGLE,
translation::bedrock::COMMANDS_TP_SUCCESSVICTIM,
[
player.get_display_name(),
entity.get_display_name(),
destination_entity.get_display_name(),
],
),
@ -270,10 +258,15 @@ impl CommandExecutor for EntitiesToPosFacingPosExecutor {
let facing_pos =
Vec3ArgumentType::get_coordinates(context, "facingLocation")?.resolve(&context.source);
let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_pos);
let world = context.source.world();
for target in &targets {
let eye_offset = match context.source.entity_anchor {
EntityAnchor::Feet => 0.0,
EntityAnchor::Eyes => target.get_entity().get_eye_height(),
};
let looking_from = Vector3::new(pos.x, pos.y + eye_offset, pos.z);
let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &facing_pos);
target.teleport(pos, Some(yaw), Some(pitch), world.clone());
}
@ -300,7 +293,9 @@ impl CommandExecutor for EntitiesToPosFacingPosExecutor {
}
}
struct EntitiesToPosFacingEntityExecutor;
struct EntitiesToPosFacingEntityExecutor {
has_anchor: bool,
}
impl CommandExecutor for EntitiesToPosFacingEntityExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
@ -312,10 +307,22 @@ impl CommandExecutor for EntitiesToPosFacingEntityExecutor {
}
let facing_entity = EntityArgumentType::get_entity(context, "facingEntity")?;
let (yaw, pitch) = yaw_pitch_facing_position(&pos, &facing_entity.get_entity().pos.load());
let anchor = if self.has_anchor {
EntityAnchorArgumentType::get(context, "facingAnchor")?
} else {
EntityAnchor::Feet
};
let facing_pos = anchor.position_at_entity(facing_entity.get_entity());
let world = context.source.world();
for target in &targets {
let eye_offset = match context.source.entity_anchor {
EntityAnchor::Feet => 0.0,
EntityAnchor::Eyes => target.get_entity().get_eye_height(),
};
let looking_from = Vector3::new(pos.x, pos.y + eye_offset, pos.z);
let (yaw, pitch) = yaw_pitch_facing_position(&looking_from, &facing_pos);
target.teleport(pos, Some(yaw), Some(pitch), world.clone());
}
@ -367,7 +374,15 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
.then(
literal("entity").then(
argument("facingEntity", EntityArgumentType::Entity)
.executes(EntitiesToPosFacingEntityExecutor),
.executes(EntitiesToPosFacingEntityExecutor {
has_anchor: false,
})
.then(
argument("facingAnchor", EntityAnchorArgumentType)
.executes(EntitiesToPosFacingEntityExecutor {
has_anchor: true,
}),
),
),
)
.then(

View File

@ -148,6 +148,7 @@ impl TickExecutor {
TextComponent::text(nanos_to_millis_string(p50)),
TextComponent::text(nanos_to_millis_string(p95)),
TextComponent::text(nanos_to_millis_string(p99)),
TextComponent::text(slice.len().to_string()),
],
),
false,

View File

@ -87,10 +87,10 @@ impl CommandExecutor for SimpleTriggerExecutor {
translation::java::COMMANDS_TRIGGER_SIMPLE_SUCCESS,
[objective_display_name],
),
false,
true,
);
Ok(1)
Ok(new_value)
}
}
@ -158,10 +158,10 @@ impl CommandExecutor for AddTriggerExecutor {
TextComponent::text(value.to_string()),
],
),
false,
true,
);
Ok(value)
Ok(new_value)
}
}
@ -221,7 +221,7 @@ impl CommandExecutor for SetTriggerExecutor {
TextComponent::text(value.to_string()),
],
),
false,
true,
);
Ok(value)

View File

@ -33,7 +33,7 @@ impl CommandExecutor for WeatherExecutor {
};
let world = context.source.world();
let message = {
let (message, return_val) = {
let mut weather = world
.weather
.lock()
@ -45,10 +45,13 @@ impl CommandExecutor for WeatherExecutor {
duration.unwrap_or_else(|| rand::random_range(12_000..=180_000));
weather.set_weather_parameters(world, processed_duration, 0, false, false);
(
TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_CLEAR,
translation::bedrock::COMMANDS_WEATHER_CLEAR,
[],
),
duration.unwrap_or(-1),
)
}
WeatherMode::Rain => {
@ -56,10 +59,13 @@ impl CommandExecutor for WeatherExecutor {
duration.unwrap_or_else(|| rand::random_range(12_000..=24_000));
weather.set_weather_parameters(world, 0, processed_duration, true, false);
(
TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_RAIN,
translation::bedrock::COMMANDS_WEATHER_RAIN,
[],
),
duration.unwrap_or(-1),
)
}
WeatherMode::Thunder => {
@ -67,10 +73,13 @@ impl CommandExecutor for WeatherExecutor {
duration.unwrap_or_else(|| rand::random_range(3600..=15_600));
weather.set_weather_parameters(world, 0, processed_duration, true, true);
(
TextComponent::translate_cross(
translation::java::COMMANDS_WEATHER_SET_THUNDER,
translation::bedrock::COMMANDS_WEATHER_THUNDER,
[],
),
duration.unwrap_or(-1),
)
}
}
@ -78,7 +87,7 @@ impl CommandExecutor for WeatherExecutor {
context.source.send_feedback(message, true);
Ok(1)
Ok(return_val)
}
}
@ -98,7 +107,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
mode: WeatherMode::Clear,
has_duration: false,
})
.then(argument("duration", TimeArgumentType::any()).executes(
.then(argument("duration", TimeArgumentType::new(1)).executes(
WeatherExecutor {
mode: WeatherMode::Clear,
has_duration: true,
@ -111,7 +120,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
mode: WeatherMode::Rain,
has_duration: false,
})
.then(argument("duration", TimeArgumentType::any()).executes(
.then(argument("duration", TimeArgumentType::new(1)).executes(
WeatherExecutor {
mode: WeatherMode::Rain,
has_duration: true,
@ -124,7 +133,7 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
mode: WeatherMode::Thunder,
has_duration: false,
})
.then(argument("duration", TimeArgumentType::any()).executes(
.then(argument("duration", TimeArgumentType::new(1)).executes(
WeatherExecutor {
mode: WeatherMode::Thunder,
has_duration: true,

View File

@ -12,6 +12,8 @@ use crate::command::context::command_context::CommandContext;
use crate::command::errors::error_types::CommandErrorType;
use crate::command::node::dispatcher::CommandDispatcher;
use crate::command::node::{CommandExecutor, CommandExecutorResult};
use crate::command::suggestion::provider::{SuggestionProvider, SuggestionProviderResult};
use crate::command::suggestion::suggestions::SuggestionsBuilder;
use crate::data::{LoadJSONConfiguration, SaveJSONConfiguration, whitelist::WhitelistConfig};
use crate::net::DisconnectReason;
use crate::server::Server;
@ -39,11 +41,14 @@ const ERROR_REMOVE_FAILED: CommandErrorType<0> = CommandErrorType::new(
translation::bedrock::COMMANDS_ALLOWLIST_REMOVE_FAILED,
);
fn kick_non_whitelisted_players(server: &Server) {
pub fn kick_non_whitelisted_players(server: &Server) {
let whitelist = server.data.whitelist_config.read().unwrap();
let ops = server.data.operator_config.read().unwrap();
if server.basic_config.enforce_whitelist && server.white_list.load(Ordering::Relaxed) {
for player in server.get_all_players() {
if whitelist.is_whitelisted(&player.gameprofile) {
if ops.get_entry(&player.gameprofile.id).is_some()
|| whitelist.is_whitelisted(&player.gameprofile)
{
continue;
}
player.kick(
@ -66,7 +71,6 @@ impl CommandExecutor for OnExecutor {
if previous {
Err(ERROR_ALREADY_ON.create_without_context())
} else {
kick_non_whitelisted_players(server);
context.source.send_feedback(
pumpkin_macros::translate_cross!(
translation::java::COMMANDS_WHITELIST_ENABLED,
@ -74,6 +78,7 @@ impl CommandExecutor for OnExecutor {
),
true,
);
kick_non_whitelisted_players(server);
Ok(1)
}
}
@ -109,9 +114,10 @@ impl CommandExecutor for ListExecutor {
let whitelist = &whitelist_guard.whitelist;
if whitelist.is_empty() {
context.source.send_feedback(
pumpkin_macros::translate_cross!(
TextComponent::translate_cross(
translation::java::COMMANDS_WHITELIST_NONE,
translation::bedrock::COMMANDS_ALLOWLIST_LIST
translation::java::COMMANDS_WHITELIST_NONE,
[],
),
false,
);
@ -124,19 +130,21 @@ impl CommandExecutor for ListExecutor {
.collect::<Vec<&str>>()
.join(", ");
let names_len = names.len() as i32;
let count = whitelist.len() as i32;
context.source.send_feedback(
pumpkin_macros::translate_cross!(
TextComponent::translate_cross(
translation::java::COMMANDS_WHITELIST_LIST,
translation::bedrock::COMMANDS_ALLOWLIST_LIST,
TextComponent::text(whitelist.len().to_string()),
TextComponent::text(names)
translation::java::COMMANDS_WHITELIST_LIST,
[
TextComponent::text(count.to_string()),
TextComponent::text(names),
],
),
false,
);
Ok(names_len)
Ok(count)
}
}
@ -146,7 +154,6 @@ impl CommandExecutor for ReloadExecutor {
fn execute(&self, context: &CommandContext) -> CommandExecutorResult {
let server = context.source.server();
*server.data.whitelist_config.write().unwrap() = WhitelistConfig::load();
kick_non_whitelisted_players(server);
context.source.send_feedback(
pumpkin_macros::translate_cross!(
translation::java::COMMANDS_WHITELIST_RELOADED,
@ -154,10 +161,29 @@ impl CommandExecutor for ReloadExecutor {
),
true,
);
kick_non_whitelisted_players(server);
Ok(1)
}
}
struct AddSuggestionProvider;
impl SuggestionProvider for AddSuggestionProvider {
fn suggest(
&self,
context: &CommandContext,
mut builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let whitelist = context.server().data.whitelist_config.read().unwrap();
for player in context.server().get_all_players() {
if !whitelist.is_whitelisted(&player.gameprofile) {
builder = builder.suggest(player.gameprofile.name.clone());
}
}
builder.build()
}
}
struct AddExecutor;
impl CommandExecutor for AddExecutor {
@ -166,6 +192,8 @@ impl CommandExecutor for AddExecutor {
let server = context.source.server();
let mut whitelist = server.data.whitelist_config.write().unwrap();
let mut successes: i32 = 0;
let mut modified = false;
for profile in &targets {
if let Some(existing_entry) = whitelist
.whitelist
@ -174,6 +202,7 @@ impl CommandExecutor for AddExecutor {
{
if existing_entry.name != profile.name {
existing_entry.name.clone_from(&profile.name);
modified = true;
}
continue;
}
@ -189,9 +218,12 @@ impl CommandExecutor for AddExecutor {
true,
);
successes += 1;
modified = true;
}
if modified {
whitelist.save();
}
if successes == 0 {
Err(ERROR_ADD_FAILED.create_without_context())
@ -201,6 +233,22 @@ impl CommandExecutor for AddExecutor {
}
}
struct RemoveSuggestionProvider;
impl SuggestionProvider for RemoveSuggestionProvider {
fn suggest(
&self,
context: &CommandContext,
mut builder: SuggestionsBuilder,
) -> SuggestionProviderResult {
let whitelist = context.server().data.whitelist_config.read().unwrap();
for entry in &whitelist.whitelist {
builder = builder.suggest(entry.name.clone());
}
builder.build()
}
}
struct RemoveExecutor;
impl CommandExecutor for RemoveExecutor {
@ -229,14 +277,14 @@ impl CommandExecutor for RemoveExecutor {
}
}
if successes == 0 {
Err(ERROR_REMOVE_FAILED.create_without_context())
} else {
whitelist.save();
drop(whitelist);
kick_non_whitelisted_players(server);
if successes == 0 {
Err(ERROR_REMOVE_FAILED.create_without_context())
} else {
Ok(successes)
}
}
@ -255,14 +303,20 @@ pub fn register(dispatcher: &mut CommandDispatcher, registry: &PermissionRegistr
.then(literal("on").executes(OnExecutor))
.then(literal("off").executes(OffExecutor))
.then(literal("list").executes(ListExecutor))
.then(literal("reload").executes(ReloadExecutor))
.then(
literal("add")
.then(argument("targets", GameProfileArgumentType).executes(AddExecutor)),
literal("add").then(
argument("targets", GameProfileArgumentType)
.suggests(AddSuggestionProvider)
.executes(AddExecutor),
),
)
.then(
literal("remove")
.then(argument("targets", GameProfileArgumentType).executes(RemoveExecutor)),
literal("remove").then(
argument("targets", GameProfileArgumentType)
.suggests(RemoveSuggestionProvider)
.executes(RemoveExecutor),
),
)
.then(literal("reload").executes(ReloadExecutor)),
);
}

View File

@ -121,6 +121,8 @@ pub struct Server {
std::sync::Mutex<std::collections::HashMap<String, pumpkin_nbt::compound::NbtCompound>>,
/// Global server stopwatches
pub stopwatches: std::sync::Mutex<crate::world::stopwatches::Stopwatches>,
/// Global server random sequences
pub random_sequences: std::sync::Mutex<crate::world::random_sequences::RandomSequences>,
// Manages player advancement
pub advancement_manager: Arc<AdvancementManager>,
// Whether the server whitelist is on or off
@ -299,6 +301,9 @@ impl Server {
player_data_storage,
command_storage: std::sync::Mutex::new(std::collections::HashMap::new()),
stopwatches: std::sync::Mutex::new(crate::world::stopwatches::Stopwatches::new()),
random_sequences: std::sync::Mutex::new(
crate::world::random_sequences::RandomSequences::new(),
),
advancement_manager,
white_list,
tick_rate_manager,

View File

@ -23,6 +23,7 @@ pub mod loot;
pub mod map;
pub mod portal;
pub mod raid;
pub mod random_sequences;
pub mod stopwatches;
pub mod time;
pub mod villager_poi;

View File

@ -0,0 +1,140 @@
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hasher};
use pumpkin_util::identifier::Identifier;
use pumpkin_util::random::RandomImpl;
use pumpkin_util::random::xoroshiro128::Xoroshiro;
/// A single random sequence wrapper.
pub struct RandomSequence {
rng: Xoroshiro,
}
impl RandomSequence {
#[must_use]
pub const fn new(seed: u64) -> Self {
Self {
rng: Xoroshiro::from_seed(seed),
}
}
pub fn random_between_inclusive(&mut self, min: i32, max: i32) -> i32 {
if min >= max {
return min;
}
self.rng.next_inbetween_i32(min, max)
}
}
/// Persistent/runtime manager for server random sequences.
pub struct RandomSequences {
salt: i32,
include_world_seed: bool,
include_sequence_id: bool,
sequences: HashMap<String, RandomSequence>,
}
impl Default for RandomSequences {
fn default() -> Self {
Self::new()
}
}
impl RandomSequences {
#[must_use]
pub fn new() -> Self {
Self {
salt: 0,
include_world_seed: true,
include_sequence_id: true,
sequences: HashMap::new(),
}
}
fn calculate_seed(
sequence: &Identifier,
world_seed: i64,
salt: i32,
include_world_seed: bool,
include_sequence_id: bool,
) -> u64 {
let base_seed = if include_world_seed { world_seed } else { 0 };
let mut seed = (base_seed ^ i64::from(salt)) as u64;
if include_sequence_id {
let mut hasher = DefaultHasher::new();
hasher.write(sequence.to_string().as_bytes());
seed ^= hasher.finish();
}
seed
}
pub fn get_or_create(&mut self, sequence: &Identifier, world_seed: i64) -> &mut RandomSequence {
let key = sequence.to_string();
let salt = self.salt;
let include_world_seed = self.include_world_seed;
let include_sequence_id = self.include_sequence_id;
self.sequences.entry(key).or_insert_with(|| {
let seed = Self::calculate_seed(
sequence,
world_seed,
salt,
include_world_seed,
include_sequence_id,
);
RandomSequence::new(seed)
})
}
pub fn reset(&mut self, sequence: &Identifier, world_seed: i64) {
let key = sequence.to_string();
let seed = Self::calculate_seed(
sequence,
world_seed,
self.salt,
self.include_world_seed,
self.include_sequence_id,
);
self.sequences.insert(key, RandomSequence::new(seed));
}
pub fn reset_with_options(
&mut self,
sequence: &Identifier,
world_seed: i64,
salt: i32,
include_world_seed: bool,
include_sequence_id: bool,
) {
let key = sequence.to_string();
let seed = Self::calculate_seed(
sequence,
world_seed,
salt,
include_world_seed,
include_sequence_id,
);
self.sequences.insert(key, RandomSequence::new(seed));
}
pub fn clear(&mut self) -> usize {
let count = self.sequences.len();
self.sequences.clear();
count
}
pub const fn set_seed_defaults(
&mut self,
salt: i32,
include_world_seed: bool,
include_sequence_id: bool,
) {
self.salt = salt;
self.include_world_seed = include_world_seed;
self.include_sequence_id = include_sequence_id;
}
#[must_use]
pub fn get_sequence_keys(&self) -> Vec<String> {
self.sequences.keys().cloned().collect()
}
}