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