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