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