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", [(
2318 "id",
2319 uid.0.get(),
2320 )]),
2321 ),
2322 );
2323 }
2324 }
2325 server.notify_client(
2326 client,
2327 ServerGeneral::server_msg(
2328 ChatType::CommandInfo,
2329 Content::Plain(format!("Spawned {} entities", amount)),
2330 ),
2331 );
2332 Ok(())
2333 },
2334 _ => Err(action.help_content()),
2335 }
2336}
2337
2338fn handle_spawn_training_dummy(
2339 server: &mut Server,
2340 client: EcsEntity,
2341 target: EcsEntity,
2342 _args: Vec<String>,
2343 _action: &ServerChatCommand,
2344) -> CmdResult<()> {
2345 let pos = position(server, target, "target")?;
2346 let vel = Vec3::new(
2347 rng().random_range(-2.0..3.0),
2348 rng().random_range(-2.0..3.0),
2349 10.0,
2350 );
2351
2352 let body = comp::Body::Object(comp::object::Body::TrainingDummy);
2353
2354 let stats = comp::Stats::new(
2355 Content::with_attr("name-custom-village-dummy", "neut"),
2356 body,
2357 );
2358 let skill_set = comp::SkillSet::default();
2359 let health = comp::Health::new(body);
2360 let poise = comp::Poise::new(body);
2361
2362 server
2363 .state
2364 .create_npc(
2365 pos,
2366 comp::Ori::default(),
2367 stats,
2368 skill_set,
2369 Some(health),
2370 poise,
2371 Inventory::with_empty(),
2372 body,
2373 comp::Scale(1.0),
2374 )
2375 .with(comp::Vel(vel))
2376 .build();
2377
2378 server.notify_client(
2379 client,
2380 ServerGeneral::server_msg(
2381 ChatType::CommandInfo,
2382 Content::localized("command-spawned-dummy"),
2383 ),
2384 );
2385 Ok(())
2386}
2387
2388fn handle_spawn_airship(
2389 server: &mut Server,
2390 client: EcsEntity,
2391 target: EcsEntity,
2392 args: Vec<String>,
2393 _action: &ServerChatCommand,
2394) -> CmdResult<()> {
2395 let (body_name, angle) = parse_cmd_args!(args, String, f32);
2396 let mut pos = position(server, target, "target")?;
2397 pos.0.z += 50.0;
2398 const DESTINATION_RADIUS: f32 = 2000.0;
2399 let angle = angle.map(|a| a * std::f32::consts::PI / 180.0);
2400 let dir = angle.map(|a| Vec3::new(a.cos(), a.sin(), 0.0));
2401 let destination = dir.map(|dir| pos.0 + dir * DESTINATION_RADIUS + Vec3::new(0.0, 0.0, 200.0));
2402 let ship = if let Some(body_name) = body_name {
2403 *comp::ship::ALL_AIRSHIPS
2404 .iter()
2405 .find(|body| format!("{body:?}") == body_name)
2406 .ok_or_else(|| Content::Plain(format!("No such airship '{body_name}'.")))?
2407 } else {
2408 comp::ship::Body::random_airship_with(&mut rng())
2409 };
2410 let ori = comp::Ori::from(common::util::Dir::new(dir.unwrap_or(Vec3::unit_y())));
2411 let mut builder = server
2412 .state
2413 .create_ship(pos, ori, ship, |ship| ship.make_collider());
2414 if let Some(pos) = destination {
2415 let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
2416 .with_destination(pos)
2417 .with_altitude_pid_controller(PidControllers::<16>::new_multi_pid_controllers(
2418 FlightMode::FlyThrough,
2419 pos,
2420 ));
2421 builder = builder.with(agent);
2422 }
2423 builder.build();
2424
2425 server.notify_client(
2426 client,
2427 ServerGeneral::server_msg(
2428 ChatType::CommandInfo,
2429 Content::localized("command-spawned-airship"),
2430 ),
2431 );
2432 Ok(())
2433}
2434
2435fn handle_spawn_ship(
2436 server: &mut Server,
2437 client: EcsEntity,
2438 target: EcsEntity,
2439 args: Vec<String>,
2440 _action: &ServerChatCommand,
2441) -> CmdResult<()> {
2442 let (body_name, tethered, angle) = parse_cmd_args!(args, String, bool, f32);
2443 let mut pos = position(server, target, "target")?;
2444 pos.0.z += 5.0;
2445 const DESTINATION_RADIUS: f32 = 2000.0;
2446 let angle = angle.map(|a| a * std::f32::consts::PI / 180.0);
2447 let dir = angle.map(|a| Vec3::new(a.cos(), a.sin(), 0.0));
2448 let destination = dir.map(|dir| pos.0 + dir * DESTINATION_RADIUS + Vec3::new(0.0, 0.0, 200.0));
2449 let ship = if let Some(body_name) = body_name {
2450 *comp::ship::ALL_SHIPS
2451 .iter()
2452 .find(|body| format!("{body:?}") == body_name)
2453 .ok_or_else(|| Content::Plain(format!("No such airship '{body_name}'.")))?
2454 } else {
2455 comp::ship::Body::random_airship_with(&mut rng())
2456 };
2457 let ori = comp::Ori::from(common::util::Dir::new(dir.unwrap_or(Vec3::unit_y())));
2458 let mut builder = server
2459 .state
2460 .create_ship(pos, ori, ship, |ship| ship.make_collider());
2461
2462 if let Some(pos) = destination {
2463 let agent = comp::Agent::from_body(&comp::Body::Ship(ship))
2464 .with_destination(pos)
2465 .with_altitude_pid_controller(PidControllers::<16>::new_multi_pid_controllers(
2466 FlightMode::FlyThrough,
2467 pos,
2468 ));
2469 builder = builder.with(agent);
2470 }
2471
2472 let new_entity = builder.build();
2473
2474 if tethered == Some(true) {
2475 let tether_leader = server
2476 .state
2477 .read_component_cloned::<Is<Rider>>(target)
2478 .map(|is_rider| is_rider.mount)
2479 .or_else(|| {
2480 server
2481 .state
2482 .read_component_cloned::<Is<VolumeRider>>(target)
2483 .and_then(|is_volume_rider| {
2484 if let Volume::Entity(uid) = is_volume_rider.pos.kind {
2485 Some(uid)
2486 } else {
2487 None
2488 }
2489 })
2490 })
2491 .or_else(|| server.state.ecs().uid_from_entity(target));
2492 let tether_follower = server.state.ecs().uid_from_entity(new_entity);
2493
2494 if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
2495 let tether_length = tether_leader
2496 .and_then(|uid| server.state.ecs().entity_from_uid(uid))
2497 .and_then(|e| server.state.read_component_cloned::<comp::Body>(e))
2498 .map(|b| b.dimensions().y * 1.5 + 1.0)
2499 .unwrap_or(6.0);
2500 server
2501 .state
2502 .link(Tethered {
2503 leader,
2504 follower,
2505 tether_length,
2506 })
2507 .map_err(|_| Content::Plain("Failed to tether entities".to_string()))?;
2508 } else {
2509 return Err(Content::Plain("Tether members don't have Uids.".into()));
2510 }
2511 }
2512
2513 server.notify_client(
2514 client,
2515 ServerGeneral::server_msg(
2516 ChatType::CommandInfo,
2517 Content::Plain("Spawned a ship".to_string()),
2518 ),
2519 );
2520 Ok(())
2521}
2522
2523fn handle_make_volume(
2524 server: &mut Server,
2525 client: EcsEntity,
2526 target: EcsEntity,
2527 args: Vec<String>,
2528 _action: &ServerChatCommand,
2529) -> CmdResult<()> {
2530 use comp::body::ship::figuredata::VoxelCollider;
2531
2532 let pos = position(server, target, "target")?;
2534 let ship = comp::ship::Body::Volume;
2535 let sz = parse_cmd_args!(args, u32).unwrap_or(15);
2536 if !(1..=127).contains(&sz) {
2537 return Err(Content::localized("command-volume-size-incorrect"));
2538 };
2539 let sz = Vec3::broadcast(sz);
2540 let collider = {
2541 let terrain = server.state().terrain();
2542 comp::Collider::Volume(Arc::new(VoxelCollider::from_fn(sz, |rpos| {
2543 terrain
2544 .get(pos.0.map(|e| e.floor() as i32) + rpos - sz.map(|e| e as i32) / 2)
2545 .ok()
2546 .copied()
2547 .unwrap_or_else(Block::empty)
2548 })))
2549 };
2550 server
2551 .state
2552 .create_ship(
2553 comp::Pos(pos.0 + Vec3::unit_z() * (50.0 + sz.z as f32 / 2.0)),
2554 comp::Ori::default(),
2555 ship,
2556 move |_| collider,
2557 )
2558 .build();
2559
2560 server.notify_client(
2561 client,
2562 ServerGeneral::server_msg(
2563 ChatType::CommandInfo,
2564 Content::localized("command-volume-created"),
2565 ),
2566 );
2567 Ok(())
2568}
2569
2570fn handle_spawn_campfire(
2571 server: &mut Server,
2572 client: EcsEntity,
2573 target: EcsEntity,
2574 _args: Vec<String>,
2575 _action: &ServerChatCommand,
2576) -> CmdResult<()> {
2577 let pos = position(server, target, "target")?;
2578 server
2579 .state
2580 .ecs()
2581 .read_resource::<EventBus<CreateSpecialEntityEvent>>()
2582 .emit_now(CreateSpecialEntityEvent {
2583 pos: pos.0,
2584 entity: SpecialEntity::Waypoint,
2585 });
2586
2587 server.notify_client(
2588 client,
2589 ServerGeneral::server_msg(
2590 ChatType::CommandInfo,
2591 Content::localized("command-spawned-campfire"),
2592 ),
2593 );
2594 Ok(())
2595}
2596
2597#[cfg(feature = "persistent_world")]
2598fn handle_clear_persisted_terrain(
2599 server: &mut Server,
2600 _client: EcsEntity,
2601 target: EcsEntity,
2602 args: Vec<String>,
2603 action: &ServerChatCommand,
2604) -> CmdResult<()> {
2605 let Some(radius) = parse_cmd_args!(args, i32) else {
2606 return Err(action.help_content());
2607 };
2608 let radius = radius.clamp(0, 64);
2610
2611 let pos = position(server, target, "target")?;
2612 let chunk_key = server.state.terrain().pos_key(pos.0.as_());
2613
2614 let mut terrain_persistence2 = server
2615 .state
2616 .ecs()
2617 .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>();
2618 if let Some(ref mut terrain_persistence) = terrain_persistence2 {
2619 for offset in Spiral2d::with_radius(radius) {
2620 let chunk_key = chunk_key + offset;
2621 terrain_persistence.clear_chunk(chunk_key);
2622 }
2623
2624 drop(terrain_persistence2);
2625 reload_chunks_inner(server, pos.0, Some(radius));
2626
2627 Ok(())
2628 } else {
2629 Err(Content::localized(
2630 "command-experimental-terrain-persistence-disabled",
2631 ))
2632 }
2633}
2634
2635#[cfg(not(feature = "persistent_world"))]
2636fn handle_clear_persisted_terrain(
2637 _server: &mut Server,
2638 _client: EcsEntity,
2639 _target: EcsEntity,
2640 _args: Vec<String>,
2641 _action: &ServerChatCommand,
2642) -> CmdResult<()> {
2643 Err(Content::localized(
2644 "command-server-no-experimental-terrain-persistence",
2645 ))
2646}
2647
2648fn handle_safezone(
2649 server: &mut Server,
2650 client: EcsEntity,
2651 target: EcsEntity,
2652 args: Vec<String>,
2653 _action: &ServerChatCommand,
2654) -> CmdResult<()> {
2655 let range = parse_cmd_args!(args, f32);
2656 let pos = position(server, target, "target")?;
2657 server.state.create_safezone(range, pos).build();
2658
2659 server.notify_client(
2660 client,
2661 ServerGeneral::server_msg(
2662 ChatType::CommandInfo,
2663 Content::localized("command-spawned-safezone"),
2664 ),
2665 );
2666 Ok(())
2667}
2668
2669fn handle_permit_build(
2670 server: &mut Server,
2671 client: EcsEntity,
2672 target: EcsEntity,
2673 args: Vec<String>,
2674 action: &ServerChatCommand,
2675) -> CmdResult<()> {
2676 if let Some(area_name) = parse_cmd_args!(args, String) {
2677 let bb_id = area(server, &area_name, "build")?;
2678 let mut can_build = server.state.ecs().write_storage::<comp::CanBuild>();
2679 let entry = can_build
2680 .entry(target)
2681 .map_err(|_| Content::Plain("Cannot find target entity!".to_string()))?;
2682 let mut comp_can_build = entry.or_insert(comp::CanBuild {
2683 enabled: false,
2684 build_areas: HashSet::new(),
2685 });
2686 comp_can_build.build_areas.insert(bb_id);
2687 drop(can_build);
2688 if client != target {
2689 server.notify_client(
2690 target,
2691 ServerGeneral::server_msg(
2692 ChatType::CommandInfo,
2693 Content::localized_with_args("command-permit-build-given", [(
2694 "area",
2695 area_name.clone(),
2696 )]),
2697 ),
2698 );
2699 }
2700 server.notify_client(
2701 client,
2702 ServerGeneral::server_msg(
2703 ChatType::CommandInfo,
2704 Content::localized_with_args("command-permit-build-granted", [("area", area_name)]),
2705 ),
2706 );
2707 Ok(())
2708 } else {
2709 Err(action.help_content())
2710 }
2711}
2712
2713fn handle_revoke_build(
2714 server: &mut Server,
2715 client: EcsEntity,
2716 target: EcsEntity,
2717 args: Vec<String>,
2718 action: &ServerChatCommand,
2719) -> CmdResult<()> {
2720 if let Some(area_name) = parse_cmd_args!(args, String) {
2721 let bb_id = area(server, &area_name, "build")?;
2722 let mut can_build = server.state.ecs_mut().write_storage::<comp::CanBuild>();
2723 if let Some(mut comp_can_build) = can_build.get_mut(target) {
2724 comp_can_build.build_areas.retain(|&x| x != bb_id);
2725 drop(can_build);
2726 if client != target {
2727 server.notify_client(
2728 target,
2729 ServerGeneral::server_msg(
2730 ChatType::CommandInfo,
2731 Content::localized_with_args("command-revoke-build-recv", [(
2732 "area",
2733 area_name.clone(),
2734 )]),
2735 ),
2736 );
2737 }
2738 server.notify_client(
2739 client,
2740 ServerGeneral::server_msg(
2741 ChatType::CommandInfo,
2742 Content::localized_with_args("command-revoke-build", [("area", area_name)]),
2743 ),
2744 );
2745 Ok(())
2746 } else {
2747 Err(Content::localized("command-no-buid-perms"))
2748 }
2749 } else {
2750 Err(action.help_content())
2751 }
2752}
2753
2754fn handle_revoke_build_all(
2755 server: &mut Server,
2756 client: EcsEntity,
2757 target: EcsEntity,
2758 _args: Vec<String>,
2759 _action: &ServerChatCommand,
2760) -> CmdResult<()> {
2761 let ecs = server.state.ecs();
2762
2763 ecs.write_storage::<comp::CanBuild>().remove(target);
2764 if client != target {
2765 server.notify_client(
2766 target,
2767 ServerGeneral::server_msg(
2768 ChatType::CommandInfo,
2769 Content::localized("command-revoke-build-all"),
2770 ),
2771 );
2772 }
2773 server.notify_client(
2774 client,
2775 ServerGeneral::server_msg(
2776 ChatType::CommandInfo,
2777 Content::localized("command-revoked-all-build"),
2778 ),
2779 );
2780 Ok(())
2781}
2782
2783fn handle_players(
2784 server: &mut Server,
2785 client: EcsEntity,
2786 _target: EcsEntity,
2787 _args: Vec<String>,
2788 _action: &ServerChatCommand,
2789) -> CmdResult<()> {
2790 let ecs = server.state.ecs();
2791
2792 let entity_tuples = (
2793 &ecs.entities(),
2794 &ecs.read_storage::<comp::Player>(),
2795 &ecs.read_storage::<comp::Stats>(),
2796 );
2797
2798 let mut player_list = String::new();
2800 for (_, player, stat) in entity_tuples.join() {
2801 player_list.push_str(&format!(
2802 "[{}]{}\n",
2803 player.alias,
2804 stat.name.as_plain().unwrap_or("<?>")
2805 ));
2806 }
2807
2808 server.notify_client(
2810 client,
2811 ServerGeneral::server_msg(
2812 ChatType::CommandInfo,
2813 Content::localized_with_args("players-list-header", [
2814 (
2815 "count",
2816 LocalizationArg::from(entity_tuples.join().count() as u64),
2817 ),
2818 ("player_list", LocalizationArg::from(player_list)),
2819 ]),
2820 ),
2821 );
2822
2823 Ok(())
2824}
2825
2826fn handle_spawn_portal(
2827 server: &mut Server,
2828 client: EcsEntity,
2829 target: EcsEntity,
2830 args: Vec<String>,
2831 action: &ServerChatCommand,
2832) -> CmdResult<()> {
2833 let pos = position(server, target, "target")?;
2834
2835 if let (Some(x), Some(y), Some(z), requires_no_aggro, buildup_time) =
2836 parse_cmd_args!(args, f32, f32, f32, bool, f64)
2837 {
2838 let requires_no_aggro = requires_no_aggro.unwrap_or(false);
2839 let buildup_time = Secs(buildup_time.unwrap_or(7.));
2840 server
2841 .state
2842 .create_teleporter(pos, PortalData {
2843 target: Vec3::new(x, y, z),
2844 buildup_time,
2845 requires_no_aggro,
2846 })
2847 .build();
2848
2849 server.notify_client(
2850 client,
2851 ServerGeneral::server_msg(
2852 ChatType::CommandInfo,
2853 Content::Plain("Spawned portal".to_string()),
2854 ),
2855 );
2856 Ok(())
2857 } else {
2858 Err(action.help_content())
2859 }
2860}
2861
2862fn handle_build(
2863 server: &mut Server,
2864 client: EcsEntity,
2865 target: EcsEntity,
2866 _args: Vec<String>,
2867 _action: &ServerChatCommand,
2868) -> CmdResult<()> {
2869 if let Some(mut can_build) = server
2870 .state
2871 .ecs()
2872 .write_storage::<comp::CanBuild>()
2873 .get_mut(target)
2874 {
2875 can_build.enabled ^= true;
2876
2877 let msg = Content::localized(
2878 match (
2879 can_build.enabled,
2880 server.settings().experimental_terrain_persistence,
2881 ) {
2882 (false, _) => "command-set-build-mode-off",
2883 (true, false) => "command-set-build-mode-on-unpersistent",
2884 (true, true) => "command-set-build-mode-on-persistent",
2885 },
2886 );
2887
2888 let chat_msg = ServerGeneral::server_msg(ChatType::CommandInfo, msg);
2889 if client != target {
2890 server.notify_client(target, chat_msg.clone());
2891 }
2892 server.notify_client(client, chat_msg);
2893 Ok(())
2894 } else {
2895 Err(Content::Plain(
2896 "You do not have permission to build.".into(),
2897 ))
2898 }
2899}
2900
2901fn get_areas_mut<'l>(kind: &str, state: &'l mut State) -> CmdResult<&'l mut Areas> {
2902 Ok(match AreaKind::from_str(kind).ok() {
2903 Some(AreaKind::Build) => state
2904 .mut_resource::<AreasContainer<BuildArea>>()
2905 .deref_mut(),
2906 Some(AreaKind::NoDurability) => state
2907 .mut_resource::<AreasContainer<NoDurabilityArea>>()
2908 .deref_mut(),
2909 None => Err(Content::Plain(format!("Invalid area type '{kind}'")))?,
2910 })
2911}
2912
2913fn handle_area_add(
2914 server: &mut Server,
2915 client: EcsEntity,
2916 _target: EcsEntity,
2917 args: Vec<String>,
2918 action: &ServerChatCommand,
2919) -> CmdResult<()> {
2920 if let (
2921 Some(area_name),
2922 Some(kind),
2923 Some(xlo),
2924 Some(xhi),
2925 Some(ylo),
2926 Some(yhi),
2927 Some(zlo),
2928 Some(zhi),
2929 ) = parse_cmd_args!(args, String, String, i32, i32, i32, i32, i32, i32)
2930 {
2931 let special_areas = get_areas_mut(&kind, &mut server.state)?;
2932 let msg = ServerGeneral::server_msg(
2933 ChatType::CommandInfo,
2934 Content::Plain(format!("Created {kind} zone {}", area_name)),
2935 );
2936 special_areas
2937 .insert(area_name, Aabb {
2938 min: Vec3::new(xlo, ylo, zlo),
2939 max: Vec3::new(xhi, yhi, zhi),
2940 })
2941 .map_err(|area_name| {
2942 Content::Plain(format!("{kind} zone {} already exists!", area_name))
2943 })?;
2944 server.notify_client(client, msg);
2945 Ok(())
2946 } else {
2947 Err(action.help_content())
2948 }
2949}
2950
2951fn handle_area_list(
2952 server: &mut Server,
2953 client: EcsEntity,
2954 _target: EcsEntity,
2955 _args: Vec<String>,
2956 _action: &ServerChatCommand,
2957) -> CmdResult<()> {
2958 let format_areas = |areas: &Areas, kind: &str| {
2959 areas
2960 .area_metas()
2961 .iter()
2962 .fold(format!("{kind} areas:"), |acc, (area_name, bb_id)| {
2963 if let Some(aabb) = areas.areas().get(*bb_id) {
2964 format!("{}\n{}: {} to {} ()", acc, area_name, aabb.min, aabb.max,)
2965 } else {
2966 acc
2967 }
2968 })
2969 };
2970 let build_message = format_areas(
2971 server.state.mut_resource::<AreasContainer<BuildArea>>(),
2972 "Build",
2973 );
2974 let no_dura_message = format_areas(
2975 server
2976 .state
2977 .mut_resource::<AreasContainer<NoDurabilityArea>>(),
2978 "Durability free",
2979 );
2980
2981 let msg = ServerGeneral::server_msg(
2982 ChatType::CommandInfo,
2983 Content::Plain([build_message, no_dura_message].join("\n")),
2984 );
2985
2986 server.notify_client(client, msg);
2987 Ok(())
2988}
2989
2990fn handle_area_remove(
2991 server: &mut Server,
2992 client: EcsEntity,
2993 _target: EcsEntity,
2994 args: Vec<String>,
2995 action: &ServerChatCommand,
2996) -> CmdResult<()> {
2997 if let (Some(area_name), Some(kind)) = parse_cmd_args!(args, String, String) {
2998 let areas = get_areas_mut(&kind, &mut server.state)?;
2999
3000 areas.remove(&area_name).map_err(|err| match err {
3001 SpecialAreaError::Reserved => Content::Plain(format!(
3002 "Special area is reserved and cannot be removed: {}",
3003 area_name
3004 )),
3005 SpecialAreaError::NotFound => {
3006 Content::Plain(format!("No such build area {}", area_name))
3007 },
3008 })?;
3009 server.notify_client(
3010 client,
3011 ServerGeneral::server_msg(
3012 ChatType::CommandInfo,
3013 Content::Plain(format!("Removed {kind} zone {area_name}")),
3014 ),
3015 );
3016 Ok(())
3017 } else {
3018 Err(action.help_content())
3019 }
3020}
3021
3022fn parse_alignment(owner: Uid, alignment: &str) -> CmdResult<Alignment> {
3023 match alignment {
3024 "wild" => Ok(Alignment::Wild),
3025 "enemy" => Ok(Alignment::Enemy),
3026 "npc" => Ok(Alignment::Npc),
3027 "pet" => Ok(comp::Alignment::Owned(owner)),
3028 _ => Err(Content::localized_with_args("command-invalid-alignment", [
3029 ("alignment", alignment),
3030 ])),
3031 }
3032}
3033
3034fn handle_kill_npcs(
3035 server: &mut Server,
3036 client: EcsEntity,
3037 target: EcsEntity,
3038 args: Vec<String>,
3039 _action: &ServerChatCommand,
3040) -> CmdResult<()> {
3041 let (radius, options) = parse_cmd_args!(args, f32, String);
3042 let kill_pets = if let Some(kill_option) = options {
3043 kill_option.contains("--also-pets")
3044 } else {
3045 false
3046 };
3047
3048 let position = radius
3049 .map(|_| position(server, target, "target"))
3050 .transpose()?;
3051
3052 let to_kill = {
3053 let ecs = server.state.ecs();
3054 let entities = ecs.entities();
3055 let positions = ecs.write_storage::<comp::Pos>();
3056 let healths = ecs.write_storage::<comp::Health>();
3057 let players = ecs.read_storage::<comp::Player>();
3058 let alignments = ecs.read_storage::<Alignment>();
3059 let rtsim_entities = ecs.read_storage::<common::rtsim::RtSimEntity>();
3060 let mut rtsim = ecs.write_resource::<crate::rtsim::RtSim>();
3061 let spatial_grid;
3062
3063 let mut iter_a;
3064 let mut iter_b;
3065
3066 let iter: &mut dyn Iterator<
3067 Item = (
3068 EcsEntity,
3069 &comp::Health,
3070 (),
3071 Option<&comp::Alignment>,
3072 &comp::Pos,
3073 ),
3074 > = if let (Some(radius), Some(position)) = (radius, position) {
3075 spatial_grid = ecs.read_resource::<CachedSpatialGrid>();
3076 iter_a = spatial_grid
3077 .0
3078 .in_circle_aabr(position.0.xy(), radius)
3079 .filter_map(|entity| {
3080 (
3081 &entities,
3082 &healths,
3083 !&players,
3084 alignments.maybe(),
3085 &positions,
3086 )
3087 .lend_join()
3088 .get(entity, &entities)
3089 })
3090 .filter(move |(_, _, _, _, pos)| {
3091 pos.0.distance_squared(position.0) <= radius.powi(2)
3092 });
3093
3094 &mut iter_a as _
3095 } else {
3096 iter_b = (
3097 &entities,
3098 &healths,
3099 !&players,
3100 alignments.maybe(),
3101 &positions,
3102 )
3103 .join();
3104
3105 &mut iter_b as _
3106 };
3107
3108 iter.filter_map(|(entity, _health, (), alignment, pos)| {
3109 let should_kill = kill_pets
3110 || if let Some(Alignment::Owned(owned)) = alignment {
3111 ecs.entity_from_uid(*owned)
3112 .is_none_or(|owner| !players.contains(owner))
3113 } else {
3114 true
3115 };
3116
3117 if should_kill {
3118 if let Some(rtsim_entity) = rtsim_entities.get(entity).copied() {
3119 rtsim.hook_rtsim_actor_death(
3120 &ecs.read_resource::<Arc<world::World>>(),
3121 ecs.read_resource::<world::IndexOwned>().as_index_ref(),
3122 Actor::Npc(rtsim_entity),
3123 Some(pos.0),
3124 None,
3125 );
3126 }
3127 Some(entity)
3128 } else {
3129 None
3130 }
3131 })
3132 .collect::<Vec<_>>()
3133 };
3134 let count = to_kill.len();
3135 for entity in to_kill {
3136 if let Err(e) = server.state.delete_entity_recorded(entity) {
3138 error!(?e, ?entity, "Failed to delete entity");
3139 }
3140 }
3141 let text = if count > 0 {
3142 format!("Destroyed {} NPCs.", count)
3143 } else {
3144 "No NPCs on server.".to_string()
3145 };
3146
3147 server.notify_client(
3148 client,
3149 ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(text)),
3150 );
3151
3152 Ok(())
3153}
3154
3155enum KitEntry {
3156 Spec(KitSpec),
3157 Item(Item),
3158}
3159
3160impl From<KitSpec> for KitEntry {
3161 fn from(spec: KitSpec) -> Self { Self::Spec(spec) }
3162}
3163
3164fn handle_kit(
3165 server: &mut Server,
3166 client: EcsEntity,
3167 target: EcsEntity,
3168 args: Vec<String>,
3169 action: &ServerChatCommand,
3170) -> CmdResult<()> {
3171 use common::cmd::KitManifest;
3172
3173 let notify = |server: &mut Server, kit_name: &str| {
3174 server.notify_client(
3175 client,
3176 ServerGeneral::server_msg(
3177 ChatType::CommandInfo,
3178 Content::Plain(format!("Gave kit: {}", kit_name)),
3179 ),
3180 );
3181 };
3182 let name = parse_cmd_args!(args, String).ok_or_else(|| action.help_content())?;
3183
3184 match name.as_str() {
3185 "all" => {
3186 let items = all_items_expect();
3188 let total = items.len();
3189
3190 let res = push_kit(
3191 items.into_iter().map(|item| (KitEntry::Item(item), 1)),
3192 total,
3193 server,
3194 target,
3195 );
3196 if res.is_ok() {
3197 notify(server, "all");
3198 }
3199 res
3200 },
3201 kit_name => {
3202 let kits = KitManifest::load(KIT_MANIFEST_PATH)
3203 .map(|kits| kits.read())
3204 .map_err(|_| {
3205 Content::Plain(format!(
3206 "Could not load manifest file {}",
3207 KIT_MANIFEST_PATH
3208 ))
3209 })?;
3210
3211 let kit = kits
3212 .0
3213 .get(kit_name)
3214 .ok_or(Content::Plain(format!("Kit '{}' not found", kit_name)))?;
3215
3216 let res = push_kit(
3217 kit.iter()
3218 .map(|(item_id, quantity)| (item_id.clone().into(), *quantity)),
3219 kit.len(),
3220 server,
3221 target,
3222 );
3223 if res.is_ok() {
3224 notify(server, kit_name);
3225 }
3226 res
3227 },
3228 }
3229}
3230
3231fn push_kit<I>(kit: I, count: usize, server: &mut Server, target: EcsEntity) -> CmdResult<()>
3232where
3233 I: Iterator<Item = (KitEntry, u32)>,
3234{
3235 if let (Some(mut target_inventory), mut target_inv_update) = (
3236 server
3237 .state()
3238 .ecs()
3239 .write_storage::<Inventory>()
3240 .get_mut(target),
3241 server.state.ecs().write_storage::<comp::InventoryUpdate>(),
3242 ) {
3243 if target_inventory.free_slots() < count {
3245 return Err(Content::localized("command-kit-not-enough-slots"));
3246 }
3247
3248 for (item_id, quantity) in kit {
3249 push_item(item_id, quantity, server, &mut |item| {
3250 let res = target_inventory.push(item);
3251 let _ = target_inv_update.insert(
3252 target,
3253 comp::InventoryUpdate::new(comp::InventoryUpdateEvent::Debug),
3254 );
3255
3256 res
3257 })?;
3258 }
3259
3260 Ok(())
3261 } else {
3262 Err(Content::localized("command-kit-inventory-unavailable"))
3263 }
3264}
3265
3266fn push_item(
3267 item_id: KitEntry,
3268 quantity: u32,
3269 server: &Server,
3270 push: &mut dyn FnMut(Item) -> Result<(), (Item, Option<NonZeroU32>)>,
3271) -> CmdResult<()> {
3272 let items = match item_id {
3273 KitEntry::Spec(KitSpec::Item(item_id)) => vec![
3274 Item::new_from_asset(&item_id)
3275 .map_err(|_| Content::Plain(format!("Unknown item: {:#?}", item_id)))?,
3276 ],
3277 KitEntry::Spec(KitSpec::ModularWeaponSet {
3278 tool,
3279 material,
3280 hands,
3281 }) => comp::item::modular::generate_weapons(tool, material, hands)
3282 .map_err(|err| Content::Plain(format!("{:#?}", err)))?,
3283 KitEntry::Spec(KitSpec::ModularWeaponRandom {
3284 tool,
3285 material,
3286 hands,
3287 }) => {
3288 let mut rng = rand::rng();
3289 vec![
3290 comp::item::modular::random_weapon(tool, material, hands, &mut rng)
3291 .map_err(|err| Content::Plain(format!("{:#?}", err)))?,
3292 ]
3293 },
3294 KitEntry::Item(item) => vec![item],
3295 };
3296
3297 let mut res = Ok(());
3298 for mut item in items {
3299 if item.is_stackable() {
3301 let _ = item.set_amount(quantity);
3304 res = push(item);
3305 } else {
3306 let ability_map = server.state.ecs().read_resource::<AbilityMap>();
3307 let msm = server.state.ecs().read_resource::<MaterialStatManifest>();
3308
3309 for _ in 0..quantity {
3310 res = push(item.duplicate(&ability_map, &msm));
3311
3312 if res.is_err() {
3313 break;
3314 }
3315 }
3316 }
3317
3318 if res.is_err() {
3321 return Err(Content::localized("command-inventory-cant-fit-item"));
3322 }
3323 }
3324
3325 Ok(())
3326}
3327
3328fn handle_object(
3329 server: &mut Server,
3330 client: EcsEntity,
3331 target: EcsEntity,
3332 args: Vec<String>,
3333 _action: &ServerChatCommand,
3334) -> CmdResult<()> {
3335 let obj_type = parse_cmd_args!(args, String);
3336
3337 let pos = position(server, target, "target")?;
3338 let ori = server
3339 .state
3340 .ecs()
3341 .read_storage::<comp::Ori>()
3342 .get(target)
3343 .copied()
3344 .ok_or_else(|| Content::Plain("Cannot get orientation for target".to_string()))?;
3345 let obj_str_res = obj_type.as_deref();
3349 if let Some(obj_type) = comp::object::ALL_OBJECTS
3350 .iter()
3351 .find(|o| Some(o.to_string()) == obj_str_res)
3352 {
3353 server
3354 .state
3355 .create_object(pos, *obj_type)
3356 .with(
3357 comp::Ori::from_unnormalized_vec(
3358 {
3361 let look_dir = ori.look_dir();
3362 look_dir.map(|e| {
3363 if e.abs() == look_dir.map(|e| e.abs()).reduce_partial_max() {
3364 e
3365 } else {
3366 0.0
3367 }
3368 })
3369 },
3370 )
3371 .unwrap_or_default(),
3372 )
3373 .build();
3374 server.notify_client(
3375 client,
3376 ServerGeneral::server_msg(
3377 ChatType::CommandInfo,
3378 Content::Plain(format!(
3379 "Spawned: {}",
3380 obj_str_res.unwrap_or("<Unknown object>")
3381 )),
3382 ),
3383 );
3384 Ok(())
3385 } else {
3386 Err(Content::Plain("Object not found!".into()))
3387 }
3388}
3389
3390fn handle_outcome(
3391 server: &mut Server,
3392 _client: EcsEntity,
3393 target: EcsEntity,
3394 args: Vec<String>,
3395 _action: &ServerChatCommand,
3396) -> CmdResult<()> {
3397 let mut i = 0;
3398
3399 macro_rules! arg {
3400 () => {
3401 args.get(i).map(|r| {
3402 i += 1;
3403 r
3404 })
3405 };
3406 ($err:expr) => {
3407 arg!().ok_or_else(|| Content::Key(($err).to_string()))
3408 };
3409 }
3410
3411 let target_pos = server
3412 .state
3413 .read_component_copied::<comp::Pos>(target)
3414 .unwrap_or(comp::Pos(Vec3::zero()));
3415 let target_uid = server
3416 .state
3417 .read_component_copied::<Uid>(target)
3418 .expect("All entities should have uids");
3419
3420 macro_rules! vec_arg {
3421 () => {{
3422 let old_i = i;
3423 let pos = arg!().and_then(|arg| {
3424 let x = arg.parse().ok()?;
3425 let y = arg!()?.parse().ok()?;
3426 let z = arg!()?.parse().ok()?;
3427
3428 Some(Vec3::new(x, y, z))
3429 });
3430
3431 #[allow(unused_assignments)]
3432 if let Some(pos) = pos {
3433 pos
3434 } else {
3435 i = old_i;
3436 Vec3::default()
3437 }
3438 }};
3439 }
3440
3441 macro_rules! pos_arg {
3442 () => {{
3443 let old_i = i;
3444 let pos = arg!().and_then(|arg| {
3445 let x = arg.parse().ok()?;
3446 let y = arg!()?.parse().ok()?;
3447 let z = arg!()?.parse().ok()?;
3448
3449 Some(Vec3::new(x, y, z))
3450 });
3451
3452 #[allow(unused_assignments)]
3453 if let Some(pos) = pos {
3454 pos
3455 } else {
3456 i = old_i;
3457 target_pos.0.as_()
3458 }
3459 }};
3460 }
3461
3462 macro_rules! parse {
3463 ($err:expr, $expr:expr) => {
3464 arg!()
3465 .and_then($expr)
3466 .ok_or_else(|| Content::Key(($err).to_string()))
3467 };
3468 ($err:expr) => {
3469 parse!($err, |arg| arg.parse().ok())
3470 };
3471 }
3472
3473 macro_rules! body_arg {
3474 () => {{ parse!("command-outcome-expected_body_arg").map(|npc::NpcBody(_, mut body)| body()) }};
3475 }
3476
3477 macro_rules! uid_arg {
3478 () => {{
3479 parse!("command-outcome-expected_entity_arg").and_then(|entity| {
3480 let entity = get_entity_target(entity, server)?;
3481 Ok(server
3482 .state()
3483 .read_component_copied::<Uid>(entity)
3484 .expect("All entities have uids"))
3485 })
3486 }};
3487 }
3488
3489 macro_rules! parse_or_default {
3490 ($default:expr, @$expr:expr) => {{
3491 let old_i = i;
3492 let f = arg!().and_then($expr);
3493
3494 #[allow(unused_assignments)]
3495 if let Some(f) = f {
3496 f
3497 } else {
3498 i = old_i;
3499 $default
3500 }
3501 }};
3502 (@$expr:expr) => {{ parse_or_default!(Default::default(), @$expr) }};
3503 ($default:expr) => {{ parse_or_default!($default, @|arg| arg.parse().ok()) }};
3504 () => {
3505 parse_or_default!(Default::default())
3506 };
3507 }
3508
3509 let mut rng = rand::rng();
3510
3511 let outcome = arg!("command-outcome-variant_expected")?;
3512
3513 let outcome = match outcome.as_str() {
3514 "Explosion" => Outcome::Explosion {
3515 pos: pos_arg!(),
3516 power: parse_or_default!(1.0),
3517 radius: parse_or_default!(1.0),
3518 is_attack: parse_or_default!(),
3519 reagent: parse_or_default!(@|arg| comp::item::Reagent::from_str(arg).ok().map(Some)),
3520 },
3521 "Lightning" => Outcome::Lightning { pos: pos_arg!() },
3522 "ProjectileShot" => Outcome::ProjectileShot {
3523 pos: pos_arg!(),
3524 body: body_arg!()?,
3525 vel: vec_arg!(),
3526 },
3527 "ProjectileHit" => Outcome::ProjectileHit {
3528 pos: pos_arg!(),
3529 body: body_arg!()?,
3530 vel: vec_arg!(),
3531 source: uid_arg!().ok(),
3532 target: uid_arg!().ok(),
3533 },
3534 "Beam" => Outcome::Beam {
3535 pos: pos_arg!(),
3536 specifier: parse!("command-outcome-expected_frontent_specifier", |arg| {
3537 comp::beam::FrontendSpecifier::from_str(arg).ok()
3538 })?,
3539 },
3540 "ExpChange" => Outcome::ExpChange {
3541 uid: uid_arg!().unwrap_or(target_uid),
3542 exp: parse!("command-outcome-expected_integer")?,
3543 xp_pools: {
3544 let mut hashset = HashSet::new();
3545 while let Some(arg) = arg!() {
3546 hashset.insert(ron::from_str(arg).map_err(|_| {
3547 Content::Key("command-outcome-expected_skill_group_kind".to_string())
3548 })?);
3549 }
3550 hashset
3551 },
3552 },
3553 "SkillPointGain" => Outcome::SkillPointGain {
3554 uid: uid_arg!().unwrap_or(target_uid),
3555 skill_tree: arg!("command-outcome-expected_skill_group_kind").and_then(|arg| {
3556 ron::from_str(arg).map_err(|_| {
3557 Content::Key("command-outcome-expected_skill_group_kind".to_string())
3558 })
3559 })?,
3560 total_points: parse!("Expected an integer amount of points")?,
3561 },
3562 "ComboChange" => Outcome::ComboChange {
3563 uid: uid_arg!().unwrap_or(target_uid),
3564 combo: parse!("command-outcome-expected_integer")?,
3565 },
3566 "BreakBlock" => Outcome::BreakBlock {
3567 pos: pos_arg!(),
3568 color: Some(Rgb::from(vec_arg!())),
3569 tool: None,
3570 },
3571 "SummonedCreature" => Outcome::SummonedCreature {
3572 pos: pos_arg!(),
3573 body: body_arg!()?,
3574 },
3575 "HealthChange" => Outcome::HealthChange {
3576 pos: pos_arg!(),
3577 info: common::outcome::HealthChangeInfo {
3578 amount: parse_or_default!(),
3579 precise: parse_or_default!(),
3580 target: uid_arg!().unwrap_or(target_uid),
3581 by: uid_arg!().map(common::combat::DamageContributor::Solo).ok(),
3582 cause: None,
3583 instance: rng.random(),
3584 },
3585 },
3586 "Death" => Outcome::Death { pos: pos_arg!() },
3587 "Block" => Outcome::Block {
3588 pos: pos_arg!(),
3589 parry: parse_or_default!(),
3590 uid: uid_arg!().unwrap_or(target_uid),
3591 },
3592 "PoiseChange" => Outcome::PoiseChange {
3593 pos: pos_arg!(),
3594 state: parse_or_default!(comp::PoiseState::Normal, @|arg| comp::PoiseState::from_str(arg).ok()),
3595 },
3596 "GroundSlam" => Outcome::GroundSlam { pos: pos_arg!() },
3597 "IceSpikes" => Outcome::IceSpikes { pos: pos_arg!() },
3598 "IceCrack" => Outcome::IceCrack { pos: pos_arg!() },
3599 "FlashFreeze" => Outcome::FlashFreeze { pos: pos_arg!() },
3600 "Steam" => Outcome::Steam { pos: pos_arg!() },
3601 "LaserBeam" => Outcome::LaserBeam { pos: pos_arg!() },
3602 "CyclopsCharge" => Outcome::CyclopsCharge { pos: pos_arg!() },
3603 "FlamethrowerCharge" => Outcome::FlamethrowerCharge { pos: pos_arg!() },
3604 "FuseCharge" => Outcome::FuseCharge { pos: pos_arg!() },
3605 "TerracottaStatueCharge" => Outcome::TerracottaStatueCharge { pos: pos_arg!() },
3606 "SurpriseEgg" => Outcome::SurpriseEgg { pos: pos_arg!() },
3607 "Utterance" => Outcome::Utterance {
3608 pos: pos_arg!(),
3609 body: body_arg!()?,
3610 kind: parse_or_default!(comp::UtteranceKind::Greeting, @|arg| comp::UtteranceKind::from_str(arg).ok()),
3611 },
3612 "Glider" => Outcome::Glider {
3613 pos: pos_arg!(),
3614 wielded: parse_or_default!(true),
3615 },
3616 "SpriteDelete" => Outcome::SpriteDelete {
3617 pos: pos_arg!(),
3618 sprite: parse!("command-outcome-expected_sprite_kind", |arg| {
3619 SpriteKind::try_from(arg.as_str()).ok()
3620 })?,
3621 },
3622 "SpriteUnlocked" => Outcome::SpriteUnlocked { pos: pos_arg!() },
3623 "FailedSpriteUnlock" => Outcome::FailedSpriteUnlock { pos: pos_arg!() },
3624 "Whoosh" => Outcome::Whoosh { pos: pos_arg!() },
3625 "Swoosh" => Outcome::Swoosh { pos: pos_arg!() },
3626 "Slash" => Outcome::Slash { pos: pos_arg!() },
3627 "FireShockwave" => Outcome::FireShockwave { pos: pos_arg!() },
3628 "FireLowShockwave" => Outcome::FireLowShockwave { pos: pos_arg!() },
3629 "GroundDig" => Outcome::GroundDig { pos: pos_arg!() },
3630 "PortalActivated" => Outcome::PortalActivated { pos: pos_arg!() },
3631 "TeleportedByPortal" => Outcome::TeleportedByPortal { pos: pos_arg!() },
3632 "FromTheAshes" => Outcome::FromTheAshes { pos: pos_arg!() },
3633 "ClayGolemDash" => Outcome::ClayGolemDash { pos: pos_arg!() },
3634 "Bleep" => Outcome::Bleep { pos: pos_arg!() },
3635 "Charge" => Outcome::Charge { pos: pos_arg!() },
3636 "HeadLost" => Outcome::HeadLost {
3637 uid: uid_arg!().unwrap_or(target_uid),
3638 head: parse_or_default!(),
3639 },
3640 "Splash" => Outcome::Splash {
3641 vel: vec_arg!(),
3642 pos: pos_arg!(),
3643 mass: parse_or_default!(1.0),
3644 kind: parse_or_default!(
3645 comp::fluid_dynamics::LiquidKind::Water,
3646 @|arg| comp::fluid_dynamics::LiquidKind::from_str(arg).ok()
3647 ),
3648 },
3649 _ => {
3650 return Err(Content::localized_with_args(
3651 "command-outcome-invalid_outcome",
3652 [("outcome", Content::Plain(outcome.to_string()))],
3653 ));
3654 },
3655 };
3656
3657 server
3658 .state()
3659 .ecs()
3660 .read_resource::<EventBus<Outcome>>()
3661 .emit_now(outcome);
3662
3663 Ok(())
3664}
3665
3666fn handle_light(
3667 server: &mut Server,
3668 client: EcsEntity,
3669 target: EcsEntity,
3670 args: Vec<String>,
3671 _action: &ServerChatCommand,
3672) -> CmdResult<()> {
3673 let (opt_r, opt_g, opt_b, opt_x, opt_y, opt_z, opt_s) =
3674 parse_cmd_args!(args, f32, f32, f32, f32, f32, f32, f32);
3675
3676 let mut light_emitter = LightEmitter {
3677 col: Rgb::zero(),
3678 strength: 0.0,
3679 flicker: 0.0,
3680 animated: false,
3681 dir: None,
3682 };
3683 let mut light_offset_opt = None;
3684
3685 if let (Some(r), Some(g), Some(b)) = (opt_r, opt_g, opt_b) {
3686 if r < 0.0 || g < 0.0 || b < 0.0 {
3687 return Err(Content::Plain(
3688 "cr, cg and cb values mustn't be negative.".into(),
3689 ));
3690 }
3691
3692 let r = r.clamp(0.0, 1.0);
3693 let g = g.clamp(0.0, 1.0);
3694 let b = b.clamp(0.0, 1.0);
3695 light_emitter.col = Rgb::new(r, g, b)
3696 };
3697 if let (Some(x), Some(y), Some(z)) = (opt_x, opt_y, opt_z) {
3698 light_offset_opt = Some(comp::LightAnimation {
3699 offset: Vec3::new(x, y, z),
3700 col: light_emitter.col,
3701 strength: 0.0,
3702 dir: None,
3703 })
3704 };
3705 if let Some(s) = opt_s {
3706 light_emitter.strength = s.max(0.0)
3707 };
3708 let pos = position(server, target, "target")?;
3709 let builder = server
3710 .state
3711 .ecs_mut()
3712 .create_entity_synced()
3713 .with(pos)
3714 .with(comp::ForceUpdate::forced())
3716 .with(light_emitter);
3717 if let Some(light_offset) = light_offset_opt {
3718 builder.with(light_offset).build();
3719 } else {
3720 builder.build();
3721 }
3722 server.notify_client(
3723 client,
3724 ServerGeneral::server_msg(
3725 ChatType::CommandInfo,
3726 Content::Plain("Spawned object.".to_string()),
3727 ),
3728 );
3729 Ok(())
3730}
3731
3732fn handle_lantern(
3733 server: &mut Server,
3734 client: EcsEntity,
3735 target: EcsEntity,
3736 args: Vec<String>,
3737 action: &ServerChatCommand,
3738) -> CmdResult<()> {
3739 if let (Some(s), r, g, b) = parse_cmd_args!(args, f32, f32, f32, f32) {
3740 if let Some(mut light) = server
3741 .state
3742 .ecs()
3743 .write_storage::<LightEmitter>()
3744 .get_mut(target)
3745 {
3746 light.strength = s.clamp(0.1, 10.0);
3747 if let (Some(r), Some(g), Some(b)) = (r, g, b) {
3748 light.col = (r.clamp(0.0, 1.0), g.clamp(0.0, 1.0), b.clamp(0.0, 1.0)).into();
3749 server.notify_client(
3750 client,
3751 ServerGeneral::server_msg(
3752 ChatType::CommandInfo,
3753 Content::localized("command-lantern-adjusted-strength-color"),
3754 ),
3755 )
3756 } else {
3757 server.notify_client(
3758 client,
3759 ServerGeneral::server_msg(
3760 ChatType::CommandInfo,
3761 Content::localized("command-lantern-adjusted-strength"),
3762 ),
3763 )
3764 }
3765 Ok(())
3766 } else {
3767 Err(Content::localized("command-lantern-unequiped"))
3768 }
3769 } else {
3770 Err(action.help_content())
3771 }
3772}
3773
3774fn handle_explosion(
3775 server: &mut Server,
3776 _client: EcsEntity,
3777 target: EcsEntity,
3778 args: Vec<String>,
3779 _action: &ServerChatCommand,
3780) -> CmdResult<()> {
3781 let power = parse_cmd_args!(args, f32).unwrap_or(8.0);
3782
3783 const MIN_POWER: f32 = 0.0;
3784 const MAX_POWER: f32 = 512.0;
3785
3786 if power > MAX_POWER {
3787 return Err(Content::localized_with_args(
3788 "command-explosion-power-too-high",
3789 [("power", MAX_POWER.to_string())],
3790 ));
3791 } else if power <= MIN_POWER {
3792 return Err(Content::localized_with_args(
3793 "command-explosion-power-too-low",
3794 [("power", MIN_POWER.to_string())],
3795 ));
3796 }
3797
3798 let pos = position(server, target, "target")?;
3799 let owner = server
3800 .state
3801 .ecs()
3802 .read_storage::<Uid>()
3803 .get(target)
3804 .copied();
3805 server.state.emit_event_now(ExplosionEvent {
3806 pos: pos.0,
3807 explosion: Explosion {
3808 effects: vec![
3809 RadiusEffect::Entity(Effect::Damage(Damage {
3810 source: DamageSource::Explosion,
3811 kind: DamageKind::Energy,
3812 value: 100.0 * power,
3813 })),
3814 RadiusEffect::TerrainDestruction(power, Rgb::black()),
3815 ],
3816 radius: 3.0 * power,
3817 reagent: None,
3818 min_falloff: 0.0,
3819 },
3820 owner,
3821 });
3822 Ok(())
3823}
3824
3825fn handle_set_waypoint(
3826 server: &mut Server,
3827 client: EcsEntity,
3828 target: EcsEntity,
3829 _args: Vec<String>,
3830 _action: &ServerChatCommand,
3831) -> CmdResult<()> {
3832 let pos = position(server, target, "target")?;
3833 let time = *server.state.mut_resource::<Time>();
3834 let location_name = server
3835 .world()
3836 .get_location_name(server.index.as_index_ref(), pos.0.xy().as_::<i32>());
3837
3838 insert_or_replace_component(
3839 server,
3840 target,
3841 comp::Waypoint::temp_new(pos.0, time),
3842 "target",
3843 )?;
3844 server.notify_client(
3845 client,
3846 ServerGeneral::server_msg(
3847 ChatType::CommandInfo,
3848 Content::localized("command-set-waypoint-result"),
3849 ),
3850 );
3851
3852 if let Some(location_name) = location_name {
3853 server.notify_client(
3854 target,
3855 ServerGeneral::Notification(Notification::WaypointSaved { location_name }),
3856 );
3857 } else {
3858 error!(
3859 "Failed to get location name for waypoint. Client was not notified of new waypoint."
3860 );
3861 }
3862
3863 Ok(())
3864}
3865
3866fn handle_spawn_wiring(
3867 server: &mut Server,
3868 client: EcsEntity,
3869 target: EcsEntity,
3870 _args: Vec<String>,
3871 _action: &ServerChatCommand,
3872) -> CmdResult<()> {
3873 let mut pos = position(server, target, "target")?;
3874 pos.0.x += 3.0;
3875
3876 let mut outputs1 = HashMap::new();
3877 outputs1.insert("button".to_string(), OutputFormula::OnCollide {
3878 value: 1.0,
3879 });
3880
3881 let builder1 = server
3889 .state
3890 .create_wiring(pos, comp::object::Body::Pebble, WiringElement {
3891 inputs: HashMap::new(),
3892 outputs: outputs1,
3893 actions: Vec::new(),
3894 })
3895 .with(comp::Density(100_f32));
3896 let ent1 = builder1.build();
3897
3898 pos.0.x += 3.0;
3899 let builder2 = server
3904 .state
3905 .create_wiring(pos, comp::object::Body::Pebble, WiringElement {
3906 inputs: HashMap::new(),
3907 outputs: HashMap::new(),
3908 actions: vec![WiringAction {
3909 formula: OutputFormula::Input {
3910 name: String::from("button"),
3911 },
3912 threshold: 0.0,
3913 effects: vec![WiringActionEffect::SetLight {
3914 r: OutputFormula::Input {
3915 name: String::from("button"),
3916 },
3917 g: OutputFormula::Input {
3918 name: String::from("button"),
3919 },
3920 b: OutputFormula::Input {
3921 name: String::from("button"),
3922 },
3923 }],
3924 }],
3925 })
3926 .with(comp::Density(100_f32));
3927 let ent2 = builder2.build();
3928
3929 pos.0.x += 3.0;
3930 let builder3 = server
3931 .state
3932 .create_wiring(pos, comp::object::Body::TrainingDummy, WiringElement {
3933 inputs: HashMap::new(),
3934 outputs: HashMap::new(),
3935 actions: Vec::new(),
3936 })
3937 .with(comp::Density(comp::object::Body::TrainingDummy.density().0))
3938 .with(Circuit::new(vec![Wire {
3939 input: WireNode::new(ent1, "button".to_string()),
3940 output: WireNode::new(ent2, "button".to_string()),
3941 }]));
3942 builder3.build();
3943
3944 server.notify_client(
3945 client,
3946 ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain("Wire".to_string())),
3947 );
3948 Ok(())
3949}
3950
3951fn handle_adminify(
3952 server: &mut Server,
3953 client: EcsEntity,
3954 _target: EcsEntity,
3955 args: Vec<String>,
3956 action: &ServerChatCommand,
3957) -> CmdResult<()> {
3958 if let (Some(alias), desired_role) = parse_cmd_args!(args, String, String) {
3959 let desired_role = if let Some(mut desired_role) = desired_role {
3960 desired_role.make_ascii_lowercase();
3961 Some(match &*desired_role {
3962 "admin" => AdminRole::Admin,
3963 "moderator" => AdminRole::Moderator,
3964 _ => {
3965 return Err(action.help_content());
3966 },
3967 })
3968 } else {
3969 None
3970 };
3971 let (player, player_uuid) = find_alias(server.state.ecs(), &alias, true)?;
3972 let client_uuid = uuid(server, client, "client")?;
3973 let uid = uid(server, player, "player")?;
3974
3975 let client_real_role = real_role(server, client_uuid, "client")?;
3978
3979 verify_above_role(
3984 server,
3985 (client, client_uuid),
3986 (player, player_uuid),
3987 Content::localized("command-adminify-reassign-to-above"),
3988 )?;
3989
3990 if desired_role > Some(client_real_role) {
4005 return Err(Content::localized(
4006 "command-adminify-assign-higher-than-own",
4007 ));
4008 }
4009
4010 let mut admin_storage = server.state.ecs().write_storage::<comp::Admin>();
4011 let entry = admin_storage
4012 .entry(player)
4013 .map_err(|_| Content::localized("command-adminify-cannot-find-player"))?;
4014 match (entry, desired_role) {
4015 (StorageEntry::Vacant(_), None) => {
4016 return Err(Content::localized("command-adminify-already-has-no-role"));
4017 },
4018 (StorageEntry::Occupied(o), None) => {
4019 let old_role = o.remove().0;
4020 server.notify_client(
4021 client,
4022 ServerGeneral::server_msg(
4023 ChatType::CommandInfo,
4024 Content::localized_with_args("command-adminify-removed-role", [
4025 ("player", alias),
4026 ("role", format!("{:?}", old_role)),
4027 ]),
4028 ),
4029 );
4030 },
4031 (entry, Some(desired_role)) => {
4032 let key = match entry
4033 .replace(comp::Admin(desired_role))
4034 .map(|old_admin| old_admin.0.cmp(&desired_role))
4035 {
4036 Some(Ordering::Equal) => {
4037 return Err(Content::localized("command-adminify-already-has-role"));
4038 },
4039 Some(Ordering::Greater) => "command-adminify-role-downgraded",
4040 Some(Ordering::Less) | None => "command-adminify-role-upgraded",
4041 };
4042 server.notify_client(
4043 client,
4044 ServerGeneral::server_msg(
4045 ChatType::CommandInfo,
4046 Content::localized_with_args(key, [
4047 ("player", alias),
4048 ("role", format!("{:?}", desired_role)),
4049 ]),
4050 ),
4051 );
4052 },
4053 };
4054
4055 server.notify_client(player, ServerGeneral::SetPlayerRole(desired_role));
4057
4058 if server
4059 .state
4060 .ecs()
4061 .read_storage::<Client>()
4062 .get(player)
4063 .is_some_and(|client| client.client_type.emit_login_events())
4064 {
4065 let is_moderator = desired_role.is_some();
4070 let msg =
4071 ServerGeneral::PlayerListUpdate(PlayerListUpdate::Moderator(uid, is_moderator));
4072 server.state.notify_players(msg);
4073 }
4074 Ok(())
4075 } else {
4076 Err(action.help_content())
4077 }
4078}
4079
4080fn handle_tell(
4081 server: &mut Server,
4082 client: EcsEntity,
4083 target: EcsEntity,
4084 args: Vec<String>,
4085 action: &ServerChatCommand,
4086) -> CmdResult<()> {
4087 no_sudo(client, target)?;
4088 can_send_message(target, server)?;
4089
4090 if let (Some(alias), message_opt) = parse_cmd_args!(args, String, ..Vec<String>) {
4091 let ecs = server.state.ecs();
4092 let player = find_alias(ecs, &alias, false)?.0;
4093
4094 if player == target {
4095 return Err(Content::localized("command-tell-to-yourself"));
4096 }
4097 let target_uid = uid(server, target, "target")?;
4098 let player_uid = uid(server, player, "player")?;
4099 let mode = comp::ChatMode::Tell(player_uid);
4100 insert_or_replace_component(server, target, mode.clone(), "target")?;
4101 if !message_opt.is_empty() {
4102 let msg = Content::Plain(message_opt.join(" "));
4103 server
4104 .state
4105 .send_chat(mode.to_msg(target_uid, msg, None)?, false);
4106 };
4107 server.notify_client(target, ServerGeneral::ChatMode(mode));
4108 Ok(())
4109 } else {
4110 Err(action.help_content())
4111 }
4112}
4113
4114fn handle_faction(
4115 server: &mut Server,
4116 client: EcsEntity,
4117 target: EcsEntity,
4118 args: Vec<String>,
4119 _action: &ServerChatCommand,
4120) -> CmdResult<()> {
4121 no_sudo(client, target)?;
4122 can_send_message(target, server)?;
4123
4124 let factions = server.state.ecs().read_storage();
4125 if let Some(comp::Faction(faction)) = factions.get(target) {
4126 let mode = comp::ChatMode::Faction(faction.to_string());
4127 drop(factions);
4128 insert_or_replace_component(server, target, mode.clone(), "target")?;
4129 let msg = args.join(" ");
4130 if !msg.is_empty()
4131 && let Some(uid) = server.state.ecs().read_storage().get(target)
4132 {
4133 server
4134 .state
4135 .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4136 }
4137 server.notify_client(target, ServerGeneral::ChatMode(mode));
4138 Ok(())
4139 } else {
4140 Err(Content::localized("command-faction-join"))
4141 }
4142}
4143
4144fn handle_group(
4145 server: &mut Server,
4146 client: EcsEntity,
4147 target: EcsEntity,
4148 args: Vec<String>,
4149 _action: &ServerChatCommand,
4150) -> CmdResult<()> {
4151 no_sudo(client, target)?;
4152 can_send_message(target, server)?;
4153
4154 let groups = server.state.ecs().read_storage::<comp::Group>();
4155 if let Some(group) = groups.get(target).copied() {
4156 let mode = comp::ChatMode::Group;
4157 drop(groups);
4158 insert_or_replace_component(server, target, mode.clone(), "target")?;
4159 let msg = args.join(" ");
4160 if !msg.is_empty()
4161 && let Some(uid) = server.state.ecs().read_storage().get(target)
4162 {
4163 server
4164 .state
4165 .send_chat(mode.to_msg(*uid, Content::Plain(msg), Some(group))?, false);
4166 }
4167 server.notify_client(target, ServerGeneral::ChatMode(mode));
4168 Ok(())
4169 } else {
4170 Err(Content::localized("command-group-join"))
4171 }
4172}
4173
4174fn handle_group_invite(
4175 server: &mut Server,
4176 client: EcsEntity,
4177 target: EcsEntity,
4178 args: Vec<String>,
4179 action: &ServerChatCommand,
4180) -> CmdResult<()> {
4181 can_send_message(target, server)?;
4184
4185 if let Some(target_alias) = parse_cmd_args!(args, String) {
4186 let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4187 let uid = uid(server, target_player, "player")?;
4188
4189 server
4190 .state
4191 .emit_event_now(InitiateInviteEvent(target, uid, InviteKind::Group));
4192
4193 if client != target {
4194 server.notify_client(
4195 target,
4196 ServerGeneral::server_msg(
4197 ChatType::CommandInfo,
4198 Content::localized_with_args("command-group_invite-invited-to-your-group", [(
4199 "player",
4200 target_alias.to_owned(),
4201 )]),
4202 ),
4203 );
4204 }
4205
4206 server.notify_client(
4207 client,
4208 ServerGeneral::server_msg(
4209 ChatType::CommandInfo,
4210 Content::localized_with_args("command-group_invite-invited-to-group", [(
4211 "player",
4212 target_alias.to_owned(),
4213 )]),
4214 ),
4215 );
4216 Ok(())
4217 } else {
4218 Err(action.help_content())
4219 }
4220}
4221
4222fn handle_group_kick(
4223 server: &mut Server,
4224 _client: EcsEntity,
4225 target: EcsEntity,
4226 args: Vec<String>,
4227 action: &ServerChatCommand,
4228) -> CmdResult<()> {
4229 if let Some(target_alias) = parse_cmd_args!(args, String) {
4231 let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4232 let uid = uid(server, target_player, "player")?;
4233
4234 server
4235 .state
4236 .emit_event_now(GroupManipEvent(target, comp::GroupManip::Kick(uid)));
4237 Ok(())
4238 } else {
4239 Err(action.help_content())
4240 }
4241}
4242
4243fn handle_group_leave(
4244 server: &mut Server,
4245 _client: EcsEntity,
4246 target: EcsEntity,
4247 _args: Vec<String>,
4248 _action: &ServerChatCommand,
4249) -> CmdResult<()> {
4250 server
4251 .state
4252 .emit_event_now(GroupManipEvent(target, comp::GroupManip::Leave));
4253 Ok(())
4254}
4255
4256fn handle_group_promote(
4257 server: &mut Server,
4258 _client: EcsEntity,
4259 target: EcsEntity,
4260 args: Vec<String>,
4261 action: &ServerChatCommand,
4262) -> CmdResult<()> {
4263 if let Some(target_alias) = parse_cmd_args!(args, String) {
4265 let target_player = find_alias(server.state.ecs(), &target_alias, false)?.0;
4266 let uid = uid(server, target_player, "player")?;
4267
4268 server
4269 .state
4270 .emit_event_now(GroupManipEvent(target, comp::GroupManip::AssignLeader(uid)));
4271 Ok(())
4272 } else {
4273 Err(action.help_content())
4274 }
4275}
4276
4277fn handle_reset_recipes(
4278 server: &mut Server,
4279 _client: EcsEntity,
4280 target: EcsEntity,
4281 _args: Vec<String>,
4282 action: &ServerChatCommand,
4283) -> CmdResult<()> {
4284 if let Some(mut inventory) = server
4285 .state
4286 .ecs()
4287 .write_storage::<comp::Inventory>()
4288 .get_mut(target)
4289 {
4290 inventory.reset_recipes();
4291 server.notify_client(target, ServerGeneral::UpdateRecipes);
4292 Ok(())
4293 } else {
4294 Err(action.help_content())
4295 }
4296}
4297
4298fn handle_region(
4299 server: &mut Server,
4300 client: EcsEntity,
4301 target: EcsEntity,
4302 args: Vec<String>,
4303 _action: &ServerChatCommand,
4304) -> CmdResult<()> {
4305 no_sudo(client, target)?;
4306 can_send_message(target, server)?;
4307
4308 let mode = comp::ChatMode::Region;
4309 insert_or_replace_component(server, target, mode.clone(), "target")?;
4310 let msg = args.join(" ");
4311 if !msg.is_empty()
4312 && let Some(uid) = server.state.ecs().read_storage().get(target)
4313 {
4314 server
4315 .state
4316 .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4317 }
4318 server.notify_client(target, ServerGeneral::ChatMode(mode));
4319 Ok(())
4320}
4321
4322fn handle_say(
4323 server: &mut Server,
4324 client: EcsEntity,
4325 target: EcsEntity,
4326 args: Vec<String>,
4327 _action: &ServerChatCommand,
4328) -> CmdResult<()> {
4329 no_sudo(client, target)?;
4330 can_send_message(target, server)?;
4331
4332 let mode = comp::ChatMode::Say;
4333 insert_or_replace_component(server, target, mode.clone(), "target")?;
4334 let msg = args.join(" ");
4335 if !msg.is_empty()
4336 && let Some(uid) = server.state.ecs().read_storage().get(target)
4337 {
4338 server
4339 .state
4340 .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4341 }
4342 server.notify_client(target, ServerGeneral::ChatMode(mode));
4343 Ok(())
4344}
4345
4346fn handle_world(
4347 server: &mut Server,
4348 client: EcsEntity,
4349 target: EcsEntity,
4350 args: Vec<String>,
4351 _action: &ServerChatCommand,
4352) -> CmdResult<()> {
4353 no_sudo(client, target)?;
4354 can_send_message(target, server)?;
4355
4356 let mode = comp::ChatMode::World;
4357 insert_or_replace_component(server, target, mode.clone(), "target")?;
4358 let msg = args.join(" ");
4359 if !msg.is_empty()
4360 && let Some(uid) = server.state.ecs().read_storage().get(target)
4361 {
4362 server
4363 .state
4364 .send_chat(mode.to_msg(*uid, Content::Plain(msg), None)?, false);
4365 }
4366 server.notify_client(target, ServerGeneral::ChatMode(mode));
4367 Ok(())
4368}
4369
4370fn handle_join_faction(
4371 server: &mut Server,
4372 client: EcsEntity,
4373 target: EcsEntity,
4374 args: Vec<String>,
4375 _action: &ServerChatCommand,
4376) -> CmdResult<()> {
4377 no_sudo(client, target)?;
4378 let emit_join_message = server
4379 .state
4380 .ecs()
4381 .read_storage::<Client>()
4382 .get(target)
4383 .is_some_and(|client| client.client_type.emit_login_events());
4384
4385 let players = server.state.ecs().read_storage::<comp::Player>();
4386 if let Some(alias) = players.get(target).map(|player| player.alias.clone()) {
4387 drop(players);
4388 let (faction_leave, mode) = if let Some(faction) = parse_cmd_args!(args, String) {
4389 let mode = comp::ChatMode::Faction(faction.clone());
4390 insert_or_replace_component(server, target, mode.clone(), "target")?;
4391 let faction_join = server
4392 .state
4393 .ecs()
4394 .write_storage()
4395 .insert(target, comp::Faction(faction.clone()))
4396 .ok()
4397 .flatten()
4398 .map(|f| f.0);
4399
4400 if emit_join_message {
4401 server.state.send_chat(
4402 ChatType::FactionMeta(faction.clone())
4404 .into_plain_msg(format!("[{}] joined faction ({})", alias, faction)),
4405 false,
4406 );
4407 }
4408 (faction_join, mode)
4409 } else {
4410 let mode = comp::ChatMode::default();
4411 insert_or_replace_component(server, target, mode.clone(), "target")?;
4412 let faction_leave = server
4413 .state
4414 .ecs()
4415 .write_storage()
4416 .remove(target)
4417 .map(|comp::Faction(f)| f);
4418 (faction_leave, mode)
4419 };
4420 if let Some(faction) = faction_leave
4421 && emit_join_message
4422 {
4423 server.state.send_chat(
4424 ChatType::FactionMeta(faction.clone())
4426 .into_plain_msg(format!("[{}] left faction ({})", alias, faction)),
4427 false,
4428 );
4429 }
4430 server.notify_client(target, ServerGeneral::ChatMode(mode));
4431 Ok(())
4432 } else {
4433 Err(Content::Plain("Could not find your player alias".into()))
4434 }
4435}
4436
4437fn handle_death_effect(
4438 server: &mut Server,
4439 _client: EcsEntity,
4440 target: EcsEntity,
4441 args: Vec<String>,
4442 action: &ServerChatCommand,
4443) -> CmdResult<()> {
4444 let mut args = args.into_iter();
4445
4446 let Some(effect_str) = args.next() else {
4447 return Err(action.help_content());
4448 };
4449
4450 let effect = match effect_str.as_str() {
4451 "transform" => {
4452 let entity_config = args.next().ok_or(action.help_content())?;
4453
4454 if Ron::<EntityConfig>::load(&entity_config).is_err() {
4457 return Err(Content::localized_with_args(
4458 "command-entity-load-failed",
4459 [("config", entity_config)],
4460 ));
4461 }
4462
4463 combat::DeathEffect::Transform {
4464 entity_spec: entity_config,
4465 allow_players: true,
4466 }
4467 },
4468 unknown_effect => {
4469 return Err(Content::localized_with_args(
4470 "command-death_effect-unknown",
4471 [("effect", unknown_effect)],
4472 ));
4473 },
4474 };
4475
4476 let mut death_effects = server.state.ecs().write_storage::<combat::DeathEffects>();
4477
4478 if let Some(death_effects) = death_effects.get_mut(target) {
4479 death_effects.0.push(effect);
4480 } else {
4481 death_effects
4482 .insert(target, combat::DeathEffects(vec![effect]))
4483 .unwrap();
4484 }
4485
4486 Ok(())
4487}
4488
4489#[cfg(not(feature = "worldgen"))]
4490fn handle_debug_column(
4491 _server: &mut Server,
4492 _client: EcsEntity,
4493 _target: EcsEntity,
4494 _args: Vec<String>,
4495 _action: &ServerChatCommand,
4496) -> CmdResult<()> {
4497 Err(Content::Plain(
4498 "Unsupported without worldgen enabled".into(),
4499 ))
4500}
4501
4502#[cfg(feature = "worldgen")]
4503fn handle_debug_column(
4504 server: &mut Server,
4505 client: EcsEntity,
4506 target: EcsEntity,
4507 args: Vec<String>,
4508 _action: &ServerChatCommand,
4509) -> CmdResult<()> {
4510 let sim = server.world.sim();
4511 let calendar = (*server.state.ecs().read_resource::<Calendar>()).clone();
4512 let sampler = server.world.sample_columns();
4513 let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) {
4514 Vec2::new(x, y)
4515 } else {
4516 let pos = position(server, target, "target")?;
4517 pos.0.xy().map(|x| x as i32)
4519 };
4520 let msg_generator = |calendar| {
4521 let alt = sim.get_interpolated(wpos, |chunk| chunk.alt)?;
4522 let basement = sim.get_interpolated(wpos, |chunk| chunk.basement)?;
4523 let water_alt = sim.get_interpolated(wpos, |chunk| chunk.water_alt)?;
4524 let chaos = sim.get_interpolated(wpos, |chunk| chunk.chaos)?;
4525 let temp = sim.get_interpolated(wpos, |chunk| chunk.temp)?;
4526 let humidity = sim.get_interpolated(wpos, |chunk| chunk.humidity)?;
4527 let rockiness = sim.get_interpolated(wpos, |chunk| chunk.rockiness)?;
4528 let tree_density = sim.get_interpolated(wpos, |chunk| chunk.tree_density)?;
4529 let spawn_rate = sim.get_interpolated(wpos, |chunk| chunk.spawn_rate)?;
4530 let chunk_pos = wpos.wpos_to_cpos();
4531 let chunk = sim.get(chunk_pos)?;
4532 let col = sampler.get((wpos, server.index.as_index_ref(), Some(calendar)))?;
4533 let gradient = sim.get_gradient_approx(chunk_pos)?;
4534 let downhill = chunk.downhill;
4535 let river = &chunk.river;
4536 let flux = chunk.flux;
4537 let path = chunk.path;
4538 let cliff_height = chunk.cliff_height;
4539
4540 Some(format!(
4541 r#"wpos: {:?}
4542alt {:?} ({:?})
4543water_alt {:?} ({:?})
4544basement {:?}
4545river {:?}
4546gradient {:?}
4547downhill {:?}
4548chaos {:?}
4549flux {:?}
4550temp {:?}
4551humidity {:?}
4552rockiness {:?}
4553tree_density {:?}
4554spawn_rate {:?}
4555path {:?}
4556cliff_height {:?} "#,
4557 wpos,
4558 alt,
4559 col.alt,
4560 water_alt,
4561 col.water_level,
4562 basement,
4563 river,
4564 gradient,
4565 downhill,
4566 chaos,
4567 flux,
4568 temp,
4569 humidity,
4570 rockiness,
4571 tree_density,
4572 spawn_rate,
4573 path,
4574 cliff_height,
4575 ))
4576 };
4577 if let Some(s) = msg_generator(&calendar) {
4578 server.notify_client(
4579 client,
4580 ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(s)),
4581 );
4582 Ok(())
4583 } else {
4584 Err(Content::Plain("Not a pre-generated chunk.".into()))
4585 }
4586}
4587
4588#[cfg(not(feature = "worldgen"))]
4589fn handle_debug_ways(
4590 _server: &mut Server,
4591 _client: EcsEntity,
4592 _target: EcsEntity,
4593 _args: Vec<String>,
4594 _action: &ServerChatCommand,
4595) -> CmdResult<()> {
4596 Err(Content::Plain(
4597 "Unsupported without worldgen enabled".into(),
4598 ))
4599}
4600
4601#[cfg(feature = "worldgen")]
4602fn handle_debug_ways(
4603 server: &mut Server,
4604 client: EcsEntity,
4605 target: EcsEntity,
4606 args: Vec<String>,
4607 _action: &ServerChatCommand,
4608) -> CmdResult<()> {
4609 let sim = server.world.sim();
4610 let wpos = if let (Some(x), Some(y)) = parse_cmd_args!(args, i32, i32) {
4611 Vec2::new(x, y)
4612 } else {
4613 let pos = position(server, target, "target")?;
4614 pos.0.xy().map(|x| x as i32)
4616 };
4617 let msg_generator = || {
4618 let chunk_pos = wpos.wpos_to_cpos();
4619 let mut ret = String::new();
4620 for delta in LOCALITY {
4621 let pos = chunk_pos + delta;
4622 let chunk = sim.get(pos)?;
4623 writeln!(ret, "{:?}: {:?}", pos, chunk.path).ok()?;
4624 }
4625 Some(ret)
4626 };
4627 if let Some(s) = msg_generator() {
4628 server.notify_client(
4629 client,
4630 ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(s)),
4631 );
4632 Ok(())
4633 } else {
4634 Err(Content::Plain("Not a pre-generated chunk.".into()))
4635 }
4636}
4637
4638fn handle_disconnect_all_players(
4639 server: &mut Server,
4640 client: EcsEntity,
4641 _target: EcsEntity,
4642 args: Vec<String>,
4643 _action: &ServerChatCommand,
4644) -> CmdResult<()> {
4645 let client_uuid = uuid(server, client, "client")?;
4646 let _role = real_role(server, client_uuid, "role")?;
4648
4649 if parse_cmd_args!(args, String).as_deref() != Some("confirm") {
4650 return Err(Content::localized("command-disconnectall-confirm"));
4651 }
4652
4653 let ecs = server.state.ecs();
4654 let players = &ecs.read_storage::<comp::Player>();
4655
4656 let player_name = if let Some(player) = players.get(client) {
4660 &*player.alias
4661 } else {
4662 warn!(
4663 "Failed to get player name for admin who used /disconnect_all_players - ignoring \
4664 command."
4665 );
4666 return Err(Content::localized("command-you-dont-exist"));
4667 };
4668
4669 info!(
4670 "Disconnecting all clients due to admin command from {}",
4671 player_name
4672 );
4673 server.disconnect_all_clients_requested = true;
4674
4675 Ok(())
4676}
4677
4678fn handle_skill_point(
4679 server: &mut Server,
4680 _client: EcsEntity,
4681 target: EcsEntity,
4682 args: Vec<String>,
4683 action: &ServerChatCommand,
4684) -> CmdResult<()> {
4685 if let (Some(a_skill_tree), Some(sp), entity_target) =
4686 parse_cmd_args!(args, String, u16, EntityTarget)
4687 {
4688 let skill_tree = parse_skill_tree(&a_skill_tree)?;
4689 let player = entity_target
4690 .map(|entity_target| get_entity_target(entity_target, server))
4691 .unwrap_or(Ok(target))?;
4692
4693 if let Some(mut skill_set) = server
4694 .state
4695 .ecs_mut()
4696 .write_storage::<comp::SkillSet>()
4697 .get_mut(player)
4698 {
4699 skill_set.add_skill_points(skill_tree, sp);
4700 Ok(())
4701 } else {
4702 Err(Content::Plain("Entity has no stats!".into()))
4703 }
4704 } else {
4705 Err(action.help_content())
4706 }
4707}
4708
4709fn parse_skill_tree(skill_tree: &str) -> CmdResult<comp::skillset::SkillGroupKind> {
4710 use comp::{item::tool::ToolKind, skillset::SkillGroupKind};
4711 match skill_tree {
4712 "general" => Ok(SkillGroupKind::General),
4713 "sword" => Ok(SkillGroupKind::Weapon(ToolKind::Sword)),
4714 "axe" => Ok(SkillGroupKind::Weapon(ToolKind::Axe)),
4715 "hammer" => Ok(SkillGroupKind::Weapon(ToolKind::Hammer)),
4716 "bow" => Ok(SkillGroupKind::Weapon(ToolKind::Bow)),
4717 "staff" => Ok(SkillGroupKind::Weapon(ToolKind::Staff)),
4718 "sceptre" => Ok(SkillGroupKind::Weapon(ToolKind::Sceptre)),
4719 "mining" => Ok(SkillGroupKind::Weapon(ToolKind::Pick)),
4720 _ => Err(Content::localized_with_args(
4721 "command-invalid-skill-group",
4722 [("group", skill_tree)],
4723 )),
4724 }
4725}
4726
4727fn reload_chunks_inner(server: &mut Server, pos: Vec3<f32>, radius: Option<i32>) -> usize {
4728 let mut removed = 0;
4729
4730 if let Some(radius) = radius {
4731 let chunk_key = server.state.terrain().pos_key(pos.as_());
4732
4733 for key_offset in Spiral2d::with_radius(radius) {
4734 let chunk_key = chunk_key + key_offset;
4735
4736 #[cfg(feature = "persistent_world")]
4737 server
4738 .state
4739 .ecs()
4740 .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>()
4741 .map(|mut terrain_persistence| terrain_persistence.unload_chunk(chunk_key));
4742 if server.state.remove_chunk(chunk_key) {
4743 removed += 1;
4744 }
4745 }
4746 } else {
4747 #[cfg(feature = "persistent_world")]
4748 server
4749 .state
4750 .ecs()
4751 .try_fetch_mut::<crate::terrain_persistence::TerrainPersistence>()
4752 .map(|mut terrain_persistence| terrain_persistence.unload_all());
4753 removed = server.state.clear_terrain();
4754 }
4755
4756 removed
4757}
4758
4759fn handle_reload_chunks(
4760 server: &mut Server,
4761 client: EcsEntity,
4762 target: EcsEntity,
4763 args: Vec<String>,
4764 _action: &ServerChatCommand,
4765) -> CmdResult<()> {
4766 let radius = parse_cmd_args!(args, i32);
4767
4768 let pos = position(server, target, "target")?.0;
4769 let removed = reload_chunks_inner(server, pos, radius.map(|radius| radius.clamp(0, 64)));
4770
4771 server.notify_client(
4772 client,
4773 ServerGeneral::server_msg(
4774 ChatType::CommandInfo,
4775 Content::localized_with_args("command-reloaded-chunks", [(
4776 "reloaded",
4777 removed.to_string(),
4778 )]),
4779 ),
4780 );
4781
4782 Ok(())
4783}
4784
4785fn handle_remove_lights(
4786 server: &mut Server,
4787 client: EcsEntity,
4788 target: EcsEntity,
4789 args: Vec<String>,
4790 _action: &ServerChatCommand,
4791) -> CmdResult<()> {
4792 let opt_radius = parse_cmd_args!(args, f32);
4793 let player_pos = position(server, target, "target")?;
4794 let mut to_delete = vec![];
4795
4796 let ecs = server.state.ecs();
4797 for (entity, pos, _, _, _) in (
4798 &ecs.entities(),
4799 &ecs.read_storage::<comp::Pos>(),
4800 &ecs.read_storage::<LightEmitter>(),
4801 !&ecs.read_storage::<WaypointArea>(),
4802 !&ecs.read_storage::<comp::Player>(),
4803 )
4804 .join()
4805 {
4806 if opt_radius
4807 .map(|r| pos.0.distance(player_pos.0) < r)
4808 .unwrap_or(true)
4809 {
4810 to_delete.push(entity);
4811 }
4812 }
4813
4814 let size = to_delete.len();
4815
4816 for entity in to_delete {
4817 if let Err(e) = server.state.delete_entity_recorded(entity) {
4818 error!(?e, "Failed to delete light: {:?}", e);
4819 }
4820 }
4821
4822 server.notify_client(
4823 client,
4824 ServerGeneral::server_msg(
4825 ChatType::CommandInfo,
4826 Content::Plain(format!("Removed {} lights!", size)),
4827 ),
4828 );
4829 Ok(())
4830}
4831
4832fn get_entity_target(entity_target: EntityTarget, server: &Server) -> CmdResult<EcsEntity> {
4833 match entity_target {
4834 EntityTarget::Player(alias) => Ok(find_alias(server.state.ecs(), &alias, true)?.0),
4835 EntityTarget::RtsimNpc(id) => {
4836 let (npc_id, _) = server
4837 .state
4838 .ecs()
4839 .read_resource::<crate::rtsim::RtSim>()
4840 .state()
4841 .data()
4842 .npcs
4843 .iter()
4844 .find(|(_, npc)| npc.uid == id)
4845 .ok_or(Content::Plain(format!(
4846 "Could not find rtsim npc with id {id}."
4847 )))?;
4848 server
4849 .state()
4850 .ecs()
4851 .read_resource::<common::uid::IdMaps>()
4852 .rtsim_entity(npc_id)
4853 .ok_or(Content::Plain(format!("Npc with id {id} isn't loaded.")))
4854 },
4855 EntityTarget::Uid(uid) => server
4856 .state
4857 .ecs()
4858 .entity_from_uid(uid)
4859 .ok_or(Content::Plain(format!("{uid:?} not found."))),
4860 }
4861}
4862
4863fn handle_sudo(
4864 server: &mut Server,
4865 client: EcsEntity,
4866 _target: EcsEntity,
4867 args: Vec<String>,
4868 action: &ServerChatCommand,
4869) -> CmdResult<()> {
4870 if let (Some(entity_target), Some(cmd), cmd_args) =
4871 parse_cmd_args!(args, EntityTarget, String, ..Vec<String>)
4872 {
4873 if let Ok(action) = cmd.parse() {
4874 let entity = get_entity_target(entity_target, server)?;
4875 let client_uuid = uuid(server, client, "client")?;
4876
4877 {
4879 let players = server.state.ecs().read_storage::<comp::Player>();
4880 if let Some(player) = players.get(entity) {
4881 let player_uuid = player.uuid();
4882 drop(players);
4883 verify_above_role(
4884 server,
4885 (client, client_uuid),
4886 (entity, player_uuid),
4887 Content::localized("command-sudo-higher-role"),
4888 )?;
4889 } else if server.entity_admin_role(client) < Some(AdminRole::Admin) {
4890 return Err(Content::localized(
4891 "command-sudo-no-permission-for-non-players",
4892 ));
4893 }
4894 }
4895
4896 do_command(server, client, entity, cmd_args, &action)
4900 } else {
4901 Err(Content::localized("command-unknown"))
4902 }
4903 } else {
4904 Err(action.help_content())
4905 }
4906}
4907
4908fn handle_version(
4909 server: &mut Server,
4910 client: EcsEntity,
4911 _target: EcsEntity,
4912 _args: Vec<String>,
4913 _action: &ServerChatCommand,
4914) -> CmdResult<()> {
4915 server.notify_client(
4916 client,
4917 ServerGeneral::server_msg(
4918 ChatType::CommandInfo,
4919 Content::localized_with_args("command-version-current", [
4920 ("hash", (*common::util::GIT_HASH).to_owned()),
4921 ("date", (*common::util::GIT_DATE).to_owned()),
4922 ]),
4923 ),
4924 );
4925 Ok(())
4926}
4927
4928fn handle_whitelist(
4929 server: &mut Server,
4930 client: EcsEntity,
4931 _target: EcsEntity,
4932 args: Vec<String>,
4933 action: &ServerChatCommand,
4934) -> CmdResult<()> {
4935 let now = Utc::now();
4936
4937 if let (Some(whitelist_action), Some(username)) = parse_cmd_args!(args, String, String) {
4938 let client_uuid = uuid(server, client, "client")?;
4939 let client_username = uuid_to_username(server, client, client_uuid)?;
4940 let client_role = real_role(server, client_uuid, "client")?;
4941
4942 if whitelist_action.eq_ignore_ascii_case("add") {
4943 let uuid = find_username(server, &username)?;
4944
4945 let record = WhitelistRecord {
4946 date: now,
4947 info: Some(WhitelistInfo {
4948 username_when_whitelisted: username.clone(),
4949 whitelisted_by: client_uuid,
4950 whitelisted_by_username: client_username,
4951 whitelisted_by_role: client_role.into(),
4952 }),
4953 };
4954
4955 let edit =
4956 server
4957 .editable_settings_mut()
4958 .whitelist
4959 .edit(server.data_dir().as_ref(), |w| {
4960 if w.insert(uuid, record).is_some() {
4961 None
4962 } else {
4963 Some(Content::localized_with_args("command-whitelist-added", [(
4964 "username",
4965 username.to_owned(),
4966 )]))
4967 }
4968 });
4969 edit_setting_feedback(server, client, edit, || {
4970 Content::localized_with_args("command-whitelist-already-added", [(
4971 "username", username,
4972 )])
4973 })
4974 } else if whitelist_action.eq_ignore_ascii_case("remove") {
4975 let client_uuid = uuid(server, client, "client")?;
4976 let client_role = real_role(server, client_uuid, "client")?;
4977
4978 let uuid = find_username(server, &username)?;
4979 let mut err_key = "command-whitelist-unlisted";
4980 let edit =
4981 server
4982 .editable_settings_mut()
4983 .whitelist
4984 .edit(server.data_dir().as_ref(), |w| {
4985 w.remove(&uuid)
4986 .filter(|record| {
4987 if record.whitelisted_by_role() <= client_role.into() {
4988 true
4989 } else {
4990 err_key = "command-whitelist-permission-denied";
4991 false
4992 }
4993 })
4994 .map(|_| {
4995 Content::localized_with_args("command-whitelist-removed", [(
4996 "username",
4997 username.to_owned(),
4998 )])
4999 })
5000 });
5001 edit_setting_feedback(server, client, edit, || {
5002 Content::localized_with_args(err_key, [("username", username)])
5003 })
5004 } else {
5005 Err(action.help_content())
5006 }
5007 } else {
5008 Err(action.help_content())
5009 }
5010}
5011
5012fn kick_player(
5013 server: &mut Server,
5014 (client, client_uuid): (EcsEntity, Uuid),
5015 (target_player, target_player_uuid): (EcsEntity, Uuid),
5016 reason: DisconnectReason,
5017) -> CmdResult<()> {
5018 verify_above_role(
5019 server,
5020 (client, client_uuid),
5021 (target_player, target_player_uuid),
5022 Content::localized("command-kick-higher-role"),
5023 )?;
5024 server.notify_client(target_player, ServerGeneral::Disconnect(reason));
5025 server
5026 .state
5027 .mut_resource::<EventBus<ClientDisconnectEvent>>()
5028 .emit_now(ClientDisconnectEvent(
5029 target_player,
5030 comp::DisconnectReason::Kicked,
5031 ));
5032 Ok(())
5033}
5034
5035fn handle_kick(
5036 server: &mut Server,
5037 client: EcsEntity,
5038 _target: EcsEntity,
5039 args: Vec<String>,
5040 action: &ServerChatCommand,
5041) -> CmdResult<()> {
5042 if let (Some(target_alias), reason_opt) = parse_cmd_args!(args, String, String) {
5043 let client_uuid = uuid(server, client, "client")?;
5044 let reason = reason_opt.unwrap_or_default();
5045 let ecs = server.state.ecs();
5046 let target_player = find_alias(ecs, &target_alias, true)?;
5047
5048 kick_player(
5049 server,
5050 (client, client_uuid),
5051 target_player,
5052 DisconnectReason::Kicked(reason.clone()),
5053 )?;
5054 server.notify_client(
5055 client,
5056 ServerGeneral::server_msg(
5057 ChatType::CommandInfo,
5058 Content::Plain(format!(
5059 "Kicked {} from the server with reason: {}",
5060 target_alias, reason
5061 )),
5062 ),
5063 );
5064 Ok(())
5065 } else {
5066 Err(action.help_content())
5067 }
5068}
5069
5070fn make_ban_info(server: &mut Server, client: EcsEntity, client_uuid: Uuid) -> CmdResult<BanInfo> {
5071 let client_username = uuid_to_username(server, client, client_uuid)?;
5072 let client_role = real_role(server, client_uuid, "client")?;
5073 let ban_info = BanInfo {
5074 performed_by: client_uuid,
5075 performed_by_username: client_username,
5076 performed_by_role: client_role.into(),
5077 };
5078 Ok(ban_info)
5079}
5080
5081fn ban_end_date(
5082 now: chrono::DateTime<Utc>,
5083 parse_duration: Option<HumanDuration>,
5084) -> CmdResult<Option<chrono::DateTime<Utc>>> {
5085 let end_date = parse_duration
5086 .map(|duration| chrono::Duration::from_std(duration.into()))
5087 .transpose()
5088 .map_err(|err| {
5089 Content::localized_with_args(
5090 "command-parse-duration-error",
5091 [("error", format!("{err:?}"))]
5092 )
5093 })?
5094 .and_then(|duration| now.checked_add_signed(duration));
5097 Ok(end_date)
5098}
5099
5100fn handle_ban(
5101 server: &mut Server,
5102 client: EcsEntity,
5103 _target: EcsEntity,
5104 args: Vec<String>,
5105 action: &ServerChatCommand,
5106) -> CmdResult<()> {
5107 let (Some(username), overwrite, parse_duration, reason_opt) =
5108 parse_cmd_args!(args, String, bool, HumanDuration, String)
5109 else {
5110 return Err(action.help_content());
5111 };
5112
5113 let reason = reason_opt.unwrap_or_default();
5114 let overwrite = overwrite.unwrap_or(false);
5115
5116 let client_uuid = uuid(server, client, "client")?;
5117 let ban_info = make_ban_info(server, client, client_uuid)?;
5118
5119 let player_uuid = find_username(server, &username)?;
5120
5121 let now = Utc::now();
5122 let end_date = ban_end_date(now, parse_duration)?;
5123
5124 let result = server.editable_settings_mut().banlist.ban_operation(
5125 server.data_dir().as_ref(),
5126 now,
5127 player_uuid,
5128 username.clone(),
5129 BanOperation::Ban {
5130 reason: reason.clone(),
5131 info: ban_info,
5132 upgrade_to_ip: false,
5133 end_date,
5134 },
5135 overwrite,
5136 );
5137 let (result, ban_info) = match result {
5138 Ok(info) => (Ok(()), info),
5139 Err(err) => (Err(err), None),
5140 };
5141
5142 edit_banlist_feedback(
5143 server,
5144 client,
5145 result,
5146 || {
5147 Content::localized_with_args("command-ban-added", [
5148 ("player", username.clone()),
5149 ("reason", reason),
5150 ])
5151 },
5152 || {
5153 Content::localized_with_args("command-ban-already-added", [(
5154 "player",
5155 username.clone(),
5156 )])
5157 },
5158 )?;
5159 let ecs = server.state.ecs();
5163 if let Ok(target_player) = find_uuid(ecs, player_uuid) {
5164 let _ = kick_player(
5165 server,
5166 (client, client_uuid),
5167 (target_player, player_uuid),
5168 ban_info.map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
5169 );
5170 }
5171 Ok(())
5172}
5173
5174fn handle_aura(
5175 server: &mut Server,
5176 client: EcsEntity,
5177 target: EcsEntity,
5178 args: Vec<String>,
5179 action: &ServerChatCommand,
5180) -> CmdResult<()> {
5181 let target_uid = uid(server, target, "target")?;
5182
5183 let (Some(aura_radius), aura_duration, new_entity, aura_target, Some(aura_kind_variant), spec) =
5184 parse_cmd_args!(args, f32, f32, bool, GroupTarget, AuraKindVariant, ..Vec<String>)
5185 else {
5186 return Err(action.help_content());
5187 };
5188 let new_entity = new_entity.unwrap_or(false);
5189 let aura_kind = match aura_kind_variant {
5190 AuraKindVariant::Buff => {
5191 let (Some(buff), strength, duration, misc_data_spec) =
5192 parse_cmd_args!(spec, String, f32, f64, String)
5193 else {
5194 return Err(Content::localized("command-aura-invalid-buff-parameters"));
5195 };
5196 let buffkind = parse_buffkind(&buff).ok_or_else(|| {
5197 Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
5198 })?;
5199 let buffdata = build_buff(
5200 buffkind,
5201 strength.unwrap_or(1.0),
5202 duration.unwrap_or(10.0),
5203 (!buffkind.is_simple())
5204 .then(|| {
5205 misc_data_spec.ok_or_else(|| {
5206 Content::localized_with_args("command-buff-data", [(
5207 "buff",
5208 buff.clone(),
5209 )])
5210 })
5211 })
5212 .transpose()?,
5213 )?;
5214
5215 AuraKind::Buff {
5216 kind: buffkind,
5217 data: buffdata,
5218 category: BuffCategory::Natural,
5219 source: if new_entity {
5220 BuffSource::World
5221 } else {
5222 BuffSource::Character { by: target_uid }
5223 },
5224 }
5225 },
5226 AuraKindVariant::FriendlyFire => AuraKind::FriendlyFire,
5227 AuraKindVariant::ForcePvP => AuraKind::ForcePvP,
5228 };
5229 let aura_target = server
5230 .state
5231 .read_component_copied::<Uid>(target)
5232 .map(|uid| match aura_target {
5233 Some(GroupTarget::InGroup) => AuraTarget::GroupOf(uid),
5234 Some(GroupTarget::OutOfGroup) => AuraTarget::NotGroupOf(uid),
5235 Some(GroupTarget::All) | None => AuraTarget::All,
5236 })
5237 .unwrap_or(AuraTarget::All);
5238
5239 let time = Time(server.state.get_time());
5240 let aura = Aura::new(
5241 aura_kind,
5242 aura_radius,
5243 aura_duration.map(|duration| Secs(duration as f64)),
5244 aura_target,
5245 time,
5246 );
5247
5248 if new_entity {
5249 let pos = position(server, target, "target")?;
5250 server
5251 .state
5252 .create_empty(pos)
5253 .with(comp::Auras::new(vec![aura]))
5254 .maybe_with(aura_duration.map(|duration| comp::Object::DeleteAfter {
5255 spawned_at: time,
5256 timeout: Duration::from_secs_f32(duration),
5257 }))
5258 .build();
5259 } else {
5260 let mut auras = server.state.ecs().write_storage::<comp::Auras>();
5261 if let Some(mut auras) = auras.get_mut(target) {
5262 auras.insert(aura);
5263 }
5264 }
5265
5266 server.notify_client(
5267 client,
5268 ServerGeneral::server_msg(
5269 ChatType::CommandInfo,
5270 Content::localized(if new_entity {
5271 "command-aura-spawn-new-entity"
5272 } else {
5273 "command-aura-spawn"
5274 }),
5275 ),
5276 );
5277
5278 Ok(())
5279}
5280
5281fn handle_ban_ip(
5282 server: &mut Server,
5283 client: EcsEntity,
5284 _target: EcsEntity,
5285 args: Vec<String>,
5286 action: &ServerChatCommand,
5287) -> CmdResult<()> {
5288 let (Some(username), overwrite, parse_duration, reason_opt) =
5289 parse_cmd_args!(args, String, bool, HumanDuration, String)
5290 else {
5291 return Err(action.help_content());
5292 };
5293
5294 let reason = reason_opt.unwrap_or_default();
5295 let overwrite = overwrite.unwrap_or(false);
5296
5297 let client_uuid = uuid(server, client, "client")?;
5298 let ban_info = make_ban_info(server, client, client_uuid)?;
5299
5300 let player_uuid = find_username(server, &username)?;
5301 let now = Utc::now();
5302 let end_date = ban_end_date(now, parse_duration)?;
5303
5304 let (players_to_kick, ban_result, frontend_info);
5305
5306 let player_ip_addr = if let Some(player_addr) = find_uuid(server.state.ecs(), player_uuid)
5307 .ok()
5308 .and_then(|player_entity| socket_addr(server, player_entity, &username).ok())
5309 {
5310 Some(NormalizedIpAddr::from(player_addr.ip()))
5311 } else {
5312 server
5313 .state()
5314 .ecs()
5315 .read_resource::<crate::RecentClientIPs>()
5316 .last_addrs
5317 .peek(&player_uuid)
5318 .cloned()
5319 };
5320
5321 if let Some(player_ip_addr) = player_ip_addr {
5323 let result = server.editable_settings_mut().banlist.ban_operation(
5324 server.data_dir().as_ref(),
5325 now,
5326 player_uuid,
5327 username.clone(),
5328 BanOperation::BanIp {
5329 reason: reason.clone(),
5330 info: ban_info,
5331 end_date,
5332 ip: player_ip_addr,
5333 },
5334 overwrite,
5335 );
5336 (ban_result, frontend_info) = match result {
5337 Ok(info) => (Ok(()), info),
5338 Err(err) => (Err(err), None),
5339 };
5340
5341 edit_banlist_feedback(
5342 server,
5343 client,
5344 ban_result,
5345 || {
5346 Content::localized_with_args("command-ban-ip-added", [
5347 ("player", username.clone()),
5348 ("reason", reason),
5349 ])
5350 },
5351 || {
5352 Content::localized_with_args("command-ban-already-added", [(
5353 "player",
5354 username.clone(),
5355 )])
5356 },
5357 )?;
5358
5359 let ecs = server.state.ecs();
5363 players_to_kick = (
5364 &ecs.entities(),
5365 &ecs.read_storage::<Client>(),
5366 &ecs.read_storage::<comp::Player>(),
5367 )
5368 .join()
5369 .filter(|(_, client, _)| {
5370 client
5371 .current_ip_addrs
5372 .iter()
5373 .any(|socket_addr| NormalizedIpAddr::from(socket_addr.ip()) == player_ip_addr)
5374 })
5375 .map(|(entity, _, player)| (entity, player.uuid()))
5376 .collect::<Vec<_>>();
5377 } else {
5380 let result = server.editable_settings_mut().banlist.ban_operation(
5381 server.data_dir().as_ref(),
5382 now,
5383 player_uuid,
5384 username.clone(),
5385 BanOperation::Ban {
5386 reason: reason.clone(),
5387 info: ban_info,
5388 upgrade_to_ip: true,
5389 end_date,
5390 },
5391 overwrite,
5392 );
5393
5394 (ban_result, frontend_info) = match result {
5395 Ok(info) => (Ok(()), info),
5396 Err(err) => (Err(err), None),
5397 };
5398
5399 edit_banlist_feedback(
5400 server,
5401 client,
5402 ban_result,
5403 || {
5404 Content::localized_with_args("command-ban-ip-queued", [
5405 ("player", username.clone()),
5406 ("reason", reason),
5407 ])
5408 },
5409 || {
5410 Content::localized_with_args("command-ban-already-added", [(
5411 "player",
5412 username.clone(),
5413 )])
5414 },
5415 )?;
5416
5417 let ecs = server.state.ecs();
5418 players_to_kick = find_uuid(ecs, player_uuid)
5419 .map(|entity| (entity, player_uuid))
5420 .into_iter()
5421 .collect();
5422 }
5423
5424 for (player_entity, player_uuid) in players_to_kick {
5425 let _ = kick_player(
5426 server,
5427 (client, client_uuid),
5428 (player_entity, player_uuid),
5429 frontend_info
5430 .clone()
5431 .map_or(DisconnectReason::Shutdown, DisconnectReason::Banned),
5432 );
5433 }
5434
5435 Ok(())
5436}
5437
5438fn handle_ban_log(
5439 server: &mut Server,
5440 client: EcsEntity,
5441 _target: EcsEntity,
5442 args: Vec<String>,
5443 action: &ServerChatCommand,
5444) -> CmdResult<()> {
5445 let (Some(username), max_entries) = parse_cmd_args!(args, String, i32) else {
5446 return Err(action.help_content());
5447 };
5448 let max_entries = max_entries
5449 .and_then(|i| usize::try_from(i).ok())
5450 .unwrap_or(10);
5451
5452 let player_uuid = find_username(server, &username)?;
5453
5454 let display_record = |action: &BanAction,
5455 username_at_ban: Option<&str>,
5456 date: &DateTime<Utc>| {
5457 let display_ban_info = |info: &BanInfo| {
5458 format!(
5459 "By: {} [{}] ({:?})",
5460 info.performed_by_username, info.performed_by, info.performed_by_role
5461 )
5462 };
5463 let action = match action {
5464 BanAction::Unban(ban_info) => format!("Unbanned\n {}", display_ban_info(ban_info)),
5465 BanAction::Ban(ban) => {
5466 format!(
5467 "Banned\n Reason: {}\n Until: {}{}{}",
5468 ban.reason,
5469 ban.end_date
5470 .map_or_else(|| "permanent".to_string(), |end_date| end_date.to_rfc3339()),
5471 if ban.upgrade_to_ip {
5472 "\n Will be upgraded to IP ban"
5473 } else {
5474 Default::default()
5475 },
5476 ban.info
5477 .as_ref()
5478 .map(|info| format!("\n {}", display_ban_info(info)))
5479 .unwrap_or_default()
5480 )
5481 },
5482 };
5483 format!(
5484 "\n{action}\n At: {}{}\n-------",
5485 date.to_rfc3339(),
5486 username_at_ban
5487 .filter(|at_ban| username != *at_ban)
5488 .map(|username| format!("\n Username at ban: {username}"))
5489 .unwrap_or_default(),
5490 )
5491 };
5492
5493 let editable_settings = server.editable_settings();
5494 let Some(entry) = editable_settings.banlist.uuid_bans().get(&player_uuid) else {
5495 return Err(Content::Plain(
5496 "No entries exist for this player".to_string(),
5497 ));
5498 };
5499 let mut ban_log = format!("Ban log for '{username}' [{player_uuid}]:\n\nBans:");
5500
5501 for (entry_i, record) in entry
5502 .history
5503 .iter()
5504 .chain([&entry.current])
5505 .rev()
5506 .enumerate()
5507 {
5508 if entry_i >= max_entries {
5509 ban_log.push_str(&format!(
5510 "\n...{} More...",
5511 entry.history.len() + 1 - entry_i
5512 ));
5513 break;
5514 }
5515 ban_log.push_str(&display_record(
5516 &record.action,
5517 Some(&record.username_when_performed),
5518 &record.date,
5519 ));
5520 }
5521
5522 ban_log.push_str("\n\nIP Bans:");
5523
5524 for (entry_i, record) in editable_settings
5525 .banlist
5526 .ip_bans()
5527 .values()
5528 .flat_map(|entry| {
5529 entry
5530 .history
5531 .iter()
5532 .chain([&entry.current])
5533 .rev()
5534 .filter(|record| {
5535 record
5536 .uuid_when_performed
5537 .is_some_and(|ip_ban_uuid| ip_ban_uuid == player_uuid)
5538 })
5539 })
5540 .enumerate()
5541 {
5542 if entry_i >= max_entries {
5543 ban_log.push_str("\n...More...");
5544 break;
5545 }
5546 ban_log.push_str(&display_record(&record.action, None, &record.date));
5547 }
5548
5549 server.notify_client(
5550 client,
5551 ServerGeneral::server_msg(ChatType::CommandInfo, Content::Plain(ban_log)),
5552 );
5553
5554 Ok(())
5555}
5556
5557fn handle_battlemode(
5558 server: &mut Server,
5559 client: EcsEntity,
5560 _target: EcsEntity,
5561 args: Vec<String>,
5562 _action: &ServerChatCommand,
5563) -> CmdResult<()> {
5564 if let Some(argument) = parse_cmd_args!(args, String) {
5565 let battle_mode = match argument.as_str() {
5566 "pvp" => BattleMode::PvP,
5567 "pve" => BattleMode::PvE,
5568 _ => return Err(Content::localized("command-battlemode-available-modes")),
5569 };
5570
5571 server.set_battle_mode_for(client, battle_mode);
5572 } else {
5573 server.get_battle_mode_for(client);
5574 }
5575
5576 Ok(())
5577}
5578
5579fn handle_battlemode_force(
5580 server: &mut Server,
5581 client: EcsEntity,
5582 target: EcsEntity,
5583 args: Vec<String>,
5584 action: &ServerChatCommand,
5585) -> CmdResult<()> {
5586 let ecs = server.state.ecs();
5587 let settings = ecs.read_resource::<Settings>();
5588
5589 if !settings.gameplay.battle_mode.allow_choosing() {
5590 return Err(Content::localized("command-disabled-by-settings"));
5591 }
5592
5593 let mode = parse_cmd_args!(args, String).ok_or_else(|| action.help_content())?;
5594 let mode = match mode.as_str() {
5595 "pvp" => BattleMode::PvP,
5596 "pve" => BattleMode::PvE,
5597 _ => return Err(Content::localized("command-battlemode-available-modes")),
5598 };
5599
5600 let mut players = ecs.write_storage::<comp::Player>();
5601 let mut player_info = players.get_mut(target).ok_or(Content::Plain(
5602 "Cannot get player component for target".to_string(),
5603 ))?;
5604 player_info.battle_mode = mode;
5605
5606 server.notify_client(
5607 client,
5608 ServerGeneral::server_msg(
5609 ChatType::CommandInfo,
5610 Content::localized_with_args("command-battlemode-updated", [(
5611 "battlemode",
5612 format!("{mode:?}"),
5613 )]),
5614 ),
5615 );
5616 Ok(())
5617}
5618
5619fn handle_unban(
5620 server: &mut Server,
5621 client: EcsEntity,
5622 _target: EcsEntity,
5623 args: Vec<String>,
5624 action: &ServerChatCommand,
5625) -> CmdResult<()> {
5626 let Some(username) = parse_cmd_args!(args, String) else {
5627 return Err(action.help_content());
5628 };
5629
5630 let player_uuid = find_username(server, &username)?;
5631
5632 let client_uuid = uuid(server, client, "client")?;
5633 let ban_info = make_ban_info(server, client, client_uuid)?;
5634
5635 let now = Utc::now();
5636
5637 let unban = BanOperation::Unban { info: ban_info };
5638
5639 let result = server.editable_settings_mut().banlist.ban_operation(
5640 server.data_dir().as_ref(),
5641 now,
5642 player_uuid,
5643 username.clone(),
5644 unban,
5645 false,
5646 );
5647
5648 edit_banlist_feedback(
5649 server,
5650 client,
5651 result.map(|_| ()),
5652 || Content::localized_with_args("command-unban-successful", [("player", username.clone())]),
5655 || {
5656 Content::localized_with_args("command-unban-already-unbanned", [(
5657 "player",
5658 username.clone(),
5659 )])
5660 },
5661 )
5662}
5663
5664fn handle_unban_ip(
5665 server: &mut Server,
5666 client: EcsEntity,
5667 _target: EcsEntity,
5668 args: Vec<String>,
5669 action: &ServerChatCommand,
5670) -> CmdResult<()> {
5671 let Some(username) = parse_cmd_args!(args, String) else {
5672 return Err(action.help_content());
5673 };
5674
5675 let player_uuid = find_username(server, &username)?;
5676
5677 let client_uuid = uuid(server, client, "client")?;
5678 let ban_info = make_ban_info(server, client, client_uuid)?;
5679
5680 let now = Utc::now();
5681
5682 let unban = BanOperation::UnbanIp {
5683 info: ban_info,
5684 uuid: player_uuid,
5685 };
5686
5687 let result = server.editable_settings_mut().banlist.ban_operation(
5688 server.data_dir().as_ref(),
5689 now,
5690 player_uuid,
5691 username.clone(),
5692 unban,
5693 false,
5694 );
5695
5696 edit_banlist_feedback(
5697 server,
5698 client,
5699 result.map(|_| ()),
5700 || {
5701 Content::localized_with_args("command-unban-ip-successful", [(
5702 "player",
5703 username.clone(),
5704 )])
5705 },
5706 || {
5707 Content::localized_with_args("command-unban-already-unbanned", [(
5708 "player",
5709 username.clone(),
5710 )])
5711 },
5712 )
5713}
5714
5715fn handle_server_physics(
5716 server: &mut Server,
5717 client: EcsEntity,
5718 _target: EcsEntity,
5719 args: Vec<String>,
5720 action: &ServerChatCommand,
5721) -> CmdResult<()> {
5722 if let (Some(username), enabled_opt, reason) = parse_cmd_args!(args, String, bool, String) {
5723 let uuid = find_username(server, &username)?;
5724 let server_force = enabled_opt.unwrap_or(true);
5725 let data_dir = server.data_dir();
5726
5727 let result = server
5728 .editable_settings_mut()
5729 .server_physics_force_list
5730 .edit(data_dir.as_ref(), |list| {
5731 if server_force {
5732 let Some(by) = server
5733 .state()
5734 .ecs()
5735 .read_storage::<comp::Player>()
5736 .get(client)
5737 .map(|player| (player.uuid(), player.alias.clone()))
5738 else {
5739 return Some(Some(Content::localized("command-you-dont-exist")));
5740 };
5741 list.insert(uuid, ServerPhysicsForceRecord {
5742 by: Some(by),
5743 reason,
5744 });
5745 Some(None)
5746 } else {
5747 list.remove(&uuid);
5748 Some(None)
5749 }
5750 });
5751
5752 if let Some((Some(error), _)) = result {
5753 return Err(error);
5754 }
5755
5756 server.notify_client(
5757 client,
5758 ServerGeneral::server_msg(
5759 ChatType::CommandInfo,
5760 Content::Plain(format!(
5761 "Updated physics settings for {} ({}): {:?}",
5762 username, uuid, server_force
5763 )),
5764 ),
5765 );
5766 Ok(())
5767 } else {
5768 Err(action.help_content())
5769 }
5770}
5771
5772fn handle_buff(
5773 server: &mut Server,
5774 _client: EcsEntity,
5775 target: EcsEntity,
5776 args: Vec<String>,
5777 action: &ServerChatCommand,
5778) -> CmdResult<()> {
5779 let (Some(buff), strength, duration, misc_data_spec) =
5780 parse_cmd_args!(args, String, f32, f64, String)
5781 else {
5782 return Err(action.help_content());
5783 };
5784
5785 let strength = strength.unwrap_or(0.01);
5786
5787 match buff.as_str() {
5788 "all" => {
5789 let duration = duration.unwrap_or(5.0);
5790 let buffdata = BuffData::new(strength, Some(Secs(duration)));
5791
5792 BUFF_PACK
5797 .iter()
5798 .filter_map(|kind_key| parse_buffkind(kind_key))
5799 .filter(|buffkind| buffkind.is_simple())
5800 .for_each(|buffkind| cast_buff(buffkind, buffdata, server, target));
5801 },
5802 "clear" => {
5803 if let Some(mut buffs) = server
5804 .state
5805 .ecs()
5806 .write_storage::<comp::Buffs>()
5807 .get_mut(target)
5808 {
5809 buffs.buffs.clear();
5810 buffs.kinds.clear();
5811 }
5812 },
5813 _ => {
5814 let buffkind = parse_buffkind(&buff).ok_or_else(|| {
5815 Content::localized_with_args("command-buff-unknown", [("buff", buff.clone())])
5816 })?;
5817 let buffdata = build_buff(
5818 buffkind,
5819 strength,
5820 duration.unwrap_or(match buffkind {
5821 BuffKind::ComboGeneration => 1.0,
5822 _ => 10.0,
5823 }),
5824 (!buffkind.is_simple())
5825 .then(|| {
5826 misc_data_spec.ok_or_else(|| {
5827 Content::localized_with_args("command-buff-data", [(
5828 "buff",
5829 buff.clone(),
5830 )])
5831 })
5832 })
5833 .transpose()?,
5834 )?;
5835
5836 cast_buff(buffkind, buffdata, server, target);
5837 },
5838 }
5839
5840 Ok(())
5841}
5842
5843fn build_buff(
5844 buff_kind: BuffKind,
5845 strength: f32,
5846 duration: f64,
5847 spec: Option<String>,
5848) -> CmdResult<BuffData> {
5849 if buff_kind.is_simple() {
5850 Ok(BuffData::new(strength, Some(Secs(duration))))
5851 } else {
5852 let spec = spec.expect("spec must be passed to build_buff if buff_kind is not simple");
5853
5854 let misc_data = match buff_kind {
5856 BuffKind::Polymorphed => {
5857 let Ok(npc::NpcBody(_id, mut body)) = spec.parse() else {
5858 return Err(Content::localized_with_args("command-buff-body-unknown", [
5859 ("spec", spec.clone()),
5860 ]));
5861 };
5862 MiscBuffData::Body(body())
5863 },
5864 BuffKind::Regeneration
5865 | BuffKind::Saturation
5866 | BuffKind::Potion
5867 | BuffKind::Agility
5868 | BuffKind::RestingHeal
5869 | BuffKind::Frenzied
5870 | BuffKind::EnergyRegen
5871 | BuffKind::ComboGeneration
5872 | BuffKind::IncreaseMaxEnergy
5873 | BuffKind::IncreaseMaxHealth
5874 | BuffKind::Invulnerability
5875 | BuffKind::ProtectingWard
5876 | BuffKind::Hastened
5877 | BuffKind::Fortitude
5878 | BuffKind::Reckless
5879 | BuffKind::Flame
5880 | BuffKind::Frigid
5881 | BuffKind::Lifesteal
5882 | BuffKind::ImminentCritical
5883 | BuffKind::Fury
5884 | BuffKind::Sunderer
5885 | BuffKind::Defiance
5886 | BuffKind::Bloodfeast
5887 | BuffKind::Berserk
5888 | BuffKind::Bleeding
5889 | BuffKind::Cursed
5890 | BuffKind::Burning
5891 | BuffKind::Crippled
5892 | BuffKind::Frozen
5893 | BuffKind::Wet
5894 | BuffKind::Ensnared
5895 | BuffKind::Poisoned
5896 | BuffKind::Parried
5897 | BuffKind::PotionSickness
5898 | BuffKind::Heatstroke
5899 | BuffKind::ScornfulTaunt
5900 | BuffKind::Rooted
5901 | BuffKind::Winded
5902 | BuffKind::Amnesia
5903 | BuffKind::OffBalance
5904 | BuffKind::Tenacity
5905 | BuffKind::Resilience => {
5906 if buff_kind.is_simple() {
5907 unreachable!("is_simple() above")
5908 } else {
5909 panic!("Buff Kind {buff_kind:?} is complex but has no defined spec parser")
5910 }
5911 },
5912 };
5913
5914 Ok(BuffData::new(strength, Some(Secs(duration))).with_misc_data(misc_data))
5915 }
5916}
5917
5918fn cast_buff(buffkind: BuffKind, data: BuffData, server: &mut Server, target: EcsEntity) {
5919 let ecs = &server.state.ecs();
5920 let mut buffs_all = ecs.write_storage::<comp::Buffs>();
5921 let stats = ecs.read_storage::<comp::Stats>();
5922 let masses = ecs.read_storage::<comp::Mass>();
5923 let time = ecs.read_resource::<Time>();
5924 if let Some(mut buffs) = buffs_all.get_mut(target) {
5925 let dest_info = DestInfo {
5926 stats: stats.get(target),
5927 mass: masses.get(target),
5928 };
5929 buffs.insert(
5930 Buff::new(
5931 buffkind,
5932 data,
5933 vec![],
5934 BuffSource::Command,
5935 *time,
5936 dest_info,
5937 None,
5938 ),
5939 *time,
5940 );
5941 }
5942}
5943
5944fn parse_buffkind(buff: &str) -> Option<BuffKind> { BUFF_PARSER.get(buff).copied() }
5945
5946fn handle_skill_preset(
5947 server: &mut Server,
5948 _client: EcsEntity,
5949 target: EcsEntity,
5950 args: Vec<String>,
5951 action: &ServerChatCommand,
5952) -> CmdResult<()> {
5953 if let Some(preset) = parse_cmd_args!(args, String) {
5954 if let Some(mut skill_set) = server
5955 .state
5956 .ecs_mut()
5957 .write_storage::<comp::SkillSet>()
5958 .get_mut(target)
5959 {
5960 match preset.as_str() {
5961 "clear" => {
5962 clear_skillset(&mut skill_set);
5963 Ok(())
5964 },
5965 preset => set_skills(&mut skill_set, preset),
5966 }
5967 } else {
5968 Err(Content::Plain("Player has no stats!".into()))
5969 }
5970 } else {
5971 Err(action.help_content())
5972 }
5973}
5974
5975fn clear_skillset(skill_set: &mut comp::SkillSet) { *skill_set = comp::SkillSet::default(); }
5976
5977fn set_skills(skill_set: &mut comp::SkillSet, preset: &str) -> CmdResult<()> {
5978 let presets = match common::cmd::SkillPresetManifest::load(PRESET_MANIFEST_PATH) {
5979 Ok(presets) => presets.read().0.clone(),
5980 Err(err) => {
5981 warn!("Error in preset: {}", err);
5982 return Err(Content::localized("command-skillpreset-load-error"));
5983 },
5984 };
5985 if let Some(preset) = presets.get(preset) {
5986 for (skill, level) in preset {
5987 let group = if let Some(group) = skill.skill_group_kind() {
5988 group
5989 } else {
5990 warn!("Skill in preset doesn't exist in any group");
5991 return Err(Content::localized("command-skillpreset-broken"));
5992 };
5993 for _ in 0..*level {
5994 let cost = skill_set.skill_cost(*skill);
5995 skill_set.add_skill_points(group, cost);
5996 match skill_set.unlock_skill(*skill) {
5997 Ok(_) | Err(comp::skillset::SkillUnlockError::SkillAlreadyUnlocked) => Ok(()),
5998 Err(err) => Err(Content::Plain(format!("{:?}", err))),
5999 }?;
6000 }
6001 }
6002 Ok(())
6003 } else {
6004 Err(Content::localized_with_args(
6005 "command-skillpreset-missing",
6006 [("preset", preset)],
6007 ))
6008 }
6009}
6010
6011fn handle_location(
6012 server: &mut Server,
6013 client: EcsEntity,
6014 target: EcsEntity,
6015 args: Vec<String>,
6016 _action: &ServerChatCommand,
6017) -> CmdResult<()> {
6018 if let Some(name) = parse_cmd_args!(args, String) {
6019 let loc = server.state.ecs().read_resource::<Locations>().get(&name)?;
6020 server.state.position_mut(target, true, |target_pos| {
6021 target_pos.0 = loc;
6022 })
6023 } else {
6024 let locations = server.state.ecs().read_resource::<Locations>();
6025 let mut locations = locations.iter().map(|s| s.as_str()).collect::<Vec<_>>();
6026 locations.sort_unstable();
6027 server.notify_client(
6028 client,
6029 ServerGeneral::server_msg(
6030 ChatType::CommandInfo,
6031 if locations.is_empty() {
6032 Content::localized("command-locations-empty")
6033 } else {
6034 Content::localized_with_args("command-locations-list", [(
6035 "locations",
6036 locations.join(", "),
6037 )])
6038 },
6039 ),
6040 );
6041 Ok(())
6042 }
6043}
6044
6045fn handle_create_location(
6046 server: &mut Server,
6047 client: EcsEntity,
6048 target: EcsEntity,
6049 args: Vec<String>,
6050 action: &ServerChatCommand,
6051) -> CmdResult<()> {
6052 if let Some(name) = parse_cmd_args!(args, String) {
6053 let target_pos = position(server, target, "target")?;
6054
6055 server
6056 .state
6057 .ecs_mut()
6058 .write_resource::<Locations>()
6059 .insert(name.clone(), target_pos.0)?;
6060 server.notify_client(
6061 client,
6062 ServerGeneral::server_msg(
6063 ChatType::CommandInfo,
6064 Content::localized_with_args("command-location-created", [("location", name)]),
6065 ),
6066 );
6067
6068 Ok(())
6069 } else {
6070 Err(action.help_content())
6071 }
6072}
6073
6074fn handle_delete_location(
6075 server: &mut Server,
6076 client: EcsEntity,
6077 _target: EcsEntity,
6078 args: Vec<String>,
6079 action: &ServerChatCommand,
6080) -> CmdResult<()> {
6081 if let Some(name) = parse_cmd_args!(args, String) {
6082 server
6083 .state
6084 .ecs_mut()
6085 .write_resource::<Locations>()
6086 .remove(&name)?;
6087 server.notify_client(
6088 client,
6089 ServerGeneral::server_msg(
6090 ChatType::CommandInfo,
6091 Content::localized_with_args("command-location-deleted", [("location", name)]),
6092 ),
6093 );
6094
6095 Ok(())
6096 } else {
6097 Err(action.help_content())
6098 }
6099}
6100
6101#[cfg(not(feature = "worldgen"))]
6102fn handle_weather_zone(
6103 _server: &mut Server,
6104 _client: EcsEntity,
6105 _target: EcsEntity,
6106 _args: Vec<String>,
6107 _action: &ServerChatCommand,
6108) -> CmdResult<()> {
6109 Err(Content::Plain(
6110 "Unsupported without worldgen enabled".into(),
6111 ))
6112}
6113
6114#[cfg(feature = "worldgen")]
6115fn handle_weather_zone(
6116 server: &mut Server,
6117 client: EcsEntity,
6118 _target: EcsEntity,
6119 args: Vec<String>,
6120 action: &ServerChatCommand,
6121) -> CmdResult<()> {
6122 if let (Some(name), radius, time) = parse_cmd_args!(args, String, f32, f32) {
6123 let radius = radius.map(|r| r / weather::CELL_SIZE as f32).unwrap_or(1.0);
6124 let time = time.unwrap_or(100.0);
6125
6126 let mut add_zone = |weather: weather::Weather| {
6127 if let Ok(pos) = position(server, client, "player") {
6128 let pos = pos.0.xy() / weather::CELL_SIZE as f32;
6129 if let Some(weather_job) = server
6130 .state
6131 .ecs_mut()
6132 .write_resource::<Option<WeatherJob>>()
6133 .as_mut()
6134 {
6135 weather_job.queue_zone(weather, pos, radius, time);
6136 }
6137 }
6138 };
6139 match name.as_str() {
6140 "clear" => {
6141 add_zone(weather::Weather {
6142 cloud: 0.0,
6143 rain: 0.0,
6144 wind: Vec2::zero(),
6145 });
6146 Ok(())
6147 },
6148 "cloudy" => {
6149 add_zone(weather::Weather {
6150 cloud: 0.4,
6151 rain: 0.0,
6152 wind: Vec2::zero(),
6153 });
6154 Ok(())
6155 },
6156 "rain" => {
6157 add_zone(weather::Weather {
6158 cloud: 0.1,
6159 rain: 0.15,
6160 wind: Vec2::new(1.0, -1.0),
6161 });
6162 Ok(())
6163 },
6164 "wind" => {
6165 add_zone(weather::Weather {
6166 cloud: 0.0,
6167 rain: 0.0,
6168 wind: Vec2::new(10.0, 10.0),
6169 });
6170 Ok(())
6171 },
6172 "storm" => {
6173 add_zone(weather::Weather {
6174 cloud: 0.3,
6175 rain: 0.3,
6176 wind: Vec2::new(15.0, 20.0),
6177 });
6178 Ok(())
6179 },
6180 _ => Err(Content::localized("command-weather-valid-values")),
6181 }
6182 } else {
6183 Err(action.help_content())
6184 }
6185}
6186
6187fn handle_lightning(
6188 server: &mut Server,
6189 client: EcsEntity,
6190 _target: EcsEntity,
6191 _args: Vec<String>,
6192 _action: &ServerChatCommand,
6193) -> CmdResult<()> {
6194 let pos = position(server, client, "player")?.0;
6195 server
6196 .state
6197 .ecs()
6198 .read_resource::<EventBus<Outcome>>()
6199 .emit_now(Outcome::Lightning { pos });
6200 Ok(())
6201}
6202
6203fn assign_body(server: &mut Server, target: EcsEntity, body: comp::Body) -> CmdResult<()> {
6204 insert_or_replace_component(server, target, body, "body")?;
6205 insert_or_replace_component(server, target, body.mass(), "mass")?;
6206 insert_or_replace_component(server, target, body.density(), "density")?;
6207 insert_or_replace_component(server, target, body.collider(), "collider")?;
6208
6209 if let Some(mut stat) = server
6210 .state
6211 .ecs_mut()
6212 .write_storage::<comp::Stats>()
6213 .get_mut(target)
6214 {
6215 stat.original_body = body;
6216 }
6217
6218 Ok(())
6219}
6220
6221fn handle_body(
6222 server: &mut Server,
6223 _client: EcsEntity,
6224 target: EcsEntity,
6225 args: Vec<String>,
6226 action: &ServerChatCommand,
6227) -> CmdResult<()> {
6228 if let Some(npc::NpcBody(_id, mut body)) = parse_cmd_args!(args, npc::NpcBody) {
6229 let body = body();
6230
6231 assign_body(server, target, body)
6232 } else {
6233 Err(action.help_content())
6234 }
6235}
6236
6237fn handle_scale(
6238 server: &mut Server,
6239 client: EcsEntity,
6240 target: EcsEntity,
6241 args: Vec<String>,
6242 action: &ServerChatCommand,
6243) -> CmdResult<()> {
6244 if let (Some(scale), reset_mass) = parse_cmd_args!(args, f32, bool) {
6245 let scale = scale.clamped(0.025, 1000.0);
6246 insert_or_replace_component(server, target, comp::Scale(scale), "target")?;
6247 if reset_mass.unwrap_or(true) {
6248 let mass = server.state.ecs()
6249 .read_storage::<comp::Body>()
6250 .get(target)
6251 .map(|body| body.mass().0 * scale.powi(3));
6253 if let Some(mass) = mass {
6254 insert_or_replace_component(server, target, comp::Mass(mass), "target")?;
6255 }
6256 }
6257 server.notify_client(
6258 client,
6259 ServerGeneral::server_msg(
6260 ChatType::CommandInfo,
6261 Content::localized_with_args("command-scale-set", [(
6262 "scale",
6263 format!("{scale:.1}"),
6264 )]),
6265 ),
6266 );
6267 Ok(())
6268 } else {
6269 Err(action.help_content())
6270 }
6271}
6272
6273fn handle_repair_equipment(
6275 server: &mut Server,
6276 client: EcsEntity,
6277 target: EcsEntity,
6278 args: Vec<String>,
6279 action: &ServerChatCommand,
6280) -> CmdResult<()> {
6281 let repair_inventory = parse_cmd_args!(args, bool).unwrap_or(false);
6282 let ecs = server.state.ecs();
6283 if let Some(mut inventory) = ecs.write_storage::<comp::Inventory>().get_mut(target) {
6284 let ability_map = ecs.read_resource::<AbilityMap>();
6285 let msm = ecs.read_resource::<MaterialStatManifest>();
6286 let slots = inventory
6287 .equipped_items_with_slot()
6288 .filter(|(_, item)| item.has_durability())
6289 .map(|(slot, _)| Slot::Equip(slot))
6290 .chain(
6291 repair_inventory
6292 .then(|| {
6293 inventory
6294 .slots_with_id()
6295 .filter(|(_, item)| {
6296 item.as_ref().is_some_and(|item| item.has_durability())
6297 })
6298 .map(|(slot, _)| Slot::Inventory(slot))
6299 })
6300 .into_iter()
6301 .flatten(),
6302 )
6303 .collect::<Vec<Slot>>();
6304
6305 for slot in slots {
6306 inventory.repair_item_at_slot(slot, &ability_map, &msm);
6307 }
6308
6309 let key = if repair_inventory {
6310 "command-repaired-inventory_items"
6311 } else {
6312 "command-repaired-items"
6313 };
6314 server.notify_client(
6315 client,
6316 ServerGeneral::server_msg(ChatType::CommandInfo, Content::localized(key)),
6317 );
6318 Ok(())
6319 } else {
6320 Err(action.help_content())
6321 }
6322}
6323
6324fn handle_tether(
6325 server: &mut Server,
6326 _client: EcsEntity,
6327 target: EcsEntity,
6328 args: Vec<String>,
6329 action: &ServerChatCommand,
6330) -> CmdResult<()> {
6331 enum Either<A, B> {
6332 Left(A),
6333 Right(B),
6334 }
6335
6336 impl<A: FromStr, B: FromStr> FromStr for Either<A, B> {
6337 type Err = B::Err;
6338
6339 fn from_str(s: &str) -> Result<Self, Self::Err> {
6340 A::from_str(s)
6341 .map(Either::Left)
6342 .or_else(|_| B::from_str(s).map(Either::Right))
6343 }
6344 }
6345 if let (Some(entity_target), length) = parse_cmd_args!(args, EntityTarget, Either<f32, bool>) {
6346 let entity_target = get_entity_target(entity_target, server)?;
6347
6348 let tether_leader = server.state.ecs().uid_from_entity(target);
6349 let tether_follower = server.state.ecs().uid_from_entity(entity_target);
6350
6351 if let (Some(leader), Some(follower)) = (tether_leader, tether_follower) {
6352 let base_len = server
6353 .state
6354 .read_component_cloned::<comp::Body>(target)
6355 .map(|b| b.dimensions().y * 1.5 + 1.0)
6356 .unwrap_or(6.0);
6357 let tether_length = match length {
6358 Some(Either::Left(l)) => l.max(0.0) + base_len,
6359 Some(Either::Right(true)) => {
6360 let leader_pos = position(server, target, "leader")?;
6361 let follower_pos = position(server, entity_target, "follower")?;
6362
6363 leader_pos.0.distance(follower_pos.0) + base_len
6364 },
6365 _ => base_len,
6366 };
6367 server
6368 .state
6369 .link(Tethered {
6370 leader,
6371 follower,
6372 tether_length,
6373 })
6374 .map_err(|_| Content::Plain("Failed to tether entities".into()))
6375 } else {
6376 Err(Content::Plain("Tether members don't have Uids.".into()))
6377 }
6378 } else {
6379 Err(action.help_content())
6380 }
6381}
6382
6383fn handle_destroy_tethers(
6384 server: &mut Server,
6385 client: EcsEntity,
6386 target: EcsEntity,
6387 _args: Vec<String>,
6388 _action: &ServerChatCommand,
6389) -> CmdResult<()> {
6390 let mut destroyed = false;
6391 destroyed |= server
6392 .state
6393 .ecs()
6394 .write_storage::<Is<common::tether::Leader>>()
6395 .remove(target)
6396 .is_some();
6397 destroyed |= server
6398 .state
6399 .ecs()
6400 .write_storage::<Is<common::tether::Follower>>()
6401 .remove(target)
6402 .is_some();
6403 if destroyed {
6404 server.notify_client(
6405 client,
6406 ServerGeneral::server_msg(
6407 ChatType::CommandInfo,
6408 Content::localized("command-destroyed-tethers"),
6409 ),
6410 );
6411 Ok(())
6412 } else {
6413 Err(Content::localized("command-destroyed-no-tethers"))
6414 }
6415}
6416
6417fn handle_mount(
6418 server: &mut Server,
6419 _client: EcsEntity,
6420 target: EcsEntity,
6421 args: Vec<String>,
6422 action: &ServerChatCommand,
6423) -> CmdResult<()> {
6424 if let Some(entity_target) = parse_cmd_args!(args, EntityTarget) {
6425 let entity_target = get_entity_target(entity_target, server)?;
6426
6427 let rider = server.state.ecs().uid_from_entity(target);
6428 let mount = server.state.ecs().uid_from_entity(entity_target);
6429
6430 if let (Some(rider), Some(mount)) = (rider, mount) {
6431 server
6432 .state
6433 .link(common::mounting::Mounting { mount, rider })
6434 .map_err(|_| Content::Plain("Failed to mount entities".into()))
6435 } else {
6436 Err(Content::Plain(
6437 "Mount and/or rider doesn't have an Uid component.".into(),
6438 ))
6439 }
6440 } else {
6441 Err(action.help_content())
6442 }
6443}
6444
6445fn handle_dismount(
6446 server: &mut Server,
6447 client: EcsEntity,
6448 target: EcsEntity,
6449 _args: Vec<String>,
6450 _action: &ServerChatCommand,
6451) -> CmdResult<()> {
6452 let mut destroyed = false;
6453 destroyed |= server
6454 .state
6455 .ecs()
6456 .write_storage::<Is<common::mounting::Rider>>()
6457 .remove(target)
6458 .is_some();
6459 destroyed |= server
6460 .state
6461 .ecs()
6462 .write_storage::<Is<common::mounting::VolumeRider>>()
6463 .remove(target)
6464 .is_some();
6465 destroyed |= server
6466 .state
6467 .ecs()
6468 .write_storage::<Is<common::mounting::Mount>>()
6469 .remove(target)
6470 .is_some();
6471 destroyed |= server
6472 .state
6473 .ecs()
6474 .write_storage::<common::mounting::VolumeRiders>()
6475 .get_mut(target)
6476 .is_some_and(|volume_riders| volume_riders.clear());
6477
6478 if destroyed {
6479 server.notify_client(
6480 client,
6481 ServerGeneral::server_msg(
6482 ChatType::CommandInfo,
6483 Content::localized("command-dismounted"),
6484 ),
6485 );
6486 Ok(())
6487 } else {
6488 Err(Content::localized("command-no-dismount"))
6489 }
6490}
6491
6492#[cfg(feature = "worldgen")]
6493fn handle_spot(
6494 server: &mut Server,
6495 _client: EcsEntity,
6496 target: EcsEntity,
6497 args: Vec<String>,
6498 action: &ServerChatCommand,
6499) -> CmdResult<()> {
6500 let Some(target_spot) = parse_cmd_args!(args, String) else {
6501 return Err(action.help_content());
6502 };
6503
6504 let maybe_spot_kind = SPOT_PARSER.get(&target_spot);
6505
6506 let target_pos = server
6507 .state
6508 .read_component_copied::<comp::Pos>(target)
6509 .ok_or(Content::localized_with_args(
6510 "command-position-unavailable",
6511 [("target", "target")],
6512 ))?;
6513 let target_chunk = target_pos.0.xy().wpos_to_cpos().as_();
6514
6515 let world = server.state.ecs().read_resource::<Arc<world::World>>();
6516 let spot_chunk = Spiral2d::new()
6517 .map(|o| target_chunk + o)
6518 .filter(|chunk| world.sim().get(*chunk).is_some())
6519 .take(world.sim().map_size_lg().chunks_len())
6520 .find(|chunk| {
6521 world.sim().get(*chunk).is_some_and(|chunk| {
6522 if let Some(spot) = &chunk.spot {
6523 match spot {
6524 Spot::RonFile(spot) => spot.base_structures == target_spot,
6525 spot_kind => Some(spot_kind) == maybe_spot_kind,
6526 }
6527 } else {
6528 false
6529 }
6530 })
6531 });
6532
6533 if let Some(spot_chunk) = spot_chunk {
6534 let pos = spot_chunk.cpos_to_wpos_center();
6535 let uplift = 100.0;
6539 let pos = (pos.as_() + 0.5).with_z(world.sim().get_surface_alt_approx(pos) + uplift);
6540 drop(world);
6541 server.state.position_mut(target, true, |target_pos| {
6542 *target_pos = comp::Pos(pos);
6543 })?;
6544 Ok(())
6545 } else {
6546 Err(Content::localized("command-spot-spot_not_found"))
6547 }
6548}
6549
6550#[cfg(not(feature = "worldgen"))]
6551fn handle_spot(
6552 _: &mut Server,
6553 _: EcsEntity,
6554 _: EcsEntity,
6555 _: Vec<String>,
6556 _: &ServerChatCommand,
6557) -> CmdResult<()> {
6558 Err(Content::localized("command-spot-world_feature"))
6559}