Resume interrupted editor co-op runs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-05 21:08:33 +01:00
1 parent f87b59390e
commit c7c3e979d6
24 files changed
+145 -6

No files matched your search

+41 -1
View File
@@ -230,10 +230,23 @@ impl MatchClient {
// a co-op match of an editor campaign: the whole campaign file goes to the relay, which hands
// it to every client with the start message
pub fn create_coop_doc(&mut self, doc: &crate::campaign_doc::CampaignDoc, mission: usize, sides: &[u8], difficulty: u8) {
self.create_coop_doc_resumed(doc, mission, sides, difficulty, None);
}
// the same, going on with an interrupted run: its seed and what every master carries along
pub fn create_coop_doc_resumed(&mut self, doc: &crate::campaign_doc::CampaignDoc, mission: usize, sides: &[u8], difficulty: u8, resume: Option<(u32, &[(u8, crate::campaign::Carry)])>) {
let Some(m) = doc.missions.get(mission) else { return };
let (map_w, map_h) = m.size;
let doc = serde_json::to_value(doc).unwrap_or(Value::Null);
let n = self.outbox.len();
self.send_create_with(sides, map_w, map_h, difficulty, Some(mission), Some(doc));
if let (Some((seed, carry)), Some(text)) = (resume, self.outbox.get_mut(n)) {
if let Ok(mut msg) = serde_json::from_str::<Value>(text) {
msg["seed"] = json!(seed);
msg["carry"] = serde_json::to_value(carry).unwrap_or(Value::Array(Vec::new()));
*text = msg.to_string();
}
}
}
fn send_create(&mut self, sides: &[u8], map_w: usize, map_h: usize, difficulty: u8, mission: Option<usize>) {
@@ -534,7 +547,10 @@ impl MatchClient {
}
r.ai = ai.clone();
}
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: false, carry: Vec::new() });
// a resumed editor campaign run goes on like the next mission of a running one
let resume = editor && v.get("resume").and_then(|x| x.as_bool()).unwrap_or(false);
let carry: Vec<(u8, crate::campaign::Carry)> = if resume { v.get("carry").cloned().and_then(|c| serde_json::from_value(c).ok()).unwrap_or_default() } else { Vec::new() };
self.start = Some(StartInfo { seed, map_w, map_h, difficulty, sides, local_side, mission, ai, doc, editor, teams, next: resume, carry });
self.error = None;
self.status = None;
}
@@ -898,6 +914,30 @@ mod tests {
assert_eq!(start.teams, vec![(0, 1), (2, 1)]);
}
#[test]
fn an_interrupted_coop_run_goes_on_with_its_seed_and_carry() {
let doc = coop_doc();
let mut host = MatchClient::new();
host.on_connect("red");
pop(&mut host); // rooms
let carry = vec![(0u8, crate::campaign::Carry { gold: 250.0, ..Default::default() })];
host.create_coop_doc_resumed(&doc, 1, &[0, 2, 3, 4], 1, Some((4242, &carry)));
let create = pop(&mut host);
assert_eq!(create["seed"].as_u64(), Some(4242));
assert_eq!(create["carry"][0][0].as_u64(), Some(0));
assert_eq!(create["mission"].as_u64(), Some(1));
let start = json!({"t":"start","seed":4242,"map_w":96,"map_h":96,"difficulty":1,"sides":[0,2],"mission":1,"ai":[],"doc":serde_json::to_value(&doc).unwrap(),"carry":create["carry"].clone(),"resume":true}).to_string();
host.feed(&start);
let info = host.take_start().unwrap();
assert!(info.next, "a resumed run keeps its progress");
assert_eq!(info.seed, 4242);
assert_eq!(info.carry.len(), 1);
assert_eq!(info.carry[0].1.gold, 250.0);
// old stored progress without the run position still reads
let p: crate::campaign_doc::CampaignProgress = serde_json::from_str(r#"{"done":[true],"completed_choice":[null],"run_seed":5}"#).unwrap();
assert_eq!((p.next, p.carry.len()), (None, 0));
}
#[test]
fn two_clients_play_the_same_editor_campaign_mission_in_coop() {
let doc = coop_doc();