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