veloren_server/
cmd.rs

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