feat: stabilize OpenSim interactions and landmarks
This commit is contained in:
@@ -32,7 +32,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
use libremetaverse_structured_data::{OSD, OSDMap, OSDParser};
|
||||||
use libremetaverse_types::compat::{EventHandler, Subscription, Uri};
|
use libremetaverse_types::compat::{EventHandler, Subscription, Uri};
|
||||||
use libremetaverse_types::{Color4, UUID, Vector3, Vector3d, Vector4};
|
use libremetaverse_types::{Color4, UUID, Utils, Vector3, Vector3d, Vector4};
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::panic::{AssertUnwindSafe, catch_unwind};
|
use std::panic::{AssertUnwindSafe, catch_unwind};
|
||||||
@@ -82,6 +82,13 @@ fn send_simulator_packet(
|
|||||||
simulator.native_send_packet_data(data, length, packet_type, zerocoded)
|
simulator.native_send_packet_data(data, length, packet_type, zerocoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_sim_im_wire(mut data: Vec<u8>) -> Result<Vec<u8>, Error> {
|
||||||
|
// OpenSim's own IM sender deliberately disables protocol zerocoding.
|
||||||
|
let flags = data.first_mut().ok_or(Error::Argument)?;
|
||||||
|
*flags &= !crate::Helpers::MSG_ZEROCODED;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_mute_list(text: &str) -> HashMap<String, MuteEntry> {
|
fn parse_mute_list(text: &str) -> HashMap<String, MuteEntry> {
|
||||||
let mut result = HashMap::new();
|
let mut result = HashMap::new();
|
||||||
for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) {
|
for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) {
|
||||||
@@ -2331,10 +2338,10 @@ impl AgentManager {
|
|||||||
packet.agent_data.agent_id = self.native_agent_id();
|
packet.agent_data.agent_id = self.native_agent_id();
|
||||||
packet.agent_data.session_id = self.native_session_id();
|
packet.agent_data.session_id = self.native_session_id();
|
||||||
packet.message_block.dialog = dialog as u8;
|
packet.message_block.dialog = dialog as u8;
|
||||||
packet.message_block.from_agent_name = from_name.as_bytes().to_vec();
|
packet.message_block.from_agent_name = Utils::string_to_bytes(from_name.clone())?;
|
||||||
packet.message_block.from_group = false;
|
packet.message_block.from_group = false;
|
||||||
packet.message_block.id = im_session_id;
|
packet.message_block.id = im_session_id;
|
||||||
packet.message_block.message = message.into_bytes();
|
packet.message_block.message = Utils::string_to_bytes(message)?;
|
||||||
packet.message_block.offline = offline as u8;
|
packet.message_block.offline = offline as u8;
|
||||||
packet.message_block.to_agent_id = target;
|
packet.message_block.to_agent_id = target;
|
||||||
packet
|
packet
|
||||||
@@ -2346,7 +2353,7 @@ impl AgentManager {
|
|||||||
packet.message_block.region_id = region_id;
|
packet.message_block.region_id = region_id;
|
||||||
self.send_packet(
|
self.send_packet(
|
||||||
crate::packets::PacketType::ImprovedInstantMessage,
|
crate::packets::PacketType::ImprovedInstantMessage,
|
||||||
packet.to_bytes_with_method()?,
|
open_sim_im_wire(packet.to_bytes_with_method()?)?,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -4767,6 +4774,39 @@ mod tests {
|
|||||||
encoded
|
encoded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_sim_im_wire_uses_legacy_unencoded_layout() {
|
||||||
|
let packet = crate::packets::ImprovedInstantMessagePacket::new_with_constructor().unwrap();
|
||||||
|
let generated = packet.to_bytes_with_method().unwrap();
|
||||||
|
let wire = open_sim_im_wire(generated.clone()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(wire.len(), generated.len());
|
||||||
|
assert_eq!(wire[0] & crate::Helpers::MSG_ZEROCODED, 0);
|
||||||
|
assert_eq!(&wire[wire.len() - 5..], &[0, 0, 0, 0, 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn open_sim_im_without_metadata_count_reaches_subscribers() {
|
||||||
|
let client = GridClient::new().unwrap();
|
||||||
|
let inner = AgentManagerInner::default();
|
||||||
|
let received = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
let values = Arc::clone(&received);
|
||||||
|
let _subscription = inner.instant_message.subscribe(Arc::new(move |event| {
|
||||||
|
values.lock().unwrap().push(event.im().message);
|
||||||
|
}));
|
||||||
|
let mut packet =
|
||||||
|
crate::packets::ImprovedInstantMessagePacket::new_with_constructor().unwrap();
|
||||||
|
packet.message_block.dialog = InstantMessageDialog::MessageFromAgent as u8;
|
||||||
|
packet.message_block.message = b"legacy OpenSim IM\0".to_vec();
|
||||||
|
let mut wire = open_sim_im_wire(packet.to_bytes_with_method().unwrap()).unwrap();
|
||||||
|
assert_eq!(wire.pop(), Some(0));
|
||||||
|
|
||||||
|
inner
|
||||||
|
.handle_instant_message_packet(&wire, simulator(&client, loopback(13000)))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(*received.lock().unwrap(), ["legacy OpenSim IM"]);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn chat_uses_utf8_safe_protocol_chunks_and_exact_identity() {
|
fn chat_uses_utf8_safe_protocol_chunks_and_exact_identity() {
|
||||||
let client = Arc::new(GridClient::new().expect("client"));
|
let client = Arc::new(GridClient::new().expect("client"));
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ use crate::packets::{
|
|||||||
use crate::{
|
use crate::{
|
||||||
AgentFlags, AgentManagerControlFlags, AgentManagerCrossingFailureReason,
|
AgentFlags, AgentManagerControlFlags, AgentManagerCrossingFailureReason,
|
||||||
AgentManagerCrossingState, AgentState, BorderCrossingDirection, Error, GridClient,
|
AgentManagerCrossingState, AgentState, BorderCrossingDirection, Error, GridClient,
|
||||||
InstantMessageDialog, InstantMessageOnline, NetworkManager, Simulator, TeleportFlags,
|
GridLayerType, InstantMessageDialog, InstantMessageOnline, NetworkManager, Simulator,
|
||||||
TeleportStatus,
|
TeleportFlags, TeleportStatus,
|
||||||
};
|
};
|
||||||
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
|
use libremetaverse_types::compat::{CancellationToken, EventHandler, Subscription, Uri};
|
||||||
use libremetaverse_types::{Quaternion, UUID, Vector3, Vector3d, Vector4};
|
use libremetaverse_types::{Quaternion, UUID, Vector3, Vector3d, Vector4};
|
||||||
@@ -2779,7 +2779,7 @@ impl AgentManager {
|
|||||||
);
|
);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
let handle = if current.name.eq_ignore_ascii_case(&sim_name) {
|
let connected_handle = if current.name.eq_ignore_ascii_case(&sim_name) {
|
||||||
Some(current.handle)
|
Some(current.handle)
|
||||||
} else {
|
} else {
|
||||||
self.client
|
self.client
|
||||||
@@ -2790,6 +2790,20 @@ impl AgentManager {
|
|||||||
.find(|simulator| simulator.name.eq_ignore_ascii_case(&sim_name))
|
.find(|simulator| simulator.name.eq_ignore_ascii_case(&sim_name))
|
||||||
.map(|simulator| simulator.handle)
|
.map(|simulator| simulator.handle)
|
||||||
};
|
};
|
||||||
|
let handle = if let Some(handle) = connected_handle {
|
||||||
|
Some(handle)
|
||||||
|
} else {
|
||||||
|
self.client
|
||||||
|
.grid()
|
||||||
|
.get_grid_region_with_string_grid_layer_type_cancellation_token(
|
||||||
|
sim_name.clone(),
|
||||||
|
GridLayerType::Objects,
|
||||||
|
cancellation_token.clone(),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.flatten()
|
||||||
|
.map(|region| region.region_handle)
|
||||||
|
};
|
||||||
let Some(handle) = handle else {
|
let Some(handle) = handle else {
|
||||||
self.movement.runtime.teleport_event(
|
self.movement.runtime.teleport_event(
|
||||||
format!("Unable to resolve simulator named: {sim_name}"),
|
format!("Unable to resolve simulator named: {sim_name}"),
|
||||||
|
|||||||
@@ -570,7 +570,7 @@ impl GridManager {
|
|||||||
p.agent_data.agent_id = agent;
|
p.agent_data.agent_id = agent;
|
||||||
p.agent_data.session_id = session;
|
p.agent_data.session_id = session;
|
||||||
p.agent_data.flags = layer as u32;
|
p.agent_data.flags = layer as u32;
|
||||||
p.name_data.name = Utils::string_to_bytes(name.to_owned())?;
|
p.name_data.name = Utils::string_to_bytes(name.to_ascii_lowercase())?;
|
||||||
self.send(&p, PacketType::MapNameRequest)
|
self.send(&p, PacketType::MapNameRequest)
|
||||||
}
|
}
|
||||||
pub fn request_map_items(
|
pub fn request_map_items(
|
||||||
|
|||||||
@@ -3393,11 +3393,14 @@ impl InventoryManager {
|
|||||||
packet_type: PacketType,
|
packet_type: PacketType,
|
||||||
bytes: Vec<u8>,
|
bytes: Vec<u8>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
|
let zerocoded = bytes
|
||||||
|
.first()
|
||||||
|
.is_some_and(|flags| flags & crate::Helpers::MSG_ZEROCODED != 0);
|
||||||
simulator.native_send_packet_data(
|
simulator.native_send_packet_data(
|
||||||
bytes.clone(),
|
bytes.clone(),
|
||||||
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
|
i32::try_from(bytes.len()).map_err(|_| Error::Argument)?,
|
||||||
packet_type,
|
packet_type,
|
||||||
false,
|
zerocoded,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149272,7 +149272,11 @@ impl GeneratedPacket for crate::packets::ImprovedInstantMessagePacket {
|
|||||||
GeneratedBlock::decode_payload(&mut self.agent_data, reader)?;
|
GeneratedBlock::decode_payload(&mut self.agent_data, reader)?;
|
||||||
GeneratedBlock::decode_payload(&mut self.message_block, reader)?;
|
GeneratedBlock::decode_payload(&mut self.message_block, reader)?;
|
||||||
GeneratedBlock::decode_payload(&mut self.estate_block, reader)?;
|
GeneratedBlock::decode_payload(&mut self.estate_block, reader)?;
|
||||||
let meta_data_count = usize::from(reader.read_u8()?);
|
let meta_data_count = if reader.is_empty() {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
usize::from(reader.read_u8()?)
|
||||||
|
};
|
||||||
let mut meta_data = Vec::new();
|
let mut meta_data = Vec::new();
|
||||||
meta_data
|
meta_data
|
||||||
.try_reserve_exact(meta_data_count)
|
.try_reserve_exact(meta_data_count)
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ impl<'a> WireReader<'a> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn is_empty(&self) -> bool {
|
||||||
|
self.position == self.end
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn read_u8(&mut self) -> Result<u8, Error> {
|
pub(crate) fn read_u8(&mut self) -> Result<u8, Error> {
|
||||||
Ok(self.take(1, "truncated u8")?[0])
|
Ok(self.take(1, "truncated u8")?[0])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -313,6 +313,14 @@ impl LibremetaverseClientOwner {
|
|||||||
|
|
||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
impl LibremetaverseWorldSnapshotSource {
|
impl LibremetaverseWorldSnapshotSource {
|
||||||
|
#[must_use]
|
||||||
|
pub fn current_region_handle(&self) -> Option<u64> {
|
||||||
|
self.client
|
||||||
|
.network()
|
||||||
|
.native_current_sim()
|
||||||
|
.map(|simulator| simulator.handle)
|
||||||
|
}
|
||||||
|
|
||||||
// One atomic projection keeps native cache reads together. Terrain lookup
|
// One atomic projection keeps native cache reads together. Terrain lookup
|
||||||
// takes integral local coordinates, so finite region positions are floored.
|
// takes integral local coordinates, so finite region positions are floored.
|
||||||
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
#[allow(clippy::too_many_lines, clippy::cast_possible_truncation)]
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ pub struct RuntimeView {
|
|||||||
pub transport_connected: bool,
|
pub transport_connected: bool,
|
||||||
pub agent_ready: bool,
|
pub agent_ready: bool,
|
||||||
pub region_id: Option<String>,
|
pub region_id: Option<String>,
|
||||||
|
pub region_handle: Option<u64>,
|
||||||
pub region_name: Option<String>,
|
pub region_name: Option<String>,
|
||||||
pub position: Option<[f64; 3]>,
|
pub position: Option<[f64; 3]>,
|
||||||
pub behavior_mode: String,
|
pub behavior_mode: String,
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ impl ControlTarget for FakeTarget {
|
|||||||
transport_connected: true,
|
transport_connected: true,
|
||||||
agent_ready: true,
|
agent_ready: true,
|
||||||
region_id: Some("00000000-0000-4000-8000-000000000001".into()),
|
region_id: Some("00000000-0000-4000-8000-000000000001".into()),
|
||||||
|
region_handle: Some(9_007_199_254_740_992),
|
||||||
region_name: Some("Safe Region".into()),
|
region_name: Some("Safe Region".into()),
|
||||||
position: Some([128.0, 128.0, 24.0]),
|
position: Some([128.0, 128.0, 24.0]),
|
||||||
behavior_mode: "available".into(),
|
behavior_mode: "available".into(),
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ struct RuntimeState {
|
|||||||
behavior: BehaviorMode,
|
behavior: BehaviorMode,
|
||||||
service_state: &'static str,
|
service_state: &'static str,
|
||||||
region_id: Option<String>,
|
region_id: Option<String>,
|
||||||
|
region_handle: Option<u64>,
|
||||||
region_name: Option<String>,
|
region_name: Option<String>,
|
||||||
position: Option<[f64; 3]>,
|
position: Option<[f64; 3]>,
|
||||||
}
|
}
|
||||||
@@ -70,6 +71,7 @@ impl Default for RuntimeState {
|
|||||||
behavior: BehaviorMode::Offline,
|
behavior: BehaviorMode::Offline,
|
||||||
service_state: "starting",
|
service_state: "starting",
|
||||||
region_id: None,
|
region_id: None,
|
||||||
|
region_handle: None,
|
||||||
region_name: None,
|
region_name: None,
|
||||||
position: None,
|
position: None,
|
||||||
}
|
}
|
||||||
@@ -173,11 +175,13 @@ impl AgentControlTarget {
|
|||||||
pub fn update_region(
|
pub fn update_region(
|
||||||
&self,
|
&self,
|
||||||
region_id: Option<String>,
|
region_id: Option<String>,
|
||||||
|
region_handle: Option<u64>,
|
||||||
region_name: Option<String>,
|
region_name: Option<String>,
|
||||||
position: Option<[f64; 3]>,
|
position: Option<[f64; 3]>,
|
||||||
) {
|
) {
|
||||||
let mut state = lock(&self.state);
|
let mut state = lock(&self.state);
|
||||||
state.region_id = region_id.filter(|value| value.len() <= 64);
|
state.region_id = region_id.filter(|value| value.len() <= 64);
|
||||||
|
state.region_handle = region_handle.filter(|value| *value != 0);
|
||||||
state.region_name = region_name.filter(|value| value.len() <= 256);
|
state.region_name = region_name.filter(|value| value.len() <= 256);
|
||||||
state.position =
|
state.position =
|
||||||
position.filter(|value| value.iter().all(|component| component.is_finite()));
|
position.filter(|value| value.iter().all(|component| component.is_finite()));
|
||||||
@@ -259,6 +263,7 @@ impl AgentControlTarget {
|
|||||||
transport_connected: state.session.transport_connected,
|
transport_connected: state.session.transport_connected,
|
||||||
agent_ready: state.session.agent_ready,
|
agent_ready: state.session.agent_ready,
|
||||||
region_id: state.region_id,
|
region_id: state.region_id,
|
||||||
|
region_handle: state.region_handle,
|
||||||
region_name: state.region_name,
|
region_name: state.region_name,
|
||||||
position: state.position,
|
position: state.position,
|
||||||
behavior_mode: behavior_name(state.behavior).to_owned(),
|
behavior_mode: behavior_name(state.behavior).to_owned(),
|
||||||
|
|||||||
@@ -154,6 +154,7 @@ async fn production_target_projects_state_and_routes_real_mutations() {
|
|||||||
target.attach_vision_control(vision.clone());
|
target.attach_vision_control(vision.clone());
|
||||||
target.update_region(
|
target.update_region(
|
||||||
Some("00000000-0000-4000-8000-000000000001".into()),
|
Some("00000000-0000-4000-8000-000000000001".into()),
|
||||||
|
Some(9_007_199_254_740_992),
|
||||||
Some("Test Region".into()),
|
Some("Test Region".into()),
|
||||||
Some([128.0, 128.0, 24.0]),
|
Some([128.0, 128.0, 24.0]),
|
||||||
);
|
);
|
||||||
@@ -172,6 +173,7 @@ async fn production_target_projects_state_and_routes_real_mutations() {
|
|||||||
panic!("runtime projection")
|
panic!("runtime projection")
|
||||||
};
|
};
|
||||||
assert_eq!(runtime.region_name.as_deref(), Some("Test Region"));
|
assert_eq!(runtime.region_name.as_deref(), Some("Test Region"));
|
||||||
|
assert_eq!(runtime.region_handle, Some(9_007_199_254_740_992));
|
||||||
assert_eq!(runtime.control_queue_capacity, 8);
|
assert_eq!(runtime.control_queue_capacity, 8);
|
||||||
assert_eq!(runtime.active_visual_capture.as_deref(), Some("visual-1"));
|
assert_eq!(runtime.active_visual_capture.as_deref(), Some("visual-1"));
|
||||||
assert_eq!(runtime.visual_progress.as_deref(), Some("rendering"));
|
assert_eq!(runtime.visual_progress.as_deref(), Some("rendering"));
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
use crate::conversation::{ConversationChannel, ConversationKey, ConversationStore, MemoryRecord};
|
use crate::conversation::{ConversationChannel, ConversationKey, ConversationStore, MemoryRecord};
|
||||||
use crate::llm::{CompletionMessage, ContentPart};
|
use crate::llm::{CompletionMessage, ContentPart};
|
||||||
use crate::types::{BoundedText, MAX_BODY_BYTES, MAX_IDENTIFIER_BYTES, MAX_MESSAGE_BYTES};
|
use crate::types::{
|
||||||
|
BoundedText, MAX_BODY_BYTES, MAX_IDENTIFIER_BYTES, MAX_MESSAGE_BYTES, MessageRole,
|
||||||
|
};
|
||||||
use libremetaverse_types::UUID;
|
use libremetaverse_types::UUID;
|
||||||
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
|
use libremetaverse_types::compat::{CancellationToken, CancellationTokenSource};
|
||||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||||
@@ -27,6 +29,7 @@ const MAX_CONCURRENT_INFERENCE: usize = 64;
|
|||||||
const MAX_DUPLICATE_IDS: usize = 16_384;
|
const MAX_DUPLICATE_IDS: usize = 16_384;
|
||||||
const MAX_DEBOUNCE_FRAGMENTS: usize = 16;
|
const MAX_DEBOUNCE_FRAGMENTS: usize = 16;
|
||||||
const MAX_OBSERVATIONS: usize = 8_192;
|
const MAX_OBSERVATIONS: usize = 8_192;
|
||||||
|
const CURRENT_TURN_SYSTEM_PROMPT: &str = "Handle only the newest avatar message. Earlier turns are already handled context. Never repeat an earlier mutation unless the newest message explicitly requests it. Do not mention unrelated or unavailable actions.";
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||||
pub enum InteractionChannel {
|
pub enum InteractionChannel {
|
||||||
@@ -232,6 +235,7 @@ pub struct ResponseRequest {
|
|||||||
pub channel: InteractionChannel,
|
pub channel: InteractionChannel,
|
||||||
pub origin: InteractionOrigin,
|
pub origin: InteractionOrigin,
|
||||||
pub intent: InteractionIntent,
|
pub intent: InteractionIntent,
|
||||||
|
pub capabilities: BTreeSet<crate::policy::Capability>,
|
||||||
pub messages: Vec<CompletionMessage>,
|
pub messages: Vec<CompletionMessage>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1133,6 +1137,7 @@ async fn process_batch(
|
|||||||
channel: trigger.channel,
|
channel: trigger.channel,
|
||||||
origin,
|
origin,
|
||||||
intent,
|
intent,
|
||||||
|
capabilities: capabilities_for_intent(intent, &body),
|
||||||
messages,
|
messages,
|
||||||
};
|
};
|
||||||
observe(
|
observe(
|
||||||
@@ -1441,24 +1446,7 @@ fn classify_intent(
|
|||||||
|| normalized
|
|| normalized
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.any(|token| token.starts_with(['/', '!']))
|
.any(|token| token.starts_with(['/', '!']))
|
||||||
|| normalized
|
|| command_capabilities(&normalized).len() > 1;
|
||||||
.split(|character: char| !character.is_ascii_alphanumeric())
|
|
||||||
.any(|token| {
|
|
||||||
matches!(
|
|
||||||
token,
|
|
||||||
"teleport"
|
|
||||||
| "move"
|
|
||||||
| "rez"
|
|
||||||
| "build"
|
|
||||||
| "delete"
|
|
||||||
| "give"
|
|
||||||
| "upload"
|
|
||||||
| "run"
|
|
||||||
| "execute"
|
|
||||||
| "wear"
|
|
||||||
| "attach"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
match (channel, authorized, command) {
|
match (channel, authorized, command) {
|
||||||
(InteractionChannel::PublicChat, _, true) => InteractionIntent::PublicCommandDenied,
|
(InteractionChannel::PublicChat, _, true) => InteractionIntent::PublicCommandDenied,
|
||||||
(InteractionChannel::DirectIm, true, true) => InteractionIntent::PolicyGatedCommand,
|
(InteractionChannel::DirectIm, true, true) => InteractionIntent::PolicyGatedCommand,
|
||||||
@@ -1466,6 +1454,57 @@ fn classify_intent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capabilities_for_intent(
|
||||||
|
intent: InteractionIntent,
|
||||||
|
message: &str,
|
||||||
|
) -> BTreeSet<crate::policy::Capability> {
|
||||||
|
match intent {
|
||||||
|
InteractionIntent::Informational => {
|
||||||
|
BTreeSet::from([crate::policy::Capability::Informational])
|
||||||
|
}
|
||||||
|
InteractionIntent::PolicyGatedLslRequest => {
|
||||||
|
BTreeSet::from([crate::policy::Capability::PublicLslRequest])
|
||||||
|
}
|
||||||
|
InteractionIntent::PolicyGatedCommand => {
|
||||||
|
command_capabilities(&message.trim().to_ascii_lowercase())
|
||||||
|
}
|
||||||
|
InteractionIntent::PublicCommandDenied => BTreeSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_capabilities(message: &str) -> BTreeSet<crate::policy::Capability> {
|
||||||
|
use crate::policy::Capability;
|
||||||
|
|
||||||
|
let mut capabilities = BTreeSet::from([Capability::Informational]);
|
||||||
|
let tokens = message
|
||||||
|
.split(|character: char| !character.is_ascii_alphanumeric())
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
if tokens.iter().any(|token| {
|
||||||
|
matches!(
|
||||||
|
*token,
|
||||||
|
"teleport" | "move" | "walk" | "stop" | "sit" | "stand" | "face" | "look" | "schedule"
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
capabilities.insert(Capability::Movement);
|
||||||
|
}
|
||||||
|
if tokens
|
||||||
|
.iter()
|
||||||
|
.any(|token| matches!(*token, "give" | "upload" | "wear" | "attach"))
|
||||||
|
|| (tokens.contains("create") && tokens.contains("landmark"))
|
||||||
|
{
|
||||||
|
capabilities.insert(Capability::InventoryMutation);
|
||||||
|
}
|
||||||
|
if tokens.iter().any(|token| matches!(*token, "rez" | "build"))
|
||||||
|
|| (tokens.contains("create") && !tokens.contains("landmark"))
|
||||||
|
{
|
||||||
|
capabilities.insert(Capability::Build);
|
||||||
|
}
|
||||||
|
if tokens.contains("delete") {
|
||||||
|
capabilities.insert(Capability::ObjectMutation);
|
||||||
|
}
|
||||||
|
capabilities
|
||||||
|
}
|
||||||
|
|
||||||
fn safe_visible_response(text: &str, maximum: usize) -> Option<String> {
|
fn safe_visible_response(text: &str, maximum: usize) -> Option<String> {
|
||||||
let lower = text.to_ascii_lowercase();
|
let lower = text.to_ascii_lowercase();
|
||||||
if text.trim().is_empty()
|
if text.trim().is_empty()
|
||||||
@@ -1775,25 +1814,11 @@ impl InteractionResponder for PolicyLlmResponder {
|
|||||||
request.delivery_id.as_str(),
|
request.delivery_id.as_str(),
|
||||||
)
|
)
|
||||||
.map_err(|_| InteractionModelError::PolicyRejected)?;
|
.map_err(|_| InteractionModelError::PolicyRejected)?;
|
||||||
let capabilities = match request.intent {
|
let tools = self.gateway.tools_for_capability_set(
|
||||||
InteractionIntent::Informational => {
|
&context,
|
||||||
BTreeSet::from([crate::policy::Capability::Informational])
|
(self.now)(),
|
||||||
}
|
&request.capabilities,
|
||||||
InteractionIntent::PolicyGatedLslRequest => {
|
);
|
||||||
BTreeSet::from([crate::policy::Capability::PublicLslRequest])
|
|
||||||
}
|
|
||||||
InteractionIntent::PolicyGatedCommand => BTreeSet::from([
|
|
||||||
crate::policy::Capability::Informational,
|
|
||||||
crate::policy::Capability::InventoryMutation,
|
|
||||||
crate::policy::Capability::Movement,
|
|
||||||
crate::policy::Capability::Build,
|
|
||||||
crate::policy::Capability::ObjectMutation,
|
|
||||||
]),
|
|
||||||
InteractionIntent::PublicCommandDenied => BTreeSet::new(),
|
|
||||||
};
|
|
||||||
let tools =
|
|
||||||
self.gateway
|
|
||||||
.tools_for_capability_set(&context, (self.now)(), &capabilities);
|
|
||||||
let loop_owner = crate::tool_loop::SessionGeneration::default();
|
let loop_owner = crate::tool_loop::SessionGeneration::default();
|
||||||
let generation = loop_owner.current();
|
let generation = loop_owner.current();
|
||||||
let tool_loop = crate::tool_loop::ToolLoop::new(
|
let tool_loop = crate::tool_loop::ToolLoop::new(
|
||||||
@@ -1810,14 +1835,14 @@ impl InteractionResponder for PolicyLlmResponder {
|
|||||||
Arc::clone(&self.now),
|
Arc::clone(&self.now),
|
||||||
)
|
)
|
||||||
.map_err(|_| InteractionModelError::PolicyRejected)?;
|
.map_err(|_| InteractionModelError::PolicyRejected)?;
|
||||||
|
let mut messages = request.messages;
|
||||||
|
messages.insert(
|
||||||
|
0,
|
||||||
|
CompletionMessage::text(MessageRole::System, CURRENT_TURN_SYSTEM_PROMPT)
|
||||||
|
.map_err(|_| InteractionModelError::Failed)?,
|
||||||
|
);
|
||||||
let outcome = tool_loop
|
let outcome = tool_loop
|
||||||
.run(
|
.run(messages, &loop_owner, generation, &cancellation, &executor)
|
||||||
request.messages,
|
|
||||||
&loop_owner,
|
|
||||||
generation,
|
|
||||||
&cancellation,
|
|
||||||
&executor,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| match error {
|
.map_err(|error| match error {
|
||||||
crate::tool_loop::ToolLoopError::Cancelled
|
crate::tool_loop::ToolLoopError::Cancelled
|
||||||
|
|||||||
@@ -430,7 +430,7 @@ async fn im_authorization_and_malicious_claims_cannot_change_origin() {
|
|||||||
"authorized",
|
"authorized",
|
||||||
1,
|
1,
|
||||||
InteractionChannel::DirectIm,
|
InteractionChannel::DirectIm,
|
||||||
"/move north",
|
"create a landmark here",
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.expect("authorized IM");
|
.expect("authorized IM");
|
||||||
@@ -453,6 +453,13 @@ async fn im_authorization_and_malicious_claims_cannot_change_origin() {
|
|||||||
.expect("authorized request");
|
.expect("authorized request");
|
||||||
assert_eq!(authorized.origin, InteractionOrigin::AuthorizedIm);
|
assert_eq!(authorized.origin, InteractionOrigin::AuthorizedIm);
|
||||||
assert_eq!(authorized.intent, InteractionIntent::PolicyGatedCommand);
|
assert_eq!(authorized.intent, InteractionIntent::PolicyGatedCommand);
|
||||||
|
assert_eq!(
|
||||||
|
authorized.capabilities,
|
||||||
|
BTreeSet::from([
|
||||||
|
crate::Capability::Informational,
|
||||||
|
crate::Capability::InventoryMutation,
|
||||||
|
])
|
||||||
|
);
|
||||||
let unprivileged = requests
|
let unprivileged = requests
|
||||||
.iter()
|
.iter()
|
||||||
.find(|request| request.delivery_id.as_str() == "unprivileged")
|
.find(|request| request.delivery_id.as_str() == "unprivileged")
|
||||||
@@ -462,6 +469,21 @@ async fn im_authorization_and_malicious_claims_cannot_change_origin() {
|
|||||||
handle.shutdown().await.expect("shutdown");
|
handle.shutdown().await.expect("shutdown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn explicit_command_only_exposes_its_capability_family() {
|
||||||
|
let teleport = capabilities_for_intent(
|
||||||
|
InteractionIntent::PolicyGatedCommand,
|
||||||
|
"Teleport using this landmark now",
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
teleport,
|
||||||
|
BTreeSet::from([
|
||||||
|
crate::Capability::Informational,
|
||||||
|
crate::Capability::Movement,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test(start_paused = true)]
|
#[tokio::test(start_paused = true)]
|
||||||
async fn debounce_preserves_fragment_order_and_expiry_creates_a_fresh_session() {
|
async fn debounce_preserves_fragment_order_and_expiry_creates_a_fresh_session() {
|
||||||
let responder = Arc::new(FakeResponder::with_response("Combined."));
|
let responder = Arc::new(FakeResponder::with_response("Combined."));
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ fn id(value: u128) -> UUID {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FakeGrid {
|
struct FakeGrid {
|
||||||
|
created: Mutex<Vec<String>>,
|
||||||
accepted: Mutex<Vec<String>>,
|
accepted: Mutex<Vec<String>>,
|
||||||
declined: Mutex<Vec<String>>,
|
declined: Mutex<Vec<String>>,
|
||||||
teleports: Mutex<Vec<UUID>>,
|
teleports: Mutex<Vec<UUID>>,
|
||||||
@@ -23,6 +24,17 @@ struct FakeGrid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LandmarkGrid for FakeGrid {
|
impl LandmarkGrid for FakeGrid {
|
||||||
|
fn create_current_landmark(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
_: CancellationToken,
|
||||||
|
) -> LandmarkFuture<'_, OfferedInventoryNode> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.created.lock().expect("created").push(name.clone());
|
||||||
|
Ok(landmark(980, 981, 70, &name))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn accept_offer(&self, offer_id: &str, _: CancellationToken) -> LandmarkFuture<'_, ()> {
|
fn accept_offer(&self, offer_id: &str, _: CancellationToken) -> LandmarkFuture<'_, ()> {
|
||||||
let offer_id = offer_id.to_owned();
|
let offer_id = offer_id.to_owned();
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
@@ -57,6 +69,33 @@ impl LandmarkGrid for FakeGrid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn authorized_agent_creates_current_landmark_in_inventory_and_catalog() {
|
||||||
|
let grid = Arc::new(FakeGrid::default());
|
||||||
|
let service = service(grid.clone(), &[70]);
|
||||||
|
let entry = service
|
||||||
|
.create_current(
|
||||||
|
id(70),
|
||||||
|
"Current Test Location",
|
||||||
|
CancellationToken::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("create current landmark");
|
||||||
|
assert_eq!(entry.display_name, "Current Test Location");
|
||||||
|
assert_eq!(service.entries(), vec![entry]);
|
||||||
|
assert_eq!(
|
||||||
|
grid.created.lock().expect("created").as_slice(),
|
||||||
|
&["Current Test Location"]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
service
|
||||||
|
.create_current(id(71), "Denied", CancellationToken::default())
|
||||||
|
.await,
|
||||||
|
Err(LandmarkError::Unauthorized)
|
||||||
|
);
|
||||||
|
assert_eq!(grid.created.lock().expect("created").len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct FixedRandom;
|
struct FixedRandom;
|
||||||
impl RoamingRandom for FixedRandom {
|
impl RoamingRandom for FixedRandom {
|
||||||
@@ -513,6 +552,11 @@ fn public_and_unauthorized_im_cannot_select_teleport_or_change_roaming() {
|
|||||||
)
|
)
|
||||||
.expect("gateway");
|
.expect("gateway");
|
||||||
for (origin, name, arguments) in [
|
for (origin, name, arguments) in [
|
||||||
|
(
|
||||||
|
ActionOrigin::public_chat(id(60)),
|
||||||
|
LANDMARK_CREATE_TOOL,
|
||||||
|
serde_json::json!({"name":"Denied"}),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
ActionOrigin::public_chat(id(60)),
|
ActionOrigin::public_chat(id(60)),
|
||||||
LANDMARK_TELEPORT_TOOL,
|
LANDMARK_TELEPORT_TOOL,
|
||||||
@@ -555,3 +599,33 @@ fn public_and_unauthorized_im_cannot_select_teleport_or_change_roaming() {
|
|||||||
.is_some()
|
.is_some()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schedule_configuration_does_not_spend_teleport_distance_budget() {
|
||||||
|
let gateway = PolicyGateway::new(
|
||||||
|
BTreeSet::from([id(62)]),
|
||||||
|
landmark_policy_tools(LandmarkLimits::default()).expect("tools"),
|
||||||
|
PolicyLimits::default(),
|
||||||
|
Arc::new(MemoryPolicyAudit::new(16).expect("audit")),
|
||||||
|
)
|
||||||
|
.expect("gateway");
|
||||||
|
let arguments = serde_json::json!({
|
||||||
|
"schedule_id":"budget-safe",
|
||||||
|
"minimum_interval_seconds":300,
|
||||||
|
"maximum_interval_seconds":300,
|
||||||
|
"enabled":false
|
||||||
|
});
|
||||||
|
let context =
|
||||||
|
PolicyRequestContext::new(ActionOrigin::instant_message(id(62)), "session", "schedule")
|
||||||
|
.expect("context");
|
||||||
|
let call =
|
||||||
|
ProposedToolCall::new("call", LANDMARK_SCHEDULE_TOOL, arguments.to_string()).expect("call");
|
||||||
|
assert!(
|
||||||
|
gateway
|
||||||
|
.evaluate(&context, &call, &arguments, None, 1)
|
||||||
|
.expect("decision")
|
||||||
|
.into_authorization()
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
assert_eq!(gateway.global_budget_usage().movement_millimeters, 0);
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use std::sync::{Arc, Mutex};
|
|||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
pub const MAX_LANDMARK_NAME_BYTES: usize = 256;
|
pub const MAX_LANDMARK_NAME_BYTES: usize = 256;
|
||||||
|
pub const LANDMARK_CREATE_TOOL: &str = "landmark_create_current";
|
||||||
pub const LANDMARK_LIST_TOOL: &str = "landmark_catalog_list";
|
pub const LANDMARK_LIST_TOOL: &str = "landmark_catalog_list";
|
||||||
pub const LANDMARK_TELEPORT_TOOL: &str = "landmark_teleport";
|
pub const LANDMARK_TELEPORT_TOOL: &str = "landmark_teleport";
|
||||||
pub const LANDMARK_SCHEDULE_TOOL: &str = "landmark_roaming_schedule";
|
pub const LANDMARK_SCHEDULE_TOOL: &str = "landmark_roaming_schedule";
|
||||||
@@ -219,6 +220,7 @@ pub enum LandmarkError {
|
|||||||
Busy,
|
Busy,
|
||||||
Cooldown,
|
Cooldown,
|
||||||
Cancelled,
|
Cancelled,
|
||||||
|
CreateFailed,
|
||||||
TeleportFailed,
|
TeleportFailed,
|
||||||
Persistence,
|
Persistence,
|
||||||
CorruptCatalog,
|
CorruptCatalog,
|
||||||
@@ -242,6 +244,7 @@ impl fmt::Display for LandmarkError {
|
|||||||
Self::Busy => "another teleport is active",
|
Self::Busy => "another teleport is active",
|
||||||
Self::Cooldown => "teleport cooldown is active",
|
Self::Cooldown => "teleport cooldown is active",
|
||||||
Self::Cancelled => "landmark operation was cancelled",
|
Self::Cancelled => "landmark operation was cancelled",
|
||||||
|
Self::CreateFailed => "current-location landmark creation failed",
|
||||||
Self::TeleportFailed => "landmark teleport failed",
|
Self::TeleportFailed => "landmark teleport failed",
|
||||||
Self::Persistence => "landmark catalog persistence failed",
|
Self::Persistence => "landmark catalog persistence failed",
|
||||||
Self::CorruptCatalog => "landmark catalog is corrupt",
|
Self::CorruptCatalog => "landmark catalog is corrupt",
|
||||||
@@ -257,6 +260,11 @@ pub type LandmarkFuture<'a, T> =
|
|||||||
Pin<Box<dyn Future<Output = Result<T, LandmarkError>> + Send + 'a>>;
|
Pin<Box<dyn Future<Output = Result<T, LandmarkError>> + Send + 'a>>;
|
||||||
|
|
||||||
pub trait LandmarkGrid: Send + Sync + 'static {
|
pub trait LandmarkGrid: Send + Sync + 'static {
|
||||||
|
fn create_current_landmark(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> LandmarkFuture<'_, OfferedInventoryNode>;
|
||||||
fn accept_offer(
|
fn accept_offer(
|
||||||
&self,
|
&self,
|
||||||
offer_id: &str,
|
offer_id: &str,
|
||||||
@@ -464,6 +472,38 @@ impl LandmarkService {
|
|||||||
lock(&self.observations).iter().cloned().collect()
|
lock(&self.observations).iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_current(
|
||||||
|
&self,
|
||||||
|
requesting_avatar: UUID,
|
||||||
|
name: &str,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> Result<LandmarkEntry, LandmarkError> {
|
||||||
|
if !self.authorized.contains(&requesting_avatar) {
|
||||||
|
return Err(LandmarkError::Unauthorized);
|
||||||
|
}
|
||||||
|
validate_landmark_name(name)?;
|
||||||
|
if cancellation.is_cancellation_requested() {
|
||||||
|
return Err(LandmarkError::Cancelled);
|
||||||
|
}
|
||||||
|
if lock(&self.state).entries.len() >= self.limits.max_catalog_entries {
|
||||||
|
return Err(LandmarkError::CatalogFull);
|
||||||
|
}
|
||||||
|
let node = self
|
||||||
|
.grid
|
||||||
|
.create_current_landmark(name.to_owned(), cancellation)
|
||||||
|
.await?;
|
||||||
|
let offer = LandmarkOffer {
|
||||||
|
offer_id: "created-current-location".to_owned(),
|
||||||
|
sender_id: node.owner_id,
|
||||||
|
root: node,
|
||||||
|
received_unix_millis: unix_millis(),
|
||||||
|
};
|
||||||
|
let mut entries = flatten_offer(&offer, self.limits)?;
|
||||||
|
let entry = entries.pop().ok_or(LandmarkError::CreateFailed)?;
|
||||||
|
self.commit_entries(vec![entry.clone()])?;
|
||||||
|
Ok(entry)
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_authorized(&self, avatar_id: UUID) -> bool {
|
pub fn is_authorized(&self, avatar_id: UUID) -> bool {
|
||||||
self.authorized.contains(&avatar_id)
|
self.authorized.contains(&avatar_id)
|
||||||
@@ -491,16 +531,20 @@ impl LandmarkService {
|
|||||||
if token.is_cancellation_requested() {
|
if token.is_cancellation_requested() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
let now = unix_millis();
|
||||||
|
if pause.active()
|
||||||
|
|| !schedule.enabled
|
||||||
|
|| schedule.remaining_runs == 0
|
||||||
|
|| now < schedule.next_run_unix_millis
|
||||||
|
|| now >= schedule.expires_unix_millis
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(behavior) = &behavior {
|
if let Some(behavior) = &behavior {
|
||||||
let _ = behavior.set_roaming(true);
|
let _ = behavior.set_roaming(true);
|
||||||
}
|
}
|
||||||
let _ = service
|
let _ = service
|
||||||
.execute_due_roaming(
|
.execute_due_roaming(&schedule.schedule_id, now, pause, token.clone())
|
||||||
&schedule.schedule_id,
|
|
||||||
unix_millis(),
|
|
||||||
pause,
|
|
||||||
token.clone(),
|
|
||||||
)
|
|
||||||
.await;
|
.await;
|
||||||
if let Some(behavior) = &behavior {
|
if let Some(behavior) = &behavior {
|
||||||
let _ = behavior.set_roaming(false);
|
let _ = behavior.set_roaming(false);
|
||||||
@@ -939,12 +983,7 @@ fn flatten_offer(
|
|||||||
{
|
{
|
||||||
return Err(LandmarkError::InvalidOffer);
|
return Err(LandmarkError::InvalidOffer);
|
||||||
}
|
}
|
||||||
if node.name.is_empty()
|
validate_landmark_name(&node.name)?;
|
||||||
|| node.name.len() > MAX_LANDMARK_NAME_BYTES
|
|
||||||
|| node.name.chars().any(char::is_control)
|
|
||||||
{
|
|
||||||
return Err(LandmarkError::InvalidOffer);
|
|
||||||
}
|
|
||||||
match node.kind {
|
match node.kind {
|
||||||
OfferedInventoryKind::Landmark {
|
OfferedInventoryKind::Landmark {
|
||||||
asset_id,
|
asset_id,
|
||||||
@@ -986,6 +1025,17 @@ fn flatten_offer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn validate_landmark_name(value: &str) -> Result<(), LandmarkError> {
|
||||||
|
if value.is_empty()
|
||||||
|
|| value.len() > MAX_LANDMARK_NAME_BYTES
|
||||||
|
|| value.chars().any(char::is_control)
|
||||||
|
{
|
||||||
|
Err(LandmarkError::InvalidOffer)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn validate_offer_id(value: &str) -> Result<(), LandmarkError> {
|
fn validate_offer_id(value: &str) -> Result<(), LandmarkError> {
|
||||||
if value.is_empty()
|
if value.is_empty()
|
||||||
|| value.len() > 128
|
|| value.len() > 128
|
||||||
@@ -1042,6 +1092,12 @@ struct SelectorArguments {
|
|||||||
selector: String,
|
selector: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct CreateArguments {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct ScheduleArguments {
|
struct ScheduleArguments {
|
||||||
@@ -1087,6 +1143,22 @@ impl AuthorizedToolBackend for LandmarkToolBackend {
|
|||||||
let principal = action.authenticated_avatar_id();
|
let principal = action.authenticated_avatar_id();
|
||||||
let result: Result<String, LandmarkError> = async {
|
let result: Result<String, LandmarkError> = async {
|
||||||
match action.call().name.as_str() {
|
match action.call().name.as_str() {
|
||||||
|
LANDMARK_CREATE_TOOL => {
|
||||||
|
let principal = principal.ok_or(LandmarkError::Unauthorized)?;
|
||||||
|
let arguments: CreateArguments =
|
||||||
|
serde_json::from_str(action.call().arguments_json.as_str())
|
||||||
|
.map_err(|_| LandmarkError::InvalidOffer)?;
|
||||||
|
self.service
|
||||||
|
.create_current(principal, &arguments.name, cancellation)
|
||||||
|
.await
|
||||||
|
.and_then(|entry| {
|
||||||
|
serde_json::to_string(&json!({
|
||||||
|
"stable_id": entry.stable_id,
|
||||||
|
"name": entry.display_name,
|
||||||
|
}))
|
||||||
|
.map_err(|_| LandmarkError::CorruptCatalog)
|
||||||
|
})
|
||||||
|
}
|
||||||
LANDMARK_LIST_TOOL => {
|
LANDMARK_LIST_TOOL => {
|
||||||
let empty: BTreeMap<String, String> =
|
let empty: BTreeMap<String, String> =
|
||||||
serde_json::from_str(action.call().arguments_json.as_str())
|
serde_json::from_str(action.call().arguments_json.as_str())
|
||||||
@@ -1229,6 +1301,11 @@ pub fn landmark_policy_tools(limits: LandmarkLimits) -> Result<Vec<PolicyTool>,
|
|||||||
required: BTreeSet::from(["selector".to_owned()]),
|
required: BTreeSet::from(["selector".to_owned()]),
|
||||||
additional_properties: false,
|
additional_properties: false,
|
||||||
};
|
};
|
||||||
|
let create = ToolSchema::Object {
|
||||||
|
properties: BTreeMap::from([("name".to_owned(), ToolSchema::String)]),
|
||||||
|
required: BTreeSet::from(["name".to_owned()]),
|
||||||
|
additional_properties: false,
|
||||||
|
};
|
||||||
let schedule = ToolSchema::Object {
|
let schedule = ToolSchema::Object {
|
||||||
properties: BTreeMap::from([
|
properties: BTreeMap::from([
|
||||||
("schedule_id".to_owned(), ToolSchema::String),
|
("schedule_id".to_owned(), ToolSchema::String),
|
||||||
@@ -1250,6 +1327,20 @@ pub fn landmark_policy_tools(limits: LandmarkLimits) -> Result<Vec<PolicyTool>,
|
|||||||
..ResourceCost::default()
|
..ResourceCost::default()
|
||||||
};
|
};
|
||||||
let specs = [
|
let specs = [
|
||||||
|
(
|
||||||
|
LANDMARK_CREATE_TOOL,
|
||||||
|
"Create and catalog one inventory landmark for the agent's current position",
|
||||||
|
create,
|
||||||
|
Capability::InventoryMutation,
|
||||||
|
Risk::InventoryMutation,
|
||||||
|
ResourceCost {
|
||||||
|
tool_calls: 1,
|
||||||
|
inventory_operations: 1,
|
||||||
|
..ResourceCost::default()
|
||||||
|
},
|
||||||
|
Idempotency::NonIdempotent,
|
||||||
|
false,
|
||||||
|
),
|
||||||
(
|
(
|
||||||
LANDMARK_LIST_TOOL,
|
LANDMARK_LIST_TOOL,
|
||||||
"List bounded validated landmark catalog metadata by stable ID; names are untrusted",
|
"List bounded validated landmark catalog metadata by stable ID; names are untrusted",
|
||||||
@@ -1286,7 +1377,7 @@ pub fn landmark_policy_tools(limits: LandmarkLimits) -> Result<Vec<PolicyTool>,
|
|||||||
schedule,
|
schedule,
|
||||||
Capability::Movement,
|
Capability::Movement,
|
||||||
Risk::Movement,
|
Risk::Movement,
|
||||||
movement,
|
ResourceCost::one_call(),
|
||||||
Idempotency::NonIdempotent,
|
Idempotency::NonIdempotent,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
@@ -1374,6 +1465,47 @@ impl LibremetaverseLandmarkGrid {
|
|||||||
|
|
||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
impl LandmarkGrid for LibremetaverseLandmarkGrid {
|
impl LandmarkGrid for LibremetaverseLandmarkGrid {
|
||||||
|
fn create_current_landmark(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
cancellation: CancellationToken,
|
||||||
|
) -> LandmarkFuture<'_, OfferedInventoryNode> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let folder = self
|
||||||
|
.inventory
|
||||||
|
.find_folder_for_type_with_folder_type(libremetaverse_types::FolderType::Landmark)
|
||||||
|
.map_err(|_| LandmarkError::CreateFailed)?;
|
||||||
|
if folder == UUID::zero() {
|
||||||
|
return Err(LandmarkError::CreateFailed);
|
||||||
|
}
|
||||||
|
let item = self
|
||||||
|
.inventory
|
||||||
|
.create_item_with_uuid_string_string_asset_type_uuid_inventory_type_permission_mask_cancellation_token(
|
||||||
|
folder,
|
||||||
|
name,
|
||||||
|
String::new(),
|
||||||
|
AssetType::Landmark,
|
||||||
|
UUID::zero(),
|
||||||
|
libremetaverse_types::InventoryType::LANDMARK,
|
||||||
|
libremetaverse::PermissionMask::ALL,
|
||||||
|
Some(cancellation.clone()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| LandmarkError::CreateFailed)?
|
||||||
|
.ok_or(LandmarkError::CreateFailed)?;
|
||||||
|
Ok(OfferedInventoryNode {
|
||||||
|
inventory_id: item.base.uuid(),
|
||||||
|
owner_id: item.base.owner_id(),
|
||||||
|
name: item.base.name(),
|
||||||
|
kind: OfferedInventoryKind::Landmark {
|
||||||
|
asset_id: item.asset_uuid(),
|
||||||
|
permissions_fingerprint: permission_fingerprint(item.permissions()),
|
||||||
|
},
|
||||||
|
children: Vec::new(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn accept_offer(
|
fn accept_offer(
|
||||||
&self,
|
&self,
|
||||||
offer_id: &str,
|
offer_id: &str,
|
||||||
|
|||||||
@@ -115,9 +115,9 @@ pub use interaction::{
|
|||||||
VisibleResponse, split_utf8,
|
VisibleResponse, split_utf8,
|
||||||
};
|
};
|
||||||
pub use landmarks::{
|
pub use landmarks::{
|
||||||
LANDMARK_LIST_TOOL, LANDMARK_SCHEDULE_TOOL, LANDMARK_STATUS_TOOL, LANDMARK_TELEPORT_TOOL,
|
LANDMARK_CREATE_TOOL, LANDMARK_LIST_TOOL, LANDMARK_SCHEDULE_TOOL, LANDMARK_STATUS_TOOL,
|
||||||
LandmarkEntry, LandmarkError, LandmarkFuture, LandmarkGrid, LandmarkLimits,
|
LANDMARK_TELEPORT_TOOL, LandmarkEntry, LandmarkError, LandmarkFuture, LandmarkGrid,
|
||||||
LandmarkObservation, LandmarkObservationKind, LandmarkOffer, LandmarkOutcome,
|
LandmarkLimits, LandmarkObservation, LandmarkObservationKind, LandmarkOffer, LandmarkOutcome,
|
||||||
LandmarkRoamingHandle, LandmarkService, LandmarkToolBackend, LandmarkValidation, OfferDecision,
|
LandmarkRoamingHandle, LandmarkService, LandmarkToolBackend, LandmarkValidation, OfferDecision,
|
||||||
OfferedInventoryKind, OfferedInventoryNode, RoamingPause, RoamingRandom, RoamingSchedule,
|
OfferedInventoryKind, OfferedInventoryNode, RoamingPause, RoamingRandom, RoamingSchedule,
|
||||||
SystemRoamingRandom, TeleportReceipt, TeleportTrigger, landmark_policy_tools,
|
SystemRoamingRandom, TeleportReceipt, TeleportTrigger, landmark_policy_tools,
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ async fn run_live(
|
|||||||
AgentControlTarget, BehaviorObservation, ControlEventKind, ControlPlane, ControlTarget,
|
AgentControlTarget, BehaviorObservation, ControlEventKind, ControlPlane, ControlTarget,
|
||||||
GridSessionBackend, LibremetaverseClientOwner, OperatingMode, RuntimeControlCommand,
|
GridSessionBackend, LibremetaverseClientOwner, OperatingMode, RuntimeControlCommand,
|
||||||
SessionControl, SessionObservation, SessionState, SessionSupervisor, TcpControlConfig,
|
SessionControl, SessionObservation, SessionState, SessionSupervisor, TcpControlConfig,
|
||||||
TcpControlServer,
|
TcpControlServer, WorldSnapshotSource,
|
||||||
};
|
};
|
||||||
|
|
||||||
let connection = config.grid.clone().ok_or_else(|| {
|
let connection = config.grid.clone().ok_or_else(|| {
|
||||||
@@ -449,6 +449,24 @@ async fn run_live(
|
|||||||
if let SessionObservation::Transition { status, reason, retry_in } = event {
|
if let SessionObservation::Transition { status, reason, retry_in } = event {
|
||||||
live.vision.set_generation(status.generation);
|
live.vision.set_generation(status.generation);
|
||||||
control_target.update_session(status);
|
control_target.update_session(status);
|
||||||
|
if status.agent_ready
|
||||||
|
&& let Ok(snapshot) = live
|
||||||
|
.world
|
||||||
|
.capture(
|
||||||
|
status.generation,
|
||||||
|
libremetaverse_types::compat::CancellationToken::default(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
control_target.update_region(
|
||||||
|
Some(snapshot.region_id.to_string()),
|
||||||
|
live.world.current_region_handle(),
|
||||||
|
Some(snapshot.region_name),
|
||||||
|
snapshot.agent.position.map(|position| {
|
||||||
|
[position.x, position.y, position.z]
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
live.landmark_roaming
|
live.landmark_roaming
|
||||||
.update_pause(|pause| pause.degraded = !status.agent_ready);
|
.update_pause(|pause| pause.degraded = !status.agent_ready);
|
||||||
control_plane.publish(ControlEventKind::StateChanged {
|
control_plane.publish(ControlEventKind::StateChanged {
|
||||||
@@ -582,6 +600,7 @@ struct LiveInteractions {
|
|||||||
build_control: Arc<dyn metacrate_grid_agent::BuildControl>,
|
build_control: Arc<dyn metacrate_grid_agent::BuildControl>,
|
||||||
vision:
|
vision:
|
||||||
Arc<metacrate_grid_agent::VisionService<metacrate_grid_agent::LibremetaverseSceneSource>>,
|
Arc<metacrate_grid_agent::VisionService<metacrate_grid_agent::LibremetaverseSceneSource>>,
|
||||||
|
world: Arc<metacrate_grid_agent::LibremetaverseWorldSnapshotSource>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "live-grid")]
|
#[cfg(feature = "live-grid")]
|
||||||
@@ -612,8 +631,9 @@ fn start_live_interactions(
|
|||||||
..LlmTransportLimits::default()
|
..LlmTransportLimits::default()
|
||||||
};
|
};
|
||||||
let conversation = Arc::new(ConversationStore::from_config(config)?);
|
let conversation = Arc::new(ConversationStore::from_config(config)?);
|
||||||
|
let world = Arc::new(owner.world_snapshot_source());
|
||||||
let perception = Arc::new(PerceptionBackend::new(
|
let perception = Arc::new(PerceptionBackend::new(
|
||||||
Arc::new(owner.world_snapshot_source()),
|
world.clone(),
|
||||||
Arc::clone(&conversation),
|
Arc::clone(&conversation),
|
||||||
config.limits.observable_queue,
|
config.limits.observable_queue,
|
||||||
)?);
|
)?);
|
||||||
@@ -760,6 +780,7 @@ fn start_live_interactions(
|
|||||||
landmark_roaming,
|
landmark_roaming,
|
||||||
build_control,
|
build_control,
|
||||||
vision,
|
vision,
|
||||||
|
world,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ use crate::conversation::ConversationClock;
|
|||||||
use crate::interaction::{
|
use crate::interaction::{
|
||||||
DeliveryFuture, InteractionDeliveryError, InteractionSink, OutboundInteraction,
|
DeliveryFuture, InteractionDeliveryError, InteractionSink, OutboundInteraction,
|
||||||
};
|
};
|
||||||
use crate::landmarks::{LandmarkError, LandmarkFuture, LandmarkGrid, RoamingRandom};
|
use crate::landmarks::{
|
||||||
|
LandmarkError, LandmarkFuture, LandmarkGrid, OfferedInventoryKind, OfferedInventoryNode,
|
||||||
|
RoamingRandom,
|
||||||
|
};
|
||||||
use crate::perception::{SnapshotFuture, WorldPosition, WorldSnapshot, WorldSnapshotSource};
|
use crate::perception::{SnapshotFuture, WorldPosition, WorldSnapshot, WorldSnapshotSource};
|
||||||
use crate::script_delivery::{
|
use crate::script_delivery::{
|
||||||
GeneratedScript, ScriptDeliveryError, ScriptInventory, ScriptInventoryFuture,
|
GeneratedScript, ScriptDeliveryError, ScriptInventory, ScriptInventoryFuture,
|
||||||
@@ -849,6 +852,30 @@ fn build_unit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LandmarkGrid for Arc<FakeGrid> {
|
impl LandmarkGrid for Arc<FakeGrid> {
|
||||||
|
fn create_current_landmark(
|
||||||
|
&self,
|
||||||
|
name: String,
|
||||||
|
_: CancellationToken,
|
||||||
|
) -> LandmarkFuture<'_, OfferedInventoryNode> {
|
||||||
|
let grid = self.clone();
|
||||||
|
Box::pin(async move {
|
||||||
|
grid.record("landmark", "landmark.create", None, name.as_bytes(), "ok");
|
||||||
|
Ok(OfferedInventoryNode {
|
||||||
|
inventory_id: UUID::new_with_string("00000000-0000-0000-0000-000000000980".into())
|
||||||
|
.map_err(|_| LandmarkError::CreateFailed)?,
|
||||||
|
owner_id: UUID::new_with_string("00000000-0000-0000-0000-000000000070".into())
|
||||||
|
.map_err(|_| LandmarkError::CreateFailed)?,
|
||||||
|
name,
|
||||||
|
kind: OfferedInventoryKind::Landmark {
|
||||||
|
asset_id: UUID::new_with_string("00000000-0000-0000-0000-000000000981".into())
|
||||||
|
.map_err(|_| LandmarkError::CreateFailed)?,
|
||||||
|
permissions_fingerprint: 0,
|
||||||
|
},
|
||||||
|
children: Vec::new(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn accept_offer(&self, offer_id: &str, _: CancellationToken) -> LandmarkFuture<'_, ()> {
|
fn accept_offer(&self, offer_id: &str, _: CancellationToken) -> LandmarkFuture<'_, ()> {
|
||||||
landmark_unit(self.clone(), "landmark.accept", offer_id, &[])
|
landmark_unit(self.clone(), "landmark.accept", offer_id, &[])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ impl TuiTransport for FakeTransport {
|
|||||||
transport_connected: true,
|
transport_connected: true,
|
||||||
agent_ready: true,
|
agent_ready: true,
|
||||||
region_id: Some("region".into()),
|
region_id: Some("region".into()),
|
||||||
|
region_handle: Some(9_007_199_254_740_992),
|
||||||
region_name: Some("Café 世界".into()),
|
region_name: Some("Café 世界".into()),
|
||||||
position: Some([1.0, 2.0, 3.0]),
|
position: Some([1.0, 2.0, 3.0]),
|
||||||
behavior_mode: "active".into(),
|
behavior_mode: "active".into(),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::vision::*;
|
use crate::vision::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
BoundedText, CompletionMessage, InteractionChannel, InteractionIntent, InteractionModelError,
|
BoundedText, Capability, CompletionMessage, InteractionChannel, InteractionIntent,
|
||||||
InteractionOrigin, InteractionResponder, MessageRole, ResponderFuture, ResponseRequest,
|
InteractionModelError, InteractionOrigin, InteractionResponder, MessageRole, ResponderFuture,
|
||||||
VisibleResponse,
|
ResponseRequest, VisibleResponse,
|
||||||
};
|
};
|
||||||
use libremetaverse_types::{
|
use libremetaverse_types::{
|
||||||
UUID,
|
UUID,
|
||||||
@@ -302,6 +302,7 @@ fn request(text: &str) -> ResponseRequest {
|
|||||||
channel: InteractionChannel::DirectIm,
|
channel: InteractionChannel::DirectIm,
|
||||||
origin: InteractionOrigin::AuthorizedIm,
|
origin: InteractionOrigin::AuthorizedIm,
|
||||||
intent: InteractionIntent::Informational,
|
intent: InteractionIntent::Informational,
|
||||||
|
capabilities: std::collections::BTreeSet::from([Capability::Informational]),
|
||||||
messages: vec![CompletionMessage::text(MessageRole::Avatar, text).unwrap()],
|
messages: vec![CompletionMessage::text(MessageRole::Avatar, text).unwrap()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ hash. Do not record vendor presets or identifiers. Exercise, in order:
|
|||||||
5. As a privileged user, build only on controlled land, record transaction and
|
5. As a privileged user, build only on controlled land, record transaction and
|
||||||
object recovery IDs locally, verify no currency operation exists, and delete
|
object recovery IDs locally, verify no currency operation exists, and delete
|
||||||
every created prim through the ownership-checked cleanup path.
|
every created prim through the ownership-checked cleanup path.
|
||||||
6. Accept a controlled landmark offer, use a short bounded folder schedule,
|
6. Create and catalog a current-location landmark, accept a controlled landmark
|
||||||
teleport, then disable the schedule. Capture the synthetic scene and ask
|
offer, use a short bounded folder schedule, teleport, then disable the
|
||||||
one visual question. Record the endpoint
|
schedule. Capture the synthetic scene and ask one visual question. Record the endpoint
|
||||||
capability fallback if image input is rejected.
|
capability fallback if image input is rejected.
|
||||||
7. Gracefully stop. Confirm no pending approvals, scheduled jobs, inventory
|
7. Gracefully stop. Confirm no pending approvals, scheduled jobs, inventory
|
||||||
offers, owned test prims, tasks, sockets, or sessions. List any unavoidable
|
offers, owned test prims, tasks, sockets, or sessions. List any unavoidable
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
# Landmark intake, teleport, and roaming
|
# Landmark intake, teleport, and roaming
|
||||||
|
|
||||||
Landmark authority is private. Public chat and non-allow-listed IM can neither
|
Landmark authority is private. Public chat and non-allow-listed IM can neither
|
||||||
accept offers nor see teleport/schedule tools. The policy layer seals the
|
accept offers nor see create/teleport/schedule tools. An authorized agent can
|
||||||
authenticated avatar into every mutation; tool schemas contain a catalog
|
create and catalog a landmark for its current position in the standard
|
||||||
selector or bounded interval, never an avatar, region, coordinate, inventory
|
Landmarks inventory folder. The policy layer seals the authenticated avatar
|
||||||
|
into every mutation; tool schemas contain only a landmark name, catalog
|
||||||
|
selector, or bounded interval, never an avatar, region, coordinate, inventory
|
||||||
folder, or L$ field.
|
folder, or L$ field.
|
||||||
|
|
||||||
The live adapter subscribes through the original LibreMetaverse compatibility
|
The live adapter subscribes through the original LibreMetaverse compatibility
|
||||||
|
|||||||
@@ -1012,7 +1012,7 @@ fn communication_commands() -> [CommandEntry; 6] {
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
"im",
|
"im",
|
||||||
"Instant message someone. Usage: im [firstname] [lastname] [message]",
|
"Instant message someone. Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]",
|
||||||
CommandCategory::Communication,
|
CommandCategory::Communication,
|
||||||
CommandHandler::Im,
|
CommandHandler::Im,
|
||||||
),
|
),
|
||||||
@@ -1878,15 +1878,28 @@ async fn execute_communication_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
CommandHandler::Im => {
|
CommandHandler::Im => {
|
||||||
if args.len() < 3 {
|
if args.is_empty() {
|
||||||
return "Usage: im [firstname] [lastname] [message]".into();
|
return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
|
||||||
|
.into();
|
||||||
}
|
}
|
||||||
let name = format!("{} {}", args[0], args[1]);
|
let (target, message) = if let Ok(target) = UUID::new_with_string(args[0].clone()) {
|
||||||
let message = bounded_message(&args[2..].join(" "));
|
if args.len() < 2 {
|
||||||
let target = match client.backend.resolve_avatar(&name, cancellation).await {
|
return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
|
||||||
Ok(Some(target)) => target,
|
.into();
|
||||||
Ok(None) => return format!("Name lookup for {name} failed"),
|
}
|
||||||
Err(error) => return format!("Name lookup for {name} failed: {error}"),
|
(target, bounded_message(&args[1..].join(" ")))
|
||||||
|
} else {
|
||||||
|
if args.len() < 3 {
|
||||||
|
return "Usage: im [firstname] [lastname] [message] | im [avatar-uuid] [message]"
|
||||||
|
.into();
|
||||||
|
}
|
||||||
|
let name = format!("{} {}", args[0], args[1]);
|
||||||
|
let target = match client.backend.resolve_avatar(&name, cancellation).await {
|
||||||
|
Ok(Some(target)) => target,
|
||||||
|
Ok(None) => return format!("Name lookup for {name} failed"),
|
||||||
|
Err(error) => return format!("Name lookup for {name} failed: {error}"),
|
||||||
|
};
|
||||||
|
(target, bounded_message(&args[2..].join(" ")))
|
||||||
};
|
};
|
||||||
match client.backend.instant_message(target, &message) {
|
match client.backend.instant_message(target, &message) {
|
||||||
Ok(()) => format!("Instant Messaged {target} with message: {message}"),
|
Ok(()) => format!("Instant Messaged {target} with message: {message}"),
|
||||||
|
|||||||
@@ -452,6 +452,7 @@ enum Movement {
|
|||||||
Follow(Option<(UUID, Vector3)>),
|
Follow(Option<(UUID, Vector3)>),
|
||||||
GoHome,
|
GoHome,
|
||||||
TeleportRegion(String, Vector3),
|
TeleportRegion(String, Vector3),
|
||||||
|
TeleportRegionHandle(u64, Vector3),
|
||||||
TeleportLandmark(UUID),
|
TeleportLandmark(UUID),
|
||||||
Jump,
|
Jump,
|
||||||
AutoPilot { local: Vector3, global: [f64; 3] },
|
AutoPilot { local: Vector3, global: [f64; 3] },
|
||||||
@@ -904,11 +905,12 @@ async fn goto<B: Backend + ?Sized>(
|
|||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
"Usage: goto sim/x/y/z --confirm",
|
"Usage: goto sim/x/y/z --confirm",
|
||||||
)?;
|
)?;
|
||||||
|
let movement = parts[0].parse::<u64>().map_or_else(
|
||||||
|
|_| Movement::TeleportRegion(parts[0].into(), position),
|
||||||
|
|handle| Movement::TeleportRegionHandle(handle, position),
|
||||||
|
);
|
||||||
backend
|
backend
|
||||||
.world_mutate(
|
.world_mutate(Mutation::Movement(movement), cancellation)
|
||||||
Mutation::Movement(Movement::TeleportRegion(parts[0].into(), position)),
|
|
||||||
cancellation,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
Ok(format!("Teleported to {}", parts[0]))
|
Ok(format!("Teleported to {}", parts[0]))
|
||||||
}
|
}
|
||||||
@@ -2210,6 +2212,12 @@ fn apply_fake_movement(
|
|||||||
backend.record(format!("CALL teleport-region {name} {position:?}"));
|
backend.record(format!("CALL teleport-region {name} {position:?}"));
|
||||||
"Teleport complete".into()
|
"Teleport complete".into()
|
||||||
}
|
}
|
||||||
|
Movement::TeleportRegionHandle(handle, position) => {
|
||||||
|
state.fake.region_handle = handle;
|
||||||
|
state.fake.position = position;
|
||||||
|
backend.record(format!("CALL teleport-region-handle {handle} {position:?}"));
|
||||||
|
"Teleport complete".into()
|
||||||
|
}
|
||||||
Movement::TeleportLandmark(id) => {
|
Movement::TeleportLandmark(id) => {
|
||||||
backend.record(format!("CALL teleport-landmark {id}"));
|
backend.record(format!("CALL teleport-landmark {id}"));
|
||||||
"Teleport complete".into()
|
"Teleport complete".into()
|
||||||
@@ -3188,6 +3196,22 @@ async fn live_movement(
|
|||||||
Err(format!("Teleport failed: {}", agent.teleport_message()))
|
Err(format!("Teleport failed: {}", agent.teleport_message()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Movement::TeleportRegionHandle(handle, position) => {
|
||||||
|
let agent = live_agent(backend)?;
|
||||||
|
let success = agent
|
||||||
|
.teleport_with_u_int64_vector3_cancellation_token(
|
||||||
|
handle,
|
||||||
|
position,
|
||||||
|
Some(cancellation),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "Teleport failed")?;
|
||||||
|
if success {
|
||||||
|
Ok("Teleport complete".into())
|
||||||
|
} else {
|
||||||
|
Err(format!("Teleport failed: {}", agent.teleport_message()))
|
||||||
|
}
|
||||||
|
}
|
||||||
Movement::TeleportLandmark(id) => {
|
Movement::TeleportLandmark(id) => {
|
||||||
let agent = live_agent(backend)?;
|
let agent = live_agent(backend)?;
|
||||||
if agent
|
if agent
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ fn scripted_terminal_exercises_registry_communication_system_and_remote_auth() {
|
|||||||
shout loud\n\
|
shout loud\n\
|
||||||
@ Alice Bot\n\
|
@ Alice Bot\n\
|
||||||
im Target Resident private hello\n\
|
im Target Resident private hello\n\
|
||||||
|
im {TARGET} private by uuid\n\
|
||||||
imgroup {GROUP} group hello\n\
|
imgroup {GROUP} group hello\n\
|
||||||
echomaster\n\
|
echomaster\n\
|
||||||
showeffects on\n\
|
showeffects on\n\
|
||||||
@@ -161,6 +162,7 @@ fn scripted_terminal_exercises_registry_communication_system_and_remote_auth() {
|
|||||||
"[Alice Bot] CALL chat channel=7 type=Normal hello world",
|
"[Alice Bot] CALL chat channel=7 type=Normal hello world",
|
||||||
"[Bob Bot] CALL chat channel=7 type=Normal hello world",
|
"[Bob Bot] CALL chat channel=7 type=Normal hello world",
|
||||||
"[Alice Bot] CALL instant-message 22222222-3333-4444-5555-666666666666 private hello",
|
"[Alice Bot] CALL instant-message 22222222-3333-4444-5555-666666666666 private hello",
|
||||||
|
"[Alice Bot] CALL instant-message 22222222-3333-4444-5555-666666666666 private by uuid",
|
||||||
"[Alice Bot] CALL group-chat-join 33333333-4444-5555-6666-777777777777",
|
"[Alice Bot] CALL group-chat-join 33333333-4444-5555-6666-777777777777",
|
||||||
"[Alice Bot] CALL group-instant-message 33333333-4444-5555-6666-777777777777 group hello",
|
"[Alice Bot] CALL group-instant-message 33333333-4444-5555-6666-777777777777 group hello",
|
||||||
"[Alice Bot] CALL chat channel=0 type=Normal echo this",
|
"[Alice Bot] CALL chat channel=0 type=Normal echo this",
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ fn fake_grid_exercises_every_owned_world_command() {
|
|||||||
forward 0 --confirm\n\
|
forward 0 --confirm\n\
|
||||||
gohome --confirm\n\
|
gohome --confirm\n\
|
||||||
goto Test Region/128/128/25 --confirm\n\
|
goto Test Region/128/128/25 --confirm\n\
|
||||||
|
goto {HANDLE}/129/129/26 --confirm\n\
|
||||||
goto_landmark {LANDMARK} --confirm\n\
|
goto_landmark {LANDMARK} --confirm\n\
|
||||||
jump --confirm\n\
|
jump --confirm\n\
|
||||||
left 0 --confirm\n\
|
left 0 --confirm\n\
|
||||||
@@ -236,6 +237,7 @@ fn fake_grid_exercises_every_owned_world_command() {
|
|||||||
"CALL movement-forward duration-ms=0",
|
"CALL movement-forward duration-ms=0",
|
||||||
"CALL teleport-home",
|
"CALL teleport-home",
|
||||||
"CALL teleport-region Test Region",
|
"CALL teleport-region Test Region",
|
||||||
|
"CALL teleport-region-handle 4294967298000",
|
||||||
"CALL teleport-landmark 50000000-0000-0000-0000-000000000001",
|
"CALL teleport-landmark 50000000-0000-0000-0000-000000000001",
|
||||||
"CALL movement-jump",
|
"CALL movement-jump",
|
||||||
"CALL movement-left duration-ms=0",
|
"CALL movement-left duration-ms=0",
|
||||||
|
|||||||
@@ -1743,10 +1743,17 @@ fn append_packet_impls(body: &mut String, packets: &[&PacketDefinition]) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
BlockRepetition::Variable => {
|
BlockRepetition::Variable => {
|
||||||
let _ = writeln!(
|
if packet.name == "ImprovedInstantMessage" && block.name == "MetaData" {
|
||||||
body,
|
let _ = writeln!(
|
||||||
" let {field_name}_count = usize::from(reader.read_u8()?);"
|
body,
|
||||||
);
|
" let {field_name}_count = if reader.is_empty() {{ 0 }} else {{ usize::from(reader.read_u8()?) }};"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let _ = writeln!(
|
||||||
|
body,
|
||||||
|
" let {field_name}_count = usize::from(reader.read_u8()?);"
|
||||||
|
);
|
||||||
|
}
|
||||||
let _ = writeln!(body, " let mut {field_name} = Vec::new();");
|
let _ = writeln!(body, " let mut {field_name} = Vec::new();");
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
body,
|
body,
|
||||||
|
|||||||
Reference in New Issue
Block a user