Skip to main content

veloren_server/
cmd.rs

1//! # Implementing new commands.
2//! To implement a new command provide a handler function
3//! in [do_command].
4#[cfg(feature = "worldgen")]
5use crate::weather::WeatherJob;
6use crate::{
7    Server, Settings, StateExt,
8    client::Client,
9    location::Locations,
10    login_provider::LoginProvider,
11    settings::{
12        BanInfo, BanOperation, BanOperationError, EditableSetting, SettingError, WhitelistInfo,
13        WhitelistRecord,
14        banlist::{BanAction, NormalizedIpAddr},
15        server_description::ServerDescription,
16        server_physics::ServerPhysicsForceRecord,
17    },
18    sys::terrain::SpawnEntityData,
19    wiring::{self, OutputFormula},
20};
21#[cfg(feature = "worldgen")]
22use common::{cmd::SPOT_PARSER, spot::Spot};
23use slotmap::Key as _;
24
25use assets::{AssetExt, Ron};
26use authc::Uuid;
27use chrono::{DateTime, NaiveTime, Timelike, Utc};
28use common::{
29    CachedSpatialGrid, Damage, DamageKind, Explosion, GroupTarget, LoadoutBuilder, RadiusEffect,
30    assets,
31    calendar::Calendar,
32    cmd::{
33        AreaKind, BUFF_PACK, BUFF_PARSER, EntityTarget, KIT_MANIFEST_PATH, KitSpec,
34        PRESET_MANIFEST_PATH, ServerChatCommand,
35    },
36    combat,
37    comp::{
38        self, AdminRole, Aura, AuraKind, ChatType, Content, GizmoSubscriber, Inventory, Item,
39        LightEmitter, LocalizationArg, WaypointArea,
40        agent::{FlightMode, PidControllers},
41        aura::{AuraKindVariant, AuraTarget},
42        buff::{Buff, BuffData, BuffKind, BuffSource, DestInfo, MiscBuffData},
43        inventory::{
44            item::{MaterialStatManifest, Quality, all_items_expect, tool::AbilityMap},
45            slot::Slot,
46        },
47        invite::InviteKind,
48        misc::PortalData,
49    },
50    depot,
51    effect::Effect,
52    event::{
53        ClientDisconnectEvent, CreateNpcEvent, CreateSpecialEntityEvent, EventBus, ExplosionEvent,
54        GroupManipEvent, InitiateInviteEvent, PermanentChange, TamePetEvent,
55    },
56    generation::{EntityConfig, EntityInfo, SpecialEntity},
57    link::Is,
58    mounting::{Rider, Volume, VolumeRider},
59    npc::{self, get_npc_name},
60    outcome::Outcome,
61    parse_cmd_args,
62    resources::{BattleMode, ProgramTime, Secs, Time, TimeOfDay, TimeScale},
63    rtsim::Role,
64    spiral::Spiral2d,
65    terrain::{Block, BlockKind, CoordinateConversions, SpriteKind, StructureSprite},
66    tether::Tethered,
67    uid::Uid,
68    vol::ReadVol,
69};
70#[cfg(feature = "worldgen")]
71use common::{terrain::TERRAIN_CHUNK_BLOCKS_LG, weather};
72use common_net::{
73    msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
74    sync::WorldSyncExt,
75};
76use common_state::{
77    Areas, AreasContainer, BattleModeChangeArea, BuildArea, NoDurabilityArea, SpecialAreaError,
78    State,
79};
80use core::{cmp::Ordering, convert::TryFrom};
81use hashbrown::{HashMap, HashSet};
82use humantime::Duration as HumanDuration;
83use rand::{RngExt, rng};
84use specs::{Builder, Entity as EcsEntity, Join, LendJoin, WorldExt, storage::StorageEntry};
85use std::{
86    fmt::Write, net::SocketAddr, num::NonZeroU32, ops::DerefMut, str::FromStr, sync::Arc,
87    time::Duration,
88};
89use vek::*;
90use wiring::{Circuit, Wire, WireNode, WiringAction, WiringActionEffect, WiringElement};
91#[cfg(feature = "worldgen")]
92use world::util::{LOCALITY, Sampler};
93
94use common::comp::Alignment;
95use tracing::{error, info, warn};
96
97pub trait ChatCommandExt {
98    fn execute(&self, server: &mut Server, entity: EcsEntity, args: Vec<String>);
99}
100impl ChatCommandExt for ServerChatCommand {
101    fn execute(&self, server: &mut Server, entity: EcsEntity, args: Vec<String>) {
102        if let Err(err) = do_command(server, entity, entity, args, self) {
103            server.notify_client(
104                entity,
105                ServerGeneral::server_msg(ChatType::CommandError, err),
106            );
107        }
108    }
109}
110
111type CmdResult<T> = Result<T, Content>;
112
113/// Handler function called when the command is executed.
114/// # Arguments
115/// * `&mut Server` - the `Server` instance executing the command.
116/// * `EcsEntity` - an `Entity` corresponding to the player that invoked the
117///   command.
118/// * `EcsEntity` - an `Entity` for the player on whom the command is invoked.
119///   This differs from the previous argument when using /sudo
120/// * `Vec<String>` - a `Vec<String>` containing the arguments of the command
121///   after the keyword.
122/// * `&ChatCommand` - the command to execute with the above arguments --
123///   Handler functions must parse arguments from the the given `String`
124///   (`parse_args!` exists for this purpose).
125///
126/// # Returns
127///
128/// A `Result` that is `Ok` if the command went smoothly, and `Err` if it
129/// failed; on failure, the string is sent to the client who initiated the
130/// command.
131type CommandHandler =
132    fn(&mut Server, EcsEntity, EcsEntity, Vec<String>, &ServerChatCommand) -> CmdResult<()>;
133
134fn do_command(
135    server: &mut Server,
136    client: EcsEntity,
137    target: EcsEntity,
138    args: Vec<String>,
139    cmd: &ServerChatCommand,
140) -> CmdResult<()> {
141    // Make sure your role is at least high enough to execute this command.
142    if cmd.needs_role() > server.entity_admin_role(client) {
143        return Err(Content::localized_with_args("command-no-permission", [(
144            "command_name",
145            cmd.keyword(),
146        )]));
147    }
148
149    let handler: CommandHandler = match cmd {
150        ServerChatCommand::Adminify => handle_adminify,
151        ServerChatCommand::Airship => handle_spawn_airship,
152        ServerChatCommand::Alias => handle_alias,
153        ServerChatCommand::AreaAdd => handle_area_add,
154        ServerChatCommand::AreaList => handle_area_list,
155        ServerChatCommand::AreaRemove => handle_area_remove,
156        ServerChatCommand::Aura => handle_aura,
157        ServerChatCommand::Ban => handle_ban,
158        ServerChatCommand::BanIp => handle_ban_ip,
159        ServerChatCommand::BanLog => handle_ban_log,
160        ServerChatCommand::BattleMode => handle_battlemode,
161        ServerChatCommand::BattleModeForce => handle_battlemode_force,
162        ServerChatCommand::Body => handle_body,
163        ServerChatCommand::Buff => handle_buff,
164        ServerChatCommand::Build => handle_build,
165        ServerChatCommand::Campfire => handle_spawn_campfire,
166        ServerChatCommand::ClearPersistedTerrain => handle_clear_persisted_terrain,
167        ServerChatCommand::DeathEffect => handle_death_effect,
168        ServerChatCommand::DebugColumn => handle_debug_column,
169        ServerChatCommand::DebugWays => handle_debug_ways,
170        ServerChatCommand::DisconnectAllPlayers => handle_disconnect_all_players,
171        ServerChatCommand::DropAll => handle_drop_all,
172        ServerChatCommand::Dummy => handle_spawn_training_dummy,
173        ServerChatCommand::Explosion => handle_explosion,
174        ServerChatCommand::Faction => handle_faction,
175        ServerChatCommand::GiveItem => handle_give_item,
176        ServerChatCommand::Gizmos => handle_gizmos,
177        ServerChatCommand::GizmosRange => handle_gizmos_range,
178        ServerChatCommand::Goto => handle_goto,
179        ServerChatCommand::GotoRand => handle_goto_rand,
180        ServerChatCommand::Group => handle_group,
181        ServerChatCommand::GroupInvite => handle_group_invite,
182        ServerChatCommand::GroupKick => handle_group_kick,
183        ServerChatCommand::GroupLeave => handle_group_leave,
184        ServerChatCommand::GroupPromote => handle_group_promote,
185        ServerChatCommand::Health => handle_health,
186        ServerChatCommand::IntoNpc => handle_into_npc,
187        ServerChatCommand::JoinFaction => handle_join_faction,
188        ServerChatCommand::Jump => handle_jump,
189        ServerChatCommand::Kick => handle_kick,
190        ServerChatCommand::Kill => handle_kill,
191        ServerChatCommand::KillNpcs => handle_kill_npcs,
192        ServerChatCommand::Kit => handle_kit,
193        ServerChatCommand::Lantern => handle_lantern,
194        ServerChatCommand::Light => handle_light,
195        ServerChatCommand::MakeBlock => handle_make_block,
196        ServerChatCommand::MakeNpc => handle_make_npc,
197        ServerChatCommand::MakeSprite => handle_make_sprite,
198        ServerChatCommand::Motd => handle_motd,
199        ServerChatCommand::Object => handle_object,
200        ServerChatCommand::Outcome => handle_outcome,
201        ServerChatCommand::PermitBuild => handle_permit_build,
202        ServerChatCommand::Players => handle_players,
203        ServerChatCommand::Poise => handle_poise,
204        ServerChatCommand::Portal => handle_spawn_portal,
205        ServerChatCommand::ResetRecipes => handle_reset_recipes,
206        ServerChatCommand::Region => handle_region,
207        ServerChatCommand::ReloadChunks => handle_reload_chunks,
208        ServerChatCommand::RemoveLights => handle_remove_lights,
209        ServerChatCommand::Respawn => handle_respawn,
210        ServerChatCommand::RevokeBuild => handle_revoke_build,
211        ServerChatCommand::RevokeBuildAll => handle_revoke_build_all,
212        ServerChatCommand::Safezone => handle_safezone,
213        ServerChatCommand::Say => handle_say,
214        ServerChatCommand::ServerPhysics => handle_server_physics,
215        ServerChatCommand::SetBodyType => handle_set_body_type,
216        ServerChatCommand::SetMotd => handle_set_motd,
217        ServerChatCommand::SetWaypoint => handle_set_waypoint,
218        ServerChatCommand::Ship => handle_spawn_ship,
219        ServerChatCommand::Site => handle_site,
220        ServerChatCommand::SkillPoint => handle_skill_point,
221        ServerChatCommand::SkillPreset => handle_skill_preset,
222        ServerChatCommand::Spawn => handle_spawn,
223        ServerChatCommand::Spot => handle_spot,
224        ServerChatCommand::Sudo => handle_sudo,
225        ServerChatCommand::Tell => handle_tell,
226        ServerChatCommand::Time => handle_time,
227        ServerChatCommand::TimeScale => handle_time_scale,
228        ServerChatCommand::Tp => handle_tp,
229        ServerChatCommand::RtsimTp => handle_rtsim_tp,
230        ServerChatCommand::RtsimInfo => handle_rtsim_info,
231        ServerChatCommand::RtsimNpc => handle_rtsim_npc,
232        ServerChatCommand::RtsimPurge => handle_rtsim_purge,
233        ServerChatCommand::RtsimChunk => handle_rtsim_chunk,
234        ServerChatCommand::Unban => handle_unban,
235        ServerChatCommand::UnbanIp => handle_unban_ip,
236        ServerChatCommand::Version => handle_version,
237        ServerChatCommand::Wiring => handle_spawn_wiring,
238        ServerChatCommand::Whitelist => handle_whitelist,
239        ServerChatCommand::World => handle_world,
240        ServerChatCommand::MakeVolume => handle_make_volume,
241        ServerChatCommand::Location => handle_location,
242        ServerChatCommand::CreateLocation => handle_create_location,
243        ServerChatCommand::DeleteLocation => handle_delete_location,
244        ServerChatCommand::WeatherZone => handle_weather_zone,
245        ServerChatCommand::Lightning => handle_lightning,
246        ServerChatCommand::Scale => handle_scale,
247        ServerChatCommand::RepairEquipment => handle_repair_equipment,
248        ServerChatCommand::Tether => handle_tether,
249        ServerChatCommand::DestroyTethers => handle_destroy_tethers,
250        ServerChatCommand::Mount => handle_mount,
251        ServerChatCommand::Dismount => handle_dismount,
252    };
253
254    handler(server, client, target, args, cmd)
255}
256
257fn key_matches(key: impl slotmap::Key, query: &str) -> bool {
258    let key = format!("{:?}", key.data());
259    if query.contains("v") {
260        key == query
261    } else if let Some((idx, _)) = key.split_once('v') {
262        idx == query
263    } else {
264        false
265    }
266}
267
268// Fallibly get position of entity with the given descriptor (used for error
269// message).
270fn position(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult<comp::Pos> {
271    server
272        .state
273        .ecs()
274        .read_storage::<comp::Pos>()
275        .get(entity)
276        .copied()
277        .ok_or_else(|| {
278            Content::localized_with_args("command-position-unavailable", [("target", descriptor)])
279        })
280}
281
282fn insert_or_replace_component<C: specs::Component>(
283    server: &mut Server,
284    entity: EcsEntity,
285    component: C,
286    descriptor: &str,
287) -> CmdResult<()> {
288    server
289        .state
290        .ecs_mut()
291        .write_storage()
292        .insert(entity, component)
293        .and(Ok(()))
294        .map_err(|_| Content::localized_with_args("command-entity-dead", [("entity", descriptor)]))
295}
296
297fn uuid(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult<Uuid> {
298    server
299        .state
300        .ecs()
301        .read_storage::<comp::Player>()
302        .get(entity)
303        .map(|player| player.uuid())
304        .ok_or_else(|| {
305            Content::localized_with_args("command-player-info-unavailable", [(
306                "target", descriptor,
307            )])
308        })
309}
310
311fn socket_addr(server: &Server, entity: EcsEntity, descriptor: &str) -> CmdResult<SocketAddr> {
312    server
313        .state
314        .ecs()
315        .read_storage::<Client>()
316        .get(entity)
317        .ok_or_else(|| {
318            Content::localized_with_args("command-entity-has-no-client", [("target", descriptor)])
319        })?
320        .connected_from_addr()
321        .socket_addr()
322        .ok_or_else(|| {
323            Content::localized_with_args("command-client-has-no-socketaddr", [(
324                "target", descriptor,
325            )])
326        })
327}
328
329fn real_role(server: &Server, uuid: Uuid, descriptor: &str) -> CmdResult<AdminRole> {
330    server
331        .editable_settings()
332        .admins
333        .get(&uuid)
334        .map(|record| record.role.into())
335        .ok_or_else(|| {
336            Content::localized_with_args("command-player-role-unavailable", [(
337                "target", descriptor,
338            )])
339        })
340}
341
342// Fallibly get uid of entity with the given descriptor (used for error
343// message).
344fn uid(server: &Server, target: EcsEntity, descriptor: &str) -> CmdResult<Uid> {
345    server
346        .state
347        .ecs()
348        .read_storage::<Uid>()
349        .get(target)
350        .copied()
351        .ok_or_else(|| {
352            Content::localized_with_args("command-uid-unavailable", [("target", descriptor)])
353        })
354}
355
356fn area(server: &mut Server, area_name: &str, kind: &str) -> CmdResult<depot::Id<Aabb<i32>>> {
357    get_areas_mut(kind, &mut server.state)?
358        .area_metas()
359        .get(area_name)
360        .copied()
361        .ok_or_else(|| {
362            Content::localized_with_args("command-area-not-found", [("area", area_name)])
363        })
364}
365
366// Prevent use through sudo.
367fn no_sudo(client: EcsEntity, target: EcsEntity) -> CmdResult<()> {
368    if client == target {
369        Ok(())
370    } else {
371        // This happens when [ab]using /sudo
372        Err(Content::localized("command-no-sudo"))
373    }
374}
375
376fn can_send_message(target: EcsEntity, server: &mut Server) -> CmdResult<()> {
377    if server
378        .state
379        .ecs()
380        .read_storage::<Client>()
381        .get(target)
382        .is_none_or(|client| !client.client_type.can_send_message())
383    {
384        Err(Content::localized("command-cannot-send-message-hidden"))
385    } else {
386        Ok(())
387    }
388}
389
390/// Ensure that client role is above target role, for the purpose of performing
391/// some (often permanent) administrative action on the target.  Note that this
392/// function is *not* a replacement for actually verifying that the client
393/// should be able to execute the command at all, which still needs to be
394/// rechecked, nor does it guarantee that either the client or the target
395/// actually have an entry in the admin settings file.
396///
397/// For our purposes, there are *two* roles--temporary role, and permanent role.
398/// For the purpose of these checks, currently *any* permanent role overrides
399/// *any* temporary role (this may change if more roles are added that aren't
400/// moderator or administrator).  If the permanent roles match, the temporary
401/// roles are used as a tiebreaker.  /adminify should ensure that no one's
402/// temporary role can be different from their permanent role without someone
403/// with a higher role than their permanent role allowing it, and only permanent
404/// roles should be recorded in the settings files.
405fn verify_above_role(
406    server: &mut Server,
407    (client, client_uuid): (EcsEntity, Uuid),
408    (player, player_uuid): (EcsEntity, Uuid),
409    reason: Content,
410) -> CmdResult<()> {
411    let client_temp = server.entity_admin_role(client);
412    let client_perm = server
413        .editable_settings()
414        .admins
415        .get(&client_uuid)
416        .map(|record| record.role);
417
418    let player_temp = server.entity_admin_role(player);
419    let player_perm = server
420        .editable_settings()
421        .admins
422        .get(&player_uuid)
423        .map(|record| record.role);
424
425    if client_perm > player_perm || client_perm == player_perm && client_temp > player_temp {
426        Ok(())
427    } else {
428        Err(reason)
429    }
430}
431
432fn find_alias(ecs: &specs::World, alias: &str, find_hidden: bool) -> CmdResult<(EcsEntity, Uuid)> {
433    (
434        &ecs.entities(),
435        &ecs.read_storage::<comp::Player>(),
436        &ecs.read_storage::<Client>(),
437    )
438        .join()
439        .find(|(_, player, client)| {
440            // If `find_hidden` is set to false, disallow discovering this player using ie.
441            // /tell or /group_invite
442            player.alias == alias && (client.client_type.emit_login_events() || find_hidden)
443        })
444        .map(|(entity, player, _)| (entity, player.uuid()))
445        .ok_or_else(|| {
446            Content::localized_with_args("command-player-not-found", [("player", alias)])
447        })
448}
449
450fn find_uuid(ecs: &specs::World, uuid: Uuid) -> CmdResult<EcsEntity> {
451    (&ecs.entities(), &ecs.read_storage::<comp::Player>())
452        .join()
453        .find(|(_, player)| player.uuid() == uuid)
454        .map(|(entity, _)| entity)
455        .ok_or_else(|| {
456            Content::localized_with_args("command-player-uuid-not-found", [(
457                "uuid",
458                uuid.to_string(),
459            )])
460        })
461}
462
463fn find_username(server: &mut Server, username: &str) -> CmdResult<Uuid> {
464    server
465        .state
466        .mut_resource::<LoginProvider>()
467        .username_to_uuid(username)
468        .map_err(|_| {
469            Content::localized_with_args("command-username-uuid-unavailable", [(
470                "username", username,
471            )])
472        })
473}
474
475/// NOTE: Intended to be run only on logged-in clients.
476fn uuid_to_username(
477    server: &mut Server,
478    fallback_entity: EcsEntity,
479    uuid: Uuid,
480) -> CmdResult<String> {
481    let make_err = || {
482        Content::localized_with_args("command-uuid-username-unavailable", [(
483            "uuid",
484            uuid.to_string(),
485        )])
486    };
487    let player_storage = server.state.ecs().read_storage::<comp::Player>();
488
489    let fallback_alias = &player_storage
490        .get(fallback_entity)
491        .ok_or_else(make_err)?
492        .alias;
493
494    server
495        .state
496        .ecs()
497        .read_resource::<LoginProvider>()
498        .uuid_to_username(uuid, fallback_alias)
499        .map_err(|_| make_err())
500}
501
502fn edit_setting_feedback<S: EditableSetting>(
503    server: &mut Server,
504    client: EcsEntity,
505    result: Option<(Content, Result<(), SettingError<S>>)>,
506    failure: impl FnOnce() -> Content,
507) -> CmdResult<()> {
508    let (info, result) = result.ok_or_else(failure)?;
509    match result {
510        Ok(()) => {
511            server.notify_client(
512                client,
513                ServerGeneral::server_msg(ChatType::CommandInfo, info),
514            );
515            Ok(())
516        },
517        Err(setting_error) => edit_setting_error_feedback(server, client, setting_error, || info),
518    }
519}
520
521fn edit_banlist_feedback(
522    server: &mut Server,
523    client: EcsEntity,
524    result: Result<(), BanOperationError>,
525    // Message to provide if the edit was succesful (even if an IO error occurred, since the
526    // setting will still be changed in memory)
527    info: impl FnOnce() -> Content,
528    // Message to provide if the edit was cancelled due to it having no effect.
529    failure: impl FnOnce() -> Content,
530) -> CmdResult<()> {
531    match result {
532        Ok(()) => {
533            server.notify_client(
534                client,
535                ServerGeneral::server_msg(ChatType::CommandInfo, info()),
536            );
537            Ok(())
538        },
539        // TODO: whether there is a typo and the supplied username has no ban entry or if the
540        // target was already banned/unbanned, the user of this command will always get the same
541        // error message here, which seems like it could be misleading.
542        Err(BanOperationError::NoEffect) => Err(failure()),
543        Err(BanOperationError::EditFailed(setting_error)) => {
544            edit_setting_error_feedback(server, client, setting_error, info)
545        },
546    }
547}
548
549fn edit_setting_error_feedback<S: EditableSetting>(
550    server: &mut Server,
551    client: EcsEntity,
552    setting_error: SettingError<S>,
553    info: impl FnOnce() -> Content,
554) -> CmdResult<()> {
555    match setting_error {
556        SettingError::Io(err) => {
557            let info = info();
558            warn!(
559                ?err,
560                "Failed to write settings file to disk, but succeeded in memory (success message: \
561                 {:?})",
562                info,
563            );
564            server.notify_client(
565                client,
566                ServerGeneral::server_msg(
567                    ChatType::CommandError,
568                    Content::localized_with_args("command-error-write-settings", [
569                        ("error", Content::Plain(format!("{:?}", err))),
570                        ("message", info),
571                    ]),
572                ),
573            );
574            Ok(())
575        },
576        SettingError::Integrity(err) => Err(Content::localized_with_args(
577            "command-error-while-evaluating-request",
578            [("error", format!("{err:?}"))],
579        )),
580    }
581}
582
583fn handle_drop_all(
584    server: &mut Server,
585    _client: EcsEntity,
586    target: EcsEntity,
587    _args: Vec<String>,
588    _action: &ServerChatCommand,
589) -> CmdResult<()> {
590    let pos = position(server, target, "target")?;
591
592    let mut items = Vec::new();
593    if let Some(mut inventory) = server
594        .state
595        .ecs()
596        .write_storage::<Inventory>()
597        .get_mut(target)
598    {
599        items = inventory.drain().collect();
600    }
601
602    let mut rng = rng();
603
604    let item_to_place = items
605        .into_iter()
606        .filter(|i| !matches!(i.quality(), Quality::Debug));
607    for item in item_to_place {
608        let vel = Vec3::new(
609            rng.random_range(-0.1..0.1),
610            rng.random_range(-0.1..0.1),
611            0.5,
612        );
613
614        server.state.create_item_drop(
615            comp::Pos(Vec3::new(
616                pos.0.x + rng.random_range(5.0..10.0),
617                pos.0.y + rng.random_range(5.0..10.0),
618                pos.0.z + 5.0,
619            )),
620            comp::Ori::default(),
621            comp::Vel(vel),
622            comp::PickupItem::new(item, ProgramTime(server.state.get_program_time()), true),
623            None,
624        );
625    }
626
627    Ok(())
628}
629
630fn handle_give_item(
631    server: &mut Server,
632    client: EcsEntity,
633    target: EcsEntity,
634    args: Vec<String>,
635    action: &ServerChatCommand,
636) -> CmdResult<()> {
637    if let (Some(item_name), give_amount_opt) = parse_cmd_args!(args, String, u32) {
638        let give_amount = give_amount_opt.unwrap_or(1);
639        if let Ok(item) = Item::new_from_asset(&item_name.replace(['/', '\\'], "."))
640            .inspect_err(|error| error!(?error, "Failed to parse item asset!"))
641        {
642            let mut item: Item = item;
643            let mut res = Ok(());
644
645            const MAX_GIVE_AMOUNT: u32 = 2000;
646            // Cap give_amount for non-stackable items
647            let give_amount = if item.is_stackable() {
648                give_amount
649            } else {
650                give_amount.min(MAX_GIVE_AMOUNT)
651            };
652
653            if let Ok(()) = item.set_amount(give_amount) {
654                server
655                    .state
656                    .ecs()
657                    .write_storage::<Inventory>()
658                    .get_mut(target)
659                    .map(|mut inv| {
660                        // NOTE: Deliberately ignores items that couldn't be pushed.
661                        if inv.push(item).is_err() {
662                            res = Err(Content::localized_with_args(
663                                "command-give-inventory-full",
664                                [("total", give_amount as u64), ("given", 0)],
665                            ));
666                        }
667                    });
668            } else {
669                let ability_map = server.state.ecs().read_resource::<AbilityMap>();
670                let msm = server.state.ecs().read_resource::<MaterialStatManifest>();
671                // This item can't stack. Give each item in a loop.
672                server
673                    .state
674                    .ecs()
675                    .write_storage::<Inventory>()
676                    .get_mut(target)
677                    .map(|mut inv| {
678                        for i in 0..give_amount {
679                            // NOTE: Deliberately ignores items that couldn't be pushed.
680                            if inv.push(item.duplicate(&ability_map, &msm)).is_err() {
681                                res = Err(Content::localized_with_args(
682                                    "command-give-inventory-full",
683                                    [("total", give_amount as u64), ("given", i as u64)],
684                                ));
685                                break;
686                            }
687                        }
688                    });
689            }
690
691            if res.is_ok() {
692                let msg = ServerGeneral::server_msg(
693                    ChatType::CommandInfo,
694                    Content::localized_with_args("command-give-inventory-success", [
695                        ("total", LocalizationArg::from(give_amount as u64)),
696                        ("item", LocalizationArg::from(item_name)),
697                    ]),
698                );
699                server.notify_client(client, msg);
700            }
701
702            let mut inventory_update_buffers = server
703                .state
704                .ecs_mut()
705                .write_storage::<comp::InventoryUpdateBuffer>();
706            if let Some(buf) = inventory_update_buffers.get_mut(target) {
707                buf.push(comp::InventoryUpdateEvent::Given);
708            }
709
710            res
711        } else {
712            Err(Content::localized_with_args("command-invalid-item", [(
713                "item", item_name,
714            )]))
715        }
716    } else {
717        Err(action.help_content())
718    }
719}
720
721fn handle_gizmos(
722    server: &mut Server,
723    _client: EcsEntity,
724    target: EcsEntity,
725    args: Vec<String>,
726    action: &ServerChatCommand,
727) -> CmdResult<()> {
728    if let (Some(kind), gizmo_target) = parse_cmd_args!(args, String, EntityTarget) {
729        let mut subscribers = server.state().ecs().write_storage::<GizmoSubscriber>();
730
731        let gizmo_target = gizmo_target
732            .map(|gizmo_target| get_entity_target(gizmo_target, server))
733            .transpose()?
734            .map(|gizmo_target| {
735                server
736                    .state()
737                    .ecs()
738                    .read_storage()
739                    .get(gizmo_target)
740                    .copied()
741                    .ok_or(Content::localized("command-entity-dead"))
742            })
743            .transpose()?;
744
745        match kind.as_str() {
746            "All" => {
747                let subscriber = subscribers
748                    .entry(target)
749                    .map_err(|_| Content::localized("command-entity-dead"))?
750                    .or_insert_with(Default::default);
751                let context = match gizmo_target {
752                    Some(uid) => comp::gizmos::GizmoContext::EnabledWithTarget(uid),
753                    None => comp::gizmos::GizmoContext::Enabled,
754                };
755                for (_, ctx) in subscriber.gizmos.iter_mut() {
756                    *ctx = context.clone();
757                }
758                Ok(())
759            },
760            "None" => {
761                subscribers.remove(target);
762                Ok(())
763            },
764            s => {
765                if let Ok(kind) = comp::gizmos::GizmoSubscription::from_str(s) {
766                    let subscriber = subscribers
767                        .entry(target)
768                        .map_err(|_| Content::localized("command-entity-dead"))?
769                        .or_insert_with(Default::default);
770
771                    subscriber.gizmos[kind] = match gizmo_target {
772                        Some(uid) => comp::gizmos::GizmoContext::EnabledWithTarget(uid),
773                        None => match subscriber.gizmos[kind] {
774                            comp::gizmos::GizmoContext::Disabled => {
775                                comp::gizmos::GizmoContext::Enabled
776                            },
777                            comp::gizmos::GizmoContext::Enabled
778                            | comp::gizmos::GizmoContext::EnabledWithTarget(_) => {
779                                comp::gizmos::GizmoContext::Disabled
780                            },
781                        },
782                    };
783
784                    Ok(())
785                } else {
786                    Err(action.help_content())
787                }
788            },
789        }
790    } else {
791        Err(action.help_content())
792    }
793}
794
795fn handle_gizmos_range(
796    server: &mut Server,
797    _client: EcsEntity,
798    target: EcsEntity,
799    args: Vec<String>,
800    action: &ServerChatCommand,
801) -> CmdResult<()> {
802    if let Some(range) = parse_cmd_args!(args, f32) {
803        let mut subscribers = server.state().ecs().write_storage::<GizmoSubscriber>();
804        subscribers
805            .entry(target)
806            .map_err(|_| Content::localized("command-entity-dead"))?
807            .or_insert_with(Default::default)
808            .range = range;
809
810        Ok(())
811    } else {
812        Err(action.help_content())
813    }
814}
815
816fn handle_make_block(
817    server: &mut Server,
818    _client: EcsEntity,
819    target: EcsEntity,
820    args: Vec<String>,
821    action: &ServerChatCommand,
822) -> CmdResult<()> {
823    if let (Some(block_name), r, g, b) = parse_cmd_args!(args, String, u8, u8, u8) {
824        if let Ok(bk) = BlockKind::from_str(block_name.as_str()) {
825            let pos = position(server, target, "target")?;
826            let new_block = Block::new(bk, Rgb::new(r, g, b).map(|e| e.unwrap_or(255)));
827            let pos = pos.0.map(|e| e.floor() as i32);
828            server.state.set_block(pos, new_block);
829            #[cfg(feature = "persistent_world")]
830            if let Some(terrain_persistence) = server
831                .state
832                .ecs()
833                .try_fetch_mut::<crate::TerrainPersistence>()
834                .as_mut()
835            {
836                terrain_persistence.set_block(pos, new_block);
837            }
838            Ok(())
839        } else {
840            Err(Content::localized_with_args(
841                "command-invalid-block-kind",
842                [("kind", block_name)],
843            ))
844        }
845    } else {
846        Err(action.help_content())
847    }
848}
849
850fn handle_into_npc(
851    server: &mut Server,
852    client: EcsEntity,
853    target: EcsEntity,
854    args: Vec<String>,
855    action: &ServerChatCommand,
856) -> CmdResult<()> {
857    use crate::events::shared::{TransformEntityError, transform_entity};
858
859    if client != target {
860        server.notify_client(
861            client,
862            ServerGeneral::server_msg(
863                ChatType::CommandInfo,
864                Content::localized("command-into_npc-warning"),
865            ),
866        );
867    }
868
869    let Some(entity_config) = parse_cmd_args!(args, String) else {
870        return Err(action.help_content());
871    };
872
873    let config = match Ron::<EntityConfig>::load(&entity_config) {
874        Ok(asset) => asset.read(),
875        Err(_err) => {
876            return Err(Content::localized_with_args(
877                "command-entity-load-failed",
878                [("config", entity_config)],
879            ));
880        },
881    };
882
883    let mut loadout_rng = rng();
884    let entity_info = EntityInfo::at(
885        server
886            .state
887            .read_component_copied::<comp::Pos>(target)
888            .map(|p| p.0)
889            .unwrap_or_default(),
890    )
891    .with_entity_config(
892        config.clone().into_inner(),
893        Some(&entity_config),
894        &mut loadout_rng,
895        None,
896    );
897
898    transform_entity(server, target, entity_info, true).map_err(|error| match error {
899        TransformEntityError::EntityDead => {
900            Content::localized_with_args("command-entity-dead", [("entity", "target")])
901        },
902        TransformEntityError::UnexpectedSpecialEntity => {
903            Content::localized("command-unimplemented-spawn-special")
904        },
905        TransformEntityError::LoadingCharacter => {
906            Content::localized("command-transform-invalid-presence")
907        },
908        TransformEntityError::EntityIsPlayer => {
909            unreachable!(
910                "Transforming players must be valid as we explicitly allowed player transformation"
911            );
912        },
913    })
914}
915
916fn handle_make_npc(
917    server: &mut Server,
918    client: EcsEntity,
919    target: EcsEntity,
920    args: Vec<String>,
921    action: &ServerChatCommand,
922) -> CmdResult<()> {
923    let (entity_config, number) = parse_cmd_args!(args, String, i8);
924
925    let entity_config = entity_config.ok_or_else(|| action.help_content())?;
926    let number = match number {
927        // Number of entities must be larger than 1
928        Some(i8::MIN..=0) => {
929            return Err(Content::localized("command-nof-entities-at-least"));
930        },
931        // But lower than 50
932        Some(50..=i8::MAX) => {
933            return Err(Content::localized("command-nof-entities-less-than"));
934        },
935        Some(number) => number,
936        None => 1,
937    };
938
939    let config = match Ron::<EntityConfig>::load(&entity_config) {
940        Ok(asset) => asset.read(),
941        Err(_err) => {
942            return Err(Content::localized_with_args(
943                "command-entity-load-failed",
944                [("config", entity_config)],
945            ));
946        },
947    };
948
949    let mut loadout_rng = rng();
950    for _ in 0..number {
951        let comp::Pos(pos) = position(server, target, "target")?;
952        let entity_info = EntityInfo::at(pos).with_entity_config(
953            config.clone().into_inner(),
954            Some(&entity_config),
955            &mut loadout_rng,
956            None,
957        );
958
959        match SpawnEntityData::from_entity_info(entity_info) {
960            SpawnEntityData::Special(_, _) => {
961                return Err(Content::localized("command-unimplemented-spawn-special"));
962            },
963            SpawnEntityData::Npc(data) => {
964                let (npc_builder, _pos) = data.to_npc_builder();
965
966                server
967                    .state
968                    .ecs()
969                    .read_resource::<EventBus<CreateNpcEvent>>()
970                    .emit_now(CreateNpcEvent {
971                        pos: comp::Pos(pos),
972                        ori: comp::Ori::default(),
973                        npc: npc_builder,
974                    });
975            },
976        };
977    }
978
979    server.notify_client(
980        client,
981        ServerGeneral::server_msg(
982            ChatType::CommandInfo,
983            Content::localized_with_args("command-spawned-entities-config", [
984                ("n", number.to_string()),
985                ("config", entity_config),
986            ]),
987        ),
988    );
989
990    Ok(())
991}
992
993fn handle_make_sprite(
994    server: &mut Server,
995    _client: EcsEntity,
996    target: EcsEntity,
997    args: Vec<String>,
998    action: &ServerChatCommand,
999) -> CmdResult<()> {
1000    if let Some(sprite_name) = parse_cmd_args!(args, String) {
1001        let pos = position(server, target, "target")?;
1002        let pos = pos.0.map(|e| e.floor() as i32);
1003        let old_block = server
1004                .state
1005                .get_block(pos)
1006                // TODO: Make more principled.
1007                .unwrap_or_else(|| Block::air(SpriteKind::Empty));
1008        let set_block = |block| {
1009            server.state.set_block(pos, block);
1010            #[cfg(feature = "persistent_world")]
1011            if let Some(terrain_persistence) = server
1012                .state
1013                .ecs()
1014                .try_fetch_mut::<crate::TerrainPersistence>()
1015                .as_mut()
1016            {
1017                terrain_persistence.set_block(pos, block);
1018            }
1019        };
1020        if let Ok(sk) = SpriteKind::try_from(sprite_name.as_str()) {
1021            set_block(old_block.with_sprite(sk));
1022
1023            Ok(())
1024        } else if let Ok(sprite) = ron::from_str::<StructureSprite>(sprite_name.as_str()) {
1025            set_block(sprite.apply_to_block(old_block).unwrap_or_else(|b| b));
1026
1027            Ok(())
1028        } else {
1029            Err(Content::localized_with_args("command-invalid-sprite", [(
1030                "kind",
1031                sprite_name,
1032            )]))
1033        }
1034    } else {
1035        Err(action.help_content())
1036    }
1037}
1038
1039fn handle_motd(
1040    server: &mut Server,
1041    client: EcsEntity,
1042    _target: EcsEntity,
1043    _args: Vec<String>,
1044    _action: &ServerChatCommand,
1045) -> CmdResult<()> {
1046    let locale = server
1047        .state
1048        .ecs()
1049        .read_storage::<Client>()
1050        .get(client)
1051        .and_then(|client| client.locale.clone());
1052
1053    server.notify_client(
1054        client,
1055        ServerGeneral::server_msg(
1056            ChatType::CommandInfo,
1057            Content::Plain(
1058                server
1059                    .editable_settings()
1060                    .server_description
1061                    .get(locale.as_deref())
1062                    .map_or("", |d| &d.motd)
1063                    .to_string(),
1064            ),
1065        ),
1066    );
1067    Ok(())
1068}
1069
1070fn handle_set_motd(
1071    server: &mut Server,
1072    client: EcsEntity,
1073    _target: EcsEntity,
1074    args: Vec<String>,
1075    action: &ServerChatCommand,
1076) -> CmdResult<()> {
1077    let data_dir = server.data_dir();
1078    let client_uuid = uuid(server, client, "client")?;
1079    // Ensure the person setting this has a real role in the settings file, since
1080    // it's persistent.
1081    let _client_real_role = real_role(server, client_uuid, "client")?;
1082    match parse_cmd_args!(args, String, String) {
1083        (Some(locale), Some(msg)) => {
1084            let edit =
1085                server
1086                    .editable_settings_mut()
1087                    .server_description
1088                    .edit(data_dir.as_ref(), |d| {
1089                        let info = Content::localized_with_args(
1090                            "command-set_motd-message-added",
1091                            [("message", format!("{:?}", msg))],
1092                        );
1093
1094                        if let Some(description) = d.descriptions.get_mut(&locale) {
1095                            description.motd = msg;
1096                        } else {
1097                            d.descriptions.insert(locale, ServerDescription {
1098                                motd: msg,
1099                                rules: None,
1100                            });
1101                        }
1102
1103                        Some(info)
1104                    });
1105            drop(data_dir);
1106            edit_setting_feedback(server, client, edit, || {
1107                unreachable!("edit always returns Some")
1108            })
1109        },
1110        (Some(locale), None) => {
1111            let edit =
1112                server
1113                    .editable_settings_mut()
1114                    .server_description
1115                    .edit(data_dir.as_ref(), |d| {
1116                        if let Some(description) = d.descriptions.get_mut(&locale) {
1117                            description.motd.clear();
1118                            Some(Content::localized("command-set_motd-message-removed"))
1119                        } else {
1120                            Some(Content::localized("command-set_motd-message-not-set"))
1121                        }
1122                    });
1123            drop(data_dir);
1124            edit_setting_feedback(server, client, edit, || {
1125                unreachable!("edit always returns Some")
1126            })
1127        },
1128        _ => Err(action.help_content()),
1129    }
1130}
1131
1132fn handle_set_body_type(
1133    server: &mut Server,
1134    _client: EcsEntity,
1135    target: EcsEntity,
1136    args: Vec<String>,
1137    action: &ServerChatCommand,
1138) -> CmdResult<()> {
1139    if let (Some(new_body_type), permanent) = parse_cmd_args!(args, String, bool) {
1140        let permananet = permanent.unwrap_or(false);
1141        let body = server
1142            .state
1143            .ecs()
1144            .read_storage::<comp::Body>()
1145            .get(target)
1146            .copied();
1147        if let Some(mut body) = body {
1148            fn parse_body_type<B: FromStr + std::fmt::Display>(
1149                input: &str,
1150                all_types: impl IntoIterator<Item = B>,
1151            ) -> CmdResult<B> {
1152                FromStr::from_str(input).map_err(|_| {
1153                    Content::localized_with_args("cmd-set_body_type-not_found", [(
1154                        "options",
1155                        all_types
1156                            .into_iter()
1157                            .map(|b| b.to_string())
1158                            .reduce(|mut a, b| {
1159                                a.push_str(",\n");
1160                                a.push_str(&b);
1161                                a
1162                            })
1163                            .unwrap_or_default(),
1164                    )])
1165                })
1166            }
1167            let old_body = body;
1168            match &mut body {
1169                comp::Body::Humanoid(body) => {
1170                    body.body_type =
1171                        parse_body_type(&new_body_type, comp::humanoid::ALL_BODY_TYPES)?;
1172                },
1173                comp::Body::QuadrupedSmall(body) => {
1174                    body.body_type =
1175                        parse_body_type(&new_body_type, comp::quadruped_small::ALL_BODY_TYPES)?;
1176                },
1177                comp::Body::QuadrupedMedium(body) => {
1178                    body.body_type =
1179                        parse_body_type(&new_body_type, comp::quadruped_medium::ALL_BODY_TYPES)?;
1180                },
1181                comp::Body::BirdMedium(body) => {
1182                    body.body_type =
1183                        parse_body_type(&new_body_type, comp::bird_medium::ALL_BODY_TYPES)?;
1184                },
1185                comp::Body::FishMedium(body) => {
1186                    body.body_type =
1187                        parse_body_type(&new_body_type, comp::fish_medium::ALL_BODY_TYPES)?;
1188                },
1189                comp::Body::Dragon(body) => {
1190                    body.body_type = parse_body_type(&new_body_type, comp::dragon::ALL_BODY_TYPES)?;
1191                },
1192                comp::Body::BirdLarge(body) => {
1193                    body.body_type =
1194                        parse_body_type(&new_body_type, comp::bird_large::ALL_BODY_TYPES)?;
1195                },
1196                comp::Body::FishSmall(body) => {
1197                    body.body_type =
1198                        parse_body_type(&new_body_type, comp::fish_small::ALL_BODY_TYPES)?;
1199                },
1200                comp::Body::BipedLarge(body) => {
1201                    body.body_type =
1202                        parse_body_type(&new_body_type, comp::biped_large::ALL_BODY_TYPES)?;
1203                },
1204                comp::Body::BipedSmall(body) => {
1205                    body.body_type =
1206                        parse_body_type(&new_body_type, comp::biped_small::ALL_BODY_TYPES)?;
1207                },
1208                comp::Body::Object(_) => {},
1209                comp::Body::Golem(body) => {
1210                    body.body_type = parse_body_type(&new_body_type, comp::golem::ALL_BODY_TYPES)?;
1211                },
1212                comp::Body::Theropod(body) => {
1213                    body.body_type =
1214                        parse_body_type(&new_body_type, comp::theropod::ALL_BODY_TYPES)?;
1215                },
1216                comp::Body::QuadrupedLow(body) => {
1217                    body.body_type =
1218                        parse_body_type(&new_body_type, comp::quadruped_low::ALL_BODY_TYPES)?;
1219                },
1220                comp::Body::Ship(_) => {},
1221                comp::Body::Arthropod(body) => {
1222                    body.body_type =
1223                        parse_body_type(&new_body_type, comp::arthropod::ALL_BODY_TYPES)?;
1224                },
1225                comp::Body::Item(_) => {},
1226                comp::Body::Crustacean(body) => {
1227                    body.body_type =
1228                        parse_body_type(&new_body_type, comp::crustacean::ALL_BODY_TYPES)?;
1229                },
1230                comp::Body::Plugin(_) => {},
1231            };
1232
1233            if old_body != body {
1234                assign_body(server, target, body)?;
1235
1236                if permananet {
1237                    if let (
1238                        Some(new_body),
1239                        Some(player),
1240                        Some(comp::Presence {
1241                            kind: comp::PresenceKind::Character(id),
1242                            ..
1243                        }),
1244                    ) = (
1245                        server.state.ecs().read_storage::<comp::Body>().get(target),
1246                        server
1247                            .state
1248                            .ecs()
1249                            .read_storage::<comp::Player>()
1250                            .get(target),
1251                        server
1252                            .state
1253                            .ecs()
1254                            .read_storage::<comp::Presence>()
1255                            .get(target),
1256                    ) {
1257                        server
1258                            .state()
1259                            .ecs()
1260                            .write_resource::<crate::persistence::character_updater::CharacterUpdater>()
1261                            .edit_character(
1262                                target,
1263                                player.uuid().to_string(),
1264                                *id,
1265                                None,
1266                                (*new_body,),
1267                                Some(PermanentChange {
1268                                    expected_old_body: old_body,
1269                                }),
1270                            );
1271                    } else {
1272                        return Err(Content::localized("cmd-set_body_type-not_character"));
1273                    }
1274                }
1275            }
1276            Ok(())
1277        } else {
1278            Err(Content::localized("cmd-set_body_type-no_body"))
1279        }
1280    } else {
1281        Err(action.help_content())
1282    }
1283}
1284
1285fn handle_jump(
1286    server: &mut Server,
1287    _client: EcsEntity,
1288    target: EcsEntity,
1289    args: Vec<String>,
1290    action: &ServerChatCommand,
1291) -> CmdResult<()> {
1292    if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
1293    {
1294        server
1295            .state
1296            .position_mut(target, dismount_volume.unwrap_or(true), |current_pos| {
1297                current_pos.0 += Vec3::new(x, y, z)
1298            })
1299    } else {
1300        Err(action.help_content())
1301    }
1302}
1303
1304fn handle_goto(
1305    server: &mut Server,
1306    _client: EcsEntity,
1307    target: EcsEntity,
1308    args: Vec<String>,
1309    action: &ServerChatCommand,
1310) -> CmdResult<()> {
1311    if let (Some(x), Some(y), Some(z), dismount_volume) = parse_cmd_args!(args, f32, f32, f32, bool)
1312    {
1313        server
1314            .state
1315            .position_mut(target, dismount_volume.unwrap_or(true), |current_pos| {
1316                current_pos.0 = Vec3::new(x, y, z)
1317            })
1318    } else {
1319        Err(action.help_content())
1320    }
1321}
1322
1323#[cfg(feature = "worldgen")]
1324fn handle_goto_rand(
1325    server: &mut Server,
1326    _client: EcsEntity,
1327    target: EcsEntity,
1328    args: Vec<String>,
1329    _action: &ServerChatCommand,
1330) -> CmdResult<()> {
1331    let mut rng = rand::rng();
1332    let map_size = server.world.sim().map_size_lg().vec();
1333    let chunk_side = 2_u32.pow(TERRAIN_CHUNK_BLOCKS_LG);
1334    let pos2d = Vec2::new(
1335        rng.random_range(0..(2_u32.pow(map_size.x) * chunk_side)) as f32,
1336        rng.random_range(0..(2_u32.pow(map_size.y) * chunk_side)) as f32,
1337    );
1338    let pos3d = pos2d.with_z(server.world.sim().get_surface_alt_approx(pos2d.as_()));
1339    server.state.position_mut_reposition(
1340        target,
1341        parse_cmd_args!(args, bool).unwrap_or(true),
1342        |current_pos| current_pos.0 = pos3d,
1343        true,
1344        false,
1345    )
1346}
1347
1348#[cfg(not(feature = "worldgen"))]
1349fn handle_goto_rand(
1350    _server: &mut Server,
1351    _client: EcsEntity,
1352    _target: EcsEntity,
1353    _args: Vec<String>,
1354    _action: &ServerChatCommand,
1355) -> CmdResult<()> {
1356    Err(Content::Plain(
1357        "Unsupported without worldgen enabled".into(),
1358    ))
1359}
1360
1361#[cfg(not(feature = "worldgen"))]
1362fn handle_site(
1363    _server: &mut Server,
1364    _client: EcsEntity,
1365    _target: EcsEntity,
1366    _args: Vec<String>,
1367    _action: &ServerChatCommand,
1368) -> CmdResult<()> {
1369    Err(Content::Plain(
1370        "Unsupported without worldgen enabled".into(),
1371    ))
1372}
1373
1374#[cfg(feature = "worldgen")]
1375fn resolve_site(
1376    server: &Server,
1377    key: &str,
1378) -> CmdResult<(
1379    common::store::Id<world::site::Site>,
1380    Option<common::store::Id<world::site::Plot>>,
1381)> {
1382    if let Some(id) = key.strip_prefix("rtsim@") {
1383        let ws = server
1384            .state
1385            .ecs()
1386            .read_resource::<crate::rtsim::RtSim>()
1387            .state()
1388            .data()
1389            .sites
1390            .iter()
1391            .find(|(site_id, _)| key_matches(*site_id, id))
1392            .map(|(_, site)| site.world_site)
1393            .ok_or(Content::Plain(format!(
1394                "Could not find rtsim site with id {id}."
1395            )))?;
1396
1397        let ws = ws.ok_or_else(|| {
1398            Content::Plain(format!(
1399                "The site '{key}' isn't associated with a world site."
1400            ))
1401        })?;
1402
1403        return Ok((ws, None));
1404    }
1405
1406    if let Some(plot_name) = key.strip_prefix("plot:") {
1407        use rand::seq::IteratorRandom;
1408        let plot_name = plot_name.to_lowercase();
1409        return server
1410            .index
1411            .sites
1412            .iter()
1413            .flat_map(|(id, site)| {
1414                site.plots
1415                    .iter()
1416                    .filter(|(_, plot)| plot.kind().to_string().to_lowercase().contains(&plot_name))
1417                    .map(move |(plot_id, _)| (id, Some(plot_id)))
1418            })
1419            .choose(&mut rng())
1420            .ok_or_else(|| {
1421                Content::Plain(format!("Couldn't find a plot with the key '{plot_name}'"))
1422            });
1423    }
1424
1425    server
1426        .index
1427        .sites
1428        .iter()
1429        .find(|(_, site)| site.name() == Some(key))
1430        .map(|(id, _)| (id, None))
1431        .ok_or_else(|| Content::localized("command-site-not-found"))
1432}
1433
1434/// TODO: Add autocompletion if possible (might require modifying enum to handle
1435/// dynamic values).
1436#[cfg(feature = "worldgen")]
1437fn handle_site(
1438    server: &mut Server,
1439    _client: EcsEntity,
1440    target: EcsEntity,
1441    args: Vec<String>,
1442    action: &ServerChatCommand,
1443) -> CmdResult<()> {
1444    if let (Some(dest_name), dismount_volume) = parse_cmd_args!(args, String, bool) {
1445        let (site, plot) = resolve_site(server, &dest_name)?;
1446        let site = server.index.sites.get(site);
1447        let site_pos = if let Some(plot) = plot {
1448            let plot = site.plots.get(plot);
1449            site.tile_center_wpos(plot.root_tile())
1450        } else {
1451            site.origin
1452        };
1453
1454        let site_pos =
1455            server
1456                .world
1457                .find_accessible_pos(server.index.as_index_ref(), site_pos, false);
1458
1459        server.state.position_mut_reposition(
1460            target,
1461            dismount_volume.unwrap_or(true),
1462            |current_pos| current_pos.0 = site_pos,
1463            true,
1464            false,
1465        )
1466    } else {
1467        Err(action.help_content())
1468    }
1469}
1470
1471fn handle_respawn(
1472    server: &mut Server,
1473    _client: EcsEntity,
1474    target: EcsEntity,
1475    _args: Vec<String>,
1476    _action: &ServerChatCommand,
1477) -> CmdResult<()> {
1478    let waypoint = server
1479        .state
1480        .read_storage::<comp::Waypoint>()
1481        .get(target)
1482        .ok_or(Content::localized("command-respawn-no-waypoint"))?
1483        .get_pos();
1484
1485    server.state.position_mut_reposition(
1486        target,
1487        true,
1488        |current_pos| {
1489            current_pos.0 = waypoint;
1490        },
1491        true,
1492        false,
1493    )
1494}
1495
1496fn handle_kill(
1497    server: &mut Server,
1498    _client: EcsEntity,
1499    target: EcsEntity,
1500    _args: Vec<String>,
1501    _action: &ServerChatCommand,
1502) -> CmdResult<()> {
1503    server
1504        .state
1505        .ecs_mut()
1506        .write_storage::<comp::Health>()
1507        .get_mut(target)
1508        .map(|mut h| h.kill());
1509    Ok(())
1510}
1511
1512fn handle_time(
1513    server: &mut Server,
1514    client: EcsEntity,
1515    _target: EcsEntity,
1516    args: Vec<String>,
1517    _action: &ServerChatCommand,
1518) -> CmdResult<()> {
1519    const DAY: u64 = 86400;
1520
1521    let time_in_seconds = server.state.mut_resource::<TimeOfDay>().0;
1522    let current_day = time_in_seconds as u64 / DAY;
1523    let day_start = (current_day * DAY) as f64;
1524
1525    // Find the next occurence of the given time in the day/night cycle
1526    let next_cycle = |time| {
1527        let new_time = day_start + time;
1528        new_time
1529            + if new_time < time_in_seconds {
1530                DAY as f64
1531            } else {
1532                0.0
1533            }
1534    };
1535
1536    let time = parse_cmd_args!(args, String);
1537    const EMSG: &str = "time always valid";
1538    let new_time = match time.as_deref() {
1539        Some("midnight") => next_cycle(
1540            NaiveTime::from_hms_opt(0, 0, 0)
1541                .expect(EMSG)
1542                .num_seconds_from_midnight() as f64,
1543        ),
1544        Some("night") => next_cycle(
1545            NaiveTime::from_hms_opt(20, 0, 0)
1546                .expect(EMSG)
1547                .num_seconds_from_midnight() as f64,
1548        ),
1549        Some("dawn") => next_cycle(
1550            NaiveTime::from_hms_opt(5, 0, 0)
1551                .expect(EMSG)
1552                .num_seconds_from_midnight() as f64,
1553        ),
1554        Some("morning") => next_cycle(
1555            NaiveTime::from_hms_opt(8, 0, 0)
1556                .expect(EMSG)
1557                .num_seconds_from_midnight() as f64,
1558        ),
1559        Some("day") => next_cycle(
1560            NaiveTime::from_hms_opt(10, 0, 0)
1561                .expect(EMSG)
1562                .num_seconds_from_midnight() as f64,
1563        ),
1564        Some("noon") => next_cycle(
1565            NaiveTime::from_hms_opt(12, 0, 0)
1566                .expect(EMSG)
1567                .num_seconds_from_midnight() as f64,
1568        ),
1569        Some("dusk") => next_cycle(
1570            NaiveTime::from_hms_opt(17, 0, 0)
1571                .expect(EMSG)
1572                .num_seconds_from_midnight() as f64,
1573        ),
1574        Some(n) => match n.parse::<f64>() {
1575            Ok(n) => {
1576                // Incase the number of digits in the number is greater than 16
1577                if n >= 1e17 {
1578                    return Err(Content::localized_with_args(
1579                        "command-time-parse-too-large",
1580                        [("n", n.to_string())],
1581                    ));
1582                }
1583                if n < 0.0 {
1584                    return Err(Content::localized_with_args(
1585                        "command-time-parse-negative",
1586                        [("n", n.to_string())],
1587                    ));
1588                }
1589                // Seconds from next midnight
1590                next_cycle(0.0) + n
1591            },
1592            Err(_) => match NaiveTime::parse_from_str(n, "%H:%M") {
1593                // Relative to current day
1594                Ok(time) => next_cycle(time.num_seconds_from_midnight() as f64),
1595                // Accept `u12345`, seconds since midnight day 0
1596                Err(_) => match n
1597                    .get(1..)
1598                    .filter(|_| n.starts_with('u'))
1599                    .and_then(|n| n.trim_start_matches('u').parse::<u64>().ok())
1600                {
1601                    // Absolute time (i.e. from world epoch)
1602                    Some(n) => {
1603                        if (n as f64) < time_in_seconds {
1604                            return Err(Content::localized_with_args("command-time-backwards", [
1605                                ("t", n),
1606                            ]));
1607                        }
1608                        n as f64
1609                    },
1610                    None => {
1611                        return Err(Content::localized_with_args("command-time-invalid", [(
1612                            "t", n,
1613                        )]));
1614                    },
1615                },
1616            },
1617        },
1618        None => {
1619            // Would this ever change? Perhaps in a few hundred thousand years some
1620            // game archeologists of the future will resurrect the best game of all
1621            // time which, obviously, would be Veloren. By that time, the inescapable
1622            // laws of thermodynamics will mean that the earth's rotation period
1623            // would be slower. Of course, a few hundred thousand years is enough
1624            // for the circadian rhythm of human biology to have shifted to account
1625            // accordingly. When booting up Veloren for the first time in 337,241
1626            // years, they might feel a touch of anguish at the fact that their
1627            // earth days and the days within the game do not neatly divide into
1628            // one-another. Understandably, they'll want to change this. Who
1629            // wouldn't? It would be like turning the TV volume up to an odd number
1630            // or having a slightly untuned radio (assuming they haven't begun
1631            // broadcasting information directly into their brains). Totally
1632            // unacceptable. No, the correct and proper thing to do would be to
1633            // release a retroactive definitive edition DLC for $99 with the very
1634            // welcome addition of shorter day periods and a complementary
1635            // 'developer commentary' mode created by digging up the long-decayed
1636            // skeletons of the Veloren team, measuring various attributes of their
1637            // jawlines, and using them to recreate their voices. But how to go about
1638            // this Herculean task? This code is gibberish! The last of the core Rust
1639            // dev team died exactly 337,194 years ago! Rust is now a long-forgotten
1640            // dialect of the ancient ones, lost to the sands of time. Ashes to ashes,
1641            // dust to dust. When all hope is lost, one particularly intrepid
1642            // post-human hominid exployed by the 'Veloren Revival Corp' (no doubt we
1643            // still won't have gotten rid of this blasted 'capitalism' thing by then)
1644            // might notice, after years of searching, a particularly curious
1645            // inscription within the code. The letters `D`, `A`, `Y`. Curious! She
1646            // consults the post-human hominid scholars of the old. Care to empathise
1647            // with her shock when she discovers that these symbols, as alien as they
1648            // may seem, correspond exactly to the word `ⓕя𝐢ᵇᵇ𝔩E`, the word for
1649            // 'day' in the post-human hominid language, which is of course universal.
1650            // Imagine also her surprise when, after much further translating, she
1651            // finds a comment predicting her very existence and her struggle to
1652            // decode this great mystery. Rejoice! The Veloren Revival Corp. may now
1653            // persist with their great Ultimate Edition DLC because the day period
1654            // might now be changed because they have found the constant that controls
1655            // it! Everybody was henceforth happy until the end of time.
1656            //
1657            // This one's for you, xMac ;)
1658            let current_time = NaiveTime::from_num_seconds_from_midnight_opt(
1659                // Wraps around back to 0s if it exceeds 24 hours (24 hours = 86400s)
1660                (time_in_seconds as u64 % DAY) as u32,
1661                0,
1662            );
1663            let msg = match current_time {
1664                Some(time) => Content::localized_with_args("command-time-current", [(
1665                    "t",
1666                    time.format("%H:%M").to_string(),
1667                )]),
1668                None => Content::localized("command-time-unknown"),
1669            };
1670            server.notify_client(
1671                client,
1672                ServerGeneral::server_msg(ChatType::CommandInfo, msg),
1673            );
1674            return Ok(());
1675        },
1676    };
1677
1678    server.state.mut_resource::<TimeOfDay>().0 = new_time;
1679    let time = server.state.ecs().read_resource::<Time>();
1680
1681    // Update all clients with the new TimeOfDay (without this they would have to
1682    // wait for the next 100th tick to receive the update).
1683    let mut tod_lazymsg = None;
1684    let clients = server.state.ecs().read_storage::<Client>();
1685    let calendar = server.state.ecs().read_resource::<Calendar>();
1686    let time_scale = server.state.ecs().read_resource::<TimeScale>();
1687    for client in (&clients).join() {
1688        let msg = tod_lazymsg.unwrap_or_else(|| {
1689            client.prepare(ServerGeneral::TimeOfDay(
1690                TimeOfDay(new_time),
1691                (*calendar).clone(),
1692                *time,
1693                *time_scale,
1694            ))
1695        });
1696        let _ = client.send_prepared(&msg);
1697        tod_lazymsg = Some(msg);
1698    }
1699
1700    if let Some(new_time) =
1701        NaiveTime::from_num_seconds_from_midnight_opt(((new_time as u64) % 86400) as u32, 0)
1702    {
1703        server.notify_client(
1704            client,
1705            ServerGeneral::server_msg(
1706                ChatType::CommandInfo,
1707                Content::Plain(format!("Time changed to: {}", new_time.format("%H:%M"))),
1708            ),
1709        );
1710    }
1711    Ok(())
1712}
1713
1714fn handle_time_scale(
1715    server: &mut Server,
1716    client: EcsEntity,
1717    _target: EcsEntity,
1718    args: Vec<String>,
1719    _action: &ServerChatCommand,
1720) -> CmdResult<()> {
1721    let time_scale = server
1722        .state
1723        .ecs_mut()
1724        .get_mut::<TimeScale>()
1725        .expect("Expected time scale to be added.");
1726    if args.is_empty() {
1727        let time_scale = time_scale.0;
1728        server.notify_client(
1729            client,
1730            ServerGeneral::server_msg(
1731                ChatType::CommandInfo,
1732                Content::localized_with_args("command-time_scale-current", [(
1733                    "scale",
1734                    time_scale.to_string(),
1735                )]),
1736            ),
1737        );
1738    } else if let Some(scale) = parse_cmd_args!(args, f64) {
1739        time_scale.0 = scale.clamp(0.0001, 1000.0);
1740        server.notify_client(
1741            client,
1742            ServerGeneral::server_msg(
1743                ChatType::CommandInfo,
1744                Content::localized_with_args("command-time_scale-changed", [(
1745                    "scale",
1746                    scale.to_string(),
1747                )]),
1748            ),
1749        );
1750        // Update all clients with the new TimeOfDay (without this they would have to
1751        // wait for the next 100th tick to receive the update).
1752        let mut tod_lazymsg = None;
1753        let clients = server.state.ecs().read_storage::<Client>();
1754        let time = server.state.ecs().read_resource::<Time>();
1755        let time_of_day = server.state.ecs().read_resource::<TimeOfDay>();
1756        let calendar = server.state.ecs().read_resource::<Calendar>();
1757        for client in (&clients).join() {
1758            let msg = tod_lazymsg.unwrap_or_else(|| {
1759                client.prepare(ServerGeneral::TimeOfDay(
1760                    *time_of_day,
1761                    (*calendar).clone(),
1762                    *time,
1763                    TimeScale(scale),
1764                ))
1765            });
1766            let _ = client.send_prepared(&msg);
1767            tod_lazymsg = Some(msg);
1768        }
1769    } else {
1770        server.notify_client(
1771            client,
1772            ServerGeneral::server_msg(
1773                ChatType::CommandError,
1774                Content::Plain("Wrong parameter, expected f32.".to_string()),
1775            ),
1776        );
1777    }
1778    Ok(())
1779}
1780
1781fn handle_health(
1782    server: &mut Server,
1783    _client: EcsEntity,
1784    target: EcsEntity,
1785    args: Vec<String>,
1786    _action: &ServerChatCommand,
1787) -> CmdResult<()> {
1788    if let Some(hp) = parse_cmd_args!(args, f32) {
1789        if let Some(mut health) = server
1790            .state
1791            .ecs()
1792            .write_storage::<comp::Health>()
1793            .get_mut(target)
1794        {
1795            let time = server.state.ecs().read_resource::<Time>();
1796            let change = comp::HealthChange {
1797                amount: hp - health.current(),
1798                by: None,
1799                cause: None,
1800                precise: false,
1801                time: *time,
1802                instance: rand::random(),
1803            };
1804            health.change_by(change);
1805            Ok(())
1806        } else {
1807            Err(Content::Plain("You have no health".into()))
1808        }
1809    } else {
1810        Err(Content::Plain("You must specify health amount!".into()))
1811    }
1812}
1813
1814fn handle_poise(
1815    server: &mut Server,
1816    _client: EcsEntity,
1817    target: EcsEntity,
1818    args: Vec<String>,
1819    _action: &ServerChatCommand,
1820) -> CmdResult<()> {
1821    if let Some(new_poise) = parse_cmd_args!(args, f32) {
1822        if let Some(mut poise) = server
1823            .state
1824            .ecs()
1825            .write_storage::<comp::Poise>()
1826            .get_mut(target)
1827        {
1828            let time = server.state.ecs().read_resource::<Time>();
1829            let change = comp::PoiseChange {
1830                amount: new_poise - poise.current(),
1831                impulse: Vec3::zero(),
1832                by: None,
1833                cause: None,
1834                time: *time,
1835            };
1836            poise.change(change);
1837            Ok(())
1838        } else {
1839            Err(Content::Plain("You have no poise".into()))
1840        }
1841    } else {
1842        Err(Content::Plain("You must specify poise amount!".into()))
1843    }
1844}
1845
1846fn handle_alias(
1847    server: &mut Server,
1848    client: EcsEntity,
1849    target: EcsEntity,
1850    args: Vec<String>,
1851    action: &ServerChatCommand,
1852) -> CmdResult<()> {
1853    if let Some(alias) = parse_cmd_args!(args, String) {
1854        // Prevent silly aliases
1855        comp::Player::alias_validate(&alias).map_err(|e| Content::Plain(e.to_string()))?;
1856
1857        let old_alias_optional = server
1858            .state
1859            .ecs_mut()
1860            .write_storage::<comp::Player>()
1861            .get_mut(target)
1862            .map(|mut player| std::mem::replace(&mut player.alias, alias));
1863
1864        // Update name on client player lists
1865        let ecs = server.state.ecs();
1866        if let (Some(uid), Some(player), Some(client), Some(old_alias)) = (
1867            ecs.read_storage::<Uid>().get(target),
1868            ecs.read_storage::<comp::Player>().get(target),
1869            ecs.read_storage::<Client>().get(target),
1870            old_alias_optional,
1871        ) && client.client_type.emit_login_events()
1872        {
1873            let msg = ServerGeneral::PlayerListUpdate(PlayerListUpdate::Alias(
1874                *uid,
1875                player.alias.clone(),
1876            ));
1877            server.state.notify_players(msg);
1878
1879            // Announce alias change if target has a Body.
1880            if ecs.read_storage::<comp::Body>().get(target).is_some() {
1881                server.state.notify_players(ServerGeneral::server_msg(
1882                    ChatType::CommandInfo,
1883                    Content::Plain(format!("{} is now known as {}.", old_alias, player.alias)),
1884                ));
1885            }
1886        }
1887        if client != target {
1888            // Notify target that an admin changed the alias due to /sudo
1889            server.notify_client(
1890                target,
1891                ServerGeneral::server_msg(
1892                    ChatType::CommandInfo,
1893                    Content::Plain("An admin changed your alias.".to_string()),
1894                ),
1895            );
1896        }
1897        Ok(())
1898    } else {
1899        Err(action.help_content())
1900    }
1901}
1902
1903fn handle_tp(
1904    server: &mut Server,
1905    client: EcsEntity,
1906    target: EcsEntity,
1907    args: Vec<String>,
1908    action: &ServerChatCommand,
1909) -> CmdResult<()> {
1910    let (entity_target, dismount_volume) = parse_cmd_args!(args, EntityTarget, bool);
1911    let player = if let Some(entity_target) = entity_target {
1912        get_entity_target(entity_target, server)?
1913    } else if client != target {
1914        client
1915    } else {
1916        return Err(action.help_content());
1917    };
1918    let player_pos = position(server, player, "player")?;
1919    server.state.position_mut_reposition(
1920        target,
1921        dismount_volume.unwrap_or(true),
1922        |target_pos| *target_pos = player_pos,
1923        false,
1924        false,
1925    )
1926}
1927
1928fn handle_rtsim_tp(
1929    server: &mut Server,
1930    _client: EcsEntity,
1931    target: EcsEntity,
1932    args: Vec<String>,
1933    action: &ServerChatCommand,
1934) -> CmdResult<()> {
1935    use crate::rtsim::RtSim;
1936    let (id, dismount_volume) = parse_cmd_args!(args, String, bool);
1937    let pos = if let Some(id) = id {
1938        server
1939            .state
1940            .ecs()
1941            .read_resource::<RtSim>()
1942            .state()
1943            .data()
1944            .actors
1945            .iter()
1946            .find(|(actor_id, _)| key_matches(*actor_id, &id))
1947            .map(|(_, actor)| actor)
1948            .ok_or_else(|| Content::Plain(format!("No NPC has the id {id}")))?
1949            .wpos
1950    } else {
1951        return Err(action.help_content());
1952    };
1953    server.state.position_mut_reposition(
1954        target,
1955        dismount_volume.unwrap_or(true),
1956        |target_pos| {
1957            target_pos.0 = pos;
1958        },
1959        false,
1960        false,
1961    )
1962}
1963
1964fn handle_rtsim_info(
1965    server: &mut Server,
1966    client: EcsEntity,
1967    _target: EcsEntity,
1968    args: Vec<String>,
1969    action: &ServerChatCommand,
1970) -> CmdResult<()> {
1971    use crate::rtsim::RtSim;
1972    let rtsim = server.state.ecs().read_resource::<RtSim>();
1973    let id = parse_cmd_args!(args.clone(), String)
1974        .map(Ok)
1975        .or_else(|| {
1976            let entity = parse_cmd_args!(args, EntityTarget)?;
1977            let entity = match get_entity_target(entity, server) {
1978                Ok(e) => e,
1979                Err(e) => return Some(Err(e)),
1980            };
1981
1982            Some(Ok(format!(
1983                "{:?}",
1984                server
1985                    .state
1986                    .ecs()
1987                    .read_storage::<common::rtsim::ActorId>()
1988                    .get(entity)?
1989            )))
1990        })
1991        .transpose()?;
1992
1993    if let Some(id) = id {
1994        let data = rtsim.state().data();
1995        let (id, actor) = data
1996            .actors
1997            .iter()
1998            .min_by_key(|(actor_id, _)| key_matches(*actor_id, &id))
1999            .ok_or_else(|| Content::Plain(format!("No actor has the id {id}")))?;
2000
2001        let mut info = String::new();
2002
2003        let _ = writeln!(&mut info, "-- General Information --");
2004        let _ = writeln!(&mut info, "Seed: {}", actor.seed);
2005        let _ = writeln!(&mut info, "Pos: {:?}", actor.wpos);
2006        let _ = writeln!(&mut info, "Role: {:?}", actor.role);
2007        let _ = writeln!(&mut info, "Home: {:?}", actor.home);
2008        let _ = writeln!(&mut info, "Faction: {:?}", actor.faction);
2009        if let Some(npc) = actor.npc() {
2010            let _ = writeln!(&mut info, "Personality: {:?}", npc.personality);
2011        }
2012        let _ = writeln!(&mut info, "-- Status --");
2013        let _ = writeln!(&mut info, "Current site: {:?}", actor.current_site);
2014        let _ = writeln!(&mut info, "Current mode: {:?}", actor.mode);
2015        let _ = writeln!(
2016            &mut info,
2017            "Riding: {:?}",
2018            data.actors
2019                .mounts
2020                .get_mount_link(id)
2021                .map(|link| format!("{:?}", link.mount.data()))
2022        );
2023        let _ = writeln!(&mut info, "-- Action State --");
2024        if let Some(npc) = actor.npc()
2025            && let Some(brain) = &npc.brain
2026        {
2027            let mut bt = Vec::new();
2028            brain.action.backtrace(&mut bt);
2029            for (i, action) in bt.into_iter().enumerate() {
2030                let _ = writeln!(&mut info, "[{}] {}", i, action);
2031            }
2032        } else {
2033            let _ = writeln!(&mut info, "<actor has no brain>");
2034        }
2035
2036        server.notify_client(
2037            client,
2038            ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(info)),
2039        );
2040
2041        Ok(())
2042    } else {
2043        Err(action.help_content())
2044    }
2045}
2046
2047fn handle_rtsim_npc(
2048    server: &mut Server,
2049    client: EcsEntity,
2050    target: EcsEntity,
2051    args: Vec<String>,
2052    action: &ServerChatCommand,
2053) -> CmdResult<()> {
2054    use crate::rtsim::RtSim;
2055    if let (Some(query), count) = parse_cmd_args!(args, String, u32) {
2056        let terms = query
2057            .split(',')
2058            .filter(|s| !s.is_empty())
2059            .map(|s| s.trim().to_lowercase())
2060            .collect::<Vec<_>>();
2061        let npc_names = &common::npc::NPC_NAMES.read();
2062        let rtsim = server.state.ecs().read_resource::<RtSim>();
2063        let data = rtsim.state().data();
2064        let mut actors = data
2065            .actors
2066            .iter()
2067            .filter(|(actor_id, actor)| {
2068                let mut tags = vec![
2069                    actor
2070                        .profession()
2071                        .map(|p| format!("{:?}", p))
2072                        .unwrap_or_default(),
2073                    match &actor.role {
2074                        Role::Civilised(_) => "civilised".to_string(),
2075                        Role::Wild => "wild".to_string(),
2076                        Role::Monster => "monster".to_string(),
2077                        Role::Vehicle => "vehicle".to_string(),
2078                    },
2079                    format!("{:?}", actor.mode),
2080                    format!("{:?}", actor_id.data()),
2081                    npc_names[&actor.body].keyword.clone(),
2082                ];
2083                if let Some(species_meta) = npc_names.get_species_meta(&actor.body) {
2084                    tags.push(species_meta.keyword.clone());
2085                }
2086                if let Some(npc) = actor.npc()
2087                    && let Some(brain) = &npc.brain
2088                {
2089                    rtsim::ai::Action::backtrace(&brain.action, &mut tags);
2090                }
2091                terms.iter().all(|term| {
2092                    tags.iter()
2093                        .any(|tag| tag.trim().to_lowercase().contains(term.as_str()))
2094                })
2095            })
2096            .collect::<Vec<_>>();
2097        if let Ok(pos) = position(server, target, "target") {
2098            actors.sort_by_key(|(_, actor)| (actor.wpos.distance_squared(pos.0) * 10.0) as u64);
2099        }
2100
2101        let mut info = String::new();
2102
2103        let _ = writeln!(&mut info, "-- NPCs matching [{}] --", terms.join(", "));
2104        for (actor_id, actor) in actors.iter().take(count.unwrap_or(!0) as usize) {
2105            let _ = write!(
2106                &mut info,
2107                "{} ({:?}), ",
2108                actor.get_name().as_deref().unwrap_or("<unknown>"),
2109                actor_id.data(),
2110            );
2111        }
2112        let _ = writeln!(&mut info);
2113        let _ = writeln!(
2114            &mut info,
2115            "Showing {}/{} matching NPCs.",
2116            count.unwrap_or(actors.len() as u32),
2117            actors.len()
2118        );
2119
2120        server.notify_client(
2121            client,
2122            ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(info)),
2123        );
2124
2125        Ok(())
2126    } else {
2127        Err(action.help_content())
2128    }
2129}
2130
2131// TODO: Remove this command when rtsim becomes more mature and we're sure we
2132// don't need purges to fix broken state.
2133fn handle_rtsim_purge(
2134    server: &mut Server,
2135    client: EcsEntity,
2136    _target: EcsEntity,
2137    args: Vec<String>,
2138    action: &ServerChatCommand,
2139) -> CmdResult<()> {
2140    use crate::rtsim::RtSim;
2141    let client_uuid = uuid(server, client, "client")?;
2142    if !matches!(real_role(server, client_uuid, "client")?, AdminRole::Admin) {
2143        return Err(Content::localized("command-rtsim-purge-perms"));
2144    }
2145
2146    if let Some(should_purge) = parse_cmd_args!(args, bool) {
2147        server
2148            .state
2149            .ecs()
2150            .write_resource::<RtSim>()
2151            .set_should_purge(should_purge);
2152        server.notify_client(
2153            client,
2154            ServerGeneral::server_msg(
2155                ChatType::CommandInfo,
2156                Content::Plain(format!(
2157                    "Rtsim data {} be purged on next startup",
2158                    if should_purge { "WILL" } else { "will NOT" },
2159                )),
2160            ),
2161        );
2162        Ok(())
2163    } else {
2164        Err(action.help_content())
2165    }
2166}
2167
2168fn handle_rtsim_chunk(
2169    server: &mut Server,
2170    client: EcsEntity,
2171    target: EcsEntity,
2172    _args: Vec<String>,
2173    _action: &ServerChatCommand,
2174) -> CmdResult<()> {
2175    use crate::rtsim::{ChunkStates, RtSim};
2176    let pos = position(server, target, "target")?;
2177
2178    let chunk_key = pos.0.xy().as_::<i32>().wpos_to_cpos();
2179
2180    let rtsim = server.state.ecs().read_resource::<RtSim>();
2181    let data = rtsim.state().data();
2182
2183    let oob_err = || {
2184        Content::localized_with_args("command-chunk-out-of-bounds", [
2185            ("x", chunk_key.x.to_string()),
2186            ("y", chunk_key.y.to_string()),
2187        ])
2188    };
2189
2190    let chunk_states = rtsim.state().resource::<ChunkStates>();
2191    let chunk_state = match chunk_states.0.get(chunk_key) {
2192        Some(Some(chunk_state)) => chunk_state,
2193        Some(None) => {
2194            return Err(Content::localized_with_args("command-chunk-not-loaded", [
2195                ("x", chunk_key.x.to_string()),
2196                ("y", chunk_key.y.to_string()),
2197            ]));
2198        },
2199        None => {
2200            return Err(oob_err());
2201        },
2202    };
2203
2204    let mut info = String::new();
2205    let _ = writeln!(
2206        &mut info,
2207        "-- Chunk {}, {} Resources --",
2208        chunk_key.x, chunk_key.y
2209    );
2210    for (res, frac) in data.nature.chunk_resources(chunk_key).ok_or_else(oob_err)? {
2211        let total = chunk_state.max_res[res];
2212        let _ = writeln!(
2213            &mut info,
2214            "{:?}: {} / {} ({}%)",
2215            res,
2216            frac * total as f32,
2217            total,
2218            frac * 100.0
2219        );
2220    }
2221
2222    server.notify_client(
2223        client,
2224        ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(info)),
2225    );
2226
2227    Ok(())
2228}
2229
2230fn handle_spawn(
2231    server: &mut Server,
2232    client: EcsEntity,
2233    target: EcsEntity,
2234    args: Vec<String>,
2235    action: &ServerChatCommand,
2236) -> CmdResult<()> {
2237    match parse_cmd_args!(args, String, npc::NpcBody, u32, bool, f32, bool) {
2238        (
2239            Some(opt_align),
2240            Some(npc::NpcBody(id, mut body)),
2241            opt_amount,
2242            opt_ai,
2243            opt_scale,
2244            opt_tethered,
2245        ) => {
2246            let uid = uid(server, target, "target")?;
2247            let alignment = parse_alignment(uid, &opt_align)?;
2248
2249            if matches!(alignment, Alignment::Owned(_))
2250                && server
2251                    .state
2252                    .ecs()
2253                    .read_storage::<comp::Anchor>()
2254                    .contains(target)
2255            {
2256                return Err(Content::Plain(
2257                    "Spawning this pet would create an anchor chain".into(),
2258                ));
2259            }
2260
2261            let amount = opt_amount.filter(|x| *x > 0).unwrap_or(1).min(50);
2262
2263            let ai = opt_ai.unwrap_or(true);
2264            let pos = position(server, target, "target")?;
2265            let mut agent = comp::Agent::from_body(&body());
2266
2267            if matches!(alignment, comp::Alignment::Owned(_)) {
2268                agent.psyche.idle_wander_factor = 0.25;
2269            } else {
2270                // If unowned, the agent should stay in a particular place
2271                agent = agent.with_patrol_origin(pos.0);
2272            }
2273
2274            for _ in 0..amount {
2275                let vel = Vec3::new(
2276                    rng().random_range(-2.0..3.0),
2277                    rng().random_range(-2.0..3.0),
2278                    10.0,
2279                );
2280
2281                let body = body();
2282                let loadout = LoadoutBuilder::from_default(&body).build();
2283                let inventory = Inventory::with_loadout(loadout, body);
2284
2285                let mut entity_base = server
2286                    .state
2287                    .create_npc(
2288                        pos,
2289                        comp::Ori::default(),
2290                        comp::Stats::new(
2291                            Content::Plain(get_npc_name(id, npc::BodyType::from_body(body))),
2292                            body,
2293                        ),
2294                        comp::SkillSet::default(),
2295                        Some(comp::Health::new(body)),
2296                        comp::Poise::new(body),
2297                        inventory,
2298                        body,
2299                        opt_scale.map(comp::Scale).unwrap_or(body.scale()),
2300                    )
2301                    .with(comp::Vel(vel))
2302                    .with(alignment);
2303
2304                if ai {
2305                    entity_base = entity_base.with(agent.clone());
2306                }
2307
2308                let new_entity = entity_base.build();
2309
2310                if opt_tethered == Some(true) {
2311                    let tether_leader = server
2312                        .state
2313                        .read_component_cloned::<Is<Rider>>(target)
2314                        .map(|is_rider| is_rider.mount)
2315                        .or_else(|| server.state.ecs().uid_from_entity(target));
2316                    let tether_follower = server.state.ecs().uid_from_entity(new_entity);
2317
2318                    if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
2319                        server
2320                            .state
2321                            .link(Tethered {
2322                                leader,
2323                                follower,
2324                                tether_length: 4.0,
2325                            })
2326                            .map_err(|_| Content::Plain("Failed to tether entities".to_string()))?;
2327                    } else {
2328                        return Err(Content::Plain("Tether members don't have Uids.".into()));
2329                    }
2330                }
2331
2332                // Add to group system if a pet
2333                if matches!(alignment, comp::Alignment::Owned { .. }) {
2334                    server.state.emit_event_now(TamePetEvent {
2335                        owner_entity: target,
2336                        pet_entity: new_entity,
2337                    });
2338                } else if let Some(group) = alignment.group() {
2339                    insert_or_replace_component(server, new_entity, group, "new entity")?;
2340                }
2341
2342                if let Some(uid) = server.state.ecs().uid_from_entity(new_entity) {
2343                    server.notify_client(
2344                        client,
2345                        ServerGeneral::server_msg(
2346                            ChatType::CommandInfo,
2347                            Content::localized_with_args("command-spawned-entity", [(
2348                                "id",
2349                                uid.0.get(),
2350                            )]),
2351                        ),
2352                    );
2353                }
2354            }
2355            server.notify_client(
2356                client,
2357                ServerGeneral::server_msg(
2358                    ChatType::CommandInfo,
2359                    Content::Plain(format!("Spawned {} entities", amount)),
2360                ),
2361            );
2362            Ok(())
2363        },
2364        _ => Err(action.help_content()),
2365    }
2366}
2367
2368fn handle_spawn_training_dummy(
2369    server: &mut Server,
2370    client: EcsEntity,
2371    target: EcsEntity,
2372    _args: Vec<String>,
2373    _action: &ServerChatCommand,
2374) -> CmdResult<()> {
2375    let pos = position(server, target, "target")?;
2376    let vel = Vec3::new(
2377        rng().random_range(-2.0..3.0),
2378        rng().random_range(-2.0..3.0),
2379        10.0,
2380    );
2381
2382    let body = comp::Body::Object(comp::object::Body::TrainingDummy);
2383
2384    let stats = comp::Stats::new(
2385        Content::with_attr("name-custom-village-dummy", "neut"),
2386        body,
2387    );
2388    let skill_set = comp::SkillSet::default();
2389    let health = comp::Health::new(body);
2390    let poise = comp::Poise::new(body);
2391
2392    server
2393        .state
2394        .create_npc(
2395            pos,
2396            comp::Ori::default(),
2397            stats,
2398            skill_set,
2399            Some(health),
2400            poise,
2401            Inventory::with_empty(),
2402            body,
2403            comp::Scale(1.0),
2404        )
2405        .with(comp::Vel(vel))
2406        .build();
2407
2408    server.notify_client(
2409        client,
2410        ServerGeneral::server_msg(
2411            ChatType::CommandInfo,
2412            Content::localized("command-spawned-dummy"),
2413        ),
2414    );
2415    Ok(())
2416}
2417
2418fn handle_spawn_airship(
2419    server: &mut Server,
2420    client: EcsEntity,
2421    target: EcsEntity,
2422    args: Vec<String>,
2423    _action: &ServerChatCommand,
2424) -> CmdResult<()> {
2425    let (body_name, angle) = parse_cmd_args!(args, String, f32);
2426    let mut pos = position(server, target, "target")?;
2427    pos.0.z += 50.0;
2428    const DESTINATION_RADIUS: f32 = 2000.0;
2429    let angle = angle.map(|a| a * std::f32::consts::PI / 180.0);
2430    let dir = angle.map(|a| Vec3::new(a.cos(), a.sin(), 0.0));
2431    let destination = dir.map(|dir| pos.0 + dir * DESTINATION_RADIUS + Vec3::new(0.0, 0.0, 200.0));
2432    let ship = if let Some(body_name) = body_name {
2433        *comp::ship::ALL_AIRSHIPS
2434            .iter()
2435            .find(|body| format!("{body:?}") == body_name)
2436            .ok_or_else(|| Content::Plain(format!("No such airship '{body_name}'.")))?
2437    } else {
2438        comp::ship::Body::random_airship_with(&mut rng())
2439    };
2440    let ori = comp::Ori::from(common::util::Dir::new(dir.unwrap_or(Vec3::unit_y())));
2441    let mut builder = server
2442        .state
2443        .create_ship(pos, ori, ship, |ship| ship.make_collider());
2444    if let Some(pos) = destination {
2445        let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
2446            .with_destination(pos)
2447            .with_altitude_pid_controller(PidControllers::<16>::new_multi_pid_controllers(
2448                FlightMode::FlyThrough,
2449                pos,
2450            ));
2451        builder = builder.with(agent);
2452    }
2453    builder.build();
2454
2455    server.notify_client(
2456        client,
2457        ServerGeneral::server_msg(
2458            ChatType::CommandInfo,
2459            Content::localized("command-spawned-airship"),
2460        ),
2461    );
2462    Ok(())
2463}
2464
2465fn handle_spawn_ship(
2466    server: &mut Server,
2467    client: EcsEntity,
2468    target: EcsEntity,
2469    args: Vec<String>,
2470    _action: &ServerChatCommand,
2471) -> CmdResult<()> {
2472    let (body_name, tethered, angle) = parse_cmd_args!(args, String, bool, f32);
2473    let mut pos = position(server, target, "target")?;
2474    pos.0.z += 5.0;
2475    const DESTINATION_RADIUS: f32 = 2000.0;
2476    let angle = angle.map(|a| a * std::f32::consts::PI / 180.0);
2477    let dir = angle.map(|a| Vec3::new(a.cos(), a.sin(), 0.0));
2478    let destination = dir.map(|dir| pos.0 + dir * DESTINATION_RADIUS + Vec3::new(0.0, 0.0, 200.0));
2479    let ship = if let Some(body_name) = body_name {
2480        *comp::ship::ALL_SHIPS
2481            .iter()
2482            .find(|body| format!("{body:?}") == body_name)
2483            .ok_or_else(|| Content::Plain(format!("No such airship '{body_name}'.")))?
2484    } else {
2485        comp::ship::Body::random_airship_with(&mut rng())
2486    };
2487    let ori = comp::Ori::from(common::util::Dir::new(dir.unwrap_or(Vec3::unit_y())));
2488    let mut builder = server
2489        .state
2490        .create_ship(pos, ori, ship, |ship| ship.make_collider());
2491
2492    if let Some(pos) = destination {
2493        let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
2494            .with_destination(pos)
2495            .with_altitude_pid_controller(PidControllers::<16>::new_multi_pid_controllers(
2496                FlightMode::FlyThrough,
2497                pos,
2498            ));
2499        builder = builder.with(agent);
2500    }
2501
2502    let new_entity = builder.build();
2503
2504    if tethered == Some(true) {
2505        let tether_leader = server
2506            .state
2507            .read_component_cloned::<Is<Rider>>(target)
2508            .map(|is_rider| is_rider.mount)
2509            .or_else(|| {
2510                server
2511                    .state
2512                    .read_component_cloned::<Is<VolumeRider>>(target)
2513                    .and_then(|is_volume_rider| {
2514                        if let Volume::Entity(uid) = is_volume_rider.pos.kind {
2515                            Some(uid)
2516                        } else {
2517                            None
2518                        }
2519                    })
2520            })
2521            .or_else(|| server.state.ecs().uid_from_entity(target));
2522        let tether_follower = server.state.ecs().uid_from_entity(new_entity);
2523
2524        if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
2525            let tether_length = tether_leader
2526                .and_then(|uid| server.state.ecs().entity_from_uid(uid))
2527                .and_then(|e| server.state.read_component_cloned::<comp::Body>(e))
2528                .map(|b| b.dimensions().y * 1.5 + 1.0)
2529                .unwrap_or(6.0);
2530            server
2531                .state
2532                .link(Tethered {
2533                    leader,
2534                    follower,
2535                    tether_length,
2536                })
2537                .map_err(|_| Content::Plain("Failed to tether entities".to_string()))?;
2538        } else {
2539            return Err(Content::Plain("Tether members don't have Uids.".into()));
2540        }
2541    }
2542
2543    server.notify_client(
2544        client,
2545        ServerGeneral::server_msg(
2546            ChatType::CommandInfo,
2547            Content::Plain("Spawned a ship".to_string()),
2548        ),
2549    );
2550    Ok(())
2551}
2552
2553fn handle_make_volume(
2554    server: &mut Server,
2555    client: EcsEntity,
2556    target: EcsEntity,
2557    args: Vec<String>,
2558    _action: &ServerChatCommand,
2559) -> CmdResult<()> {
2560    use comp::body::ship::figuredata::VoxelCollider;
2561
2562    //let () = parse_cmd_args!(args);
2563    let pos = position(server, target, "target")?;
2564    let ship = comp::ship::Body::Volume;
2565    let sz = parse_cmd_args!(args, u32).unwrap_or(15);
2566    if !(1..=127).contains(&sz) {
2567        return Err(Content::localized("command-volume-size-incorrect"));
2568    };
2569    let sz = Vec3::broadcast(sz);
2570    let collider = {
2571        let terrain = server.state().terrain();
2572        comp::Collider::Volume(Arc::new(VoxelCollider::from_fn(sz, |rpos| {
2573            terrain
2574                .get(pos.0.map(|e| e.floor() as i32) + rpos - sz.map(|e| e as i32) / 2)
2575                .ok()
2576                .copied()
2577                .unwrap_or_else(Block::empty)
2578        })))
2579    };
2580    server
2581        .state
2582        .create_ship(
2583            comp::Pos(pos.0 + Vec3::unit_z() * (50.0 + sz.z as f32 / 2.0)),
2584            comp::Ori::default(),
2585            ship,
2586            move |_| collider,
2587        )
2588        .build();
2589
2590    server.notify_client(
2591        client,
2592        ServerGeneral::server_msg(
2593            ChatType::CommandInfo,
2594            Content::localized("command-volume-created"),
2595        ),
2596    );
2597    Ok(())
2598}
2599
2600fn handle_spawn_campfire(
2601    server: &mut Server,
2602    client: EcsEntity,
2603    target: EcsEntity,
2604    _args: Vec<String>,
2605    _action: &ServerChatCommand,
2606) -> CmdResult<()> {
2607    let pos = position(server, target, "target")?;
2608    server
2609        .state
2610        .ecs()
2611        .read_resource::<EventBus<CreateSpecialEntityEvent>>()
2612        .emit_now(CreateSpecialEntityEvent {
2613            pos: pos.0,
2614            entity: SpecialEntity::Waypoint,
2615        });
2616
2617    server.notify_client(
2618        client,
2619        ServerGeneral::server_msg(
2620            ChatType::CommandInfo,
2621            Content::localized("command-spawned-campfire"),
2622        ),
2623    );
2624    Ok(())
2625}
2626
2627#[cfg(feature = "persistent_world")]
2628fn handle_clear_persisted_terrain(
2629    server: &mut Server,
2630    _client: EcsEntity,
2631    target: EcsEntity,
2632    args: Vec<String>,
2633    action: &ServerChatCommand,
2634) -> CmdResult<()> {
2635    let Some(radius) = parse_cmd_args!(args, i32) else {
2636        return Err(action.help_content());
2637    };
2638    // Clamp the radius to prevent accidentally passing too large radiuses
2639    let radius = radius.clamp(0, 64);
2640
2641    let pos = position(server, target, "target")?;
2642    let chunk_key = server.state.terrain().pos_key(pos.0.as_());
2643
2644    let mut terrain_persistence2 = server
2645        .state
2646        .ecs()
2647        .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>();
2648    if let Some(ref mut terrain_persistence) = terrain_persistence2 {
2649        for offset in Spiral2d::with_radius(radius) {
2650            let chunk_key = chunk_key + offset;
2651            terrain_persistence.clear_chunk(chunk_key);
2652        }
2653
2654        drop(terrain_persistence2);
2655        reload_chunks_inner(server, pos.0, Some(radius), false);
2656
2657        Ok(())
2658    } else {
2659        Err(Content::localized(
2660            "command-experimental-terrain-persistence-disabled",
2661        ))
2662    }
2663}
2664
2665#[cfg(not(feature = "persistent_world"))]
2666fn handle_clear_persisted_terrain(
2667    _server: &mut Server,
2668    _client: EcsEntity,
2669    _target: EcsEntity,
2670    _args: Vec<String>,
2671    _action: &ServerChatCommand,
2672) -> CmdResult<()> {
2673    Err(Content::localized(
2674        "command-server-no-experimental-terrain-persistence",
2675    ))
2676}
2677
2678fn handle_safezone(
2679    server: &mut Server,
2680    client: EcsEntity,
2681    target: EcsEntity,
2682    args: Vec<String>,
2683    _action: &ServerChatCommand,
2684) -> CmdResult<()> {
2685    let range = parse_cmd_args!(args, f32);
2686    let pos = position(server, target, "target")?;
2687    server.state.create_safezone(range, pos).build();
2688
2689    server.notify_client(
2690        client,
2691        ServerGeneral::server_msg(
2692            ChatType::CommandInfo,
2693            Content::localized("command-spawned-safezone"),
2694        ),
2695    );
2696    Ok(())
2697}
2698
2699fn handle_permit_build(
2700    server: &mut Server,
2701    client: EcsEntity,
2702    target: EcsEntity,
2703    args: Vec<String>,
2704    action: &ServerChatCommand,
2705) -> CmdResult<()> {
2706    if let Some(area_name) = parse_cmd_args!(args, String) {
2707        let bb_id = area(server, &area_name, "build")?;
2708        let mut can_build = server.state.ecs().write_storage::<comp::CanBuild>();
2709        let entry = can_build
2710            .entry(target)
2711            .map_err(|_| Content::Plain("Cannot find target entity!".to_string()))?;
2712        let mut comp_can_build = entry.or_insert(comp::CanBuild {
2713            enabled: false,
2714            build_areas: HashSet::new(),
2715        });
2716        comp_can_build.build_areas.insert(bb_id);
2717        drop(can_build);
2718        if client != target {
2719            server.notify_client(
2720                target,
2721                ServerGeneral::server_msg(
2722                    ChatType::CommandInfo,
2723                    Content::localized_with_args("command-permit-build-given", [(
2724                        "area",
2725                        area_name.clone(),
2726                    )]),
2727                ),
2728            );
2729        }
2730        server.notify_client(
2731            client,
2732            ServerGeneral::server_msg(
2733                ChatType::CommandInfo,
2734                Content::localized_with_args("command-permit-build-granted", [("area", area_name)]),
2735            ),
2736        );
2737        Ok(())
2738    } else {
2739        Err(action.help_content())
2740    }
2741}
2742
2743fn handle_revoke_build(
2744    server: &mut Server,
2745    client: EcsEntity,
2746    target: EcsEntity,
2747    args: Vec<String>,
2748    action: &ServerChatCommand,
2749) -> CmdResult<()> {
2750    if let Some(area_name) = parse_cmd_args!(args, String) {
2751        let bb_id = area(server, &area_name, "build")?;
2752        let mut can_build = server.state.ecs_mut().write_storage::<comp::CanBuild>();
2753        if let Some(mut comp_can_build) = can_build.get_mut(target) {
2754            comp_can_build.build_areas.retain(|&x| x != bb_id);
2755            drop(can_build);
2756            if client != target {
2757                server.notify_client(
2758                    target,
2759                    ServerGeneral::server_msg(
2760                        ChatType::CommandInfo,
2761                        Content::localized_with_args("command-revoke-build-recv", [(
2762                            "area",
2763                            area_name.clone(),
2764                        )]),
2765                    ),
2766                );
2767            }
2768            server.notify_client(
2769                client,
2770                ServerGeneral::server_msg(
2771                    ChatType::CommandInfo,
2772                    Content::localized_with_args("command-revoke-build", [("area", area_name)]),
2773                ),
2774            );
2775            Ok(())
2776        } else {
2777            Err(Content::localized("command-no-buid-perms"))
2778        }
2779    } else {
2780        Err(action.help_content())
2781    }
2782}
2783
2784fn handle_revoke_build_all(
2785    server: &mut Server,
2786    client: EcsEntity,
2787    target: EcsEntity,
2788    _args: Vec<String>,
2789    _action: &ServerChatCommand,
2790) -> CmdResult<()> {
2791    let ecs = server.state.ecs();
2792
2793    ecs.write_storage::<comp::CanBuild>().remove(target);
2794    if client != target {
2795        server.notify_client(
2796            target,
2797            ServerGeneral::server_msg(
2798                ChatType::CommandInfo,
2799                Content::localized("command-revoke-build-all"),
2800            ),
2801        );
2802    }
2803    server.notify_client(
2804        client,
2805        ServerGeneral::server_msg(
2806            ChatType::CommandInfo,
2807            Content::localized("command-revoked-all-build"),
2808        ),
2809    );
2810    Ok(())
2811}
2812
2813fn handle_players(
2814    server: &mut Server,
2815    client: EcsEntity,
2816    _target: EcsEntity,
2817    _args: Vec<String>,
2818    _action: &ServerChatCommand,
2819) -> CmdResult<()> {
2820    let ecs = server.state.ecs();
2821
2822    let entity_tuples = (
2823        &ecs.entities(),
2824        &ecs.read_storage::<comp::Player>(),
2825        &ecs.read_storage::<comp::Stats>(),
2826    );
2827
2828    // Contruct list of every player currently online
2829    let mut player_list = String::new();
2830    for (_, player, stat) in entity_tuples.join() {
2831        player_list.push_str(&format!(
2832            "[{}]{}\n",
2833            player.alias,
2834            stat.name.as_plain().unwrap_or("<?>")
2835        ));
2836    }
2837
2838    // Show all players currently online
2839    server.notify_client(
2840        client,
2841        ServerGeneral::server_msg(
2842            ChatType::CommandInfo,
2843            Content::localized_with_args("players-list-header", [
2844                (
2845                    "count",
2846                    LocalizationArg::from(entity_tuples.join().count() as u64),
2847                ),
2848                ("player_list", LocalizationArg::from(player_list)),
2849            ]),
2850        ),
2851    );
2852
2853    Ok(())
2854}
2855
2856fn handle_spawn_portal(
2857    server: &mut Server,
2858    client: EcsEntity,
2859    target: EcsEntity,
2860    args: Vec<String>,
2861    action: &ServerChatCommand,
2862) -> CmdResult<()> {
2863    let pos = position(server, target, "target")?;
2864
2865    if let (Some(x), Some(y), Some(z), requires_no_aggro, buildup_time) =
2866        parse_cmd_args!(args, f32, f32, f32, bool, f64)
2867    {
2868        let requires_no_aggro = requires_no_aggro.unwrap_or(false);
2869        let buildup_time = Secs(buildup_time.unwrap_or(7.));
2870        server
2871            .state
2872            .create_teleporter(pos, PortalData {
2873                target: Vec3::new(x, y, z),
2874                buildup_time,
2875                requires_no_aggro,
2876            })
2877            .build();
2878
2879        server.notify_client(
2880            client,
2881            ServerGeneral::server_msg(
2882                ChatType::CommandInfo,
2883                Content::Plain("Spawned portal".to_string()),
2884            ),
2885        );
2886        Ok(())
2887    } else {
2888        Err(action.help_content())
2889    }
2890}
2891
2892fn handle_build(
2893    server: &mut Server,
2894    client: EcsEntity,
2895    target: EcsEntity,
2896    _args: Vec<String>,
2897    _action: &ServerChatCommand,
2898) -> CmdResult<()> {
2899    if let Some(mut can_build) = server
2900        .state
2901        .ecs()
2902        .write_storage::<comp::CanBuild>()
2903        .get_mut(target)
2904    {
2905        can_build.enabled ^= true;
2906
2907        let msg = Content::localized(
2908            match (
2909                can_build.enabled,
2910                server.settings().experimental_terrain_persistence,
2911            ) {
2912                (false, _) => "command-set-build-mode-off",
2913                (true, false) => "command-set-build-mode-on-unpersistent",
2914                (true, true) => "command-set-build-mode-on-persistent",
2915            },
2916        );
2917
2918        let chat_msg = ServerGeneral::server_msg(ChatType::CommandInfo, msg);
2919        if client != target {
2920            server.notify_client(target, chat_msg.clone());
2921        }
2922        server.notify_client(client, chat_msg);
2923        Ok(())
2924    } else {
2925        Err(Content::Plain(
2926            "You do not have permission to build.".into(),
2927        ))
2928    }
2929}
2930
2931fn get_areas_mut<'l>(kind: &str, state: &'l mut State) -> CmdResult<&'l mut Areas> {
2932    Ok(match AreaKind::from_str(kind).ok() {
2933        Some(AreaKind::Build) => state
2934            .mut_resource::<AreasContainer<BuildArea>>()
2935            .deref_mut(),
2936        Some(AreaKind::NoDurability) => state
2937            .mut_resource::<AreasContainer<NoDurabilityArea>>()
2938            .deref_mut(),
2939        Some(AreaKind::BattleModeChange) => state
2940            .mut_resource::<AreasContainer<BattleModeChangeArea>>()
2941            .deref_mut(),
2942        None => Err(Content::Plain(format!("Invalid area type '{kind}'")))?,
2943    })
2944}
2945
2946fn handle_area_add(
2947    server: &mut Server,
2948    client: EcsEntity,
2949    _target: EcsEntity,
2950    args: Vec<String>,
2951    action: &ServerChatCommand,
2952) -> CmdResult<()> {
2953    if let (
2954        Some(area_name),
2955        Some(kind),
2956        Some(xlo),
2957        Some(xhi),
2958        Some(ylo),
2959        Some(yhi),
2960        Some(zlo),
2961        Some(zhi),
2962    ) = parse_cmd_args!(args, String, String, i32, i32, i32, i32, i32, i32)
2963    {
2964        let special_areas = get_areas_mut(&kind, &mut server.state)?;
2965        let msg = ServerGeneral::server_msg(
2966            ChatType::CommandInfo,
2967            Content::Plain(format!("Created {kind} zone {}", area_name)),
2968        );
2969        special_areas
2970            .insert(area_name, Aabb {
2971                min: Vec3::new(xlo, ylo, zlo),
2972                max: Vec3::new(xhi, yhi, zhi),
2973            })
2974            .map_err(|area_name| {
2975                Content::Plain(format!("{kind} zone {} already exists!", area_name))
2976            })?;
2977        server.notify_client(client, msg);
2978        Ok(())
2979    } else {
2980        Err(action.help_content())
2981    }
2982}
2983
2984fn handle_area_list(
2985    server: &mut Server,
2986    client: EcsEntity,
2987    _target: EcsEntity,
2988    _args: Vec<String>,
2989    _action: &ServerChatCommand,
2990) -> CmdResult<()> {
2991    let format_areas = |areas: &Areas, kind: &str| {
2992        areas
2993            .area_metas()
2994            .iter()
2995            .fold(format!("{kind} areas:"), |acc, (area_name, bb_id)| {
2996                if let Some(aabb) = areas.areas().get(*bb_id) {
2997                    format!("{}\n{}: {} to {} ()", acc, area_name, aabb.min, aabb.max,)
2998                } else {
2999                    acc
3000                }
3001            })
3002    };
3003    let build_message = format_areas(
3004        server.state.mut_resource::<AreasContainer<BuildArea>>(),
3005        "Build",
3006    );
3007    let no_dura_message = format_areas(
3008        server
3009            .state
3010            .mut_resource::<AreasContainer<NoDurabilityArea>>(),
3011        "Durability free",
3012    );
3013
3014    let msg = ServerGeneral::server_msg(
3015        ChatType::CommandInfo,
3016        Content::Plain([build_message, no_dura_message].join("\n")),
3017    );
3018
3019    server.notify_client(client, msg);
3020    Ok(())
3021}
3022
3023fn handle_area_remove(
3024    server: &mut Server,
3025    client: EcsEntity,
3026    _target: EcsEntity,
3027    args: Vec<String>,
3028    action: &ServerChatCommand,
3029) -> CmdResult<()> {
3030    if let (Some(area_name), Some(kind)) = parse_cmd_args!(args, String, String) {
3031        let areas = get_areas_mut(&kind, &mut server.state)?;
3032
3033        areas.remove(&area_name).map_err(|err| match err {
3034            SpecialAreaError::Reserved => Content::Plain(format!(
3035                "Special area is reserved and cannot be removed: {}",
3036                area_name
3037            )),
3038            SpecialAreaError::NotFound => {
3039                Content::Plain(format!("No such build area {}", area_name))
3040            },
3041        })?;
3042        server.notify_client(
3043            client,
3044            ServerGeneral::server_msg(
3045                ChatType::CommandInfo,
3046                Content::Plain(format!("Removed {kind} zone {area_name}")),
3047            ),
3048        );
3049        Ok(())
3050    } else {
3051        Err(action.help_content())
3052    }
3053}
3054
3055fn parse_alignment(owner: Uid, alignment: &str) -> CmdResult<Alignment> {
3056    match alignment {
3057        "wild" => Ok(Alignment::Wild),
3058        "enemy" => Ok(Alignment::Enemy),
3059        "npc" => Ok(Alignment::Npc),
3060        "pet" => Ok(comp::Alignment::Owned(owner)),
3061        _ => Err(Content::localized_with_args("command-invalid-alignment", [
3062            ("alignment", alignment),
3063        ])),
3064    }
3065}
3066
3067fn handle_kill_npcs(
3068    server: &mut Server,
3069    client: EcsEntity,
3070    target: EcsEntity,
3071    args: Vec<String>,
3072    _action: &ServerChatCommand,
3073) -> CmdResult<()> {
3074    let (radius, options) = parse_cmd_args!(args, f32, String);
3075    let kill_pets = if let Some(kill_option) = options {
3076        kill_option.contains("--also-pets")
3077    } else {
3078        false
3079    };
3080
3081    let position = radius
3082        .map(|_| position(server, target, "target"))
3083        .transpose()?;
3084
3085    let to_kill = {
3086        let ecs = server.state.ecs();
3087        let entities = ecs.entities();
3088        let positions = ecs.write_storage::<comp::Pos>();
3089        let healths = ecs.write_storage::<comp::Health>();
3090        let players = ecs.read_storage::<comp::Player>();
3091        let alignments = ecs.read_storage::<Alignment>();
3092        let rtsim_actors = ecs.read_storage::<common::rtsim::ActorId>();
3093        let mut rtsim = ecs.write_resource::<crate::rtsim::RtSim>();
3094        let spatial_grid;
3095
3096        let mut iter_a;
3097        let mut iter_b;
3098
3099        let iter: &mut dyn Iterator<
3100            Item = (
3101                EcsEntity,
3102                &comp::Health,
3103                (),
3104                Option<&comp::Alignment>,
3105                &comp::Pos,
3106            ),
3107        > = if let (Some(radius), Some(position)) = (radius, position) {
3108            spatial_grid = ecs.read_resource::<CachedSpatialGrid>();
3109            iter_a = spatial_grid
3110                .0
3111                .in_circle_aabr(position.0.xy(), radius)
3112                .filter_map(|entity| {
3113                    (
3114                        &entities,
3115                        &healths,
3116                        !&players,
3117                        alignments.maybe(),
3118                        &positions,
3119                    )
3120                        .lend_join()
3121                        .get(entity, &entities)
3122                })
3123                .filter(move |(_, _, _, _, pos)| {
3124                    pos.0.distance_squared(position.0) <= radius.powi(2)
3125                });
3126
3127            &mut iter_a as _
3128        } else {
3129            iter_b = (
3130                &entities,
3131                &healths,
3132                !&players,
3133                alignments.maybe(),
3134                &positions,
3135            )
3136                .join();
3137
3138            &mut iter_b as _
3139        };
3140
3141        iter.filter_map(|(entity, _health, (), alignment, pos)| {
3142            let should_kill = kill_pets
3143                || if let Some(Alignment::Owned(owned)) = alignment {
3144                    ecs.entity_from_uid(*owned)
3145                        .is_none_or(|owner| !players.contains(owner))
3146                } else {
3147                    true
3148                };
3149
3150            if should_kill {
3151                if let Some(actor) = rtsim_actors.get(entity).copied() {
3152                    rtsim.hook_rtsim_actor_death(
3153                        &ecs.read_resource::<Arc<world::World>>(),
3154                        ecs.read_resource::<world::IndexOwned>().as_index_ref(),
3155                        actor,
3156                        Some(pos.0),
3157                        None,
3158                    );
3159                }
3160                Some(entity)
3161            } else {
3162                None
3163            }
3164        })
3165        .collect::<Vec<_>>()
3166    };
3167    let count = to_kill.len();
3168    for entity in to_kill {
3169        // Directly remove entities instead of modifying health to avoid loot drops.
3170        if let Err(e) = server.state.delete_entity_recorded(entity) {
3171            error!(?e, ?entity, "Failed to delete entity");
3172        }
3173    }
3174    let text = if count > 0 {
3175        format!("Destroyed {} NPCs.", count)
3176    } else {
3177        "No NPCs on server.".to_string()
3178    };
3179
3180    server.notify_client(
3181        client,
3182        ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(text)),
3183    );
3184
3185    Ok(())
3186}
3187
3188enum KitEntry {
3189    Spec(KitSpec),
3190    Item(Item),
3191}
3192
3193impl From<KitSpec> for KitEntry {
3194    fn from(spec: KitSpec) -> Self { Self::Spec(spec) }
3195}
3196
3197fn handle_kit(
3198    server: &mut Server,
3199    client: EcsEntity,
3200    target: EcsEntity,
3201    args: Vec<String>,
3202    action: &ServerChatCommand,
3203) -> CmdResult<()> {
3204    use common::cmd::KitManifest;
3205
3206    let notify = |server: &mut Server, kit_name: &str| {
3207        server.notify_client(
3208            client,
3209            ServerGeneral::server_msg(
3210                ChatType::CommandInfo,
3211                Content::Plain(format!("Gave kit: {}", kit_name)),
3212            ),
3213        );
3214    };
3215    let name = parse_cmd_args!(args, String).ok_or_else(|| action.help_content())?;
3216
3217    match name.as_str() {
3218        "all" => {
3219            // This can't fail, we have tests
3220            let items = all_items_expect();
3221            let total = items.len();
3222
3223            let res = push_kit(
3224                items.into_iter().map(|item| (KitEntry::Item(item), 1)),
3225                total,
3226                server,
3227                target,
3228            );
3229            if res.is_ok() {
3230                notify(server, "all");
3231            }
3232            res
3233        },
3234        kit_name => {
3235            let kits = KitManifest::load(KIT_MANIFEST_PATH)
3236                .map(|kits| kits.read())
3237                .map_err(|_| {
3238                    Content::Plain(format!(
3239                        "Could not load manifest file {}",
3240                        KIT_MANIFEST_PATH
3241                    ))
3242                })?;
3243
3244            let kit = kits
3245                .0
3246                .get(kit_name)
3247                .ok_or(Content::Plain(format!("Kit '{}' not found", kit_name)))?;
3248
3249            let res = push_kit(
3250                kit.iter()
3251                    .map(|(item_id, quantity)| (item_id.clone().into(), *quantity)),
3252                kit.len(),
3253                server,
3254                target,
3255            );
3256            if res.is_ok() {
3257                notify(server, kit_name);
3258            }
3259            res
3260        },
3261    }
3262}
3263
3264fn push_kit<I>(kit: I, count: usize, server: &mut Server, target: EcsEntity) -> CmdResult<()>
3265where
3266    I: Iterator<Item = (KitEntry, u32)>,
3267{
3268    if let (Some(mut target_inventory), mut inv_update_buffers) = (
3269        server
3270            .state()
3271            .ecs()
3272            .write_storage::<Inventory>()
3273            .get_mut(target),
3274        server
3275            .state
3276            .ecs()
3277            .write_storage::<comp::InventoryUpdateBuffer>(),
3278    ) {
3279        // TODO: implement atomic `insert_all_or_nothing` on Inventory
3280        if target_inventory.free_slots() < count {
3281            return Err(Content::localized("command-kit-not-enough-slots"));
3282        }
3283
3284        for (item_id, quantity) in kit {
3285            push_item(item_id, quantity, server, &mut |item| {
3286                let res = target_inventory.push(item);
3287                if let Some(buf) = inv_update_buffers.get_mut(target) {
3288                    buf.push(comp::InventoryUpdateEvent::Debug);
3289                }
3290
3291                res
3292            })?;
3293        }
3294
3295        Ok(())
3296    } else {
3297        Err(Content::localized("command-kit-inventory-unavailable"))
3298    }
3299}
3300
3301fn push_item(
3302    item_id: KitEntry,
3303    quantity: u32,
3304    server: &Server,
3305    push: &mut dyn FnMut(Item) -> Result<(), (Item, Option<NonZeroU32>)>,
3306) -> CmdResult<()> {
3307    let items = match item_id {
3308        KitEntry::Spec(KitSpec::Item(item_id)) => vec![
3309            Item::new_from_asset(&item_id)
3310                .map_err(|_| Content::Plain(format!("Unknown item: {:#?}", item_id)))?,
3311        ],
3312        KitEntry::Spec(KitSpec::ModularWeaponSet {
3313            tool,
3314            material,
3315            hands,
3316        }) => comp::item::modular::generate_weapons(tool, material, hands)
3317            .map_err(|err| Content::Plain(format!("{:#?}", err)))?,
3318        KitEntry::Spec(KitSpec::ModularWeaponRandom {
3319            tool,
3320            material,
3321            hands,
3322        }) => {
3323            let mut rng = rand::rng();
3324            vec![
3325                comp::item::modular::random_weapon(tool, material, hands, &mut rng)
3326                    .map_err(|err| Content::Plain(format!("{:#?}", err)))?,
3327            ]
3328        },
3329        KitEntry::Item(item) => vec![item],
3330    };
3331
3332    let mut res = Ok(());
3333    for mut item in items {
3334        // Either push stack or push one by one.
3335        if item.is_stackable() {
3336            // FIXME: in theory, this can fail,
3337            // but we don't have stack sizes yet.
3338            let _ = item.set_amount(quantity);
3339            res = push(item);
3340        } else {
3341            let ability_map = server.state.ecs().read_resource::<AbilityMap>();
3342            let msm = server.state.ecs().read_resource::<MaterialStatManifest>();
3343
3344            for _ in 0..quantity {
3345                res = push(item.duplicate(&ability_map, &msm));
3346
3347                if res.is_err() {
3348                    break;
3349                }
3350            }
3351        }
3352
3353        // I think it's possible to pick-up item during this loop
3354        // and fail into case where you had space but now you don't?
3355        if res.is_err() {
3356            return Err(Content::localized("command-inventory-cant-fit-item"));
3357        }
3358    }
3359
3360    Ok(())
3361}
3362
3363fn handle_object(
3364    server: &mut Server,
3365    client: EcsEntity,
3366    target: EcsEntity,
3367    args: Vec<String>,
3368    _action: &ServerChatCommand,
3369) -> CmdResult<()> {
3370    let obj_type = parse_cmd_args!(args, String);
3371
3372    let pos = position(server, target, "target")?;
3373    let ori = server
3374        .state
3375        .ecs()
3376        .read_storage::<comp::Ori>()
3377        .get(target)
3378        .copied()
3379        .ok_or_else(|| Content::Plain("Cannot get orientation for target".to_string()))?;
3380    /*let builder = server.state
3381    .create_object(pos, ori, obj_type)
3382    .with(ori);*/
3383    let obj_str_res = obj_type.as_deref();
3384    if let Some(obj_type) = comp::object::ALL_OBJECTS
3385        .iter()
3386        .find(|o| Some(o.to_string()) == obj_str_res)
3387    {
3388        server
3389            .state
3390            .create_object(pos, *obj_type)
3391            .with(
3392                comp::Ori::from_unnormalized_vec(
3393                    // converts player orientation into a 90° rotation for the object by using
3394                    // the axis with the highest value
3395                    {
3396                        let look_dir = ori.look_dir();
3397                        look_dir.map(|e| {
3398                            if e.abs() == look_dir.map(|e| e.abs()).reduce_partial_max() {
3399                                e
3400                            } else {
3401                                0.0
3402                            }
3403                        })
3404                    },
3405                )
3406                .unwrap_or_default(),
3407            )
3408            .build();
3409        server.notify_client(
3410            client,
3411            ServerGeneral::server_msg(
3412                ChatType::CommandInfo,
3413                Content::Plain(format!(
3414                    "Spawned: {}",
3415                    obj_str_res.unwrap_or("<Unknown object>")
3416                )),
3417            ),
3418        );
3419        Ok(())
3420    } else {
3421        Err(Content::Plain("Object not found!".into()))
3422    }
3423}
3424
3425fn handle_outcome(
3426    server: &mut Server,
3427    _client: EcsEntity,
3428    target: EcsEntity,
3429    args: Vec<String>,
3430    _action: &ServerChatCommand,
3431) -> CmdResult<()> {
3432    let mut i = 0;
3433
3434    macro_rules! arg {
3435        () => {
3436            args.get(i).map(|r| {
3437                i += 1;
3438                r
3439            })
3440        };
3441        ($err:expr) => {
3442            arg!().ok_or_else(|| Content::Key(($err).to_string()))
3443        };
3444    }
3445
3446    let target_pos = server
3447        .state
3448        .read_component_copied::<comp::Pos>(target)
3449        .unwrap_or(comp::Pos(Vec3::zero()));
3450    let target_uid = server
3451        .state
3452        .read_component_copied::<Uid>(target)
3453        .expect("All entities should have uids");
3454
3455    macro_rules! vec_arg {
3456        () => {{
3457            let old_i = i;
3458            let pos = arg!().and_then(|arg| {
3459                let x = arg.parse().ok()?;
3460                let y = arg!()?.parse().ok()?;
3461                let z = arg!()?.parse().ok()?;
3462
3463                Some(Vec3::new(x, y, z))
3464            });
3465
3466            #[allow(unused_assignments)]
3467            if let Some(pos) = pos {
3468                pos
3469            } else {
3470                i = old_i;
3471                Vec3::default()
3472            }
3473        }};
3474    }
3475
3476    macro_rules! pos_arg {
3477        () => {{
3478            let old_i = i;
3479            let pos = arg!().and_then(|arg| {
3480                let x = arg.parse().ok()?;
3481                let y = arg!()?.parse().ok()?;
3482                let z = arg!()?.parse().ok()?;
3483
3484                Some(Vec3::new(x, y, z))
3485            });
3486
3487            #[allow(unused_assignments)]
3488            if let Some(pos) = pos {
3489                pos
3490            } else {
3491                i = old_i;
3492                target_pos.0.as_()
3493            }
3494        }};
3495    }
3496
3497    macro_rules! parse {
3498        ($err:expr, $expr:expr) => {
3499            arg!()
3500                .and_then($expr)
3501                .ok_or_else(|| Content::Key(($err).to_string()))
3502        };
3503        ($err:expr) => {
3504            parse!($err, |arg| arg.parse().ok())
3505        };
3506    }
3507
3508    macro_rules! body_arg {
3509        () => {{ parse!("command-outcome-expected_body_arg").map(|npc::NpcBody(_, mut body)| body()) }};
3510    }
3511
3512    macro_rules! uid_arg {
3513        () => {{
3514            parse!("command-outcome-expected_entity_arg").and_then(|entity| {
3515                let entity = get_entity_target(entity, server)?;
3516                Ok(server
3517                    .state()
3518                    .read_component_copied::<Uid>(entity)
3519                    .expect("All entities have uids"))
3520            })
3521        }};
3522    }
3523
3524    macro_rules! parse_or_default {
3525        ($default:expr, @$expr:expr) => {{
3526            let old_i = i;
3527            let f = arg!().and_then($expr);
3528
3529            #[allow(unused_assignments)]
3530            if let Some(f) = f {
3531                f
3532            } else {
3533                i = old_i;
3534                $default
3535            }
3536        }};
3537        (@$expr:expr) => {{ parse_or_default!(Default::default(), @$expr) }};
3538        ($default:expr) => {{ parse_or_default!($default, @|arg| arg.parse().ok()) }};
3539        () => {
3540            parse_or_default!(Default::default())
3541        };
3542    }
3543
3544    let mut rng = rand::rng();
3545
3546    let outcome = arg!("command-outcome-variant_expected")?;
3547
3548    let outcome = match outcome.as_str() {
3549        "Explosion" => Outcome::Explosion {
3550            pos: pos_arg!(),
3551            power: parse_or_default!(1.0),
3552            radius: parse_or_default!(1.0),
3553            is_attack: parse_or_default!(),
3554            reagent: parse_or_default!(@|arg| comp::item::Reagent::from_str(arg).ok().map(Some)),
3555        },
3556        "Lightning" => Outcome::Lightning { pos: pos_arg!() },
3557        "ProjectileShot" => Outcome::ProjectileShot {
3558            pos: pos_arg!(),
3559            body: body_arg!()?,
3560            vel: vec_arg!(),
3561        },
3562        "ProjectileHit" => Outcome::ProjectileHit {
3563            pos: pos_arg!(),
3564            body: body_arg!()?,
3565            vel: vec_arg!(),
3566            source: uid_arg!().ok(),
3567            target: uid_arg!().ok(),
3568        },
3569        "Beam" => Outcome::Beam {
3570            pos: pos_arg!(),
3571            specifier: parse!("command-outcome-expected_frontent_specifier", |arg| {
3572                comp::beam::FrontendSpecifier::from_str(arg).ok()
3573            })?,
3574        },
3575        "ExpChange" => Outcome::ExpChange {
3576            uid: uid_arg!().unwrap_or(target_uid),
3577            exp: parse!("command-outcome-expected_integer")?,
3578            xp_pools: {
3579                let mut hashset = HashSet::new();
3580                while let Some(arg) = arg!() {
3581                    hashset.insert(ron::from_str(arg).map_err(|_| {
3582                        Content::Key("command-outcome-expected_skill_group_kind".to_string())
3583                    })?);
3584                }
3585                hashset
3586            },
3587        },
3588        "SkillPointGain" => Outcome::SkillPointGain {
3589            uid: uid_arg!().unwrap_or(target_uid),
3590            skill_tree: arg!("command-outcome-expected_skill_group_kind").and_then(|arg| {
3591                ron::from_str(arg).map_err(|_| {
3592                    Content::Key("command-outcome-expected_skill_group_kind".to_string())
3593                })
3594            })?,
3595            total_points: parse!("Expected an integer amount of points")?,
3596        },
3597        "ComboChange" => Outcome::ComboChange {
3598            uid: uid_arg!().unwrap_or(target_uid),
3599            combo: parse!("command-outcome-expected_integer")?,
3600        },
3601        "BreakBlock" => Outcome::BreakBlock {
3602            pos: pos_arg!(),
3603            color: Some(Rgb::from(vec_arg!())),
3604            tool: None,
3605        },
3606        "SummonedCreature" => Outcome::SummonedCreature {
3607            pos: pos_arg!(),
3608            body: body_arg!()?,
3609        },
3610        "HealthChange" => Outcome::HealthChange {
3611            pos: pos_arg!(),
3612            info: common::outcome::HealthChangeInfo {
3613                amount: parse_or_default!(),
3614                precise: parse_or_default!(),
3615                target: uid_arg!().unwrap_or(target_uid),
3616                by: uid_arg!().map(common::combat::DamageContributor::Solo).ok(),
3617                cause: None,
3618                instance: rng.random(),
3619            },
3620        },
3621        "Death" => Outcome::Death { pos: pos_arg!() },
3622        "Block" => Outcome::Block {
3623            pos: pos_arg!(),
3624            parry: parse_or_default!(),
3625            uid: uid_arg!().unwrap_or(target_uid),
3626        },
3627        "PoiseChange" => Outcome::PoiseChange {
3628            pos: pos_arg!(),
3629            state: parse_or_default!(comp::PoiseState::Normal, @|arg| comp::PoiseState::from_str(arg).ok()),
3630        },
3631        "GroundSlam" => Outcome::GroundSlam { pos: pos_arg!() },
3632        "IceSpikes" => Outcome::IceSpikes { pos: pos_arg!() },
3633        "IceCrack" => Outcome::IceCrack { pos: pos_arg!() },
3634        "FlashFreeze" => Outcome::FlashFreeze { pos: pos_arg!() },
3635        "Steam" => Outcome::Steam { pos: pos_arg!() },
3636        "LaserBeam" => Outcome::LaserBeam { pos: pos_arg!() },
3637        "CyclopsCharge" => Outcome::CyclopsCharge { pos: pos_arg!() },
3638        "FlamethrowerCharge" => Outcome::FlamethrowerCharge { pos: pos_arg!() },
3639        "FuseCharge" => Outcome::FuseCharge { pos: pos_arg!() },
3640        "TerracottaStatueCharge" => Outcome::TerracottaStatueCharge { pos: pos_arg!() },
3641        "SurpriseEgg" => Outcome::SurpriseEgg { pos: pos_arg!() },
3642        "Utterance" => Outcome::Utterance {
3643            pos: pos_arg!(),
3644            body: body_arg!()?,
3645            kind: parse_or_default!(comp::UtteranceKind::Greeting, @|arg| comp::UtteranceKind::from_str(arg).ok()),
3646        },
3647        "Glider" => Outcome::Glider {
3648            pos: pos_arg!(),
3649            wielded: parse_or_default!(true),
3650        },
3651        "SpriteDelete" => Outcome::SpriteDelete {
3652            pos: pos_arg!(),
3653            sprite: parse!("command-outcome-expected_sprite_kind", |arg| {
3654                SpriteKind::try_from(arg.as_str()).ok()
3655            })?,
3656        },
3657        "SpriteUnlocked" => Outcome::SpriteUnlocked { pos: pos_arg!() },
3658        "FailedSpriteUnlock" => Outcome::FailedSpriteUnlock { pos: pos_arg!() },
3659        "Whoosh" => Outcome::Whoosh { pos: pos_arg!() },
3660        "Swoosh" => Outcome::Swoosh { pos: pos_arg!() },
3661        "Slash" => Outcome::Slash { pos: pos_arg!() },
3662        "FireShockwave" => Outcome::FireShockwave { pos: pos_arg!() },
3663        "FireLowShockwave" => Outcome::FireLowShockwave { pos: pos_arg!() },
3664        "GroundDig" => Outcome::GroundDig { pos: pos_arg!() },
3665        "PortalActivated" => Outcome::PortalActivated { pos: pos_arg!() },
3666        "TeleportedByPortal" => Outcome::TeleportedByPortal { pos: pos_arg!() },
3667        "FromTheAshes" => Outcome::FromTheAshes { pos: pos_arg!() },
3668        "ClayGolemDash" => Outcome::ClayGolemDash { pos: pos_arg!() },
3669        "Bleep" => Outcome::Bleep { pos: pos_arg!() },
3670        "Charge" => Outcome::Charge { pos: pos_arg!() },
3671        "HeadLost" => Outcome::HeadLost {
3672            uid: uid_arg!().unwrap_or(target_uid),
3673            head: parse_or_default!(),
3674        },
3675        "Splash" => Outcome::Splash {
3676            vel: vec_arg!(),
3677            pos: pos_arg!(),
3678            mass: parse_or_default!(1.0),
3679            kind: parse_or_default!(
3680                comp::fluid_dynamics::LiquidKind::Water,
3681                @|arg| comp::fluid_dynamics::LiquidKind::from_str(arg).ok()
3682            ),
3683        },
3684        _ => {
3685            return Err(Content::localized_with_args(
3686                "command-outcome-invalid_outcome",
3687                [("outcome", Content::Plain(outcome.to_string()))],
3688            ));
3689        },
3690    };
3691
3692    server
3693        .state()
3694        .ecs()
3695        .read_resource::<EventBus<Outcome>>()
3696        .emit_now(outcome);
3697
3698    Ok(())
3699}
3700
3701fn handle_light(
3702    server: &mut Server,
3703    client: EcsEntity,
3704    target: EcsEntity,
3705    args: Vec<String>,
3706    _action: &ServerChatCommand,
3707) -> CmdResult<()> {
3708    let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) =
3709        parse_cmd_args!(args, f32, f32, f32, f32, f32, f32, f32);
3710
3711    let mut light_emitter = LightEmitter {
3712        col: Rgb::zero(),
3713        strength: 0.0,
3714        flicker: 0.0,
3715        animated: false,
3716        dir: None,
3717    };
3718    let mut light_offset_opt = None;
3719
3720    if let (Some(r), Some(g), Some(b)) = (opt_r, opt_g, opt_b) {
3721        if r < 0.0 || g < 0.0 || b < 0.0 {
3722            return Err(Content::Plain(
3723                "cr, cg and cb values mustn't be negative.".into(),
3724            ));
3725        }
3726
3727        let r = r.clamp(0.0, 1.0);
3728        let g = g.clamp(0.0, 1.0);
3729        let b = b.clamp(0.0, 1.0);
3730        light_emitter.col = Rgb::new(r, g, b)
3731    };
3732    if let (Some(x), Some(y), Some(z)) = (opt_x, opt_y, opt_z) {
3733        light_offset_opt = Some(comp::LightAnimation {
3734            offset: Vec3::new(x, y, z),
3735            col: light_emitter.col,
3736            strength: 0.0,
3737            dir: None,
3738        })
3739    };
3740    if let Some(s) = opt_s {
3741        light_emitter.strength = s.max(0.0)
3742    };
3743    let pos = position(server, target, "target")?;
3744    let builder = server
3745        .state
3746        .ecs_mut()
3747        .create_entity_synced()
3748        .with(pos)
3749        // TODO: I don't think we intend to add this component to non-client entities?
3750        .with(comp::ForceUpdate::forced())
3751        .with(light_emitter);
3752    if let Some(light_offset) = light_offset_opt {
3753        builder.with(light_offset).build();
3754    } else {
3755        builder.build();
3756    }
3757    server.notify_client(
3758        client,
3759        ServerGeneral::server_msg(
3760            ChatType::CommandInfo,
3761            Content::Plain("Spawned object.".to_string()),
3762        ),
3763    );
3764    Ok(())
3765}
3766
3767fn handle_lantern(
3768    server: &mut Server,
3769    client: EcsEntity,
3770    target: EcsEntity,
3771    args: Vec<String>,
3772    action: &ServerChatCommand,
3773) -> CmdResult<()> {
3774    if let (Some(s), r, g, b) = parse_cmd_args!(args, f32, f32, f32, f32) {
3775        if let Some(mut light) = server
3776            .state
3777            .ecs()
3778            .write_storage::<LightEmitter>()
3779            .get_mut(target)
3780        {
3781            light.strength = s.clamp(0.1, 10.0);
3782            if let (Some(r), Some(g), Some(b)) = (r, g, b) {
3783                light.col = (r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)).into();
3784                server.notify_client(
3785                    client,
3786                    ServerGeneral::server_msg(
3787                        ChatType::CommandInfo,
3788                        Content::localized("command-lantern-adjusted-strength-color"),
3789                    ),
3790                )
3791            } else {
3792                server.notify_client(
3793                    client,
3794                    ServerGeneral::server_msg(
3795                        ChatType::CommandInfo,
3796                        Content::localized("command-lantern-adjusted-strength"),
3797                    ),
3798                )
3799            }
3800            Ok(())
3801        } else {
3802            Err(Content::localized("command-lantern-unequiped"))
3803        }
3804    } else {
3805        Err(action.help_content())
3806    }
3807}
3808
3809fn handle_explosion(
3810    server: &mut Server,
3811    _client: EcsEntity,
3812    target: EcsEntity,
3813    args: Vec<String>,
3814    _action: &ServerChatCommand,
3815) -> CmdResult<()> {
3816    let power = parse_cmd_args!(args, f32).unwrap_or(8.0);
3817
3818    const MIN_POWER: f32 = 0.0;
3819    const MAX_POWER: f32 = 512.0;
3820
3821    if power > MAX_POWER {
3822        return Err(Content::localized_with_args(
3823            "command-explosion-power-too-high",
3824            [("power", MAX_POWER.to_string())],
3825        ));
3826    } else if power <= MIN_POWER {
3827        return Err(Content::localized_with_args(
3828            "command-explosion-power-too-low",
3829            [("power", MIN_POWER.to_string())],
3830        ));
3831    }
3832
3833    let pos = position(server, target, "target")?;
3834    let owner = server
3835        .state
3836        .ecs()
3837        .read_storage::<Uid>()
3838        .get(target)
3839        .copied();
3840    server.state.emit_event_now(ExplosionEvent {
3841        pos: pos.0,
3842        explosion: Explosion {
3843            effects: vec![
3844                RadiusEffect::Entity(Effect::Damage(Damage {
3845                    kind: DamageKind::Energy,
3846                    value: 100.0 * power,
3847                })),
3848                RadiusEffect::TerrainDestruction(power, Rgb::black()),
3849            ],
3850            radius: 3.0 * power,
3851            reagent: None,
3852            min_falloff: 0.0,
3853        },
3854        owner,
3855    });
3856    Ok(())
3857}
3858
3859fn handle_set_waypoint(
3860    server: &mut Server,
3861    client: EcsEntity,
3862    target: EcsEntity,
3863    _args: Vec<String>,
3864    _action: &ServerChatCommand,
3865) -> CmdResult<()> {
3866    let pos = position(server, target, "target")?;
3867    let time = *server.state.mut_resource::<Time>();
3868    let location_name = server
3869        .world()
3870        .get_location_name(server.index.as_index_ref(), pos.0.xy().as_::<i32>());
3871
3872    insert_or_replace_component(
3873        server,
3874        target,
3875        comp::Waypoint::temp_new(pos.0, time),
3876        "target",
3877    )?;
3878    server.notify_client(
3879        client,
3880        ServerGeneral::server_msg(
3881            ChatType::CommandInfo,
3882            Content::localized("command-set-waypoint-result"),
3883        ),
3884    );
3885
3886    if let Some(location_name) = location_name {
3887        #[expect(deprecated, reason = "i18n location name")]
3888        let location_name = Content::legacy(location_name);
3889        server.notify_client(
3890            target,
3891            ServerGeneral::Notification(Notification::WaypointSaved { location_name }),
3892        );
3893    } else {
3894        error!(
3895            "Failed to get location name for waypoint. Client was not notified of new waypoint."
3896        );
3897    }
3898
3899    Ok(())
3900}
3901
3902fn handle_spawn_wiring(
3903    server: &mut Server,
3904    client: EcsEntity,
3905    target: EcsEntity,
3906    _args: Vec<String>,
3907    _action: &ServerChatCommand,
3908) -> CmdResult<()> {
3909    let mut pos = position(server, target, "target")?;
3910    pos.0.x += 3.0;
3911
3912    let mut outputs1 = HashMap::new();
3913    outputs1.insert("button".to_string(), OutputFormula::OnCollide {
3914        value: 1.0,
3915    });
3916
3917    // Create the first element of the circuit.
3918    // This is a coin body. This element does not have any inputs or actions.
3919    // Instead there is one output. When there is a collision with this element the
3920    // value of 1.0 will be sent as an input with the "button" label. Any
3921    // element with an `Input` for the name "button" can use this value as an
3922    // input. The threshold does not matter as there are no effects for this
3923    // element.
3924    let builder1 = server
3925        .state
3926        .create_wiring(pos, comp::object::Body::Pebble, WiringElement {
3927            inputs: HashMap::new(),
3928            outputs: outputs1,
3929            actions: Vec::new(),
3930        })
3931        .with(comp::Density(100_f32));
3932    let ent1 = builder1.build();
3933
3934    pos.0.x += 3.0;
3935    // The second element has no elements in the `inputs` field to start with. When
3936    // the circuit runs, the input as specified by the `Input` OutputFormula is
3937    // added to the inputs. The next tick the effect(s) are applied based on the
3938    // input value.
3939    let builder2 = server
3940        .state
3941        .create_wiring(pos, comp::object::Body::Pebble, WiringElement {
3942            inputs: HashMap::new(),
3943            outputs: HashMap::new(),
3944            actions: vec![WiringAction {
3945                formula: OutputFormula::Input {
3946                    name: String::from("button"),
3947                },
3948                threshold: 0.0,
3949                effects: vec![WiringActionEffect::SetLight {
3950                    r: OutputFormula::Input {
3951                        name: String::from("button"),
3952                    },
3953                    g: OutputFormula::Input {
3954                        name: String::from("button"),
3955                    },
3956                    b: OutputFormula::Input {
3957                        name: String::from("button"),
3958                    },
3959                }],
3960            }],
3961        })
3962        .with(comp::Density(100_f32));
3963    let ent2 = builder2.build();
3964
3965    pos.0.x += 3.0;
3966    let builder3 = server
3967        .state
3968        .create_wiring(pos, comp::object::Body::TrainingDummy, WiringElement {
3969            inputs: HashMap::new(),
3970            outputs: HashMap::new(),
3971            actions: Vec::new(),
3972        })
3973        .with(comp::Density(comp::object::Body::TrainingDummy.density().0))
3974        .with(Circuit::new(vec![Wire {
3975            input: WireNode::new(ent1, "button".to_string()),
3976            output: WireNode::new(ent2, "button".to_string()),
3977        }]));
3978    builder3.build();
3979
3980    server.notify_client(
3981        client,
3982        ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain("Wire".to_string())),
3983    );
3984    Ok(())
3985}
3986
3987fn handle_adminify(
3988    server: &mut Server,
3989    client: EcsEntity,
3990    _target: EcsEntity,
3991    args: Vec<String>,
3992    action: &ServerChatCommand,
3993) -> CmdResult<()> {
3994    if let (Some(alias), desired_role) = parse_cmd_args!(args, String, String) {
3995        let desired_role = if let Some(mut desired_role) = desired_role {
3996            desired_role.make_ascii_lowercase();
3997            Some(match &*desired_role {
3998                "admin" => AdminRole::Admin,
3999                "moderator" => AdminRole::Moderator,
4000                _ => {
4001                    return Err(action.help_content());
4002                },
4003            })
4004        } else {
4005            None
4006        };
4007        let (player, player_uuid) = find_alias(server.state.ecs(), &alias, true)?;
4008        let client_uuid = uuid(server, client, "client")?;
4009        let uid = uid(server, player, "player")?;
4010
4011        // Your permanent role, not your temporary role, is what's used to determine
4012        // what temporary roles you can grant.
4013        let client_real_role = real_role(server, client_uuid, "client")?;
4014
4015        // This appears to prevent de-mod / de-admin for mods / admins with access to
4016        // this command, but it does not in the case where the target is
4017        // temporary, because `verify_above_role` always values permanent roles
4018        // above temporary ones.
4019        verify_above_role(
4020            server,
4021            (client, client_uuid),
4022            (player, player_uuid),
4023            Content::localized("command-adminify-reassign-to-above"),
4024        )?;
4025
4026        // Ensure that it's not possible to assign someone a higher role than your own
4027        // (i.e. even if mods had the ability to create temporary mods, they
4028        // wouldn't be able to create temporary admins).
4029        //
4030        // Also note that we perform no more permissions checks after this point based
4031        // on the assignee's temporary role--even if the player's temporary role
4032        // is higher than the client's, we still allow the role to be reduced to
4033        // the selected role, as long as they would have permission to assign it
4034        // in the first place.  This is consistent with our
4035        // policy on bans--banning or lengthening a ban (decreasing player permissions)
4036        // can be done even after an unban or ban shortening (increasing player
4037        // permissions) by someone with a higher role than the person doing the
4038        // ban.  So if we change how bans work, we should change how things work
4039        // here, too, for consistency.
4040        if desired_role > Some(client_real_role) {
4041            return Err(Content::localized(
4042                "command-adminify-assign-higher-than-own",
4043            ));
4044        }
4045
4046        let mut admin_storage = server.state.ecs().write_storage::<comp::Admin>();
4047        let entry = admin_storage
4048            .entry(player)
4049            .map_err(|_| Content::localized("command-adminify-cannot-find-player"))?;
4050        match (entry, desired_role) {
4051            (StorageEntry::Vacant(_), None) => {
4052                return Err(Content::localized("command-adminify-already-has-no-role"));
4053            },
4054            (StorageEntry::Occupied(o), None) => {
4055                let old_role = o.remove().0;
4056                server.notify_client(
4057                    client,
4058                    ServerGeneral::server_msg(
4059                        ChatType::CommandInfo,
4060                        Content::localized_with_args("command-adminify-removed-role", [
4061                            ("player", alias),
4062                            ("role", format!("{:?}", old_role)),
4063                        ]),
4064                    ),
4065                );
4066            },
4067            (entry, Some(desired_role)) => {
4068                let key = match entry
4069                    .replace(comp::Admin(desired_role))
4070                    .map(|old_admin| old_admin.0.cmp(&desired_role))
4071                {
4072                    Some(Ordering::Equal) => {
4073                        return Err(Content::localized("command-adminify-already-has-role"));
4074                    },
4075                    Some(Ordering::Greater) => "command-adminify-role-downgraded",
4076                    Some(Ordering::Less) | None => "command-adminify-role-upgraded",
4077                };
4078                server.notify_client(
4079                    client,
4080                    ServerGeneral::server_msg(
4081                        ChatType::CommandInfo,
4082                        Content::localized_with_args(key, [
4083                            ("player", alias),
4084                            ("role", format!("{:?}", desired_role)),
4085                        ]),
4086                    ),
4087                );
4088            },
4089        };
4090
4091        // Notify the client that its role has been updated
4092        server.notify_client(player, ServerGeneral::SetPlayerRole(desired_role));
4093
4094        if server
4095            .state
4096            .ecs()
4097            .read_storage::<Client>()
4098            .get(player)
4099            .is_some_and(|client| client.client_type.emit_login_events())
4100        {
4101            // Update player list so the player shows up as moderator in client chat.
4102            //
4103            // NOTE: We deliberately choose not to differentiate between moderators and
4104            // administrators in the player list.
4105            let is_moderator = desired_role.is_some();
4106            let msg =
4107                ServerGeneral::PlayerListUpdate(PlayerListUpdate::Moderator(uid, is_moderator));
4108            server.state.notify_players(msg);
4109        }
4110        Ok(())
4111    } else {
4112        Err(action.help_content())
4113    }
4114}
4115
4116fn handle_tell(
4117    server: &mut Server,
4118    client: EcsEntity,
4119    target: EcsEntity,
4120    args: Vec<String>,
4121    action: &ServerChatCommand,
4122) -> CmdResult<()> {
4123    no_sudo(client, target)?;
4124    can_send_message(target, server)?;
4125
4126    if let (Some(alias), message_opt) = parse_cmd_args!(args, String, ..Vec<String>) {
4127        let ecs = server.state.ecs();
4128        let player = find_alias(ecs, &alias, false)?.0;
4129
4130        if player == target {
4131            return Err(Content::localized("command-tell-to-yourself"));
4132        }
4133        let target_uid = uid(server, target, "target")?;
4134        let player_uid = uid(server, player, "player")?;
4135        let mode = comp::ChatMode::Tell(player_uid);
4136        insert_or_replace_component(server, target, mode.clone(), "target")?;
4137        if !message_opt.is_empty() {
4138            let msg = Content::Plain(message_opt.join(" "));
4139            server
4140                .state
4141                .send_chat(mode.to_msg(target_uid, msg, None)?, false);
4142        };
4143        server.notify_client(target, ServerGeneral::ChatMode(mode));
4144        Ok(())
4145    } else {
4146        Err(action.help_content())
4147    }
4148}
4149
4150fn handle_faction(
4151    server: &mut Server,
4152    client: EcsEntity,
4153    target: EcsEntity,
4154    args: Vec<String>,
4155    _action: &ServerChatCommand,
4156) -> CmdResult<()> {
4157    no_sudo(client, target)?;
4158    can_send_message(target, server)?;
4159
4160    let factions = server.state.ecs().read_storage();
4161    if let Some(comp::Faction(faction)) = factions.get(target) {
4162        let mode = comp::ChatMode::Faction(faction.to_string());
4163        drop(factions);
4164        insert_or_replace_component(server, target, mode.clone(), "target")?;
4165        let msg = args.join(" ");
4166        if !msg.is_empty()
4167            && let Some(uid) = server.state.ecs().read_storage().get(target)
4168        {
4169            server
4170                .state
4171                .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4172        }
4173        server.notify_client(target, ServerGeneral::ChatMode(mode));
4174        Ok(())
4175    } else {
4176        Err(Content::localized("command-faction-join"))
4177    }
4178}
4179
4180fn handle_group(
4181    server: &mut Server,
4182    client: EcsEntity,
4183    target: EcsEntity,
4184    args: Vec<String>,
4185    _action: &ServerChatCommand,
4186) -> CmdResult<()> {
4187    no_sudo(client, target)?;
4188    can_send_message(target, server)?;
4189
4190    let groups = server.state.ecs().read_storage::<comp::Group>();
4191    if let Some(group) = groups.get(target).copied() {
4192        let mode = comp::ChatMode::Group;
4193        drop(groups);
4194        insert_or_replace_component(server, target, mode.clone(), "target")?;
4195        let msg = args.join(" ");
4196        if !msg.is_empty()
4197            && let Some(uid) = server.state.ecs().read_storage().get(target)
4198        {
4199            server
4200                .state
4201                .send_chat(mode.to_msg(*uid, Content::Plain(msg), Some(group))?, false);
4202        }
4203        server.notify_client(target, ServerGeneral::ChatMode(mode));
4204        Ok(())
4205    } else {
4206        Err(Content::localized("command-group-join"))
4207    }
4208}
4209
4210fn handle_group_invite(
4211    server: &mut Server,
4212    client: EcsEntity,
4213    target: EcsEntity,
4214    args: Vec<String>,
4215    action: &ServerChatCommand,
4216) -> CmdResult<()> {
4217    // Very hypothetical case: Prevent an admin from running /group_invite using
4218    // /sudo on a moderator who is currently in silent spectator.
4219    can_send_message(target, server)?;
4220
4221    if let Some(target_alias) = parse_cmd_args!(args, String) {
4222        let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4223        let uid = uid(server, target_player, "player")?;
4224
4225        server
4226            .state
4227            .emit_event_now(InitiateInviteEvent(target, uid, InviteKind::Group));
4228
4229        if client != target {
4230            server.notify_client(
4231                target,
4232                ServerGeneral::server_msg(
4233                    ChatType::CommandInfo,
4234                    Content::localized_with_args("command-group_invite-invited-to-your-group", [(
4235                        "player",
4236                        target_alias.to_owned(),
4237                    )]),
4238                ),
4239            );
4240        }
4241
4242        server.notify_client(
4243            client,
4244            ServerGeneral::server_msg(
4245                ChatType::CommandInfo,
4246                Content::localized_with_args("command-group_invite-invited-to-group", [(
4247                    "player",
4248                    target_alias.to_owned(),
4249                )]),
4250            ),
4251        );
4252        Ok(())
4253    } else {
4254        Err(action.help_content())
4255    }
4256}
4257
4258fn handle_group_kick(
4259    server: &mut Server,
4260    _client: EcsEntity,
4261    target: EcsEntity,
4262    args: Vec<String>,
4263    action: &ServerChatCommand,
4264) -> CmdResult<()> {
4265    // Checking if leader is already done in group_manip
4266    if let Some(target_alias) = parse_cmd_args!(args, String) {
4267        let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4268        let uid = uid(server, target_player, "player")?;
4269
4270        server
4271            .state
4272            .emit_event_now(GroupManipEvent(target, comp::GroupManip::Kick(uid)));
4273        Ok(())
4274    } else {
4275        Err(action.help_content())
4276    }
4277}
4278
4279fn handle_group_leave(
4280    server: &mut Server,
4281    _client: EcsEntity,
4282    target: EcsEntity,
4283    _args: Vec<String>,
4284    _action: &ServerChatCommand,
4285) -> CmdResult<()> {
4286    server
4287        .state
4288        .emit_event_now(GroupManipEvent(target, comp::GroupManip::Leave));
4289    Ok(())
4290}
4291
4292fn handle_group_promote(
4293    server: &mut Server,
4294    _client: EcsEntity,
4295    target: EcsEntity,
4296    args: Vec<String>,
4297    action: &ServerChatCommand,
4298) -> CmdResult<()> {
4299    // Checking if leader is already done in group_manip
4300    if let Some(target_alias) = parse_cmd_args!(args, String) {
4301        let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4302        let uid = uid(server, target_player, "player")?;
4303
4304        server
4305            .state
4306            .emit_event_now(GroupManipEvent(target, comp::GroupManip::AssignLeader(uid)));
4307        Ok(())
4308    } else {
4309        Err(action.help_content())
4310    }
4311}
4312
4313fn handle_reset_recipes(
4314    server: &mut Server,
4315    _client: EcsEntity,
4316    target: EcsEntity,
4317    _args: Vec<String>,
4318    action: &ServerChatCommand,
4319) -> CmdResult<()> {
4320    if let Some(mut inventory) = server
4321        .state
4322        .ecs()
4323        .write_storage::<comp::Inventory>()
4324        .get_mut(target)
4325    {
4326        inventory.reset_recipes();
4327        server.notify_client(target, ServerGeneral::UpdateRecipes);
4328        Ok(())
4329    } else {
4330        Err(action.help_content())
4331    }
4332}
4333
4334fn handle_region(
4335    server: &mut Server,
4336    client: EcsEntity,
4337    target: EcsEntity,
4338    args: Vec<String>,
4339    _action: &ServerChatCommand,
4340) -> CmdResult<()> {
4341    no_sudo(client, target)?;
4342    can_send_message(target, server)?;
4343
4344    let mode = comp::ChatMode::Region;
4345    insert_or_replace_component(server, target, mode.clone(), "target")?;
4346    let msg = args.join(" ");
4347    if !msg.is_empty()
4348        && let Some(uid) = server.state.ecs().read_storage().get(target)
4349    {
4350        server
4351            .state
4352            .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4353    }
4354    server.notify_client(target, ServerGeneral::ChatMode(mode));
4355    Ok(())
4356}
4357
4358fn handle_say(
4359    server: &mut Server,
4360    client: EcsEntity,
4361    target: EcsEntity,
4362    args: Vec<String>,
4363    _action: &ServerChatCommand,
4364) -> CmdResult<()> {
4365    no_sudo(client, target)?;
4366    can_send_message(target, server)?;
4367
4368    let mode = comp::ChatMode::Say;
4369    insert_or_replace_component(server, target, mode.clone(), "target")?;
4370    let msg = args.join(" ");
4371    if !msg.is_empty()
4372        && let Some(uid) = server.state.ecs().read_storage().get(target)
4373    {
4374        server
4375            .state
4376            .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4377    }
4378    server.notify_client(target, ServerGeneral::ChatMode(mode));
4379    Ok(())
4380}
4381
4382fn handle_world(
4383    server: &mut Server,
4384    client: EcsEntity,
4385    target: EcsEntity,
4386    args: Vec<String>,
4387    _action: &ServerChatCommand,
4388) -> CmdResult<()> {
4389    no_sudo(client, target)?;
4390    can_send_message(target, server)?;
4391
4392    let mode = comp::ChatMode::World;
4393    insert_or_replace_component(server, target, mode.clone(), "target")?;
4394    let msg = args.join(" ");
4395    if !msg.is_empty()
4396        && let Some(uid) = server.state.ecs().read_storage().get(target)
4397    {
4398        server
4399            .state
4400            .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4401    }
4402    server.notify_client(target, ServerGeneral::ChatMode(mode));
4403    Ok(())
4404}
4405
4406fn handle_join_faction(
4407    server: &mut Server,
4408    client: EcsEntity,
4409    target: EcsEntity,
4410    args: Vec<String>,
4411    _action: &ServerChatCommand,
4412) -> CmdResult<()> {
4413    no_sudo(client, target)?;
4414    let emit_join_message = server
4415        .state
4416        .ecs()
4417        .read_storage::<Client>()
4418        .get(target)
4419        .is_some_and(|client| client.client_type.emit_login_events());
4420
4421    let players = server.state.ecs().read_storage::<comp::Player>();
4422    if let Some(alias) = players.get(target).map(|player| player.alias.clone()) {
4423        drop(players);
4424        let (faction_leave, mode) = if let Some(faction) = parse_cmd_args!(args, String) {
4425            let mode = comp::ChatMode::Faction(faction.clone());
4426            insert_or_replace_component(server, target, mode.clone(), "target")?;
4427            let faction_join = server
4428                .state
4429                .ecs()
4430                .write_storage()
4431                .insert(target, comp::Faction(faction.clone()))
4432                .ok()
4433                .flatten()
4434                .map(|f| f.0);
4435
4436            if emit_join_message {
4437                server.state.send_chat(
4438                    // TODO: Localise
4439                    ChatType::FactionMeta(faction.clone())
4440                        .into_plain_msg(format!("[{}] joined faction ({})", alias, faction)),
4441                    false,
4442                );
4443            }
4444            (faction_join, mode)
4445        } else {
4446            let mode = comp::ChatMode::default();
4447            insert_or_replace_component(server, target, mode.clone(), "target")?;
4448            let faction_leave = server
4449                .state
4450                .ecs()
4451                .write_storage()
4452                .remove(target)
4453                .map(|comp::Faction(f)| f);
4454            (faction_leave, mode)
4455        };
4456        if let Some(faction) = faction_leave
4457            && emit_join_message
4458        {
4459            server.state.send_chat(
4460                // TODO: Localise
4461                ChatType::FactionMeta(faction.clone())
4462                    .into_plain_msg(format!("[{}] left faction ({})", alias, faction)),
4463                false,
4464            );
4465        }
4466        server.notify_client(target, ServerGeneral::ChatMode(mode));
4467        Ok(())
4468    } else {
4469        Err(Content::Plain("Could not find your player alias".into()))
4470    }
4471}
4472
4473fn handle_death_effect(
4474    server: &mut Server,
4475    _client: EcsEntity,
4476    target: EcsEntity,
4477    args: Vec<String>,
4478    action: &ServerChatCommand,
4479) -> CmdResult<()> {
4480    let mut args = args.into_iter();
4481
4482    let Some(effect_str) = args.next() else {
4483        return Err(action.help_content());
4484    };
4485
4486    let effect = match effect_str.as_str() {
4487        "transform" => {
4488            let entity_config = args.next().ok_or(action.help_content())?;
4489
4490            // We don't actually use this loaded config for anything, this is just a check
4491            // to ensure loading succeeds later on.
4492            if Ron::<EntityConfig>::load(&entity_config).is_err() {
4493                return Err(Content::localized_with_args(
4494                    "command-entity-load-failed",
4495                    [("config", entity_config)],
4496                ));
4497            }
4498
4499            let effect = combat::CombatEffect::Transform {
4500                entity_spec: entity_config,
4501                allow_players: true,
4502            };
4503            combat::StatEffect::new(combat::StatEffectTarget::Target, effect)
4504        },
4505        unknown_effect => {
4506            return Err(Content::localized_with_args(
4507                "command-death_effect-unknown",
4508                [("effect", unknown_effect)],
4509            ));
4510        },
4511    };
4512
4513    let mut death_effects = server.state.ecs().write_storage::<combat::DeathEffects>();
4514
4515    if let Some(death_effects) = death_effects.get_mut(target) {
4516        death_effects.0.push(effect);
4517    } else {
4518        death_effects
4519            .insert(target, combat::DeathEffects(vec![effect]))
4520            .unwrap();
4521    }
4522
4523    Ok(())
4524}
4525
4526#[cfg(not(feature = "worldgen"))]
4527fn handle_debug_column(
4528    _server: &mut Server,
4529    _client: EcsEntity,
4530    _target: EcsEntity,
4531    _args: Vec<String>,
4532    _action: &ServerChatCommand,
4533) -> CmdResult<()> {
4534    Err(Content::Plain(
4535        "Unsupported without worldgen enabled".into(),
4536    ))
4537}
4538
4539#[cfg(feature = "worldgen")]
4540fn handle_debug_column(
4541    server: &mut Server,
4542    client: EcsEntity,
4543    target: EcsEntity,
4544    args: Vec<String>,
4545    _action: &ServerChatCommand,
4546) -> CmdResult<()> {
4547    let sim = server.world.sim();
4548    let calendar = (*server.state.ecs().read_resource::<Calendar>()).clone();
4549    let sampler = server.world.sample_columns();
4550    let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) {
4551        Vec2::new(x, y)
4552    } else {
4553        let pos = position(server, target, "target")?;
4554        // FIXME: Deal with overflow, if needed.
4555        pos.0.xy().map(|x| x as i32)
4556    };
4557    let msg_generator = |calendar| {
4558        let alt = sim.get_interpolated(wpos, |chunk| chunk.alt)?;
4559        let basement = sim.get_interpolated(wpos, |chunk| chunk.basement)?;
4560        let water_alt = sim.get_interpolated(wpos, |chunk| chunk.water_alt)?;
4561        let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?;
4562        let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?;
4563        let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?;
4564        let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?;
4565        let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
4566        let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
4567        let chunk_pos = wpos.wpos_to_cpos();
4568        let chunk = sim.get(chunk_pos)?;
4569        let col = sampler.get((wpos, server.index.as_index_ref(), Some(calendar)))?;
4570        let gradient = sim.get_gradient_approx(chunk_pos)?;
4571        let downhill = chunk.downhill;
4572        let river = &chunk.river;
4573        let flux = chunk.flux;
4574        let path = chunk.path;
4575        let cliff_height = chunk.cliff_height;
4576
4577        Some(format!(
4578            r#"wpos: {:?}
4579alt {:?} ({:?})
4580water_alt {:?} ({:?})
4581basement {:?}
4582river {:?}
4583gradient {:?}
4584downhill {:?}
4585chaos {:?}
4586flux {:?}
4587temp {:?}
4588humidity {:?}
4589rockiness {:?}
4590tree_density {:?}
4591spawn_rate {:?}
4592path {:?}
4593cliff_height {:?} "#,
4594            wpos,
4595            alt,
4596            col.alt,
4597            water_alt,
4598            col.water_level,
4599            basement,
4600            river,
4601            gradient,
4602            downhill,
4603            chaos,
4604            flux,
4605            temp,
4606            humidity,
4607            rockiness,
4608            tree_density,
4609            spawn_rate,
4610            path,
4611            cliff_height,
4612        ))
4613    };
4614    if let Some(s) = msg_generator(&calendar) {
4615        server.notify_client(
4616            client,
4617            ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(s)),
4618        );
4619        Ok(())
4620    } else {
4621        Err(Content::Plain("Not a pre-generated chunk.".into()))
4622    }
4623}
4624
4625#[cfg(not(feature = "worldgen"))]
4626fn handle_debug_ways(
4627    _server: &mut Server,
4628    _client: EcsEntity,
4629    _target: EcsEntity,
4630    _args: Vec<String>,
4631    _action: &ServerChatCommand,
4632) -> CmdResult<()> {
4633    Err(Content::Plain(
4634        "Unsupported without worldgen enabled".into(),
4635    ))
4636}
4637
4638#[cfg(feature = "worldgen")]
4639fn handle_debug_ways(
4640    server: &mut Server,
4641    client: EcsEntity,
4642    target: EcsEntity,
4643    args: Vec<String>,
4644    _action: &ServerChatCommand,
4645) -> CmdResult<()> {
4646    let sim = server.world.sim();
4647    let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) {
4648        Vec2::new(x, y)
4649    } else {
4650        let pos = position(server, target, "target")?;
4651        // FIXME: Deal with overflow, if needed.
4652        pos.0.xy().map(|x| x as i32)
4653    };
4654    let msg_generator = || {
4655        let chunk_pos = wpos.wpos_to_cpos();
4656        let mut ret = String::new();
4657        for delta in LOCALITY {
4658            let pos = chunk_pos + delta;
4659            let chunk = sim.get(pos)?;
4660            writeln!(ret, "{:?}: {:?}", pos, chunk.path).ok()?;
4661        }
4662        Some(ret)
4663    };
4664    if let Some(s) = msg_generator() {
4665        server.notify_client(
4666            client,
4667            ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(s)),
4668        );
4669        Ok(())
4670    } else {
4671        Err(Content::Plain("Not a pre-generated chunk.".into()))
4672    }
4673}
4674
4675fn handle_disconnect_all_players(
4676    server: &mut Server,
4677    client: EcsEntity,
4678    _target: EcsEntity,
4679    args: Vec<String>,
4680    _action: &ServerChatCommand,
4681) -> CmdResult<()> {
4682    let client_uuid = uuid(server, client, "client")?;
4683    // Make sure temporary mods/admins can't run this command.
4684    let _role = real_role(server, client_uuid, "role")?;
4685
4686    if parse_cmd_args!(args, String).as_deref() != Some("confirm") {
4687        return Err(Content::localized("command-disconnectall-confirm"));
4688    }
4689
4690    let ecs = server.state.ecs();
4691    let players = &ecs.read_storage::<comp::Player>();
4692
4693    // TODO: This logging and verification of admin commands would be better moved
4694    // to a more generic method used for auditing -all- admin commands.
4695
4696    let player_name = if let Some(player) = players.get(client) {
4697        &*player.alias
4698    } else {
4699        warn!(
4700            "Failed to get player name for admin who used /disconnect_all_players - ignoring \
4701             command."
4702        );
4703        return Err(Content::localized("command-you-dont-exist"));
4704    };
4705
4706    info!(
4707        "Disconnecting all clients due to admin command from {}",
4708        player_name
4709    );
4710    server.disconnect_all_clients_requested = true;
4711
4712    Ok(())
4713}
4714
4715fn handle_skill_point(
4716    server: &mut Server,
4717    _client: EcsEntity,
4718    target: EcsEntity,
4719    args: Vec<String>,
4720    action: &ServerChatCommand,
4721) -> CmdResult<()> {
4722    if let (Some(a_skill_tree), Some(sp), entity_target) =
4723        parse_cmd_args!(args, String, u16, EntityTarget)
4724    {
4725        let skill_tree = parse_skill_tree(&a_skill_tree)?;
4726        let player = entity_target
4727            .map(|entity_target| get_entity_target(entity_target, server))
4728            .unwrap_or(Ok(target))?;
4729
4730        if let Some(mut skill_set) = server
4731            .state
4732            .ecs_mut()
4733            .write_storage::<comp::SkillSet>()
4734            .get_mut(player)
4735        {
4736            skill_set.add_skill_points(skill_tree, sp);
4737            Ok(())
4738        } else {
4739            Err(Content::Plain("Entity has no stats!".into()))
4740        }
4741    } else {
4742        Err(action.help_content())
4743    }
4744}
4745
4746fn parse_skill_tree(skill_tree: &str) -> CmdResult<comp::skillset::SkillGroupKind> {
4747    use comp::{item::tool::ToolKind, skillset::SkillGroupKind};
4748    match skill_tree {
4749        "general" => Ok(SkillGroupKind::General),
4750        "sword" => Ok(SkillGroupKind::Weapon(ToolKind::Sword)),
4751        "axe" => Ok(SkillGroupKind::Weapon(ToolKind::Axe)),
4752        "hammer" => Ok(SkillGroupKind::Weapon(ToolKind::Hammer)),
4753        "bow" => Ok(SkillGroupKind::Weapon(ToolKind::Bow)),
4754        "staff" => Ok(SkillGroupKind::Weapon(ToolKind::Staff)),
4755        "sceptre" => Ok(SkillGroupKind::Weapon(ToolKind::Sceptre)),
4756        "mining" => Ok(SkillGroupKind::Weapon(ToolKind::Pick)),
4757        _ => Err(Content::localized_with_args(
4758            "command-invalid-skill-group",
4759            [("group", skill_tree)],
4760        )),
4761    }
4762}
4763
4764pub fn reload_chunks_inner(
4765    server: &mut Server,
4766    pos: Vec3<f32>,
4767    radius: Option<i32>,
4768    only_sites: bool,
4769) -> usize {
4770    let mut removed = 0;
4771
4772    if let Some(radius) = radius {
4773        let chunk_key = server.state.terrain().pos_key(pos.as_());
4774
4775        for key_offset in Spiral2d::with_radius(radius) {
4776            let chunk_key = chunk_key + key_offset;
4777            #[cfg(feature = "worldgen")]
4778            if only_sites
4779                && server
4780                    .world
4781                    .sim()
4782                    .get(chunk_key)
4783                    .is_none_or(|c| c.sites.is_empty())
4784            {
4785                continue;
4786            }
4787
4788            #[cfg(feature = "persistent_world")]
4789            server
4790                .state
4791                .ecs()
4792                .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>()
4793                .map(|mut terrain_persistence| terrain_persistence.unload_chunk(chunk_key));
4794            if server.state.remove_chunk(chunk_key) {
4795                removed += 1;
4796            }
4797        }
4798    } else if cfg!(feature = "worldgen") && only_sites {
4799        let removed_chunks = &mut server
4800            .state
4801            .ecs()
4802            .write_resource::<common_state::TerrainChanges>()
4803            .removed_chunks;
4804        server.state.terrain_mut().retain(|chunk_key| {
4805            #[cfg(feature = "worldgen")]
4806            if server
4807                .world
4808                .sim()
4809                .get(chunk_key)
4810                .is_none_or(|c| c.sites.is_empty())
4811            {
4812                return true;
4813            }
4814
4815            #[cfg(feature = "persistent_world")]
4816            server
4817                .state
4818                .ecs()
4819                .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>()
4820                .map(|mut terrain_persistence| terrain_persistence.unload_chunk(chunk_key));
4821
4822            removed_chunks.insert(chunk_key);
4823            removed += 1;
4824
4825            false
4826        });
4827    } else {
4828        #[cfg(feature = "persistent_world")]
4829        server
4830            .state
4831            .ecs()
4832            .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>()
4833            .map(|mut terrain_persistence| terrain_persistence.unload_all());
4834        removed = server.state.clear_terrain();
4835    }
4836
4837    removed
4838}
4839
4840fn handle_reload_chunks(
4841    server: &mut Server,
4842    client: EcsEntity,
4843    target: EcsEntity,
4844    args: Vec<String>,
4845    _action: &ServerChatCommand,
4846) -> CmdResult<()> {
4847    let (radius, only_sites) = parse_cmd_args!(args, i32, bool);
4848
4849    let pos = position(server, target, "target")?.0;
4850    let removed = reload_chunks_inner(
4851        server,
4852        pos,
4853        radius.map(|radius| radius.clamp(0, 64)),
4854        only_sites.unwrap_or(false),
4855    );
4856
4857    server.notify_client(
4858        client,
4859        ServerGeneral::server_msg(
4860            ChatType::CommandInfo,
4861            Content::localized_with_args("command-reloaded-chunks", [(
4862                "reloaded",
4863                removed.to_string(),
4864            )]),
4865        ),
4866    );
4867
4868    Ok(())
4869}
4870
4871fn handle_remove_lights(
4872    server: &mut Server,
4873    client: EcsEntity,
4874    target: EcsEntity,
4875    args: Vec<String>,
4876    _action: &ServerChatCommand,
4877) -> CmdResult<()> {
4878    let opt_radius = parse_cmd_args!(args, f32);
4879    let player_pos = position(server, target, "target")?;
4880    let mut to_delete = vec![];
4881
4882    let ecs = server.state.ecs();
4883    for (entity, pos, _, _, _) in (
4884        &ecs.entities(),
4885        &ecs.read_storage::<comp::Pos>(),
4886        &ecs.read_storage::<LightEmitter>(),
4887        !&ecs.read_storage::<WaypointArea>(),
4888        !&ecs.read_storage::<comp::Player>(),
4889    )
4890        .join()
4891    {
4892        if opt_radius
4893            .map(|r| pos.0.distance(player_pos.0) < r)
4894            .unwrap_or(true)
4895        {
4896            to_delete.push(entity);
4897        }
4898    }
4899
4900    let size = to_delete.len();
4901
4902    for entity in to_delete {
4903        if let Err(e) = server.state.delete_entity_recorded(entity) {
4904            error!(?e, "Failed to delete light: {:?}", e);
4905        }
4906    }
4907
4908    server.notify_client(
4909        client,
4910        ServerGeneral::server_msg(
4911            ChatType::CommandInfo,
4912            Content::Plain(format!("Removed {} lights!", size)),
4913        ),
4914    );
4915    Ok(())
4916}
4917
4918fn get_entity_target(entity_target: EntityTarget, server: &Server) -> CmdResult<EcsEntity> {
4919    match entity_target {
4920        EntityTarget::Player(alias) => Ok(find_alias(server.state.ecs(), &alias, true)?.0),
4921        EntityTarget::RtsimNpc(id) => {
4922            let actor_id = server
4923                .state
4924                .ecs()
4925                .read_resource::<crate::rtsim::RtSim>()
4926                .state()
4927                .data()
4928                .actors
4929                .keys()
4930                .find(|actor_id| key_matches(*actor_id, &id))
4931                .ok_or(Content::Plain(format!(
4932                    "Could not find rtsim npc with id {id}."
4933                )))?;
4934            server
4935                .state()
4936                .ecs()
4937                .read_resource::<common::uid::IdMaps>()
4938                .rtsim_entity(actor_id)
4939                .ok_or(Content::Plain(format!("Npc with id {id} isn't loaded.")))
4940        },
4941        EntityTarget::Uid(uid) => server
4942            .state
4943            .ecs()
4944            .entity_from_uid(uid)
4945            .ok_or(Content::Plain(format!("{uid:?} not found."))),
4946    }
4947}
4948
4949fn handle_sudo(
4950    server: &mut Server,
4951    client: EcsEntity,
4952    _target: EcsEntity,
4953    args: Vec<String>,
4954    action: &ServerChatCommand,
4955) -> CmdResult<()> {
4956    if let (Some(entity_target), Some(cmd), cmd_args) =
4957        parse_cmd_args!(args, EntityTarget, String, ..Vec<String>)
4958    {
4959        if let Ok(action) = cmd.parse() {
4960            let entity = get_entity_target(entity_target, server)?;
4961            let client_uuid = uuid(server, client, "client")?;
4962
4963            // If the entity target is a player check if client has authority to sudo it.
4964            {
4965                let players = server.state.ecs().read_storage::<comp::Player>();
4966                if let Some(player) = players.get(entity) {
4967                    let player_uuid = player.uuid();
4968                    drop(players);
4969                    verify_above_role(
4970                        server,
4971                        (client, client_uuid),
4972                        (entity, player_uuid),
4973                        Content::localized("command-sudo-higher-role"),
4974                    )?;
4975                } else if server.entity_admin_role(client) < Some(AdminRole::Admin) {
4976                    return Err(Content::localized(
4977                        "command-sudo-no-permission-for-non-players",
4978                    ));
4979                }
4980            }
4981
4982            // TODO: consider making this into a tail call or loop (to avoid the potential
4983            // stack overflow, although it's less of a risk coming from only mods and
4984            // admins).
4985            do_command(server, client, entity, cmd_args, &action)
4986        } else {
4987            Err(Content::localized("command-unknown"))
4988        }
4989    } else {
4990        Err(action.help_content())
4991    }
4992}
4993
4994fn handle_version(
4995    server: &mut Server,
4996    client: EcsEntity,
4997    _target: EcsEntity,
4998    _args: Vec<String>,
4999    _action: &ServerChatCommand,
5000) -> CmdResult<()> {
5001    server.notify_client(
5002        client,
5003        ServerGeneral::server_msg(
5004            ChatType::CommandInfo,
5005            Content::localized_with_args("command-version-current", [(
5006                "version",
5007                (*common::util::DISPLAY_VERSION).to_owned(),
5008            )]),
5009        ),
5010    );
5011    Ok(())
5012}
5013
5014fn handle_whitelist(
5015    server: &mut Server,
5016    client: EcsEntity,
5017    _target: EcsEntity,
5018    args: Vec<String>,
5019    action: &ServerChatCommand,
5020) -> CmdResult<()> {
5021    let now = Utc::now();
5022
5023    if let (Some(whitelist_action), Some(username)) = parse_cmd_args!(args, String, String) {
5024        let client_uuid = uuid(server, client, "client")?;
5025        let client_username = uuid_to_username(server, client, client_uuid)?;
5026        let client_role = real_role(server, client_uuid, "client")?;
5027
5028        if whitelist_action.eq_ignore_ascii_case("add") {
5029            let uuid = find_username(server, &username)?;
5030
5031            let record = WhitelistRecord {
5032                date: now,
5033                info: Some(WhitelistInfo {
5034                    username_when_whitelisted: username.clone(),
5035                    whitelisted_by: client_uuid,
5036                    whitelisted_by_username: client_username,
5037                    whitelisted_by_role: client_role.into(),
5038                }),
5039            };
5040
5041            let edit =
5042                server
5043                    .editable_settings_mut()
5044                    .whitelist
5045                    .edit(server.data_dir().as_ref(), |w| {
5046                        if w.insert(uuid, record).is_some() {
5047                            None
5048                        } else {
5049                            Some(Content::localized_with_args("command-whitelist-added", [(
5050                                "username",
5051                                username.to_owned(),
5052                            )]))
5053                        }
5054                    });
5055            edit_setting_feedback(server, client, edit, || {
5056                Content::localized_with_args("command-whitelist-already-added", [(
5057                    "username", username,
5058                )])
5059            })
5060        } else if whitelist_action.eq_ignore_ascii_case("remove") {
5061            let client_uuid = uuid(server, client, "client")?;
5062            let client_role = real_role(server, client_uuid, "client")?;
5063
5064            let uuid = find_username(server, &username)?;
5065            let mut err_key = "command-whitelist-unlisted";
5066            let edit =
5067                server
5068                    .editable_settings_mut()
5069                    .whitelist
5070                    .edit(server.data_dir().as_ref(), |w| {
5071                        w.remove(&uuid)
5072                            .filter(|record| {
5073                                if record.whitelisted_by_role() <= client_role.into() {
5074                                    true
5075                                } else {
5076                                    err_key = "command-whitelist-permission-denied";
5077                                    false
5078                                }
5079                            })
5080                            .map(|_| {
5081                                Content::localized_with_args("command-whitelist-removed", [(
5082                                    "username",
5083                                    username.to_owned(),
5084                                )])
5085                            })
5086                    });
5087            edit_setting_feedback(server, client, edit, || {
5088                Content::localized_with_args(err_key, [("username", username)])
5089            })
5090        } else {
5091            Err(action.help_content())
5092        }
5093    } else {
5094        Err(action.help_content())
5095    }
5096}
5097
5098fn kick_player(
5099    server: &mut Server,
5100    (client, client_uuid): (EcsEntity, Uuid),
5101    (target_player, target_player_uuid): (EcsEntity, Uuid),
5102    reason: DisconnectReason,
5103) -> CmdResult<()> {
5104    verify_above_role(
5105        server,
5106        (client, client_uuid),
5107        (target_player, target_player_uuid),
5108        Content::localized("command-kick-higher-role"),
5109    )?;
5110    server.notify_client(target_player, ServerGeneral::Disconnect(reason));
5111    server
5112        .state
5113        .mut_resource::<EventBus<ClientDisconnectEvent>>()
5114        .emit_now(ClientDisconnectEvent(
5115            target_player,
5116            comp::DisconnectReason::Kicked,
5117        ));
5118    Ok(())
5119}
5120
5121fn handle_kick(
5122    server: &mut Server,
5123    client: EcsEntity,
5124    _target: EcsEntity,
5125    args: Vec<String>,
5126    action: &ServerChatCommand,
5127) -> CmdResult<()> {
5128    if let (Some(target_alias), reason_opt) = parse_cmd_args!(args, String, String) {
5129        let client_uuid = uuid(server, client, "client")?;
5130        let reason = reason_opt.unwrap_or_default();
5131        let ecs = server.state.ecs();
5132        let target_player = find_alias(ecs, &target_alias, true)?;
5133
5134        kick_player(
5135            server,
5136            (client, client_uuid),
5137            target_player,
5138            DisconnectReason::Kicked(reason.clone()),
5139        )?;
5140        server.notify_client(
5141            client,
5142            ServerGeneral::server_msg(
5143                ChatType::CommandInfo,
5144                Content::Plain(format!(
5145                    "Kicked {} from the server with reason: {}",
5146                    target_alias, reason
5147                )),
5148            ),
5149        );
5150        Ok(())
5151    } else {
5152        Err(action.help_content())
5153    }
5154}
5155
5156fn make_ban_info(server: &mut Server, client: EcsEntity, client_uuid: Uuid) -> CmdResult<BanInfo> {
5157    let client_username = uuid_to_username(server, client, client_uuid)?;
5158    let client_role = real_role(server, client_uuid, "client")?;
5159    let ban_info = BanInfo {
5160        performed_by: client_uuid,
5161        performed_by_username: client_username,
5162        performed_by_role: client_role.into(),
5163    };
5164    Ok(ban_info)
5165}
5166
5167fn ban_end_date(
5168    now: chrono::DateTime<Utc>,
5169    parse_duration: Option<HumanDuration>,
5170) -> CmdResult<Option<chrono::DateTime<Utc>>> {
5171    let end_date = parse_duration
5172        .map(|duration| chrono::Duration::from_std(duration.into()))
5173        .transpose()
5174        .map_err(|err| {
5175            Content::localized_with_args(
5176                "command-parse-duration-error",
5177                [("error", format!("{err:?}"))]
5178            )
5179        })?
5180        // On overflow (someone adding some ridiculous time span), just make the ban infinite.
5181        // (end date of None is an infinite ban)
5182        .and_then(|duration| now.checked_add_signed(duration));
5183    Ok(end_date)
5184}
5185
5186fn handle_ban(
5187    server: &mut Server,
5188    client: EcsEntity,
5189    _target: EcsEntity,
5190    args: Vec<String>,
5191    action: &ServerChatCommand,
5192) -> CmdResult<()> {
5193    let (Some(username), overwrite, parse_duration, reason_opt) =
5194        parse_cmd_args!(args, String, bool, HumanDuration, String)
5195    else {
5196        return Err(action.help_content());
5197    };
5198
5199    let reason = reason_opt.unwrap_or_default();
5200    let overwrite = overwrite.unwrap_or(false);
5201
5202    let client_uuid = uuid(server, client, "client")?;
5203    let ban_info = make_ban_info(server, client, client_uuid)?;
5204
5205    let player_uuid = find_username(server, &username)?;
5206
5207    let now = Utc::now();
5208    let end_date = ban_end_date(now, parse_duration)?;
5209
5210    let result = server.editable_settings_mut().banlist.ban_operation(
5211        server.data_dir().as_ref(),
5212        now,
5213        player_uuid,
5214        username.clone(),
5215        BanOperation::Ban {
5216            reason: reason.clone(),
5217            info: ban_info,
5218            upgrade_to_ip: false,
5219            end_date,
5220        },
5221        overwrite,
5222    );
5223    let (result, ban_info) = match result {
5224        Ok(info) => (Ok(()), info),
5225        Err(err) => (Err(err), None),
5226    };
5227
5228    edit_banlist_feedback(
5229        server,
5230        client,
5231        result,
5232        || {
5233            Content::localized_with_args("command-ban-added", [
5234                ("player", username.clone()),
5235                ("reason", reason),
5236            ])
5237        },
5238        || {
5239            Content::localized_with_args("command-ban-already-added", [(
5240                "player",
5241                username.clone(),
5242            )])
5243        },
5244    )?;
5245    // If the player is online kick them (this may fail if the player is a hardcoded
5246    // admin; we don't care about that case because hardcoded admins can log on even
5247    // if they're on the ban list).
5248    let ecs = server.state.ecs();
5249    if let Ok(target_player) = find_uuid(ecs, player_uuid) {
5250        let _ = kick_player(
5251            server,
5252            (client, client_uuid),
5253            (target_player, player_uuid),
5254            ban_info.map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
5255        );
5256    }
5257    Ok(())
5258}
5259
5260fn handle_aura(
5261    server: &mut Server,
5262    client: EcsEntity,
5263    target: EcsEntity,
5264    args: Vec<String>,
5265    action: &ServerChatCommand,
5266) -> CmdResult<()> {
5267    let target_uid = uid(server, target, "target")?;
5268
5269    let (Some(aura_radius), aura_duration, new_entity, aura_target, Some(aura_kind_variant), spec) =
5270        parse_cmd_args!(args, f32, f32, bool, GroupTarget, AuraKindVariant, ..Vec<String>)
5271    else {
5272        return Err(action.help_content());
5273    };
5274    let new_entity = new_entity.unwrap_or(false);
5275    let aura_kind = match aura_kind_variant {
5276        AuraKindVariant::Buff => {
5277            let (Some(buff), strength, duration, misc_data_spec) =
5278                parse_cmd_args!(spec, String, f32, f64, String)
5279            else {
5280                return Err(Content::localized("command-aura-invalid-buff-parameters"));
5281            };
5282            let buffkind = parse_buffkind(&buff).ok_or_else(|| {
5283                Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
5284            })?;
5285            let buffdata = build_buff(
5286                buffkind,
5287                strength.unwrap_or(1.0),
5288                duration.unwrap_or(10.0),
5289                (!buffkind.is_simple())
5290                    .then(|| {
5291                        misc_data_spec.ok_or_else(|| {
5292                            Content::localized_with_args("command-buff-data", [(
5293                                "buff",
5294                                buff.clone(),
5295                            )])
5296                        })
5297                    })
5298                    .transpose()?,
5299            )?;
5300
5301            AuraKind::Buff {
5302                kind: buffkind,
5303                data: buffdata,
5304                category: None,
5305                source: if new_entity {
5306                    BuffSource::World
5307                } else {
5308                    BuffSource::Character {
5309                        by: target_uid,
5310                        tool_kind: None,
5311                    }
5312                },
5313            }
5314        },
5315        AuraKindVariant::FriendlyFire => AuraKind::FriendlyFire,
5316        AuraKindVariant::ForcePvP => AuraKind::ForcePvP,
5317    };
5318    let aura_target = server
5319        .state
5320        .read_component_copied::<Uid>(target)
5321        .map(|uid| match aura_target {
5322            Some(GroupTarget::InGroup) => AuraTarget::GroupOf(uid),
5323            Some(GroupTarget::OutOfGroup) => AuraTarget::NotGroupOf(uid),
5324            Some(GroupTarget::All) | None => AuraTarget::All,
5325        })
5326        .unwrap_or(AuraTarget::All);
5327
5328    let time = Time(server.state.get_time());
5329    let aura = Aura::new(
5330        aura_kind,
5331        aura_radius,
5332        aura_duration.map(|duration| Secs(duration as f64)),
5333        aura_target,
5334        time,
5335        None,
5336    );
5337
5338    if new_entity {
5339        let pos = position(server, target, "target")?;
5340        server
5341            .state
5342            .create_empty(pos)
5343            .with(comp::Auras::new(vec![aura]))
5344            .maybe_with(aura_duration.map(|duration| comp::Object::DeleteAfter {
5345                spawned_at: time,
5346                timeout: Duration::from_secs_f32(duration),
5347            }))
5348            .build();
5349    } else {
5350        let mut auras = server.state.ecs().write_storage::<comp::Auras>();
5351        if let Some(mut auras) = auras.get_mut(target) {
5352            auras.insert(aura);
5353        }
5354    }
5355
5356    server.notify_client(
5357        client,
5358        ServerGeneral::server_msg(
5359            ChatType::CommandInfo,
5360            Content::localized(if new_entity {
5361                "command-aura-spawn-new-entity"
5362            } else {
5363                "command-aura-spawn"
5364            }),
5365        ),
5366    );
5367
5368    Ok(())
5369}
5370
5371fn handle_ban_ip(
5372    server: &mut Server,
5373    client: EcsEntity,
5374    _target: EcsEntity,
5375    args: Vec<String>,
5376    action: &ServerChatCommand,
5377) -> CmdResult<()> {
5378    let (Some(username), overwrite, parse_duration, reason_opt) =
5379        parse_cmd_args!(args, String, bool, HumanDuration, String)
5380    else {
5381        return Err(action.help_content());
5382    };
5383
5384    let reason = reason_opt.unwrap_or_default();
5385    let overwrite = overwrite.unwrap_or(false);
5386
5387    let client_uuid = uuid(server, client, "client")?;
5388    let ban_info = make_ban_info(server, client, client_uuid)?;
5389
5390    let player_uuid = find_username(server, &username)?;
5391    let now = Utc::now();
5392    let end_date = ban_end_date(now, parse_duration)?;
5393
5394    let (players_to_kick, ban_result, frontend_info);
5395
5396    let player_ip_addr = if let Some(player_addr) = find_uuid(server.state.ecs(), player_uuid)
5397        .ok()
5398        .and_then(|player_entity| socket_addr(server, player_entity, &username).ok())
5399    {
5400        Some(NormalizedIpAddr::from(player_addr.ip()))
5401    } else {
5402        server
5403            .state()
5404            .ecs()
5405            .read_resource::<crate::RecentClientIPs>()
5406            .last_addrs
5407            .peek(&player_uuid)
5408            .cloned()
5409    };
5410
5411    // If we can get the address of the target player, apply an immediate IP ban
5412    if let Some(player_ip_addr) = player_ip_addr {
5413        let result = server.editable_settings_mut().banlist.ban_operation(
5414            server.data_dir().as_ref(),
5415            now,
5416            player_uuid,
5417            username.clone(),
5418            BanOperation::BanIp {
5419                reason: reason.clone(),
5420                info: ban_info,
5421                end_date,
5422                ip: player_ip_addr,
5423            },
5424            overwrite,
5425        );
5426        (ban_result, frontend_info) = match result {
5427            Ok(info) => (Ok(()), info),
5428            Err(err) => (Err(err), None),
5429        };
5430
5431        edit_banlist_feedback(
5432            server,
5433            client,
5434            ban_result,
5435            || {
5436                Content::localized_with_args("command-ban-ip-added", [
5437                    ("player", username.clone()),
5438                    ("reason", reason),
5439                ])
5440            },
5441            || {
5442                Content::localized_with_args("command-ban-already-added", [(
5443                    "player",
5444                    username.clone(),
5445                )])
5446            },
5447        )?;
5448
5449        // Kick all online players with this IP address them (this may fail if the
5450        // player is a hardcoded admin; we don't care about that case because
5451        // hardcoded admins can log on even if they're on the ban list).
5452        let ecs = server.state.ecs();
5453        players_to_kick = (
5454            &ecs.entities(),
5455            &ecs.read_storage::<Client>(),
5456            &ecs.read_storage::<comp::Player>(),
5457        )
5458            .join()
5459            .filter(|(_, client, _)| {
5460                client
5461                    .current_ip_addrs
5462                    .iter()
5463                    .any(|socket_addr| NormalizedIpAddr::from(socket_addr.ip()) == player_ip_addr)
5464            })
5465            .map(|(entity, _, player)| (entity, player.uuid()))
5466            .collect::<Vec<_>>();
5467    // Otherwise create a regular ban which will be upgraded to an IP ban on
5468    // any subsequent login attempts
5469    } else {
5470        let result = server.editable_settings_mut().banlist.ban_operation(
5471            server.data_dir().as_ref(),
5472            now,
5473            player_uuid,
5474            username.clone(),
5475            BanOperation::Ban {
5476                reason: reason.clone(),
5477                info: ban_info,
5478                upgrade_to_ip: true,
5479                end_date,
5480            },
5481            overwrite,
5482        );
5483
5484        (ban_result, frontend_info) = match result {
5485            Ok(info) => (Ok(()), info),
5486            Err(err) => (Err(err), None),
5487        };
5488
5489        edit_banlist_feedback(
5490            server,
5491            client,
5492            ban_result,
5493            || {
5494                Content::localized_with_args("command-ban-ip-queued", [
5495                    ("player", username.clone()),
5496                    ("reason", reason),
5497                ])
5498            },
5499            || {
5500                Content::localized_with_args("command-ban-already-added", [(
5501                    "player",
5502                    username.clone(),
5503                )])
5504            },
5505        )?;
5506
5507        let ecs = server.state.ecs();
5508        players_to_kick = find_uuid(ecs, player_uuid)
5509            .map(|entity| (entity, player_uuid))
5510            .into_iter()
5511            .collect();
5512    }
5513
5514    for (player_entity, player_uuid) in players_to_kick {
5515        let _ = kick_player(
5516            server,
5517            (client, client_uuid),
5518            (player_entity, player_uuid),
5519            frontend_info
5520                .clone()
5521                .map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
5522        );
5523    }
5524
5525    Ok(())
5526}
5527
5528fn handle_ban_log(
5529    server: &mut Server,
5530    client: EcsEntity,
5531    _target: EcsEntity,
5532    args: Vec<String>,
5533    action: &ServerChatCommand,
5534) -> CmdResult<()> {
5535    let (Some(username), max_entries) = parse_cmd_args!(args, String, i32) else {
5536        return Err(action.help_content());
5537    };
5538    let max_entries = max_entries
5539        .and_then(|i| usize::try_from(i).ok())
5540        .unwrap_or(10);
5541
5542    let player_uuid = find_username(server, &username)?;
5543
5544    let display_record = |action: &BanAction,
5545                          username_at_ban: Option<&str>,
5546                          date: &DateTime<Utc>| {
5547        let display_ban_info = |info: &BanInfo| {
5548            format!(
5549                "By: {} [{}] ({:?})",
5550                info.performed_by_username, info.performed_by, info.performed_by_role
5551            )
5552        };
5553        let action = match action {
5554            BanAction::Unban(ban_info) => format!("Unbanned\n  {}", display_ban_info(ban_info)),
5555            BanAction::Ban(ban) => {
5556                format!(
5557                    "Banned\n  Reason: {}\n  Until: {}{}{}",
5558                    ban.reason,
5559                    ban.end_date
5560                        .map_or_else(|| "permanent".to_string(), |end_date| end_date.to_rfc3339()),
5561                    if ban.upgrade_to_ip {
5562                        "\n  Will be upgraded to IP ban"
5563                    } else {
5564                        Default::default()
5565                    },
5566                    ban.info
5567                        .as_ref()
5568                        .map(|info| format!("\n  {}", display_ban_info(info)))
5569                        .unwrap_or_default()
5570                )
5571            },
5572        };
5573        format!(
5574            "\n{action}\n  At: {}{}\n-------",
5575            date.to_rfc3339(),
5576            username_at_ban
5577                .filter(|at_ban| username != *at_ban)
5578                .map(|username| format!("\n  Username at ban: {username}"))
5579                .unwrap_or_default(),
5580        )
5581    };
5582
5583    let editable_settings = server.editable_settings();
5584    let Some(entry) = editable_settings.banlist.uuid_bans().get(&player_uuid) else {
5585        return Err(Content::Plain(
5586            "No entries exist for this player".to_string(),
5587        ));
5588    };
5589    let mut ban_log = format!("Ban log for '{username}' [{player_uuid}]:\n\nBans:");
5590
5591    for (entry_i, record) in entry
5592        .history
5593        .iter()
5594        .chain([&entry.current])
5595        .rev()
5596        .enumerate()
5597    {
5598        if entry_i >= max_entries {
5599            ban_log.push_str(&format!(
5600                "\n...{} More...",
5601                entry.history.len() + 1 - entry_i
5602            ));
5603            break;
5604        }
5605        ban_log.push_str(&display_record(
5606            &record.action,
5607            Some(&record.username_when_performed),
5608            &record.date,
5609        ));
5610    }
5611
5612    ban_log.push_str("\n\nIP Bans:");
5613
5614    for (entry_i, record) in editable_settings
5615        .banlist
5616        .ip_bans()
5617        .values()
5618        .flat_map(|entry| {
5619            entry
5620                .history
5621                .iter()
5622                .chain([&entry.current])
5623                .rev()
5624                .filter(|record| {
5625                    record
5626                        .uuid_when_performed
5627                        .is_some_and(|ip_ban_uuid| ip_ban_uuid == player_uuid)
5628                })
5629        })
5630        .enumerate()
5631    {
5632        if entry_i >= max_entries {
5633            ban_log.push_str("\n...More...");
5634            break;
5635        }
5636        ban_log.push_str(&display_record(&record.action, None, &record.date));
5637    }
5638
5639    server.notify_client(
5640        client,
5641        ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(ban_log)),
5642    );
5643
5644    Ok(())
5645}
5646
5647fn handle_battlemode(
5648    server: &mut Server,
5649    client: EcsEntity,
5650    _target: EcsEntity,
5651    args: Vec<String>,
5652    _action: &ServerChatCommand,
5653) -> CmdResult<()> {
5654    if let Some(argument) = parse_cmd_args!(args, String) {
5655        let battle_mode = match argument.as_str() {
5656            "pvp" => BattleMode::PvP,
5657            "pve" => BattleMode::PvE,
5658            _ => return Err(Content::localized("command-battlemode-available-modes")),
5659        };
5660
5661        server.set_battle_mode_for(client, battle_mode);
5662    } else {
5663        server.get_battle_mode_for(client);
5664    }
5665
5666    Ok(())
5667}
5668
5669fn handle_battlemode_force(
5670    server: &mut Server,
5671    client: EcsEntity,
5672    target: EcsEntity,
5673    args: Vec<String>,
5674    action: &ServerChatCommand,
5675) -> CmdResult<()> {
5676    let ecs = server.state.ecs();
5677    let settings = ecs.read_resource::<Settings>();
5678
5679    if !settings.gameplay.battle_mode.allow_choosing() {
5680        return Err(Content::localized("command-disabled-by-settings"));
5681    }
5682
5683    let mode = parse_cmd_args!(args, String).ok_or_else(|| action.help_content())?;
5684    let mode = match mode.as_str() {
5685        "pvp" => BattleMode::PvP,
5686        "pve" => BattleMode::PvE,
5687        _ => return Err(Content::localized("command-battlemode-available-modes")),
5688    };
5689
5690    let mut players = ecs.write_storage::<comp::Player>();
5691    let mut player_info = players.get_mut(target).ok_or(Content::Plain(
5692        "Cannot get player component for target".to_string(),
5693    ))?;
5694    player_info.battle_mode = mode;
5695
5696    server.notify_client(
5697        client,
5698        ServerGeneral::server_msg(
5699            ChatType::CommandInfo,
5700            Content::localized_with_args("command-battlemode-updated", [(
5701                "battlemode",
5702                format!("{mode:?}"),
5703            )]),
5704        ),
5705    );
5706    Ok(())
5707}
5708
5709fn handle_unban(
5710    server: &mut Server,
5711    client: EcsEntity,
5712    _target: EcsEntity,
5713    args: Vec<String>,
5714    action: &ServerChatCommand,
5715) -> CmdResult<()> {
5716    let Some(username) = parse_cmd_args!(args, String) else {
5717        return Err(action.help_content());
5718    };
5719
5720    let player_uuid = find_username(server, &username)?;
5721
5722    let client_uuid = uuid(server, client, "client")?;
5723    let ban_info = make_ban_info(server, client, client_uuid)?;
5724
5725    let now = Utc::now();
5726
5727    let unban = BanOperation::Unban { info: ban_info };
5728
5729    let result = server.editable_settings_mut().banlist.ban_operation(
5730        server.data_dir().as_ref(),
5731        now,
5732        player_uuid,
5733        username.clone(),
5734        unban,
5735        false,
5736    );
5737
5738    edit_banlist_feedback(
5739        server,
5740        client,
5741        result.map(|_| ()),
5742        // TODO: it would be useful to indicate here whether an IP ban was also removed but we
5743        // don't have that info.
5744        || Content::localized_with_args("command-unban-successful", [("player", username.clone())]),
5745        || {
5746            Content::localized_with_args("command-unban-already-unbanned", [(
5747                "player",
5748                username.clone(),
5749            )])
5750        },
5751    )
5752}
5753
5754fn handle_unban_ip(
5755    server: &mut Server,
5756    client: EcsEntity,
5757    _target: EcsEntity,
5758    args: Vec<String>,
5759    action: &ServerChatCommand,
5760) -> CmdResult<()> {
5761    let Some(username) = parse_cmd_args!(args, String) else {
5762        return Err(action.help_content());
5763    };
5764
5765    let player_uuid = find_username(server, &username)?;
5766
5767    let client_uuid = uuid(server, client, "client")?;
5768    let ban_info = make_ban_info(server, client, client_uuid)?;
5769
5770    let now = Utc::now();
5771
5772    let unban = BanOperation::UnbanIp {
5773        info: ban_info,
5774        uuid: player_uuid,
5775    };
5776
5777    let result = server.editable_settings_mut().banlist.ban_operation(
5778        server.data_dir().as_ref(),
5779        now,
5780        player_uuid,
5781        username.clone(),
5782        unban,
5783        false,
5784    );
5785
5786    edit_banlist_feedback(
5787        server,
5788        client,
5789        result.map(|_| ()),
5790        || {
5791            Content::localized_with_args("command-unban-ip-successful", [(
5792                "player",
5793                username.clone(),
5794            )])
5795        },
5796        || {
5797            Content::localized_with_args("command-unban-already-unbanned", [(
5798                "player",
5799                username.clone(),
5800            )])
5801        },
5802    )
5803}
5804
5805fn handle_server_physics(
5806    server: &mut Server,
5807    client: EcsEntity,
5808    _target: EcsEntity,
5809    args: Vec<String>,
5810    action: &ServerChatCommand,
5811) -> CmdResult<()> {
5812    if let (Some(username), enabled_opt, reason) = parse_cmd_args!(args, String, bool, String) {
5813        let uuid = find_username(server, &username)?;
5814        let server_force = enabled_opt.unwrap_or(true);
5815        let data_dir = server.data_dir();
5816
5817        let result = server
5818            .editable_settings_mut()
5819            .server_physics_force_list
5820            .edit(data_dir.as_ref(), |list| {
5821                if server_force {
5822                    let Some(by) = server
5823                        .state()
5824                        .ecs()
5825                        .read_storage::<comp::Player>()
5826                        .get(client)
5827                        .map(|player| (player.uuid(), player.alias.clone()))
5828                    else {
5829                        return Some(Some(Content::localized("command-you-dont-exist")));
5830                    };
5831                    list.insert(uuid, ServerPhysicsForceRecord {
5832                        by: Some(by),
5833                        reason,
5834                    });
5835                    Some(None)
5836                } else {
5837                    list.remove(&uuid);
5838                    Some(None)
5839                }
5840            });
5841
5842        if let Some((Some(error), _)) = result {
5843            return Err(error);
5844        }
5845
5846        server.notify_client(
5847            client,
5848            ServerGeneral::server_msg(
5849                ChatType::CommandInfo,
5850                Content::Plain(format!(
5851                    "Updated physics settings for {} ({}): {:?}",
5852                    username, uuid, server_force
5853                )),
5854            ),
5855        );
5856        Ok(())
5857    } else {
5858        Err(action.help_content())
5859    }
5860}
5861
5862fn handle_buff(
5863    server: &mut Server,
5864    _client: EcsEntity,
5865    target: EcsEntity,
5866    args: Vec<String>,
5867    action: &ServerChatCommand,
5868) -> CmdResult<()> {
5869    let (Some(buff), strength, duration, misc_data_spec) =
5870        parse_cmd_args!(args, String, f32, f64, String)
5871    else {
5872        return Err(action.help_content());
5873    };
5874
5875    let strength = strength.unwrap_or(0.01);
5876
5877    match buff.as_str() {
5878        "all" => {
5879            let duration = duration.unwrap_or(5.0);
5880            let buffdata = BuffData::new(strength, Some(Secs(duration)));
5881
5882            // apply every(*) non-complex buff
5883            //
5884            // (*) BUFF_PACK contains all buffs except
5885            // invulnerability
5886            BUFF_PACK
5887                .iter()
5888                .filter_map(|kind_key| parse_buffkind(kind_key))
5889                .filter(|buffkind| buffkind.is_simple())
5890                .for_each(|buffkind| cast_buff(buffkind, buffdata, server, target));
5891        },
5892        "clear" => {
5893            if let Some(mut buffs) = server
5894                .state
5895                .ecs()
5896                .write_storage::<comp::Buffs>()
5897                .get_mut(target)
5898            {
5899                buffs.buffs.clear();
5900                buffs.kinds.clear();
5901            }
5902        },
5903        _ => {
5904            let buffkind = parse_buffkind(&buff).ok_or_else(|| {
5905                Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
5906            })?;
5907            let buffdata = build_buff(
5908                buffkind,
5909                strength,
5910                duration.unwrap_or(match buffkind {
5911                    BuffKind::ComboGeneration => 1.0,
5912                    _ => 10.0,
5913                }),
5914                (!buffkind.is_simple())
5915                    .then(|| {
5916                        misc_data_spec.ok_or_else(|| {
5917                            Content::localized_with_args("command-buff-data", [(
5918                                "buff",
5919                                buff.clone(),
5920                            )])
5921                        })
5922                    })
5923                    .transpose()?,
5924            )?;
5925
5926            cast_buff(buffkind, buffdata, server, target);
5927        },
5928    }
5929
5930    Ok(())
5931}
5932
5933fn build_buff(
5934    buff_kind: BuffKind,
5935    strength: f32,
5936    duration: f64,
5937    spec: Option<String>,
5938) -> CmdResult<BuffData> {
5939    if buff_kind.is_simple() {
5940        Ok(BuffData::new(strength, Some(Secs(duration))))
5941    } else {
5942        let spec = spec.expect("spec must be passed to build_buff if buff_kind is not simple");
5943
5944        // Explicit match to remember that this function exists
5945        let misc_data = match buff_kind {
5946            BuffKind::Polymorphed => {
5947                let Ok(npc::NpcBody(_id, mut body)) = spec.parse() else {
5948                    return Err(Content::localized_with_args("command-buff-body-unknown", [
5949                        ("spec", spec.clone()),
5950                    ]));
5951                };
5952                MiscBuffData::Body(body())
5953            },
5954            BuffKind::Regeneration
5955            | BuffKind::Saturation
5956            | BuffKind::Potion
5957            | BuffKind::Agility
5958            | BuffKind::RestingHeal
5959            | BuffKind::Frenzied
5960            | BuffKind::EnergyRegen
5961            | BuffKind::ComboGeneration
5962            | BuffKind::IncreaseMaxEnergy
5963            | BuffKind::IncreaseMaxHealth
5964            | BuffKind::Invulnerability
5965            | BuffKind::ProtectingWard
5966            | BuffKind::Hastened
5967            | BuffKind::Fortitude
5968            | BuffKind::Reckless
5969            | BuffKind::Flame
5970            | BuffKind::Frigid
5971            | BuffKind::Lifesteal
5972            | BuffKind::ImminentCritical
5973            | BuffKind::Fury
5974            | BuffKind::Sunderer
5975            | BuffKind::Defiance
5976            | BuffKind::Bloodfeast
5977            | BuffKind::Berserk
5978            | BuffKind::Bleeding
5979            | BuffKind::Cursed
5980            | BuffKind::Burning
5981            | BuffKind::Crippled
5982            | BuffKind::Frozen
5983            | BuffKind::Wet
5984            | BuffKind::Ensnared
5985            | BuffKind::Poisoned
5986            | BuffKind::Parried
5987            | BuffKind::PotionSickness
5988            | BuffKind::Heatstroke
5989            | BuffKind::ScornfulTaunt
5990            | BuffKind::Rooted
5991            | BuffKind::Winded
5992            | BuffKind::Amnesia
5993            | BuffKind::OffBalance
5994            | BuffKind::Tenacity
5995            | BuffKind::Resilience
5996            | BuffKind::StormChaser
5997            | BuffKind::EagleEye
5998            | BuffKind::Chilled
5999            | BuffKind::ArdentHunt
6000            | BuffKind::IgniteArrow
6001            | BuffKind::FreezeArrow
6002            | BuffKind::DrenchArrow
6003            | BuffKind::JoltArrow => {
6004                if buff_kind.is_simple() {
6005                    unreachable!("is_simple() above")
6006                } else {
6007                    panic!("Buff Kind {buff_kind:?} is complex but has no defined spec parser")
6008                }
6009            },
6010        };
6011
6012        Ok(BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data))
6013    }
6014}
6015
6016fn cast_buff(buffkind: BuffKind, data: BuffData, server: &mut Server, target: EcsEntity) {
6017    let ecs = &server.state.ecs();
6018    let mut buffs_all = ecs.write_storage::<comp::Buffs>();
6019    let stats = ecs.read_storage::<comp::Stats>();
6020    let masses = ecs.read_storage::<comp::Mass>();
6021    let time = ecs.read_resource::<Time>();
6022    if let Some(mut buffs) = buffs_all.get_mut(target) {
6023        let dest_info = DestInfo {
6024            stats: stats.get(target),
6025            mass: masses.get(target),
6026        };
6027        buffs.insert(
6028            Buff::new(
6029                buffkind,
6030                data,
6031                vec![],
6032                BuffSource::Command,
6033                *time,
6034                dest_info,
6035                None,
6036                None,
6037            ),
6038            *time,
6039        );
6040    }
6041}
6042
6043fn parse_buffkind(buff: &str) -> Option<BuffKind> { BUFF_PARSER.get(buff).copied() }
6044
6045fn handle_skill_preset(
6046    server: &mut Server,
6047    _client: EcsEntity,
6048    target: EcsEntity,
6049    args: Vec<String>,
6050    action: &ServerChatCommand,
6051) -> CmdResult<()> {
6052    if let Some(preset) = parse_cmd_args!(args, String) {
6053        if let Some(mut skill_set) = server
6054            .state
6055            .ecs_mut()
6056            .write_storage::<comp::SkillSet>()
6057            .get_mut(target)
6058        {
6059            match preset.as_str() {
6060                "clear" => {
6061                    clear_skillset(&mut skill_set);
6062                    Ok(())
6063                },
6064                preset => set_skills(&mut skill_set, preset),
6065            }
6066        } else {
6067            Err(Content::Plain("Player has no stats!".into()))
6068        }
6069    } else {
6070        Err(action.help_content())
6071    }
6072}
6073
6074fn clear_skillset(skill_set: &mut comp::SkillSet) { *skill_set = comp::SkillSet::default(); }
6075
6076fn set_skills(skill_set: &mut comp::SkillSet, preset: &str) -> CmdResult<()> {
6077    let presets = match common::cmd::SkillPresetManifest::load(PRESET_MANIFEST_PATH) {
6078        Ok(presets) => presets.read().0.clone(),
6079        Err(err) => {
6080            warn!("Error in preset: {}", err);
6081            return Err(Content::localized("command-skillpreset-load-error"));
6082        },
6083    };
6084    if let Some(preset) = presets.get(preset) {
6085        for (skill, level) in preset {
6086            let group = if let Some(group) = skill.skill_group_kind() {
6087                group
6088            } else {
6089                warn!("Skill in preset doesn't exist in any group");
6090                return Err(Content::localized("command-skillpreset-broken"));
6091            };
6092            for _ in 0..*level {
6093                let cost = skill_set.skill_cost(*skill);
6094                skill_set.add_skill_points(group, cost);
6095                match skill_set.unlock_skill(*skill) {
6096                    Ok(_) | Err(comp::skillset::SkillUnlockError::SkillAlreadyUnlocked) => Ok(()),
6097                    Err(err) => Err(Content::Plain(format!("{:?}", err))),
6098                }?;
6099            }
6100        }
6101        Ok(())
6102    } else {
6103        Err(Content::localized_with_args(
6104            "command-skillpreset-missing",
6105            [("preset", preset)],
6106        ))
6107    }
6108}
6109
6110fn handle_location(
6111    server: &mut Server,
6112    client: EcsEntity,
6113    target: EcsEntity,
6114    args: Vec<String>,
6115    _action: &ServerChatCommand,
6116) -> CmdResult<()> {
6117    if let Some(name) = parse_cmd_args!(args, String) {
6118        let loc = server.state.ecs().read_resource::<Locations>().get(&name)?;
6119        server.state.position_mut(target, true, |target_pos| {
6120            target_pos.0 = loc;
6121        })
6122    } else {
6123        let locations = server.state.ecs().read_resource::<Locations>();
6124        let mut locations = locations.iter().map(|s| s.as_str()).collect::<Vec<_>>();
6125        locations.sort_unstable();
6126        server.notify_client(
6127            client,
6128            ServerGeneral::server_msg(
6129                ChatType::CommandInfo,
6130                if locations.is_empty() {
6131                    Content::localized("command-locations-empty")
6132                } else {
6133                    Content::localized_with_args("command-locations-list", [(
6134                        "locations",
6135                        locations.join(", "),
6136                    )])
6137                },
6138            ),
6139        );
6140        Ok(())
6141    }
6142}
6143
6144fn handle_create_location(
6145    server: &mut Server,
6146    client: EcsEntity,
6147    target: EcsEntity,
6148    args: Vec<String>,
6149    action: &ServerChatCommand,
6150) -> CmdResult<()> {
6151    if let Some(name) = parse_cmd_args!(args, String) {
6152        let target_pos = position(server, target, "target")?;
6153
6154        server
6155            .state
6156            .ecs_mut()
6157            .write_resource::<Locations>()
6158            .insert(name.clone(), target_pos.0)?;
6159        server.notify_client(
6160            client,
6161            ServerGeneral::server_msg(
6162                ChatType::CommandInfo,
6163                Content::localized_with_args("command-location-created", [("location", name)]),
6164            ),
6165        );
6166
6167        Ok(())
6168    } else {
6169        Err(action.help_content())
6170    }
6171}
6172
6173fn handle_delete_location(
6174    server: &mut Server,
6175    client: EcsEntity,
6176    _target: EcsEntity,
6177    args: Vec<String>,
6178    action: &ServerChatCommand,
6179) -> CmdResult<()> {
6180    if let Some(name) = parse_cmd_args!(args, String) {
6181        server
6182            .state
6183            .ecs_mut()
6184            .write_resource::<Locations>()
6185            .remove(&name)?;
6186        server.notify_client(
6187            client,
6188            ServerGeneral::server_msg(
6189                ChatType::CommandInfo,
6190                Content::localized_with_args("command-location-deleted", [("location", name)]),
6191            ),
6192        );
6193
6194        Ok(())
6195    } else {
6196        Err(action.help_content())
6197    }
6198}
6199
6200#[cfg(not(feature = "worldgen"))]
6201fn handle_weather_zone(
6202    _server: &mut Server,
6203    _client: EcsEntity,
6204    _target: EcsEntity,
6205    _args: Vec<String>,
6206    _action: &ServerChatCommand,
6207) -> CmdResult<()> {
6208    Err(Content::Plain(
6209        "Unsupported without worldgen enabled".into(),
6210    ))
6211}
6212
6213#[cfg(feature = "worldgen")]
6214fn handle_weather_zone(
6215    server: &mut Server,
6216    client: EcsEntity,
6217    _target: EcsEntity,
6218    args: Vec<String>,
6219    action: &ServerChatCommand,
6220) -> CmdResult<()> {
6221    if let (Some(name), radius, time) = parse_cmd_args!(args, String, f32, f32) {
6222        let radius = radius.map(|r| r / weather::CELL_SIZE as f32).unwrap_or(1.0);
6223        let time = time.unwrap_or(100.0);
6224
6225        let mut add_zone = |weather: weather::Weather| {
6226            if let Ok(pos) = position(server, client, "player") {
6227                let pos = pos.0.xy() / weather::CELL_SIZE as f32;
6228                if let Some(weather_job) = server
6229                    .state
6230                    .ecs_mut()
6231                    .write_resource::<Option<WeatherJob>>()
6232                    .as_mut()
6233                {
6234                    weather_job.queue_zone(weather, pos, radius, time);
6235                }
6236            }
6237        };
6238        match name.as_str() {
6239            "clear" => {
6240                add_zone(weather::Weather {
6241                    cloud: 0.0,
6242                    rain: 0.0,
6243                    wind: Vec2::zero(),
6244                });
6245                Ok(())
6246            },
6247            "cloudy" => {
6248                add_zone(weather::Weather {
6249                    cloud: 0.4,
6250                    rain: 0.0,
6251                    wind: Vec2::zero(),
6252                });
6253                Ok(())
6254            },
6255            "rain" => {
6256                add_zone(weather::Weather {
6257                    cloud: 0.1,
6258                    rain: 0.15,
6259                    wind: Vec2::new(1.0, -1.0),
6260                });
6261                Ok(())
6262            },
6263            "wind" => {
6264                add_zone(weather::Weather {
6265                    cloud: 0.0,
6266                    rain: 0.0,
6267                    wind: Vec2::new(10.0, 10.0),
6268                });
6269                Ok(())
6270            },
6271            "storm" => {
6272                add_zone(weather::Weather {
6273                    cloud: 0.3,
6274                    rain: 0.3,
6275                    wind: Vec2::new(15.0, 20.0),
6276                });
6277                Ok(())
6278            },
6279            _ => Err(Content::localized("command-weather-valid-values")),
6280        }
6281    } else {
6282        Err(action.help_content())
6283    }
6284}
6285
6286fn handle_lightning(
6287    server: &mut Server,
6288    client: EcsEntity,
6289    _target: EcsEntity,
6290    _args: Vec<String>,
6291    _action: &ServerChatCommand,
6292) -> CmdResult<()> {
6293    let pos = position(server, client, "player")?.0;
6294    server
6295        .state
6296        .ecs()
6297        .read_resource::<EventBus<Outcome>>()
6298        .emit_now(Outcome::Lightning { pos });
6299    Ok(())
6300}
6301
6302fn assign_body(server: &mut Server, target: EcsEntity, body: comp::Body) -> CmdResult<()> {
6303    insert_or_replace_component(server, target, body, "body")?;
6304    insert_or_replace_component(server, target, body.mass(), "mass")?;
6305    insert_or_replace_component(server, target, body.density(), "density")?;
6306    insert_or_replace_component(server, target, body.collider(), "collider")?;
6307
6308    if let Some(mut stat) = server
6309        .state
6310        .ecs_mut()
6311        .write_storage::<comp::Stats>()
6312        .get_mut(target)
6313    {
6314        stat.original_body = body;
6315    }
6316
6317    Ok(())
6318}
6319
6320fn handle_body(
6321    server: &mut Server,
6322    _client: EcsEntity,
6323    target: EcsEntity,
6324    args: Vec<String>,
6325    action: &ServerChatCommand,
6326) -> CmdResult<()> {
6327    if let Some(npc::NpcBody(_id, mut body)) = parse_cmd_args!(args, npc::NpcBody) {
6328        let body = body();
6329
6330        assign_body(server, target, body)
6331    } else {
6332        Err(action.help_content())
6333    }
6334}
6335
6336fn handle_scale(
6337    server: &mut Server,
6338    client: EcsEntity,
6339    target: EcsEntity,
6340    args: Vec<String>,
6341    action: &ServerChatCommand,
6342) -> CmdResult<()> {
6343    if let (Some(scale), reset_mass) = parse_cmd_args!(args, f32, bool) {
6344        let scale = scale.clamped(0.025, 1000.0);
6345        insert_or_replace_component(server, target, comp::Scale(scale), "target")?;
6346        if reset_mass.unwrap_or(true) {
6347            let mass = server.state.ecs()
6348                .read_storage::<comp::Body>()
6349                .get(target)
6350                // Mass is derived from volume, which changes with the third power of scale
6351                .map(|body| body.mass().0 * scale.powi(3));
6352            if let Some(mass) = mass {
6353                insert_or_replace_component(server, target, comp::Mass(mass), "target")?;
6354            }
6355        }
6356        server.notify_client(
6357            client,
6358            ServerGeneral::server_msg(
6359                ChatType::CommandInfo,
6360                Content::localized_with_args("command-scale-set", [(
6361                    "scale",
6362                    format!("{scale:.1}"),
6363                )]),
6364            ),
6365        );
6366        Ok(())
6367    } else {
6368        Err(action.help_content())
6369    }
6370}
6371
6372// /repair_equipment <false/true>
6373fn handle_repair_equipment(
6374    server: &mut Server,
6375    client: EcsEntity,
6376    target: EcsEntity,
6377    args: Vec<String>,
6378    action: &ServerChatCommand,
6379) -> CmdResult<()> {
6380    let repair_inventory = parse_cmd_args!(args, bool).unwrap_or(false);
6381    let ecs = server.state.ecs();
6382    if let Some(mut inventory) = ecs.write_storage::<comp::Inventory>().get_mut(target) {
6383        let ability_map = ecs.read_resource::<AbilityMap>();
6384        let msm = ecs.read_resource::<MaterialStatManifest>();
6385        let slots = inventory
6386            .equipped_items_with_slot()
6387            .filter(|(_, item)| item.has_durability())
6388            .map(|(slot, _)| Slot::Equip(slot))
6389            .chain(
6390                repair_inventory
6391                    .then(|| {
6392                        inventory
6393                            .slots_with_id()
6394                            .filter(|(_, item)| {
6395                                item.as_ref().is_some_and(|item| item.has_durability())
6396                            })
6397                            .map(|(slot, _)| Slot::Inventory(slot))
6398                    })
6399                    .into_iter()
6400                    .flatten(),
6401            )
6402            .collect::<Vec<Slot>>();
6403
6404        for slot in slots {
6405            inventory.repair_item_at_slot(slot, &ability_map, &msm);
6406        }
6407
6408        let key = if repair_inventory {
6409            "command-repaired-inventory_items"
6410        } else {
6411            "command-repaired-items"
6412        };
6413        server.notify_client(
6414            client,
6415            ServerGeneral::server_msg(ChatType::CommandInfo, Content::localized(key)),
6416        );
6417        Ok(())
6418    } else {
6419        Err(action.help_content())
6420    }
6421}
6422
6423fn handle_tether(
6424    server: &mut Server,
6425    _client: EcsEntity,
6426    target: EcsEntity,
6427    args: Vec<String>,
6428    action: &ServerChatCommand,
6429) -> CmdResult<()> {
6430    enum Either<A, B> {
6431        Left(A),
6432        Right(B),
6433    }
6434
6435    impl<A: FromStr, B: FromStr> FromStr for Either<A, B> {
6436        type Err = B::Err;
6437
6438        fn from_str(s: &str) -> Result<Self, Self::Err> {
6439            A::from_str(s)
6440                .map(Either::Left)
6441                .or_else(|_| B::from_str(s).map(Either::Right))
6442        }
6443    }
6444    if let (Some(entity_target), length) = parse_cmd_args!(args, EntityTarget, Either<f32, bool>) {
6445        let entity_target = get_entity_target(entity_target, server)?;
6446
6447        let tether_leader = server.state.ecs().uid_from_entity(target);
6448        let tether_follower = server.state.ecs().uid_from_entity(entity_target);
6449
6450        if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
6451            let base_len = server
6452                .state
6453                .read_component_cloned::<comp::Body>(target)
6454                .map(|b| b.dimensions().y * 1.5 + 1.0)
6455                .unwrap_or(6.0);
6456            let tether_length = match length {
6457                Some(Either::Left(l)) => l.max(0.0) + base_len,
6458                Some(Either::Right(true)) => {
6459                    let leader_pos = position(server, target, "leader")?;
6460                    let follower_pos = position(server, entity_target, "follower")?;
6461
6462                    leader_pos.0.distance(follower_pos.0) + base_len
6463                },
6464                _ => base_len,
6465            };
6466            server
6467                .state
6468                .link(Tethered {
6469                    leader,
6470                    follower,
6471                    tether_length,
6472                })
6473                .map_err(|_| Content::Plain("Failed to tether entities".into()))
6474        } else {
6475            Err(Content::Plain("Tether members don't have Uids.".into()))
6476        }
6477    } else {
6478        Err(action.help_content())
6479    }
6480}
6481
6482fn handle_destroy_tethers(
6483    server: &mut Server,
6484    client: EcsEntity,
6485    target: EcsEntity,
6486    _args: Vec<String>,
6487    _action: &ServerChatCommand,
6488) -> CmdResult<()> {
6489    let mut destroyed = false;
6490    destroyed |= server
6491        .state
6492        .ecs()
6493        .write_storage::<Is<common::tether::Leader>>()
6494        .remove(target)
6495        .is_some();
6496    destroyed |= server
6497        .state
6498        .ecs()
6499        .write_storage::<Is<common::tether::Follower>>()
6500        .remove(target)
6501        .is_some();
6502    if destroyed {
6503        server.notify_client(
6504            client,
6505            ServerGeneral::server_msg(
6506                ChatType::CommandInfo,
6507                Content::localized("command-destroyed-tethers"),
6508            ),
6509        );
6510        Ok(())
6511    } else {
6512        Err(Content::localized("command-destroyed-no-tethers"))
6513    }
6514}
6515
6516fn handle_mount(
6517    server: &mut Server,
6518    _client: EcsEntity,
6519    target: EcsEntity,
6520    args: Vec<String>,
6521    action: &ServerChatCommand,
6522) -> CmdResult<()> {
6523    if let Some(entity_target) = parse_cmd_args!(args, EntityTarget) {
6524        let entity_target = get_entity_target(entity_target, server)?;
6525
6526        let rider = server.state.ecs().uid_from_entity(target);
6527        let mount = server.state.ecs().uid_from_entity(entity_target);
6528
6529        if let (Some(rider), Some(mount)) = (rider, mount) {
6530            server
6531                .state
6532                .link(common::mounting::Mounting { mount, rider })
6533                .map_err(|_| Content::Plain("Failed to mount entities".into()))
6534        } else {
6535            Err(Content::Plain(
6536                "Mount and/or rider doesn't have an Uid component.".into(),
6537            ))
6538        }
6539    } else {
6540        Err(action.help_content())
6541    }
6542}
6543
6544fn handle_dismount(
6545    server: &mut Server,
6546    client: EcsEntity,
6547    target: EcsEntity,
6548    _args: Vec<String>,
6549    _action: &ServerChatCommand,
6550) -> CmdResult<()> {
6551    let mut destroyed = false;
6552    destroyed |= server
6553        .state
6554        .ecs()
6555        .write_storage::<Is<common::mounting::Rider>>()
6556        .remove(target)
6557        .is_some();
6558    destroyed |= server
6559        .state
6560        .ecs()
6561        .write_storage::<Is<common::mounting::VolumeRider>>()
6562        .remove(target)
6563        .is_some();
6564    destroyed |= server
6565        .state
6566        .ecs()
6567        .write_storage::<Is<common::mounting::Mount>>()
6568        .remove(target)
6569        .is_some();
6570    destroyed |= server
6571        .state
6572        .ecs()
6573        .write_storage::<common::mounting::VolumeRiders>()
6574        .get_mut(target)
6575        .is_some_and(|volume_riders| volume_riders.clear());
6576
6577    if destroyed {
6578        server.notify_client(
6579            client,
6580            ServerGeneral::server_msg(
6581                ChatType::CommandInfo,
6582                Content::localized("command-dismounted"),
6583            ),
6584        );
6585        Ok(())
6586    } else {
6587        Err(Content::localized("command-no-dismount"))
6588    }
6589}
6590
6591#[cfg(feature = "worldgen")]
6592fn handle_spot(
6593    server: &mut Server,
6594    _client: EcsEntity,
6595    target: EcsEntity,
6596    args: Vec<String>,
6597    action: &ServerChatCommand,
6598) -> CmdResult<()> {
6599    let Some(target_spot) = parse_cmd_args!(args, String) else {
6600        return Err(action.help_content());
6601    };
6602
6603    let maybe_spot_kind = SPOT_PARSER.get(&target_spot);
6604
6605    let target_pos = server
6606        .state
6607        .read_component_copied::<comp::Pos>(target)
6608        .ok_or(Content::localized_with_args(
6609            "command-position-unavailable",
6610            [("target", "target")],
6611        ))?;
6612    let target_chunk = target_pos.0.xy().wpos_to_cpos().as_();
6613
6614    let world = server.state.ecs().read_resource::<Arc<world::World>>();
6615    let spot_chunk = world
6616        .sim()
6617        .get_nearest_spot(target_chunk, |spot| match spot {
6618            Spot::RonFile(spot) => spot.base_structures == target_spot,
6619            spot_kind => Some(spot_kind) == maybe_spot_kind,
6620        });
6621
6622    if let Some(spot_chunk) = spot_chunk {
6623        let pos = spot_chunk.cpos_to_wpos_center();
6624        let pos = (pos.as_() + 0.5).with_z(world.sim().get_surface_alt_approx(pos));
6625        drop(world);
6626        server.state.position_mut_reposition(
6627            target,
6628            true,
6629            |target_pos| {
6630                *target_pos = comp::Pos(pos);
6631            },
6632            true,
6633            false,
6634        )?;
6635        Ok(())
6636    } else {
6637        Err(Content::localized("command-spot-spot_not_found"))
6638    }
6639}
6640
6641#[cfg(not(feature = "worldgen"))]
6642fn handle_spot(
6643    _: &mut Server,
6644    _: EcsEntity,
6645    _: EcsEntity,
6646    _: Vec<String>,
6647    _: &ServerChatCommand,
6648) -> CmdResult<()> {
6649    Err(Content::localized("command-spot-world_feature"))
6650}