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