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