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