mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
work
This commit is contained in:
parent
89e7e0282d
commit
4985a3fede
17 changed files with 891 additions and 63 deletions
|
|
@ -251,6 +251,13 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
|
|||
applied_patch.changed_metadata = Some(metadata);
|
||||
}
|
||||
|
||||
// Capture old class name before the mutable borrow so we can update
|
||||
// the script index afterward if the class changes.
|
||||
let old_class_name = patch.changed_class_name.and_then(|_| {
|
||||
tree.get_instance(patch.id)
|
||||
.map(|inst| inst.class_name().to_string())
|
||||
});
|
||||
|
||||
let mut instance = match tree.get_instance_mut(patch.id) {
|
||||
Some(instance) => instance,
|
||||
None => {
|
||||
|
|
@ -307,6 +314,11 @@ fn apply_update_child(context: &mut PatchApplyContext, tree: &mut RojoTree, patc
|
|||
applied_patch.changed_properties.insert(key, property_entry);
|
||||
}
|
||||
|
||||
if let (Some(old_class), Some(new_class)) = (&old_class_name, &applied_patch.changed_class_name)
|
||||
{
|
||||
tree.update_script_tracking(patch.id, old_class, new_class.as_str());
|
||||
}
|
||||
|
||||
defer_ref_properties(tree, patch.id, context);
|
||||
|
||||
context.applied_patch_set.updated.push(applied_patch)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
|
@ -12,6 +12,11 @@ use crate::{multimap::MultiMap, RojoRef};
|
|||
|
||||
use super::{InstanceMetadata, InstanceSnapshot, InstigatingSource};
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn is_script_class(class_name: &str) -> bool {
|
||||
matches!(class_name, "Script" | "LocalScript" | "ModuleScript")
|
||||
}
|
||||
|
||||
/// An expanded variant of rbx_dom_weak's `WeakDom` that tracks additional
|
||||
/// metadata per instance that's Rojo-specific.
|
||||
///
|
||||
|
|
@ -39,6 +44,11 @@ pub struct RojoTree {
|
|||
/// the same RojoRef for multiple different instances. An entry containing
|
||||
/// multiple elements is an error condition that should be raised to the user.
|
||||
specified_id_to_refs: MultiMap<RojoRef, Ref>,
|
||||
|
||||
/// Refs of all Script, LocalScript, and ModuleScript instances in the tree.
|
||||
/// Maintained incrementally for fast scripts-only mode filtering in the
|
||||
/// serve API without walking the entire tree.
|
||||
script_refs: HashSet<Ref>,
|
||||
}
|
||||
|
||||
impl RojoTree {
|
||||
|
|
@ -52,6 +62,7 @@ impl RojoTree {
|
|||
metadata_map: HashMap::new(),
|
||||
path_to_ids: MultiMap::new(),
|
||||
specified_id_to_refs: MultiMap::new(),
|
||||
script_refs: HashSet::new(),
|
||||
};
|
||||
|
||||
let root_ref = tree.inner.root_ref();
|
||||
|
|
@ -124,6 +135,8 @@ impl RojoTree {
|
|||
_ => Vec::new(),
|
||||
};
|
||||
|
||||
let is_script = is_script_class(snapshot.class_name.as_ref());
|
||||
|
||||
let builder = InstanceBuilder::empty()
|
||||
.with_class(snapshot.class_name)
|
||||
.with_name(snapshot.name.into_owned())
|
||||
|
|
@ -133,6 +146,10 @@ impl RojoTree {
|
|||
let referent = self.inner.insert(parent_ref, builder);
|
||||
self.insert_metadata(referent, snapshot.metadata);
|
||||
|
||||
if is_script {
|
||||
self.script_refs.insert(referent);
|
||||
}
|
||||
|
||||
for child in snapshot.children {
|
||||
self.insert_instance(referent, child);
|
||||
}
|
||||
|
|
@ -146,6 +163,7 @@ impl RojoTree {
|
|||
|
||||
while let Some(id) = to_move.pop_front() {
|
||||
self.remove_metadata(id);
|
||||
self.script_refs.remove(&id);
|
||||
|
||||
if let Some(instance) = self.inner.get_by_ref(id) {
|
||||
to_move.extend(instance.children().iter().copied());
|
||||
|
|
@ -231,6 +249,20 @@ impl RojoTree {
|
|||
self.specified_id_to_refs.insert(specified, id);
|
||||
}
|
||||
|
||||
pub fn script_refs(&self) -> &HashSet<Ref> {
|
||||
&self.script_refs
|
||||
}
|
||||
|
||||
pub fn update_script_tracking(&mut self, id: Ref, old_class: &str, new_class: &str) {
|
||||
let was_script = is_script_class(old_class);
|
||||
let is_script = is_script_class(new_class);
|
||||
if was_script && !is_script {
|
||||
self.script_refs.remove(&id);
|
||||
} else if !was_script && is_script {
|
||||
self.script_refs.insert(id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up an instance by its filesystem-name path in the tree.
|
||||
///
|
||||
/// Each segment is matched against the **filesystem name** of child
|
||||
|
|
|
|||
130
src/web/api.rs
130
src/web/api.rs
|
|
@ -393,6 +393,7 @@ impl ApiService {
|
|||
game_id: self.serve_session.game_id(),
|
||||
root_instance_id,
|
||||
sync_source_only: true,
|
||||
sync_scripts_only: self.serve_session.sync_scripts_only(),
|
||||
ignore_hidden_services,
|
||||
visible_services,
|
||||
git_metadata,
|
||||
|
|
@ -450,7 +451,7 @@ impl ApiService {
|
|||
|
||||
let body = request.into_body().collect().await.unwrap().to_bytes();
|
||||
|
||||
let request: WriteRequest = match deserialize_msgpack(&body) {
|
||||
let mut request: WriteRequest = match deserialize_msgpack(&body) {
|
||||
Ok(request) => request,
|
||||
Err(err) => {
|
||||
return msgpack(
|
||||
|
|
@ -467,6 +468,24 @@ impl ApiService {
|
|||
);
|
||||
}
|
||||
|
||||
if self.serve_session.sync_scripts_only() {
|
||||
let tree = self.serve_session.tree();
|
||||
let before = request.updated.len();
|
||||
request.updated.retain(|update| {
|
||||
tree.get_instance(update.id)
|
||||
.map(|inst| {
|
||||
crate::snapshot::is_script_class(inst.class_name().as_str())
|
||||
})
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let filtered = before - request.updated.len();
|
||||
if filtered > 0 {
|
||||
log::debug!(
|
||||
"Scripts-only mode: filtered {filtered} non-script update(s) from write request"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Process removed instances (syncback: delete files from Rojo filesystem)
|
||||
// Phase 1: Gather paths with the tree lock held.
|
||||
// Phase 2: Delete files without the lock.
|
||||
|
|
@ -3101,39 +3120,43 @@ impl ApiService {
|
|||
let mut instances = HashMap::new();
|
||||
|
||||
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
|
||||
|
||||
// Use the script index to avoid walking the entire tree.
|
||||
// For each script, walk UP to see if it's a descendant of a
|
||||
// requested ID, collecting the ancestor chain along the way.
|
||||
let mut included_ids: HashSet<Ref> = HashSet::new();
|
||||
let requested_set: HashSet<Ref> = requested_ids.iter().copied().collect();
|
||||
|
||||
// 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);
|
||||
for &script_id in tree.script_refs() {
|
||||
let mut chain = Vec::new();
|
||||
let mut current = script_id;
|
||||
let mut is_descendant = false;
|
||||
|
||||
while let Some(inst) = tree.get_instance(current) {
|
||||
if included_ids.contains(¤t) {
|
||||
is_descendant = true;
|
||||
break;
|
||||
}
|
||||
if requested_set.contains(¤t) {
|
||||
is_descendant = true;
|
||||
chain.push(current);
|
||||
break;
|
||||
}
|
||||
chain.push(current);
|
||||
let parent = inst.parent();
|
||||
if parent.is_none() {
|
||||
break;
|
||||
}
|
||||
current = parent;
|
||||
}
|
||||
|
||||
if is_descendant {
|
||||
included_ids.extend(chain);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
for &id in &included_ids {
|
||||
if let Some(instance) = tree.get_instance(id) {
|
||||
instances.insert(id, instance_for_scripts_only(instance, &included_ids));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -3532,17 +3555,13 @@ fn parent_requirements(class: &str) -> Option<&str> {
|
|||
})
|
||||
}
|
||||
|
||||
/// 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")
|
||||
}
|
||||
use crate::snapshot::is_script_class;
|
||||
|
||||
/// 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<'_>,
|
||||
fn filter_subscribe_message_for_scripts<'a>(
|
||||
tree: &'a crate::snapshot::RojoTree,
|
||||
msg: &mut SubscribeMessage<'a>,
|
||||
) {
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
|
@ -3574,9 +3593,19 @@ fn filter_subscribe_message_for_scripts(
|
|||
// 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
|
||||
// Inject ancestors that the plugin may not have yet. When a new script
|
||||
// appears in a previously-pruned subtree, its ancestor containers exist
|
||||
// in the tree but weren't part of this patch. Without injection the
|
||||
// plugin receives an orphaned script with an unknown parent.
|
||||
for id in included_ids.iter().copied().collect::<Vec<_>>() {
|
||||
if !msg.added.contains_key(&id) {
|
||||
if let Some(tree_inst) = tree.get_instance(id) {
|
||||
let inst = instance_for_scripts_only(tree_inst, &included_ids);
|
||||
msg.added.insert(id, inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for inst in msg.added.values_mut() {
|
||||
inst.metadata = Some(InstanceMetadata {
|
||||
ignore_unknown_instances: true,
|
||||
|
|
@ -3612,29 +3641,6 @@ fn filter_subscribe_message_for_scripts(
|
|||
});
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
|
|
|||
|
|
@ -258,6 +258,10 @@ pub struct ServerInfoResponse {
|
|||
/// Other property changes will be ignored.
|
||||
#[serde(default)]
|
||||
pub sync_source_only: bool,
|
||||
/// When true, only script instances (and their ancestors for tree structure)
|
||||
/// are synced. The plugin should skip non-script changes in two-way sync.
|
||||
#[serde(default)]
|
||||
pub sync_scripts_only: bool,
|
||||
/// When true, hidden/internal services should be ignored during sync.
|
||||
/// The plugin should not mark these services as "to delete" during forward sync.
|
||||
#[serde(default)]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue