Complete first release candidate audit (#107)
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
This commit is contained in:
@@ -9,139 +9,58 @@
|
||||
///
|
||||
/// Native Rust mapping of C# `LibreMetaverse.Utilities.ConnectionManager` using decision
|
||||
/// `native_metacrate_type`. See the [pinned C# source](https://github.com/cinderblocks/libremetaverse/tree/2aa70bb68513b39795da5d13c88f31b86e85a3ba).
|
||||
pub struct ConnectionManager;
|
||||
impl ConnectionManager {
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.#ctor(LibreMetaverse.GridClient,System.Int32)`.
|
||||
///
|
||||
/// C# signature: `LibreMetaverse.Utilities.ConnectionManager(LibreMetaverse.GridClient client, System.Int32 timerFrequency)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
pub fn new(
|
||||
client: libremetaverse::GridClient,
|
||||
timer_frequency: i32,
|
||||
) -> Result<Self, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.ConnectionManager.#ctor(LibreMetaverse.GridClient,System.Int32)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.PersistentLogin(LibreMetaverse.GridClient,System.String,System.String,System.String,System.String,System.String,System.String)`.
|
||||
///
|
||||
/// C# signature: `System.Threading.Tasks.Task<System.Boolean> LibreMetaverse.Utilities.ConnectionManager.PersistentLogin(LibreMetaverse.GridClient client, System.String firstName, System.String lastName, System.String password, System.String userAgent, System.String start, System.String author)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned,owned,owned,owned`; async `async`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
pub async fn persistent_login(
|
||||
client: libremetaverse::GridClient,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
password: String,
|
||||
user_agent: String,
|
||||
start: String,
|
||||
author: String,
|
||||
) -> Result<bool, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.ConnectionManager.PersistentLogin(LibreMetaverse.GridClient,System.String,System.String,System.String,System.String,System.String,System.String)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.StayInSim(System.UInt64,LibreMetaverse.Vector3)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.ConnectionManager.StayInSim(System.UInt64 handle, LibreMetaverse.Vector3 desiredPosition)`.
|
||||
/// Mapping contract: ownership `shared_self,owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
pub fn stay_in_sim(
|
||||
&self,
|
||||
handle: u64,
|
||||
desired_position: libremetaverse_types::Vector3,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.ConnectionManager.StayInSim(System.UInt64,LibreMetaverse.Vector3)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.Stop`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.ConnectionManager.Stop()`.
|
||||
/// Mapping contract: ownership `shared_self`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
pub fn stop(&self) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented("M:LibreMetaverse.Utilities.ConnectionManager.Stop")
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.#ctor(LibreMetaverse.GridClient,System.Int32)`.
|
||||
///
|
||||
/// C# signature: `LibreMetaverse.Utilities.ConnectionManager(LibreMetaverse.GridClient client, System.Int32 timerFrequency)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.PersistentLogin(LibreMetaverse.GridClient,System.String,System.String,System.String,System.String,System.String,System.String)`.
|
||||
///
|
||||
/// C# signature: `System.Threading.Tasks.Task<System.Boolean> LibreMetaverse.Utilities.ConnectionManager.PersistentLogin(LibreMetaverse.GridClient client, System.String firstName, System.String lastName, System.String password, System.String userAgent, System.String start, System.String author)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned,owned,owned,owned`; async `async`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.StayInSim(System.UInt64,LibreMetaverse.Vector3)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.ConnectionManager.StayInSim(System.UInt64 handle, LibreMetaverse.Vector3 desiredPosition)`.
|
||||
/// Mapping contract: ownership `shared_self,owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.ConnectionManager.Stop`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.ConnectionManager.Stop()`.
|
||||
/// Mapping contract: ownership `shared_self`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `direct_snake_case`; kind `method`.
|
||||
pub use crate::service::ConnectionManager;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Utilities.Realism`.
|
||||
///
|
||||
/// Native Rust mapping of C# `LibreMetaverse.Utilities.Realism` using decision
|
||||
/// `native_metacrate_type`. See the [pinned C# source](https://github.com/cinderblocks/libremetaverse/tree/2aa70bb68513b39795da5d13c88f31b86e85a3ba).
|
||||
pub struct Realism;
|
||||
impl Realism {
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient client, System.String message)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub fn chat_with_grid_client_string(
|
||||
client: libremetaverse::GridClient,
|
||||
message: String,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient client, System.String message, LibreMetaverse.ChatType type, System.Int32 cps)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub fn chat_with_grid_client_string_chat_type_int32(
|
||||
client: libremetaverse::GridClient,
|
||||
message: String,
|
||||
type_: libremetaverse::ChatType,
|
||||
cps: i32,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.ChatAsync(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32,System.Threading.CancellationToken)`.
|
||||
///
|
||||
/// C# signature: `System.Threading.Tasks.Task LibreMetaverse.Utilities.Realism.ChatAsync(LibreMetaverse.GridClient client, System.String message, LibreMetaverse.ChatType type, System.Int32 cps, System.Threading.CancellationToken cancellationToken = default)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned,optional_owned`; async `async`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub async fn chat_with_grid_client_string_chat_type_int32_cancellation_token(
|
||||
client: libremetaverse::GridClient,
|
||||
message: String,
|
||||
type_: libremetaverse::ChatType,
|
||||
cps: i32,
|
||||
cancellation_token: Option<libremetaverse_types::compat::CancellationToken>,
|
||||
) -> Result<(), crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.Realism.ChatAsync(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32,System.Threading.CancellationToken)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient)`.
|
||||
///
|
||||
/// C# signature: `System.Boolean LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient client)`.
|
||||
/// Mapping contract: ownership `owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub fn shoot_with_grid_client(
|
||||
client: libremetaverse::GridClient,
|
||||
) -> Result<bool, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient)",
|
||||
)
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient,LibreMetaverse.Vector3)`.
|
||||
///
|
||||
/// C# signature: `System.Boolean LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient client, LibreMetaverse.Vector3 target)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub fn shoot_with_grid_client_vector3(
|
||||
client: libremetaverse::GridClient,
|
||||
target: libremetaverse_types::Vector3,
|
||||
) -> Result<bool, crate::Error> {
|
||||
libremetaverse_types::not_implemented(
|
||||
"M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient,LibreMetaverse.Vector3)",
|
||||
)
|
||||
}
|
||||
}
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient client, System.String message)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32)`.
|
||||
///
|
||||
/// C# signature: `System.Void LibreMetaverse.Utilities.Realism.Chat(LibreMetaverse.GridClient client, System.String message, LibreMetaverse.ChatType type, System.Int32 cps)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.ChatAsync(LibreMetaverse.GridClient,System.String,LibreMetaverse.ChatType,System.Int32,System.Threading.CancellationToken)`.
|
||||
///
|
||||
/// C# signature: `System.Threading.Tasks.Task LibreMetaverse.Utilities.Realism.ChatAsync(LibreMetaverse.GridClient client, System.String message, LibreMetaverse.ChatType type, System.Int32 cps, System.Threading.CancellationToken cancellationToken = default)`.
|
||||
/// Mapping contract: ownership `owned,owned,owned,owned,optional_owned`; async `async`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient)`.
|
||||
///
|
||||
/// C# signature: `System.Boolean LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient client)`.
|
||||
/// Mapping contract: ownership `owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
/// C# member: `M:LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient,LibreMetaverse.Vector3)`.
|
||||
///
|
||||
/// C# signature: `System.Boolean LibreMetaverse.Utilities.Realism.Shoot(LibreMetaverse.GridClient client, LibreMetaverse.Vector3 target)`.
|
||||
/// Mapping contract: ownership `owned,owned`; async `sync`;
|
||||
/// errors `Result<_, crate::Error>`; overload `descriptive_overload_name`; kind `method`.
|
||||
pub use crate::service::Realism;
|
||||
|
||||
/// C# type: `T:LibreMetaverse.Utilities.WaterType`.
|
||||
///
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
extern crate self as libremetaverse_utilities;
|
||||
|
||||
mod generated;
|
||||
#[allow(clippy::all, clippy::pedantic)] // Public shapes mirror the pinned C# API.
|
||||
mod service;
|
||||
|
||||
pub use generated::*;
|
||||
pub use libremetaverse as core;
|
||||
|
||||
362
crates/libremetaverse-utilities/src/service.rs
Normal file
362
crates/libremetaverse-utilities/src/service.rs
Normal file
@@ -0,0 +1,362 @@
|
||||
//! Native implementations of the upstream connection and realism helpers.
|
||||
|
||||
#![allow(clippy::missing_errors_doc)] // Signatures and error semantics mirror the mapped C# API.
|
||||
|
||||
use libremetaverse::{ChatType, GridClient};
|
||||
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
|
||||
use libremetaverse_types::{Error, UUID, Vector3};
|
||||
use std::sync::{Arc, Mutex, MutexGuard, mpsc};
|
||||
use std::thread::JoinHandle;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
const TYPE_ANIMATION: &str = "c541c47f-e0c0-058b-ad1a-d6ae3a4584d9";
|
||||
|
||||
fn lock<T>(value: &Mutex<T>) -> MutexGuard<'_, T> {
|
||||
value
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
}
|
||||
|
||||
struct CheckLoop {
|
||||
stop: mpsc::Sender<()>,
|
||||
cancellation: Arc<CancellationTokenSource>,
|
||||
worker: JoinHandle<()>,
|
||||
}
|
||||
|
||||
/// Keeps a client in one simulator and provides the upstream persistent-login policy.
|
||||
pub struct ConnectionManager {
|
||||
client: GridClient,
|
||||
timer_frequency: Duration,
|
||||
destination: Arc<Mutex<(u64, Vector3)>>,
|
||||
check_loop: Mutex<Option<CheckLoop>>,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
/// Creates an idle connection manager. Monitoring starts with [`Self::stay_in_sim`].
|
||||
pub fn new(client: GridClient, timer_frequency: i32) -> Result<Self, Error> {
|
||||
let milliseconds = u64::try_from(timer_frequency).map_err(|_| Error::Argument)?;
|
||||
if milliseconds == 0 {
|
||||
return Err(Error::Argument);
|
||||
}
|
||||
Ok(Self {
|
||||
client,
|
||||
timer_frequency: Duration::from_millis(milliseconds),
|
||||
destination: Arc::new(Mutex::new((0, Vector3::zero()))),
|
||||
check_loop: Mutex::new(None),
|
||||
})
|
||||
}
|
||||
|
||||
/// Retries transient login errors with the delays defined by `LibreMetaverse`.
|
||||
pub async fn persistent_login(
|
||||
client: GridClient,
|
||||
first_name: String,
|
||||
last_name: String,
|
||||
password: String,
|
||||
user_agent: String,
|
||||
start: String,
|
||||
author: String,
|
||||
) -> Result<bool, Error> {
|
||||
let mut unknown_logins = 0_u8;
|
||||
loop {
|
||||
if client
|
||||
.network()
|
||||
.login_with_string_string_string_string_string_string_cancellation_token(
|
||||
first_name.clone(),
|
||||
last_name.clone(),
|
||||
password.clone(),
|
||||
user_agent.clone(),
|
||||
start.clone(),
|
||||
author.clone(),
|
||||
None,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let wait = match client.network().login_error_key().as_str() {
|
||||
"god" => Duration::from_mins(10),
|
||||
"key" | "disabled" => return Ok(false),
|
||||
"presence" | "timed out" | "no connection" | "bad response" => {
|
||||
Duration::from_mins(1)
|
||||
}
|
||||
_ => {
|
||||
unknown_logins += 1;
|
||||
if unknown_logins >= 5 {
|
||||
return Ok(false);
|
||||
}
|
||||
Duration::from_mins(2)
|
||||
}
|
||||
};
|
||||
tokio::time::sleep(wait).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Starts (or retargets) the background simulator-presence check.
|
||||
pub fn stay_in_sim(&self, handle: u64, desired_position: Vector3) -> Result<(), Error> {
|
||||
*lock(&self.destination) = (handle, desired_position);
|
||||
let mut active = lock(&self.check_loop);
|
||||
if active.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut client = self.client.clone();
|
||||
let destination = Arc::clone(&self.destination);
|
||||
let frequency = self.timer_frequency;
|
||||
let cancellation = Arc::new(CancellationTokenSource::new());
|
||||
let token = cancellation.token();
|
||||
let (stop, stopped) = mpsc::channel();
|
||||
let worker = std::thread::Builder::new()
|
||||
.name("libremetaverse-stay-in-sim".into())
|
||||
.spawn(move || {
|
||||
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
if token.is_cancellation_requested() {
|
||||
break;
|
||||
}
|
||||
let (target_handle, target_position) = *lock(&destination);
|
||||
if target_handle != 0
|
||||
&& let Some(current) = client.network().current_sim()
|
||||
&& current.handle != 0
|
||||
&& current.handle != target_handle
|
||||
{
|
||||
let _ = runtime.block_on(
|
||||
client
|
||||
.self_()
|
||||
.teleport_with_u_int64_vector3_cancellation_token(
|
||||
target_handle,
|
||||
target_position,
|
||||
Some(token.clone()),
|
||||
),
|
||||
);
|
||||
}
|
||||
match stopped.recv_timeout(frequency) {
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {}
|
||||
}
|
||||
}
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
*active = Some(CheckLoop {
|
||||
stop,
|
||||
cancellation,
|
||||
worker,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancels and joins the active presence check. Calling it repeatedly is safe.
|
||||
pub fn stop(&self) -> Result<(), Error> {
|
||||
let Some(active) = lock(&self.check_loop).take() else {
|
||||
return Ok(());
|
||||
};
|
||||
active.cancellation.cancel();
|
||||
let _ = active.stop.send(());
|
||||
active.worker.join().map_err(|_| Error::InvalidOperation)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ConnectionManager {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/// Human-like chat and mouselook helpers.
|
||||
pub struct Realism;
|
||||
|
||||
impl Realism {
|
||||
/// Sends normal chat at the upstream default of three characters per second.
|
||||
pub fn chat_with_grid_client_string(client: GridClient, message: String) -> Result<(), Error> {
|
||||
Self::chat_with_grid_client_string_chat_type_int32(client, message, ChatType::Normal, 3)
|
||||
}
|
||||
|
||||
/// Blocking compatibility wrapper around the asynchronous typing sequence.
|
||||
pub fn chat_with_grid_client_string_chat_type_int32(
|
||||
client: GridClient,
|
||||
message: String,
|
||||
type_: ChatType,
|
||||
cps: i32,
|
||||
) -> Result<(), Error> {
|
||||
std::thread::Builder::new()
|
||||
.name("libremetaverse-realism-chat".into())
|
||||
.spawn(move || {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_time()
|
||||
.build()
|
||||
.map_err(|_| Error::InvalidOperation)?;
|
||||
runtime.block_on(
|
||||
Self::chat_with_grid_client_string_chat_type_int32_cancellation_token(
|
||||
client, message, type_, cps, None,
|
||||
),
|
||||
)
|
||||
})
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
.join()
|
||||
.map_err(|_| Error::InvalidOperation)?
|
||||
}
|
||||
|
||||
/// Emits start/stop typing chat and animation traffic while delaying by `cps`.
|
||||
pub async fn chat_with_grid_client_string_chat_type_int32_cancellation_token(
|
||||
mut client: GridClient,
|
||||
message: String,
|
||||
type_: ChatType,
|
||||
cps: i32,
|
||||
cancellation_token: Option<CancellationToken>,
|
||||
) -> Result<(), Error> {
|
||||
let cps = usize::try_from(cps)
|
||||
.ok()
|
||||
.filter(|value| *value > 0)
|
||||
.ok_or(Error::Argument)?;
|
||||
let cancellation = cancellation_token.unwrap_or_default();
|
||||
cancellation.throw_if_cancellation_requested()?;
|
||||
let animation = UUID::new_with_string(TYPE_ANIMATION.into())?;
|
||||
let agent = client.self_();
|
||||
agent.chat(String::new(), 0, ChatType::StartTyping, None)?;
|
||||
if let Err(error) = agent.animation_start(animation, false) {
|
||||
let _ = agent.chat(String::new(), 0, ChatType::StopTyping, None);
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
let mut characters = 0_usize;
|
||||
let mut typing = true;
|
||||
let outcome = async {
|
||||
while characters < message.chars().count() {
|
||||
cancellation.throw_if_cancellation_requested()?;
|
||||
if !typing {
|
||||
agent.chat(String::new(), 0, ChatType::StartTyping, None)?;
|
||||
agent.animation_start(animation, false)?;
|
||||
typing = true;
|
||||
} else if pseudo_random_tenth() {
|
||||
agent.chat(String::new(), 0, ChatType::StopTyping, None)?;
|
||||
agent.animation_stop(animation, false)?;
|
||||
typing = false;
|
||||
}
|
||||
tokio::select! {
|
||||
() = tokio::time::sleep(Duration::from_secs(1)) => {}
|
||||
() = cancellation.cancelled() => return Err(Error::Cancelled),
|
||||
}
|
||||
characters = characters.saturating_add(cps);
|
||||
}
|
||||
agent.chat(message, 0, type_, None)
|
||||
}
|
||||
.await;
|
||||
|
||||
let stop_chat = agent.chat(String::new(), 0, ChatType::StopTyping, None);
|
||||
let stop_animation = agent.animation_stop(animation, false);
|
||||
outcome.and(stop_chat).and(stop_animation)
|
||||
}
|
||||
|
||||
/// Performs the mouselook press/release sequence when agent updates are enabled.
|
||||
pub fn shoot_with_grid_client(mut client: GridClient) -> Result<bool, Error> {
|
||||
if !client.settings().agent().send_updates {
|
||||
return Ok(false);
|
||||
}
|
||||
let movement = &mut client.self_().movement;
|
||||
movement.set_mouselook(true);
|
||||
movement.set_ml_button_down(true);
|
||||
let outcome = (|| {
|
||||
movement.send_update_with_boolean(None)?;
|
||||
movement.set_ml_button_up(true);
|
||||
movement.set_ml_button_down(false);
|
||||
movement.set_finish_anim(true);
|
||||
movement.send_update_with_boolean(None)?;
|
||||
movement.set_mouselook(false);
|
||||
movement.set_ml_button_up(false);
|
||||
movement.set_finish_anim(false);
|
||||
movement.send_update_with_boolean(None)
|
||||
})();
|
||||
movement.set_mouselook(false);
|
||||
movement.set_ml_button_down(false);
|
||||
movement.set_ml_button_up(false);
|
||||
movement.set_finish_anim(false);
|
||||
outcome.map(|()| true)
|
||||
}
|
||||
|
||||
/// Turns toward `target` before performing the mouselook press/release sequence.
|
||||
pub fn shoot_with_grid_client_vector3(
|
||||
mut client: GridClient,
|
||||
target: Vector3,
|
||||
) -> Result<bool, Error> {
|
||||
if !client.self_().movement.turn_toward(target, None)? {
|
||||
return Ok(false);
|
||||
}
|
||||
Self::shoot_with_grid_client(client)
|
||||
}
|
||||
}
|
||||
|
||||
fn pseudo_random_tenth() -> bool {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.subsec_nanos())
|
||||
% 10
|
||||
== 9
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn connection_manager_rejects_non_positive_frequency() {
|
||||
assert!(matches!(
|
||||
ConnectionManager::new(GridClient::new().unwrap(), 0),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
assert!(matches!(
|
||||
ConnectionManager::new(GridClient::new().unwrap(), -1),
|
||||
Err(Error::Argument)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connection_monitor_can_retarget_and_stop_without_a_login() {
|
||||
let manager = ConnectionManager::new(GridClient::new().unwrap(), 10).unwrap();
|
||||
manager.stay_in_sim(1, Vector3::unit_x()).unwrap();
|
||||
manager.stay_in_sim(2, Vector3::unit_y()).unwrap();
|
||||
manager.stop().unwrap();
|
||||
manager.stop().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shoot_reports_disabled_updates_without_network_traffic() {
|
||||
let mut client = GridClient::new().unwrap();
|
||||
client.settings().agent_mut().send_updates = false;
|
||||
assert!(!Realism::shoot_with_grid_client(client).unwrap());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn chat_rejects_zero_rate_before_network_traffic() {
|
||||
let error = Realism::chat_with_grid_client_string_chat_type_int32_cancellation_token(
|
||||
GridClient::new().unwrap(),
|
||||
"hello".into(),
|
||||
ChatType::Normal,
|
||||
0,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error, Error::Argument);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn chat_honors_pre_cancelled_token_before_network_traffic() {
|
||||
let source = CancellationTokenSource::new();
|
||||
source.cancel();
|
||||
let error = Realism::chat_with_grid_client_string_chat_type_int32_cancellation_token(
|
||||
GridClient::new().unwrap(),
|
||||
"hello".into(),
|
||||
ChatType::Normal,
|
||||
3,
|
||||
Some(source.token()),
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(error, Error::Cancelled);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user