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