gameplay: add in-memory persistence round trips
Browse files- crates/urt-gameplay/src/bin/urt-native-policy.rs +60 -0
- crates/urt-gameplay/src/native_policy.rs +175 -0
- docs/FINDINGS.md +3 -0
- docs/PROVISIONAL_TODO.md +3 -0
- docs/RESEARCH_LEDGER.md +5 -0
- docs/REVERSE_ENGINEERING_TODO.md +3 -0
- docs/research/README.md +3 -0
- docs/research/RUST_PERSISTENCE_ROUNDTRIP.md +7 -0
- docs/research/rust-health-impulse.json +1 -1
- docs/research/rust-persistence-roundtrip.json +0 -0
- scripts/analyze_rust_persistence_roundtrip.py +14 -0
- scripts/test_rust_persistence_roundtrip.py +9 -0
- xtask/checks.tsv +15 -0
crates/urt-gameplay/src/bin/urt-native-policy.rs
CHANGED
|
@@ -653,6 +653,66 @@ fn run() -> Result<String, String> {
|
|
| 653 |
command
|
| 654 |
))
|
| 655 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
_ => Err("unknown command".into()),
|
| 657 |
}
|
| 658 |
}
|
|
|
|
| 653 |
command
|
| 654 |
))
|
| 655 |
}
|
| 656 |
+
"roundtrip" => {
|
| 657 |
+
if w.len() < 85 {
|
| 658 |
+
return Err("missing roundtrip header".into());
|
| 659 |
+
}
|
| 660 |
+
let n = w[5] as usize;
|
| 661 |
+
if n > 64 || w.len() != 85 + n {
|
| 662 |
+
return Err("invalid roundtrip count".into());
|
| 663 |
+
}
|
| 664 |
+
let mut engine = MemoryPersistence::default();
|
| 665 |
+
let mut target = RestoreState {
|
| 666 |
+
values: [0; 9],
|
| 667 |
+
ignore: [2; 64],
|
| 668 |
+
position: [0; 6],
|
| 669 |
+
};
|
| 670 |
+
let mut selector = 0;
|
| 671 |
+
let indices = w[85..].iter().map(|v| *v as usize).collect::<Vec<_>>();
|
| 672 |
+
let out = persistence_roundtrip(
|
| 673 |
+
RoundtripInput {
|
| 674 |
+
position: PositionInput {
|
| 675 |
+
mode: w[0] as i32,
|
| 676 |
+
enabled: 1,
|
| 677 |
+
slot: w[1] as usize,
|
| 678 |
+
userinfo: b"\\cl_guid\\ID",
|
| 679 |
+
mapname: b"ut4_test",
|
| 680 |
+
name: b"PLAYER",
|
| 681 |
+
values: w[79..85].try_into().unwrap(),
|
| 682 |
+
handle: w[2],
|
| 683 |
+
},
|
| 684 |
+
flags: w[3],
|
| 685 |
+
values: std::array::from_fn(|j| w[6 + j] as i32),
|
| 686 |
+
ignore: std::array::from_fn(|j| w[15 + j] as i32),
|
| 687 |
+
entity_clients: &indices,
|
| 688 |
+
scratch: [w[4]; 5],
|
| 689 |
+
read_handle: 7,
|
| 690 |
+
},
|
| 691 |
+
&mut target,
|
| 692 |
+
&mut engine,
|
| 693 |
+
&mut selector,
|
| 694 |
+
)
|
| 695 |
+
.map_err(|e| format!("{e:?}"))?;
|
| 696 |
+
let file = engine
|
| 697 |
+
.files
|
| 698 |
+
.values()
|
| 699 |
+
.next()
|
| 700 |
+
.map(|v| format!("\"{}\"", hex(v)))
|
| 701 |
+
.unwrap_or("null".into());
|
| 702 |
+
Ok(format!(
|
| 703 |
+
"{{\"saved\":{},\"sets\":{},\"file\":{},\"values\":{},\"ignore\":{},\"origin\":{},\"scan_result\":{},\"cvars\":{},\"load_events\":{},\"ring\":{},\"info_ring\":{selector}}}",
|
| 704 |
+
position_events(&out.saved.events),
|
| 705 |
+
sets_json(&out.written.sets),
|
| 706 |
+
file,
|
| 707 |
+
list(&target.values.map(|v| v as u32)),
|
| 708 |
+
list(&target.ignore.map(|v| v as u32)),
|
| 709 |
+
list(&target.position),
|
| 710 |
+
out.restored.scan_result,
|
| 711 |
+
sets_json(&out.restored.cvars),
|
| 712 |
+
position_events(&out.restored.position.io.events),
|
| 713 |
+
out.ring_advance
|
| 714 |
+
))
|
| 715 |
+
}
|
| 716 |
_ => Err("unknown command".into()),
|
| 717 |
}
|
| 718 |
}
|
crates/urt-gameplay/src/native_policy.rs
CHANGED
|
@@ -902,6 +902,7 @@ pub struct PositionIo {
|
|
| 902 |
pub events: Vec<PositionEvent>,
|
| 903 |
pub ring_advance: u32,
|
| 904 |
}
|
|
|
|
| 905 |
pub struct PositionInput<'a> {
|
| 906 |
pub mode: i32,
|
| 907 |
pub enabled: i32,
|
|
@@ -1719,3 +1720,177 @@ mod normal_persist_tests {
|
|
| 1719 |
assert_eq!(ring, 1);
|
| 1720 |
}
|
| 1721 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 902 |
pub events: Vec<PositionEvent>,
|
| 903 |
pub ring_advance: u32,
|
| 904 |
}
|
| 905 |
+
#[derive(Clone, Copy)]
|
| 906 |
pub struct PositionInput<'a> {
|
| 907 |
pub mode: i32,
|
| 908 |
pub enabled: i32,
|
|
|
|
| 1720 |
assert_eq!(ring, 1);
|
| 1721 |
}
|
| 1722 |
}
|
| 1723 |
+
|
| 1724 |
+
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
| 1725 |
+
pub struct MemoryPersistence {
|
| 1726 |
+
pub cvars: std::collections::BTreeMap<Vec<u8>, Vec<u8>>,
|
| 1727 |
+
pub files: std::collections::BTreeMap<Vec<u8>, Vec<u8>>,
|
| 1728 |
+
}
|
| 1729 |
+
impl MemoryPersistence {
|
| 1730 |
+
/// Apply a single captured saved-position write sequence to an in-memory
|
| 1731 |
+
/// transcript. This owns bytes, not native handles or operating-system files.
|
| 1732 |
+
pub fn apply_position(&mut self, io: &PositionIo) {
|
| 1733 |
+
let mut path = None;
|
| 1734 |
+
for e in &io.events {
|
| 1735 |
+
match e {
|
| 1736 |
+
PositionEvent::Open { path: p, mode: 1 } => path = Some(p.clone()),
|
| 1737 |
+
PositionEvent::Write { data, .. } => {
|
| 1738 |
+
if let Some(p) = &path {
|
| 1739 |
+
self.files.insert(p.clone(), data.clone());
|
| 1740 |
+
}
|
| 1741 |
+
}
|
| 1742 |
+
_ => {}
|
| 1743 |
+
}
|
| 1744 |
+
}
|
| 1745 |
+
}
|
| 1746 |
+
pub fn apply_sets(&mut self, sets: &[(Vec<u8>, Vec<u8>)]) {
|
| 1747 |
+
self.cvars.extend(sets.iter().cloned());
|
| 1748 |
+
}
|
| 1749 |
+
}
|
| 1750 |
+
pub struct RoundtripInput<'a> {
|
| 1751 |
+
pub position: PositionInput<'a>,
|
| 1752 |
+
pub flags: u32,
|
| 1753 |
+
pub values: [i32; 9],
|
| 1754 |
+
pub ignore: [i32; 64],
|
| 1755 |
+
pub entity_clients: &'a [usize],
|
| 1756 |
+
pub scratch: [u32; 5],
|
| 1757 |
+
pub read_handle: u32,
|
| 1758 |
+
}
|
| 1759 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 1760 |
+
pub struct PersistenceRoundtrip {
|
| 1761 |
+
pub saved: PositionIo,
|
| 1762 |
+
pub written: SessionWrite,
|
| 1763 |
+
pub restored: RestoredSession,
|
| 1764 |
+
pub ring_advance: u32,
|
| 1765 |
+
}
|
| 1766 |
+
pub fn persistence_roundtrip(
|
| 1767 |
+
input: RoundtripInput<'_>,
|
| 1768 |
+
target: &mut RestoreState,
|
| 1769 |
+
engine: &mut MemoryPersistence,
|
| 1770 |
+
info_ring: &mut u32,
|
| 1771 |
+
) -> Result<PersistenceRoundtrip, Error> {
|
| 1772 |
+
if input.entity_clients.len() > 64 {
|
| 1773 |
+
return Err(Error::InvalidCount);
|
| 1774 |
+
}
|
| 1775 |
+
let mut staged_target = target.clone();
|
| 1776 |
+
let mut staged_engine = engine.clone();
|
| 1777 |
+
let mut selector = *info_ring;
|
| 1778 |
+
let context = input.position;
|
| 1779 |
+
let saved = save_position(context, &mut selector)?;
|
| 1780 |
+
let written = session_write(
|
| 1781 |
+
context.slot,
|
| 1782 |
+
input.entity_clients.len() as i32,
|
| 1783 |
+
input.values,
|
| 1784 |
+
input.entity_clients,
|
| 1785 |
+
&input.ignore,
|
| 1786 |
+
)?;
|
| 1787 |
+
staged_engine.apply_position(&saved);
|
| 1788 |
+
staged_engine.apply_sets(&written.sets);
|
| 1789 |
+
let mut unused = 0;
|
| 1790 |
+
let raw = cbytes(context.userinfo);
|
| 1791 |
+
let guid = info_lookup(
|
| 1792 |
+
Some(&raw[..raw.len().min(1023)]),
|
| 1793 |
+
Some(b"cl_guid"),
|
| 1794 |
+
&mut unused,
|
| 1795 |
+
)?;
|
| 1796 |
+
let path = position_path(context.mapname, &guid.value);
|
| 1797 |
+
let file = staged_engine.files.get(&path).cloned();
|
| 1798 |
+
let data = file.as_deref().unwrap_or_default();
|
| 1799 |
+
let length = if file.is_some() {
|
| 1800 |
+
i32::try_from(data.len()).map_err(|_| Error::InvalidInput)?
|
| 1801 |
+
} else {
|
| 1802 |
+
-1
|
| 1803 |
+
};
|
| 1804 |
+
let mut load = context;
|
| 1805 |
+
load.handle = if file.is_some() { input.read_handle } else { 0 };
|
| 1806 |
+
let session = staged_engine
|
| 1807 |
+
.cvars
|
| 1808 |
+
.get(format!("session{}", context.slot).as_bytes())
|
| 1809 |
+
.cloned()
|
| 1810 |
+
.unwrap_or_default();
|
| 1811 |
+
let ignored = staged_engine
|
| 1812 |
+
.cvars
|
| 1813 |
+
.get(format!("sessIgnore{}", context.slot).as_bytes())
|
| 1814 |
+
.cloned()
|
| 1815 |
+
.unwrap_or_default();
|
| 1816 |
+
let restored = restore_session(
|
| 1817 |
+
&mut staged_target,
|
| 1818 |
+
RestoreInput {
|
| 1819 |
+
flags: input.flags,
|
| 1820 |
+
session: &session,
|
| 1821 |
+
ignore_text: &ignored,
|
| 1822 |
+
scratch: input.scratch,
|
| 1823 |
+
position: load,
|
| 1824 |
+
file_length: length,
|
| 1825 |
+
file_data: data,
|
| 1826 |
+
},
|
| 1827 |
+
&mut selector,
|
| 1828 |
+
)?;
|
| 1829 |
+
let ring_advance = saved.ring_advance + written.ring_advance + restored.ring_advance;
|
| 1830 |
+
*target = staged_target;
|
| 1831 |
+
*engine = staged_engine;
|
| 1832 |
+
*info_ring = selector;
|
| 1833 |
+
Ok(PersistenceRoundtrip {
|
| 1834 |
+
saved,
|
| 1835 |
+
written,
|
| 1836 |
+
restored,
|
| 1837 |
+
ring_advance,
|
| 1838 |
+
})
|
| 1839 |
+
}
|
| 1840 |
+
#[cfg(test)]
|
| 1841 |
+
mod memory_persistence_tests {
|
| 1842 |
+
use super::*;
|
| 1843 |
+
#[test]
|
| 1844 |
+
fn zero_handle_open_does_not_create_file() {
|
| 1845 |
+
let mut engine = MemoryPersistence::default();
|
| 1846 |
+
engine.apply_position(&PositionIo {
|
| 1847 |
+
events: vec![PositionEvent::Open {
|
| 1848 |
+
path: b"x".to_vec(),
|
| 1849 |
+
mode: 1,
|
| 1850 |
+
}],
|
| 1851 |
+
ring_advance: 1,
|
| 1852 |
+
});
|
| 1853 |
+
assert!(engine.files.is_empty());
|
| 1854 |
+
}
|
| 1855 |
+
#[test]
|
| 1856 |
+
fn rejected_indices_do_not_commit_engine_or_selector() {
|
| 1857 |
+
let mut engine = MemoryPersistence::default();
|
| 1858 |
+
let old = engine.clone();
|
| 1859 |
+
let mut target = RestoreState {
|
| 1860 |
+
values: [0; 9],
|
| 1861 |
+
ignore: [2; 64],
|
| 1862 |
+
position: [0; 6],
|
| 1863 |
+
};
|
| 1864 |
+
let before = target.clone();
|
| 1865 |
+
let mut selector = 0;
|
| 1866 |
+
assert_eq!(
|
| 1867 |
+
persistence_roundtrip(
|
| 1868 |
+
RoundtripInput {
|
| 1869 |
+
position: PositionInput {
|
| 1870 |
+
mode: 0,
|
| 1871 |
+
enabled: 1,
|
| 1872 |
+
slot: 0,
|
| 1873 |
+
userinfo: b"",
|
| 1874 |
+
mapname: b"m",
|
| 1875 |
+
name: b"n",
|
| 1876 |
+
values: [0; 6],
|
| 1877 |
+
handle: 0
|
| 1878 |
+
},
|
| 1879 |
+
flags: 0,
|
| 1880 |
+
values: [0; 9],
|
| 1881 |
+
ignore: [0; 64],
|
| 1882 |
+
entity_clients: &[64],
|
| 1883 |
+
scratch: [0; 5],
|
| 1884 |
+
read_handle: 7
|
| 1885 |
+
},
|
| 1886 |
+
&mut target,
|
| 1887 |
+
&mut engine,
|
| 1888 |
+
&mut selector
|
| 1889 |
+
),
|
| 1890 |
+
Err(Error::InvalidIndex)
|
| 1891 |
+
);
|
| 1892 |
+
assert_eq!(engine, old);
|
| 1893 |
+
assert_eq!(target, before);
|
| 1894 |
+
assert_eq!(selector, 0);
|
| 1895 |
+
}
|
| 1896 |
+
}
|
docs/FINDINGS.md
CHANGED
|
@@ -1730,3 +1730,6 @@ RE-813–822 replaces the scoreboard decimal assumption with actual37914 executi
|
|
| 1730 |
|
| 1731 |
|
| 1732 |
**RE-910:** 144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. [Evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md).
|
|
|
|
|
|
|
|
|
|
|
|
| 1730 |
|
| 1731 |
|
| 1732 |
**RE-910:** 144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. [Evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md).
|
| 1733 |
+
|
| 1734 |
+
|
| 1735 |
+
**RE-911:** Rust matches144 original save/session-write/restore/read compositions using owned in-memory cvar/file bytes. Partial integer parsing and caller scratch preserve the observed non-invertibility of negative/INT_MIN session text; file prefixes and ignore accumulation remain exact. Engine/destination/selector changes stage atomically on errors. This adapter does not own native handles or perform OS file IO. [Evidence](research/RUST_PERSISTENCE_ROUNDTRIP.md).
|
docs/PROVISIONAL_TODO.md
CHANGED
|
@@ -1767,3 +1767,6 @@ RE-909: Rust matches64 original normal transitions with actual saved-file and cv
|
|
| 1767 |
|
| 1768 |
|
| 1769 |
RE-910: 144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. Full runtime completion remains open. [Evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md).
|
|
|
|
|
|
|
|
|
|
|
|
| 1767 |
|
| 1768 |
|
| 1769 |
RE-910: 144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. Full runtime completion remains open. [Evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md).
|
| 1770 |
+
|
| 1771 |
+
|
| 1772 |
+
RE-911: Rust matches144 original save/session-write/restore/read compositions using owned in-memory cvar/file bytes. Partial integer parsing and caller scratch preserve the observed non-invertibility of negative/INT_MIN session text; file prefixes and ignore accumulation remain exact. Engine/destination/selector changes stage atomically on errors. This adapter does not own native handles or perform OS file IO. Full runtime completion remains open. [Evidence](research/RUST_PERSISTENCE_ROUNDTRIP.md).
|
docs/RESEARCH_LEDGER.md
CHANGED
|
@@ -2270,3 +2270,8 @@ Rust matches64 original normal transitions with actual saved-file and cvar child
|
|
| 2270 |
## 2026-10-28 — RE-910 Original write-to-restore persistence round trip
|
| 2271 |
|
| 2272 |
144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. Focused `cargo xtask test-group qvm-persistence-roundtrip` passes (4 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2270 |
## 2026-10-28 — RE-910 Original write-to-restore persistence round trip
|
| 2271 |
|
| 2272 |
144 finite original246746/246963/247219/246554 compositions feed actual captured cvar/file bytes into a fresh destination snapshot. Negative and INT_MIN integer formatting is deliberately not assumed invertible; original partial scanner writes and supplied scratch are preserved. Ignore entries accumulate over the destination table, and six-float file prefixes survive appended names. The engine is a supplied in-memory transcript, not a native filesystem or real map reload. Focused `cargo xtask test-group qvm-persistence-roundtrip` passes (4 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
| 2273 |
+
|
| 2274 |
+
|
| 2275 |
+
## 2026-10-28 — RE-911 Rust in-memory persistence round trip
|
| 2276 |
+
|
| 2277 |
+
Rust matches144 original save/session-write/restore/read compositions using owned in-memory cvar/file bytes. Partial integer parsing and caller scratch preserve the observed non-invertibility of negative/INT_MIN session text; file prefixes and ignore accumulation remain exact. Engine/destination/selector changes stage atomically on errors. This adapter does not own native handles or perform OS file IO. Focused `cargo xtask test-group rust-persistence-roundtrip` passes (9 Python tests); selected reports regenerate twice deterministically, with storage, documentation links and whitespace guards. Full suite deferred to task ten. Original assets unchanged; source fixtures do not establish complete native/runtime parity.
|
docs/REVERSE_ENGINEERING_TODO.md
CHANGED
|
@@ -1804,3 +1804,6 @@ Purpose: turn the supplied installation into an evidence-backed specification of
|
|
| 1804 |
|
| 1805 |
|
| 1806 |
- [x] RE-910 Original write-to-restore persistence round trip ([evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md)).
|
|
|
|
|
|
|
|
|
|
|
|
| 1804 |
|
| 1805 |
|
| 1806 |
- [x] RE-910 Original write-to-restore persistence round trip ([evidence](research/QVM_PERSISTENCE_ROUNDTRIP.md)).
|
| 1807 |
+
|
| 1808 |
+
|
| 1809 |
+
- [x] RE-911 Rust in-memory persistence round trip ([evidence](research/RUST_PERSISTENCE_ROUNDTRIP.md)).
|
docs/research/README.md
CHANGED
|
@@ -1339,3 +1339,6 @@ The extension collector adds five focused tests for native tail calls, command-r
|
|
| 1339 |
|
| 1340 |
|
| 1341 |
[RE-910: Original write-to-restore persistence round trip](QVM_PERSISTENCE_ROUNDTRIP.md).
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
|
| 1340 |
|
| 1341 |
[RE-910: Original write-to-restore persistence round trip](QVM_PERSISTENCE_ROUNDTRIP.md).
|
| 1342 |
+
|
| 1343 |
+
|
| 1344 |
+
[RE-911: Rust in-memory persistence round trip](RUST_PERSISTENCE_ROUNDTRIP.md).
|
docs/research/RUST_PERSISTENCE_ROUNDTRIP.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rust in-memory persistence round trip
|
| 2 |
+
|
| 3 |
+
RE-911. Reproduce with `python3 scripts/analyze_rust_persistence_roundtrip.py`; focused validation: `cargo xtask test-group rust-persistence-roundtrip`. [Machine report](rust-persistence-roundtrip.json).
|
| 4 |
+
|
| 5 |
+
Rust matches144 original save/session-write/restore/read compositions using owned in-memory cvar/file bytes. Partial integer parsing and caller scratch preserve the observed non-invertibility of negative/INT_MIN session text; file prefixes and ignore accumulation remain exact. Engine/destination/selector changes stage atomically on errors. This adapter does not own native handles or perform OS file IO.
|
| 6 |
+
|
| 7 |
+
Original executable/native runtime and complete Linux/macOS Rust/Bevy/Solari acceptance remain unverified.
|
docs/research/rust-health-impulse.json
CHANGED
|
@@ -2985,7 +2985,7 @@
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
-
"rust_unit_tests":
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
|
|
|
| 2985 |
"exit_code": 2
|
| 2986 |
}
|
| 2987 |
],
|
| 2988 |
+
"rust_unit_tests": 165,
|
| 2989 |
"source_report": "docs/research/qvm-health-impulse.json",
|
| 2990 |
"source_report_sha256": "625fd8064004ea6dbdf398d51d0af844d9c635ff04c0a0790886dddb6ab81e71"
|
| 2991 |
}
|
docs/research/rust-persistence-roundtrip.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
scripts/analyze_rust_persistence_roundtrip.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import hashlib,json
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from rust_native_policy_support import binary,result,invoke
|
| 5 |
+
from analyze_qvm import write_json
|
| 6 |
+
def encode(s):
|
| 7 |
+
a=s['input'];return [a['mode'],a['slot'],7 if a['opened'] else 0,a['flag'],a['scratch'],len(a['indices']),*a['values'],*a['ignore'],*a['origin'],*a['indices']]
|
| 8 |
+
def project(s):return {k:s[k] for k in ['saved','sets','file','values','ignore','origin','scan_result','cvars','ring','info_ring']}|{'load_events':s['load']['events']}
|
| 9 |
+
|
| 10 |
+
def main():
|
| 11 |
+
b=binary();p=Path('docs/research/qvm-persistence-roundtrip.json');data=p.read_bytes();rows=[]
|
| 12 |
+
for s in json.loads(data)['probes']:rows.append({'source':project(s),'rust':result(b,'roundtrip',encode(s))})
|
| 13 |
+
write_json(Path('docs/research/rust-persistence-roundtrip.json'),{'source_report':str(p),'source_report_sha256':hashlib.sha256(data).hexdigest(),'probes':rows,'limits':'Rust matches144 original save/session-write/restore/read compositions using owned in-memory cvar/file bytes. Partial integer parsing and caller scratch preserve the observed non-invertibility of negative/INT_MIN session text; file prefixes and ignore accumulation remain exact. Engine/destination/selector changes stage atomically on errors. This adapter does not own native handles or perform OS file IO.'})
|
| 14 |
+
if __name__=='__main__':main()
|
scripts/test_rust_persistence_roundtrip.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib,json,unittest
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
class PortTests(unittest.TestCase):
|
| 4 |
+
def test_all_original_cases(self):
|
| 5 |
+
r=json.loads(Path('docs/research/rust-persistence-roundtrip.json').read_text());src=json.loads(Path(r['source_report']).read_text());self.assertEqual(len(r['probes']),len(src['probes']));self.assertGreater(len(r['probes']),20)
|
| 6 |
+
for p in r['probes']:self.assertEqual(p['rust'],p['source'])
|
| 7 |
+
def test_source_identity(self):
|
| 8 |
+
r=json.loads(Path('docs/research/rust-persistence-roundtrip.json').read_text());self.assertEqual(hashlib.sha256(Path(r['source_report']).read_bytes()).hexdigest(),r['source_report_sha256'])
|
| 9 |
+
if __name__=='__main__':unittest.main()
|
xtask/checks.tsv
CHANGED
|
@@ -2868,3 +2868,18 @@ report scripts/analyze_qvm_persistence_roundtrip.py docs/research/qvm-persistenc
|
|
| 2868 |
edge docs/research/qvm-persistence-roundtrip.json scripts/test_qvm_persistence_roundtrip.py
|
| 2869 |
group qvm-persistence-roundtrip scripts/analyze_qvm_persistence_roundtrip.py
|
| 2870 |
edge scripts/analyze_qvm_persistence_roundtrip.py scripts/analyze_qvm_resumer_audit.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2868 |
edge docs/research/qvm-persistence-roundtrip.json scripts/test_qvm_persistence_roundtrip.py
|
| 2869 |
group qvm-persistence-roundtrip scripts/analyze_qvm_persistence_roundtrip.py
|
| 2870 |
edge scripts/analyze_qvm_persistence_roundtrip.py scripts/analyze_qvm_resumer_audit.py
|
| 2871 |
+
edge Cargo.toml scripts/analyze_rust_persistence_roundtrip.py
|
| 2872 |
+
edge Cargo.lock scripts/analyze_rust_persistence_roundtrip.py
|
| 2873 |
+
edge crates/urt-gameplay/Cargo.toml scripts/analyze_rust_persistence_roundtrip.py
|
| 2874 |
+
edge crates/urt-gameplay/src/lib.rs scripts/analyze_rust_persistence_roundtrip.py
|
| 2875 |
+
edge crates/urt-gameplay/src/native_policy.rs scripts/analyze_rust_persistence_roundtrip.py
|
| 2876 |
+
edge crates/urt-gameplay/src/bin/urt-native-policy.rs scripts/analyze_rust_persistence_roundtrip.py
|
| 2877 |
+
edge docs/research/qvm-persistence-roundtrip.json scripts/analyze_rust_persistence_roundtrip.py
|
| 2878 |
+
group rust-persistence-roundtrip scripts/analyze_rust_health_impulse.py
|
| 2879 |
+
group gameplay scripts/analyze_rust_persistence_roundtrip.py
|
| 2880 |
+
edge crates/urt-gameplay/src/intermission.rs scripts/analyze_rust_persistence_roundtrip.py
|
| 2881 |
+
edge crates/urt-gameplay/src/qvm_format.rs scripts/analyze_rust_persistence_roundtrip.py
|
| 2882 |
+
report scripts/analyze_rust_persistence_roundtrip.py docs/research/rust-persistence-roundtrip.json
|
| 2883 |
+
edge docs/research/rust-persistence-roundtrip.json scripts/test_rust_persistence_roundtrip.py
|
| 2884 |
+
group rust-persistence-roundtrip scripts/analyze_rust_persistence_roundtrip.py
|
| 2885 |
+
edge scripts/analyze_rust_persistence_roundtrip.py scripts/analyze_qvm_resumer_audit.py
|