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