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