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