veloren_server/
cmd.rs

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