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