mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
scriptsOnly mode
This commit is contained in:
parent
6d8c85682d
commit
88beb1d8d7
3 changed files with 225 additions and 8 deletions
|
|
@ -126,6 +126,13 @@ pub struct Project {
|
|||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub sync_rules: Vec<SyncRule>,
|
||||
|
||||
/// When enabled, only script instances (Script, LocalScript, ModuleScript)
|
||||
/// will be synced to Roblox Studio. All other instances in the project are
|
||||
/// ignored during sync, allowing Studio to maintain its own non-script
|
||||
/// content. Defaults to `false`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sync_scripts_only: Option<bool>,
|
||||
|
||||
/// The path to the file that this project came from. Relative paths in the
|
||||
/// project should be considered relative to the parent of this field, also
|
||||
/// given by `Project::folder_location`.
|
||||
|
|
|
|||
|
|
@ -221,6 +221,12 @@ impl ServeSession {
|
|||
pub fn root_project(&self) -> &Project {
|
||||
&self.root_project
|
||||
}
|
||||
|
||||
/// Returns whether sync should only include script instances.
|
||||
/// When enabled, only Script, LocalScript, and ModuleScript are synced.
|
||||
pub fn sync_scripts_only(&self) -> bool {
|
||||
self.root_project.sync_scripts_only.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
|||
220
src/web/api.rs
220
src/web/api.rs
|
|
@ -24,7 +24,7 @@ use crate::{
|
|||
snapshot::{InstanceWithMeta, PatchSet, PatchUpdate},
|
||||
web::{
|
||||
interface::{
|
||||
ErrorResponse, Instance, MessagesPacket, OpenResponse, ReadResponse,
|
||||
ErrorResponse, Instance, InstanceMetadata, MessagesPacket, OpenResponse, ReadResponse,
|
||||
ServerInfoResponse, SocketPacket, SocketPacketBody, SocketPacketType, SubscribeMessage,
|
||||
WriteRequest, WriteResponse, PROTOCOL_VERSION, SERVER_VERSION,
|
||||
},
|
||||
|
|
@ -201,15 +201,55 @@ impl ApiService {
|
|||
let message_cursor = message_queue.cursor();
|
||||
|
||||
let tree = self.serve_session.tree();
|
||||
let scripts_only = self.serve_session.sync_scripts_only();
|
||||
|
||||
let mut instances = HashMap::new();
|
||||
|
||||
for id in requested_ids {
|
||||
if let Some(instance) = tree.get_instance(id) {
|
||||
instances.insert(id, Instance::from_rojo_instance(instance));
|
||||
if scripts_only {
|
||||
// In scripts-only mode, we need to:
|
||||
// 1. Find all scripts
|
||||
// 2. Include their ancestor chains (so the tree structure is valid)
|
||||
// 3. Mark non-script ancestors with ignoreUnknownInstances: true
|
||||
// 4. Filter children to only include instances in our set
|
||||
|
||||
for descendant in tree.descendants(id) {
|
||||
instances.insert(descendant.id(), Instance::from_rojo_instance(descendant));
|
||||
let mut included_ids: HashSet<Ref> = HashSet::new();
|
||||
|
||||
// First pass: collect all script IDs and their ancestors
|
||||
for id in &requested_ids {
|
||||
if let Some(instance) = tree.get_instance(*id) {
|
||||
collect_scripts_and_ancestors(&tree, instance.id(), &mut included_ids);
|
||||
for descendant in tree.descendants(*id) {
|
||||
collect_scripts_and_ancestors(&tree, descendant.id(), &mut included_ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: create Instance objects for included IDs
|
||||
for id in &requested_ids {
|
||||
if let Some(instance) = tree.get_instance(*id) {
|
||||
if included_ids.contains(id) {
|
||||
let inst = instance_for_scripts_only(instance, &included_ids);
|
||||
instances.insert(*id, inst);
|
||||
}
|
||||
|
||||
for descendant in tree.descendants(*id) {
|
||||
let desc_id = descendant.id();
|
||||
if included_ids.contains(&desc_id) {
|
||||
let inst = instance_for_scripts_only(descendant, &included_ids);
|
||||
instances.insert(desc_id, inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal mode: include all instances
|
||||
for id in requested_ids {
|
||||
if let Some(instance) = tree.get_instance(id) {
|
||||
instances.insert(id, Instance::from_rojo_instance(instance));
|
||||
|
||||
for descendant in tree.descendants(id) {
|
||||
instances.insert(descendant.id(), Instance::from_rojo_instance(descendant));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -458,6 +498,7 @@ async fn handle_websocket_subscription(
|
|||
let session_id = serve_session.session_id();
|
||||
let tree_handle = serve_session.tree_handle();
|
||||
let message_queue = serve_session.message_queue();
|
||||
let scripts_only = serve_session.sync_scripts_only();
|
||||
|
||||
log::debug!(
|
||||
"WebSocket subscription established for session {}",
|
||||
|
|
@ -478,9 +519,17 @@ async fn handle_websocket_subscription(
|
|||
if !messages.is_empty() {
|
||||
let json_message = {
|
||||
let tree = tree_handle.lock().unwrap();
|
||||
let api_messages = messages
|
||||
let api_messages: Vec<_> = messages
|
||||
.into_iter()
|
||||
.map(|patch| SubscribeMessage::from_patch_update(&tree, patch))
|
||||
.map(|patch| {
|
||||
let mut msg = SubscribeMessage::from_patch_update(&tree, patch);
|
||||
// In scripts-only mode, transform to only include scripts
|
||||
// and their necessary ancestors
|
||||
if scripts_only {
|
||||
filter_subscribe_message_for_scripts(&tree, &mut msg);
|
||||
}
|
||||
msg
|
||||
})
|
||||
.collect();
|
||||
|
||||
let response = SocketPacket {
|
||||
|
|
@ -567,3 +616,158 @@ fn parent_requirements(class: &str) -> Option<&str> {
|
|||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Checks if a class name is a script type (Script, LocalScript, ModuleScript).
|
||||
#[inline]
|
||||
fn is_script_class(class_name: &str) -> bool {
|
||||
matches!(class_name, "Script" | "LocalScript" | "ModuleScript")
|
||||
}
|
||||
|
||||
/// Filters a SubscribeMessage to only include scripts and their ancestors.
|
||||
/// Non-script ancestors get ignoreUnknownInstances: true.
|
||||
fn filter_subscribe_message_for_scripts(
|
||||
tree: &crate::snapshot::RojoTree,
|
||||
msg: &mut SubscribeMessage<'_>,
|
||||
) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
// Build set of IDs that should be included (scripts + their ancestors)
|
||||
let mut included_ids: HashSet<Ref> = HashSet::new();
|
||||
|
||||
// First, find all scripts in the added instances and collect their ancestors
|
||||
for (id, inst) in &msg.added {
|
||||
if is_script_class(inst.class_name.as_str()) {
|
||||
// Add this script and all its ancestors
|
||||
included_ids.insert(*id);
|
||||
let mut current_parent = inst.parent;
|
||||
while !current_parent.is_none() {
|
||||
if included_ids.contains(¤t_parent) {
|
||||
break;
|
||||
}
|
||||
included_ids.insert(current_parent);
|
||||
if let Some(parent_inst) = msg.added.get(¤t_parent) {
|
||||
current_parent = parent_inst.parent;
|
||||
} else if let Some(tree_inst) = tree.get_instance(current_parent) {
|
||||
current_parent = tree_inst.parent();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter added to only include IDs in our set
|
||||
msg.added.retain(|id, _| included_ids.contains(id));
|
||||
|
||||
// ALL instances get ignoreUnknownInstances: true in scripts-only mode
|
||||
// This ensures we only sync script content, never delete/modify other instances
|
||||
// Non-scripts also have their properties cleared - they're just tree structure
|
||||
for (_id, inst) in &mut msg.added {
|
||||
inst.metadata = Some(InstanceMetadata {
|
||||
ignore_unknown_instances: true,
|
||||
});
|
||||
|
||||
// Non-scripts don't sync properties - they're just tree structure
|
||||
if !is_script_class(inst.class_name.as_str()) {
|
||||
inst.properties.clear();
|
||||
}
|
||||
|
||||
// Filter children to only include those in our set
|
||||
let filtered_children: Vec<Ref> = inst
|
||||
.children
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|child_id| included_ids.contains(child_id))
|
||||
.collect();
|
||||
inst.children = Cow::Owned(filtered_children);
|
||||
}
|
||||
|
||||
// Only allow updates to scripts
|
||||
msg.updated.retain(|update| {
|
||||
tree.get_instance(update.id)
|
||||
.map(|inst| is_script_class(inst.class_name().as_str()))
|
||||
.unwrap_or(false)
|
||||
});
|
||||
|
||||
// Only remove scripts (plugin will ignore unknown IDs anyway)
|
||||
msg.removed.retain(|_id| {
|
||||
// We can't check removed instances in the tree since they're gone,
|
||||
// so we let all removals through - the plugin will ignore unknown IDs
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
/// Recursively collects a script instance and all its ancestors into the set.
|
||||
/// Only adds to the set if the instance is a script (then adds ancestors too).
|
||||
fn collect_scripts_and_ancestors(
|
||||
tree: &crate::snapshot::RojoTree,
|
||||
id: Ref,
|
||||
included_ids: &mut HashSet<Ref>,
|
||||
) {
|
||||
if let Some(instance) = tree.get_instance(id) {
|
||||
if is_script_class(instance.class_name().as_str()) {
|
||||
// This is a script - add it and all ancestors
|
||||
let mut current_id = id;
|
||||
while let Some(inst) = tree.get_instance(current_id) {
|
||||
included_ids.insert(current_id);
|
||||
let parent_id = inst.parent();
|
||||
if parent_id.is_none() || included_ids.contains(&parent_id) {
|
||||
break;
|
||||
}
|
||||
current_id = parent_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an Instance for scripts-only mode.
|
||||
/// - Scripts get their properties synced normally
|
||||
/// - Non-scripts only provide tree structure (no properties synced)
|
||||
fn instance_for_scripts_only<'a>(
|
||||
source: InstanceWithMeta<'a>,
|
||||
included_ids: &HashSet<Ref>,
|
||||
) -> Instance<'a> {
|
||||
use std::borrow::Cow;
|
||||
|
||||
let is_script = is_script_class(source.class_name().as_str());
|
||||
|
||||
// Only sync properties for scripts, not for ancestor containers
|
||||
let properties = if is_script {
|
||||
source
|
||||
.properties()
|
||||
.iter()
|
||||
.filter(|(_key, value)| {
|
||||
// Filter out SharedString values (can't be serialized)
|
||||
value.ty() != rbx_dom_weak::types::VariantType::SharedString
|
||||
})
|
||||
.map(|(key, value)| (*key, Cow::Borrowed(value)))
|
||||
.collect()
|
||||
} else {
|
||||
// Non-scripts don't sync any properties - they're just tree structure
|
||||
UstrMap::default()
|
||||
};
|
||||
|
||||
// Filter children to only include those in our included set
|
||||
let filtered_children: Vec<Ref> = source
|
||||
.children()
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|child_id| included_ids.contains(child_id))
|
||||
.collect();
|
||||
|
||||
// In scripts-only mode, ALL instances get ignoreUnknownInstances: true
|
||||
// This ensures we only sync script content, never delete/modify other instances
|
||||
let metadata = Some(InstanceMetadata {
|
||||
ignore_unknown_instances: true,
|
||||
});
|
||||
|
||||
Instance {
|
||||
id: source.id(),
|
||||
parent: source.parent(),
|
||||
name: Cow::Borrowed(source.name()),
|
||||
class_name: source.class_name(),
|
||||
properties,
|
||||
children: Cow::Owned(filtered_children),
|
||||
metadata,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue