Skip to main content

veloren_server/
cmd.rs

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