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