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