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