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