Implement TestClient world commands (#93)
Some checks failed
Native code generation / deterministic (push) Failing after 31s
Imaging and meshing gate / native (push) Successful in 5m30s
JPEG 2000 feature / linux (push) Successful in 2m47s
Native Rust workspace compile / compile (push) Successful in 22m56s
Skia feature / linux (push) Successful in 31m28s

This commit is contained in:
2026-08-11 12:10:19 +00:00
parent ea10b7672c
commit 374d7958f1
9 changed files with 4182 additions and 13 deletions

View File

@@ -1,6 +1,7 @@
//! Native `TestClient` shell, registry, and first-wave command groups.
mod inventory;
mod world;
use crate::commands::TEST_CLIENT_COMMANDS;
use clap::Parser;
@@ -68,6 +69,52 @@ pub const IMPLEMENTED_TEST_CLIENT_COMMANDS: &[&str] = &[
"UploadScriptCommand",
"ViewNotecardCommand",
"XferCommand",
"DownloadTerrainCommand",
"GetEstateCovenantCommand",
"UploadRawTerrainCommand",
"AgentLocationsCommand",
"FindSimCommand",
"GridLayerCommand",
"GridMapCommand",
"ParcelDetailsCommand",
"ParcelInfoCommand",
"ParcelPrimOwnersCommand",
"ParcelSelectObjectsCommand",
"SyntaxIdCommand",
"WindCommand",
"BackCommand",
"CrossRegionCommand",
"CrouchCommand",
"FlyCommand",
"FlyToCommand",
"FollowCommand",
"ForwardCommand",
"GoHome",
"GotoCommand",
"GotoLandmark",
"JumpCommand",
"LeftCommand",
"LocationCommand",
"MoveToCommand",
"RightCommand",
"SetHome",
"SitCommand",
"SitOnCommand",
"StandCommand",
"TurnToCommand",
"ChangePermsCommand",
"DeRezObjectCommand",
"DownloadTextureCommand",
"ExportCommand",
"ExportParticlesCommand",
"FindObjectsCommand",
"FindTextureCommand",
"ImportCommand",
"PrimCountCommand",
"PrimInfoCommand",
"PrimRegexCommand",
"TexturesCommand",
"TreeCommand",
"EchoMasterCommand",
"IMCommand",
"IMGroupCommand",
@@ -106,7 +153,7 @@ pub fn pending_test_client_commands() -> Vec<&'static str> {
version,
about = "Run the native LibreMetaverse multi-client command shell",
long_about = None,
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper, plus the inventory/assets and appearance groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
after_help = "Owned commands include @, debug, echomaster, help, im, imgroup, load, login, logpacket, logout, md5, quit, say, setmaster, setmasterkey, shout, showeffects, sleep, waitforlogin, whisper, plus the inventory, appearance, asset, movement, object, parcel, estate, and grid groups. Run `help` or `help COMMAND` inside the shell for the complete command registry."
)]
#[allow(clippy::struct_excessive_bools)]
struct Cli {
@@ -137,7 +184,7 @@ struct Cli {
/// Permit commands from known members of joined group-chat sessions.
#[arg(long)]
groupcommands: bool,
/// Preserve the upstream flag for later texture-aware command groups.
/// Automatically fetch newly observed object textures into the asset cache.
#[arg(long)]
gettextures: bool,
/// Run bounded terminal commands from a file before the interactive loop.
@@ -160,6 +207,10 @@ struct Cli {
/// Permit commands that spend or transfer L$; each command also requires confirmation.
#[arg(long)]
allow_spending: bool,
/// Permit estate-owner commands; mutations still require their other gates.
#[arg(long)]
allow_estate_actions: bool,
}
#[derive(Debug)]
@@ -245,7 +296,7 @@ enum ClientEvent {
type BackendFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
type InventoryFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
trait ClientBackend: Send + Sync + inventory::Backend {
trait ClientBackend: Send + Sync + inventory::Backend + world::Backend {
fn id(&self) -> UUID;
fn name(&self) -> String;
fn connected(&self) -> bool;
@@ -280,6 +331,7 @@ struct LiveBackend {
name: String,
subscriptions: Mutex<Vec<Subscription>>,
inventory_state: Mutex<inventory::State>,
world_state: Mutex<world::State>,
closed: AtomicBool,
}
@@ -290,6 +342,7 @@ impl LiveBackend {
events: mpsc::Sender<ClientEvent>,
dropped: Arc<AtomicUsize>,
account_policy: inventory::MutationPolicy,
get_textures: bool,
cancellation: CancellationToken,
) -> Result<Arc<Self>, ProgramError> {
let mut client = GridClient::new()
@@ -339,6 +392,7 @@ impl LiveBackend {
name,
subscriptions: Mutex::new(Vec::new()),
inventory_state: Mutex::new(inventory::State::new(account_policy)),
world_state: Mutex::new(world::State::new(account_policy, get_textures)),
closed: AtomicBool::new(false),
});
backend.install(&events, &dropped);
@@ -457,6 +511,7 @@ impl LiveBackend {
);
},
)));
world::install_live(self, &mut subscriptions);
*lock(&self.subscriptions) = subscriptions;
}
}
@@ -624,6 +679,7 @@ struct FakeBackend {
people: Arc<Mutex<HashMap<String, UUID>>>,
group_members: Arc<Mutex<HashSet<UUID>>>,
inventory_state: Mutex<inventory::State>,
world_state: Mutex<world::State>,
}
impl FakeBackend {
@@ -633,6 +689,7 @@ impl FakeBackend {
people: Arc<Mutex<HashMap<String, UUID>>>,
group_members: Arc<Mutex<HashSet<UUID>>>,
policy: inventory::MutationPolicy,
get_textures: bool,
) -> Self {
Self {
id,
@@ -642,6 +699,7 @@ impl FakeBackend {
people,
group_members,
inventory_state: Mutex::new(inventory::State::new(policy)),
world_state: Mutex::new(world::State::new(policy, get_textures)),
}
}
@@ -763,6 +821,10 @@ enum CommandCategory {
Appearance,
Communication,
Inventory,
Movement,
Objects,
Parcel,
Simulator,
TestClient,
Other,
}
@@ -776,6 +838,7 @@ enum CommandHandler {
Im,
ImGroup,
Inventory(inventory::Command),
World(world::Command),
Load,
Login,
LogPacket,
@@ -936,6 +999,7 @@ fn built_in_commands() -> HashMap<String, CommandDefinition> {
.into_iter()
.chain(communication_commands())
.chain(inventory::commands())
.chain(world::commands())
.map(|(name, description, category, handler)| {
(
name.into(),
@@ -1081,6 +1145,7 @@ struct ClientManager {
default_master_key: UUID,
default_group_commands: bool,
default_mutation_policy: inventory::MutationPolicy,
default_get_textures: bool,
cancellation: CancellationTokenSource,
output: Arc<ProgramOutput>,
events_tx: mpsc::Sender<ClientEvent>,
@@ -1103,6 +1168,7 @@ impl ClientManager {
master_key: UUID,
group_commands: bool,
mutation_policy: inventory::MutationPolicy,
get_textures: bool,
output: Arc<ProgramOutput>,
) -> Self {
let (events_tx, events_rx) = mpsc::channel(EVENT_QUEUE_CAPACITY);
@@ -1119,6 +1185,7 @@ impl ClientManager {
default_master_key: master_key,
default_group_commands: group_commands,
default_mutation_policy: mutation_policy,
default_get_textures: get_textures,
cancellation: CancellationTokenSource::new(),
output,
events_tx,
@@ -1179,6 +1246,7 @@ impl ClientManager {
Arc::clone(&self.fake_people),
Arc::clone(&self.fake_group_members),
self.default_mutation_policy,
self.default_get_textures,
));
self.add_client(ManagedClient {
backend,
@@ -1206,6 +1274,7 @@ impl ClientManager {
self.events_tx.clone(),
Arc::clone(&self.dropped_events),
self.default_mutation_policy,
self.default_get_textures,
self.cancellation.token(),
)
.await
@@ -1220,6 +1289,7 @@ impl ClientManager {
Arc::clone(&self.fake_people),
Arc::clone(&self.fake_group_members),
self.default_mutation_policy,
self.default_get_textures,
))),
Err(_) => Err(ProgramError::Client(
"could not make fake client UUID".into(),
@@ -1710,6 +1780,9 @@ async fn execute_client_command(
CommandHandler::Inventory(command) => {
inventory::execute(client.backend.as_ref(), *command, args, from, cancellation).await
}
CommandHandler::World(command) => {
world::execute(client.backend.as_ref(), *command, args, cancellation).await
}
CommandHandler::Md5 => {
if args.len() != 1 {
return "Usage: md5 [password]".into();
@@ -1955,8 +2028,11 @@ pub fn main_entry() -> ExitCode {
}
async fn run(cli: Cli) -> Result<(), ProgramError> {
let mutation_policy =
inventory::MutationPolicy::new(cli.allow_live_mutations, cli.allow_spending);
let mutation_policy = inventory::MutationPolicy::new(
cli.allow_live_mutations,
cli.allow_spending,
cli.allow_estate_actions,
);
let master_key = match cli.masterkey.as_deref() {
Some(value) => UUID::new_with_string(value.into())
.map_err(|_| ProgramError::Usage("--masterkey must be a UUID"))?,
@@ -1978,6 +2054,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
master_key,
cli.groupcommands,
mutation_policy,
cli.gettextures,
Arc::clone(&output),
);
run_fake_script(&mut manager, path).await?;
@@ -2011,6 +2088,7 @@ async fn run(cli: Cli) -> Result<(), ProgramError> {
master_key,
cli.groupcommands,
mutation_policy,
cli.gettextures,
Arc::clone(&output),
);
for account in accounts {
@@ -2285,6 +2363,10 @@ async fn run_fake_event_directive(
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
return Ok(());
}
if let Some(result) = world::apply_fake_directive(manager, fields) {
result.map_err(|reason| ProgramError::InvalidInput { line, reason })?;
return Ok(());
}
match fields {
["chat", client, source, name, message] => {
let event = ClientEvent::Chat {

View File

@@ -222,18 +222,23 @@ pub(super) struct MutationPolicy(u8);
impl MutationPolicy {
const MUTATIONS: u8 = 1;
const SPENDING: u8 = 1 << 1;
const ESTATE: u8 = 1 << 2;
pub(super) const fn new(mutations: bool, spending: bool) -> Self {
Self((mutations as u8) | ((spending as u8) << 1))
pub(super) const fn new(mutations: bool, spending: bool, estate: bool) -> Self {
Self((mutations as u8) | ((spending as u8) << 1) | ((estate as u8) << 2))
}
const fn mutations(self) -> bool {
pub(super) const fn mutations(self) -> bool {
self.0 & Self::MUTATIONS != 0
}
const fn spending(self) -> bool {
pub(super) const fn spending(self) -> bool {
self.0 & Self::SPENDING != 0
}
pub(super) const fn estate(self) -> bool {
self.0 & Self::ESTATE != 0
}
}
pub(super) struct State {
@@ -1662,7 +1667,7 @@ fn decode_tga(bytes: &[u8]) -> Result<ManagedImage, String> {
Ok(image)
}
fn encode_tga(image: &ManagedImage) -> Result<Vec<u8>, String> {
pub(super) fn encode_tga(image: &ManagedImage) -> Result<Vec<u8>, String> {
image
.validate()
.map_err(|_| "Invalid decoded image layout")?;

File diff suppressed because it is too large Load Diff