mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Implement live syncback service properties encoding and enhance syncback functionality
- Introduced a new `encodeService` helper in the plugin to serialize service properties, attributes, tags, and references alongside children data. - Updated the `performSyncback` function to utilize the new encoding method, ensuring all services are processed without child-only restrictions. - Extended the `ServiceChunk` struct to include fields for properties, attributes, tags, and references, improving data handling during syncback. - Modified the `build_dom_from_chunks` function to apply service properties and resolve references when reconstructing the service instances. - Added comprehensive tests to validate the new service properties handling, ensuring accurate synchronization and data integrity. These enhancements significantly improve the live syncback process, allowing for better service data management and consistency.
This commit is contained in:
parent
30e032b27e
commit
72ad84b5a7
16 changed files with 896 additions and 444 deletions
|
|
@ -527,26 +527,69 @@ fn get_port_number() -> usize {
|
|||
pub fn make_service_chunk(
|
||||
class_name: &str,
|
||||
children: Vec<rbx_dom_weak::InstanceBuilder>,
|
||||
) -> librojo::web_api::ServiceChunk {
|
||||
make_service_chunk_full(class_name, vec![], vec![], vec![], vec![], children)
|
||||
}
|
||||
|
||||
/// Build a ServiceChunk with explicit service-level properties, attributes,
|
||||
/// tags, and ref hints alongside the serialized children.
|
||||
pub fn make_service_chunk_full(
|
||||
class_name: &str,
|
||||
properties: Vec<(&str, rbx_dom_weak::types::Variant)>,
|
||||
attributes: Vec<(&str, rbx_dom_weak::types::Variant)>,
|
||||
tags: Vec<String>,
|
||||
refs: Vec<(&str, &str, &str)>,
|
||||
children: Vec<rbx_dom_weak::InstanceBuilder>,
|
||||
) -> librojo::web_api::ServiceChunk {
|
||||
use rbx_dom_weak::WeakDom;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut dom = WeakDom::new(rbx_dom_weak::InstanceBuilder::new("DataModel"));
|
||||
let root = dom.root_ref();
|
||||
for child in children {
|
||||
dom.insert(root, child);
|
||||
}
|
||||
let refs: Vec<Ref> = dom.root().children().to_vec();
|
||||
let child_refs: Vec<Ref> = dom.root().children().to_vec();
|
||||
let mut buf = Vec::new();
|
||||
rbx_binary::to_writer(&mut buf, &dom, &refs).unwrap();
|
||||
if !child_refs.is_empty() {
|
||||
rbx_binary::to_writer(&mut buf, &dom, &child_refs).unwrap();
|
||||
}
|
||||
|
||||
let props_map: HashMap<String, rbx_dom_weak::types::Variant> = properties
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
let attrs_map: HashMap<String, rbx_dom_weak::types::Variant> = attributes
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_string(), v))
|
||||
.collect();
|
||||
let refs_map: HashMap<String, librojo::web_api::ServiceRef> = refs
|
||||
.into_iter()
|
||||
.map(|(prop, name, class)| {
|
||||
(
|
||||
prop.to_string(),
|
||||
librojo::web_api::ServiceRef {
|
||||
name: name.to_string(),
|
||||
class_name: class.to_string(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
librojo::web_api::ServiceChunk {
|
||||
class_name: class_name.to_string(),
|
||||
data: buf,
|
||||
properties: props_map,
|
||||
attributes: attrs_map,
|
||||
tags,
|
||||
refs: refs_map,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a complete rbxl file from service chunks, mirroring the same data
|
||||
/// that live syncback receives. Used for CLI parity testing.
|
||||
pub fn make_rbxl_from_chunks(chunks: &[librojo::web_api::ServiceChunk]) -> Vec<u8> {
|
||||
use rbx_dom_weak::types::{Attributes, Tags, Variant};
|
||||
use rbx_dom_weak::WeakDom;
|
||||
use std::collections::HashMap;
|
||||
use std::io::Cursor;
|
||||
|
|
@ -555,63 +598,80 @@ pub fn make_rbxl_from_chunks(chunks: &[librojo::web_api::ServiceChunk]) -> Vec<u
|
|||
let root_ref = dom.root_ref();
|
||||
|
||||
for chunk in chunks {
|
||||
let chunk_dom = rbx_binary::from_reader(Cursor::new(&chunk.data))
|
||||
.unwrap_or_else(|e| panic!("Failed to parse rbxm for {}: {e}", chunk.class_name));
|
||||
let mut builder = rbx_dom_weak::InstanceBuilder::new(&chunk.class_name);
|
||||
|
||||
let service_ref = dom.insert(
|
||||
root_ref,
|
||||
rbx_dom_weak::InstanceBuilder::new(&chunk.class_name),
|
||||
);
|
||||
|
||||
let mut ref_map: HashMap<Ref, Ref> = HashMap::new();
|
||||
for &child_ref in chunk_dom.root().children() {
|
||||
deep_clone_into_test(&chunk_dom, &mut dom, child_ref, service_ref, &mut ref_map);
|
||||
for (key, value) in &chunk.properties {
|
||||
builder = builder.with_property(key.as_str(), value.clone());
|
||||
}
|
||||
|
||||
let all_refs: Vec<Ref> = ref_map.values().copied().collect();
|
||||
for inst_ref in all_refs {
|
||||
let props_to_fix: Vec<(String, Ref)> = {
|
||||
let inst = dom.get_by_ref(inst_ref).unwrap();
|
||||
inst.properties
|
||||
.iter()
|
||||
.filter_map(|(key, value)| {
|
||||
if let rbx_dom_weak::types::Variant::Ref(r) = value {
|
||||
ref_map.get(r).map(|&mapped| (key.to_string(), mapped))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !props_to_fix.is_empty() {
|
||||
let inst = dom.get_by_ref_mut(inst_ref).unwrap();
|
||||
for (key, new_ref) in props_to_fix {
|
||||
if !chunk.attributes.is_empty() {
|
||||
let mut attrs = Attributes::new();
|
||||
for (key, value) in &chunk.attributes {
|
||||
attrs.insert(key.clone(), value.clone());
|
||||
}
|
||||
builder = builder.with_property("Attributes", Variant::Attributes(attrs));
|
||||
}
|
||||
|
||||
if !chunk.tags.is_empty() {
|
||||
let tags: Tags = chunk.tags.iter().map(|s| s.as_str()).collect();
|
||||
builder = builder.with_property("Tags", Variant::Tags(tags));
|
||||
}
|
||||
|
||||
let service_ref = dom.insert(root_ref, builder);
|
||||
|
||||
if !chunk.data.is_empty() {
|
||||
let chunk_dom = rbx_binary::from_reader(Cursor::new(&chunk.data))
|
||||
.unwrap_or_else(|e| panic!("Failed to parse rbxm for {}: {e}", chunk.class_name));
|
||||
|
||||
let mut ref_map: HashMap<Ref, Ref> = HashMap::new();
|
||||
for &child_ref in chunk_dom.root().children() {
|
||||
deep_clone_into_test(&chunk_dom, &mut dom, child_ref, service_ref, &mut ref_map);
|
||||
}
|
||||
|
||||
let all_refs: Vec<Ref> = ref_map.values().copied().collect();
|
||||
for inst_ref in all_refs {
|
||||
let props_to_fix: Vec<(String, Ref)> = {
|
||||
let inst = dom.get_by_ref(inst_ref).unwrap();
|
||||
inst.properties
|
||||
.insert(key.into(), rbx_dom_weak::types::Variant::Ref(new_ref));
|
||||
.iter()
|
||||
.filter_map(|(key, value)| {
|
||||
if let Variant::Ref(r) = value {
|
||||
ref_map.get(r).map(|&mapped| (key.to_string(), mapped))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
if !props_to_fix.is_empty() {
|
||||
let inst = dom.get_by_ref_mut(inst_ref).unwrap();
|
||||
for (key, new_ref) in props_to_fix {
|
||||
inst.properties.insert(key.into(), Variant::Ref(new_ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !chunk.refs.is_empty() {
|
||||
let children: Vec<Ref> = dom.get_by_ref(service_ref).unwrap().children().to_vec();
|
||||
for (prop_name, target) in &chunk.refs {
|
||||
let found = children.iter().find(|&&child_ref| {
|
||||
let child = dom.get_by_ref(child_ref).unwrap();
|
||||
child.name.as_str() == target.name && child.class.as_str() == target.class_name
|
||||
});
|
||||
if let Some(&child_ref) = found {
|
||||
let service = dom.get_by_ref_mut(service_ref).unwrap();
|
||||
service
|
||||
.properties
|
||||
.insert(prop_name.as_str().into(), Variant::Ref(child_ref));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let visible_services = [
|
||||
"Lighting",
|
||||
"MaterialService",
|
||||
"ReplicatedFirst",
|
||||
"ReplicatedStorage",
|
||||
"ServerScriptService",
|
||||
"ServerStorage",
|
||||
"SoundService",
|
||||
"StarterGui",
|
||||
"StarterPack",
|
||||
"StarterPlayer",
|
||||
"Teams",
|
||||
"TextChatService",
|
||||
"VoiceChatService",
|
||||
"Workspace",
|
||||
];
|
||||
let existing: std::collections::HashSet<String> =
|
||||
chunks.iter().map(|c| c.class_name.clone()).collect();
|
||||
for &svc in &visible_services {
|
||||
for &svc in librojo::syncback::VISIBLE_SERVICES {
|
||||
if !existing.contains(svc) {
|
||||
dom.insert(root_ref, rbx_dom_weak::InstanceBuilder::new(svc));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ use rbx_dom_weak::InstanceBuilder;
|
|||
|
||||
use crate::rojo_test::{
|
||||
roundtrip_util::assert_dirs_equal,
|
||||
serve_util::{make_service_chunk, run_cli_syncback_on_chunks, run_serve_test},
|
||||
serve_util::{
|
||||
make_service_chunk, make_service_chunk_full, run_cli_syncback_on_chunks, run_serve_test,
|
||||
},
|
||||
};
|
||||
|
||||
use librojo::web_api::{ServiceChunk, SocketPacketType, PROTOCOL_VERSION};
|
||||
|
|
@ -182,6 +184,189 @@ fn parity_empty_services_mixed() {
|
|||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
// ── Service property parity tests ────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn parity_lighting_properties() {
|
||||
use rbx_dom_weak::types::{Color3, Variant};
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"Lighting",
|
||||
vec![
|
||||
("Ambient", Variant::Color3(Color3::new(0.5, 0.5, 0.5))),
|
||||
("Brightness", Variant::Float32(2.0)),
|
||||
("ClockTime", Variant::Float32(14.5)),
|
||||
],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![InstanceBuilder::new("PointLight").with_name("TestLight")],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_soundservice_properties() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"SoundService",
|
||||
vec![
|
||||
(
|
||||
"AmbientReverb",
|
||||
Variant::Enum(rbx_dom_weak::types::Enum::from_u32(2)),
|
||||
),
|
||||
("DistanceFactor", Variant::Float32(5.0)),
|
||||
],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_starterplayer_properties() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"StarterPlayer",
|
||||
vec![("CameraMaxZoomDistance", Variant::Float32(200.0))],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_textchatservice_properties() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"TextChatService",
|
||||
vec![(
|
||||
"ChatVersion",
|
||||
Variant::Enum(rbx_dom_weak::types::Enum::from_u32(2)),
|
||||
)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_childless_service_with_properties() {
|
||||
use rbx_dom_weak::types::Variant;
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"VoiceChatService",
|
||||
vec![(
|
||||
"UseAudioApi",
|
||||
Variant::Enum(rbx_dom_weak::types::Enum::from_u32(2)),
|
||||
)],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_service_properties_with_children() {
|
||||
use rbx_dom_weak::types::{Color3, Variant};
|
||||
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"Lighting",
|
||||
vec![
|
||||
("Ambient", Variant::Color3(Color3::new(0.3, 0.3, 0.3))),
|
||||
("Brightness", Variant::Float32(3.0)),
|
||||
],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![
|
||||
InstanceBuilder::new("PointLight").with_name("LightA"),
|
||||
InstanceBuilder::new("ModuleScript")
|
||||
.with_name("LightingConfig")
|
||||
.with_property("Source", Variant::String("return {}".into())),
|
||||
],
|
||||
)];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_multiple_services_with_properties() {
|
||||
use rbx_dom_weak::types::{Color3, Variant};
|
||||
|
||||
let chunks = vec![
|
||||
make_service_chunk_full(
|
||||
"Lighting",
|
||||
vec![("Ambient", Variant::Color3(Color3::new(0.2, 0.2, 0.2)))],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
),
|
||||
make_service_chunk_full(
|
||||
"SoundService",
|
||||
vec![("DistanceFactor", Variant::Float32(10.0))],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
),
|
||||
make_service_chunk(
|
||||
"ReplicatedStorage",
|
||||
vec![InstanceBuilder::new("Folder").with_name("Assets")],
|
||||
),
|
||||
];
|
||||
assert_live_matches_cli("live_syncback", &chunks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parity_camera_not_synced() {
|
||||
let camera_child = InstanceBuilder::new("Camera").with_name("Camera");
|
||||
let chunks = vec![make_service_chunk_full(
|
||||
"Workspace",
|
||||
vec![],
|
||||
vec![],
|
||||
vec![],
|
||||
vec![("CurrentCamera", "Camera", "Camera")],
|
||||
vec![
|
||||
camera_child,
|
||||
InstanceBuilder::new("Part")
|
||||
.with_name("Floor")
|
||||
.with_property("Anchored", rbx_dom_weak::types::Variant::Bool(true)),
|
||||
],
|
||||
)];
|
||||
|
||||
run_serve_test("live_syncback", |session, _| {
|
||||
session.post_api_syncback(None, chunks.to_vec());
|
||||
session.wait_to_come_back_online();
|
||||
|
||||
let workspace_dir = session.path().join("src/workspace");
|
||||
let has_camera = workspace_dir.join("Camera").exists()
|
||||
|| workspace_dir.join("Camera.rbxm").exists()
|
||||
|| workspace_dir.join("Camera.model.json5").exists();
|
||||
|
||||
assert!(
|
||||
!has_camera,
|
||||
"Camera should not appear on disk (syncCurrentCamera defaults to false)"
|
||||
);
|
||||
|
||||
assert!(
|
||||
workspace_dir.join("Floor.model.json5").exists(),
|
||||
"Non-camera children should still be written"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Validation / rejection tests ─────────────────────────────────
|
||||
|
||||
#[test]
|
||||
|
|
@ -461,14 +646,59 @@ fn roundtrip_build_syncback_rebuild() {
|
|||
for &service_ref in dom_a.root().children() {
|
||||
let service = dom_a.get_by_ref(service_ref).unwrap();
|
||||
let child_refs: Vec<rbx_dom_weak::types::Ref> = service.children().to_vec();
|
||||
if child_refs.is_empty() {
|
||||
continue;
|
||||
|
||||
let mut data = Vec::new();
|
||||
if !child_refs.is_empty() {
|
||||
rbx_binary::to_writer(&mut data, &dom_a, &child_refs).unwrap();
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
rbx_binary::to_writer(&mut buf, &dom_a, &child_refs).unwrap();
|
||||
|
||||
let mut properties = std::collections::HashMap::new();
|
||||
let mut refs_map = std::collections::HashMap::new();
|
||||
for (key, value) in &service.properties {
|
||||
match value {
|
||||
rbx_dom_weak::types::Variant::Ref(target) => {
|
||||
if let Some(target_inst) = dom_a.get_by_ref(*target) {
|
||||
refs_map.insert(
|
||||
key.to_string(),
|
||||
librojo::web_api::ServiceRef {
|
||||
name: target_inst.name.to_string(),
|
||||
class_name: target_inst.class.to_string(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
rbx_dom_weak::types::Variant::Attributes(_)
|
||||
| rbx_dom_weak::types::Variant::Tags(_) => {}
|
||||
_ => {
|
||||
properties.insert(key.to_string(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut attributes_map = std::collections::HashMap::new();
|
||||
if let Some(rbx_dom_weak::types::Variant::Attributes(attrs)) =
|
||||
service.properties.get(&rbx_dom_weak::ustr("Attributes"))
|
||||
{
|
||||
for (k, v) in attrs.iter() {
|
||||
attributes_map.insert(k.to_string(), v.clone());
|
||||
}
|
||||
}
|
||||
|
||||
let tags_vec = if let Some(rbx_dom_weak::types::Variant::Tags(tags)) =
|
||||
service.properties.get(&rbx_dom_weak::ustr("Tags"))
|
||||
{
|
||||
tags.iter().map(|s| s.to_string()).collect()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
chunks.push(ServiceChunk {
|
||||
class_name: service.class.to_string(),
|
||||
data: buf,
|
||||
data,
|
||||
properties,
|
||||
attributes: attributes_map,
|
||||
tags: tags_vec,
|
||||
refs: refs_map,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue