Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion client/src/adapter/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2433,7 +2433,9 @@ export type PlayerActionKind =
| "CollectEvidence"
| "ShuffledLibrary"
| "Proliferate"
| "Investigate";
| "Investigate"
| "Draw"
| "Forage";

export type GameEvent =
| { type: "GameStarted" }
Expand Down
3 changes: 2 additions & 1 deletion client/src/adapter/ws-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export class NativeEngineVersionMismatchError extends Error {
* `crates/server-core/src/protocol.rs`. Bump in lockstep when either side
* adds, removes, renames, or changes the type of a protocol variant field.
*
* 30 — Serialized player-action completion provenance and modal continuations.
* 29 — Added requester-correlated ResolveAllRejected response frames.
* 28 — Added native ResolveAll request/result frames.
* 27 — Added DraftKind.Sealed, serialized by draft WebSocket messages.
Expand Down Expand Up @@ -236,7 +237,7 @@ export class NativeEngineVersionMismatchError extends Error {
* into a MulliganDecisionPhase::BottomCards sub-phase on
* WaitingFor::MulliganDecision.
*/
export const PROTOCOL_VERSION = 29;
export const PROTOCOL_VERSION = 30;

/**
* Lowest server protocol version this client will accept in the handshake.
Expand Down
4 changes: 2 additions & 2 deletions client/src/network/__tests__/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ const viewerInteractionWithProducedMana = {
} as never;

describe("encodeWireMessage / decodeWireMessage", () => {
it("pins the P2P wire protocol to v18", () => {
expect(WIRE_PROTOCOL_VERSION).toBe(19);
it("pins the P2P wire protocol to v20", () => {
expect(WIRE_PROTOCOL_VERSION).toBe(20);
});

it("defaults shortcut actions for a legacy payload created before the additive field", () => {
Expand Down
3 changes: 2 additions & 1 deletion client/src/network/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function legalActionsFromWire(wire: LegalActionsWire): LegalActionsResult
* of silently corrupting state.
*
* Bumps to date:
* 20 — Serialized player-action completion provenance and modal continuations.
* 19 — Added an action_noop acknowledgement for accepted transport no-ops.
* 18 — DebugCardEntries added a serialized, private resolution frame for
* multi-card sandbox battlefield entries that pause for replacement or
Expand Down Expand Up @@ -111,7 +112,7 @@ export function legalActionsFromWire(wire: LegalActionsWire): LegalActionsResult
* sub-phase on WaitingFor::MulliganDecision; the MulliganBottomCards
* variant was removed
*/
export const WIRE_PROTOCOL_VERSION = 19 as const;
export const WIRE_PROTOCOL_VERSION = 20 as const;

export type P2PMessage = P2PAuthorityWire & (
| { type: "guest_deck"; deckData: unknown; displayName?: string; reservationToken?: string }
Expand Down
1 change: 1 addition & 0 deletions crates/engine/src/analysis/ability_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1016,6 +1016,7 @@ fn effect_projection(effect: &Effect) -> Projection {
| Effect::Adapt { .. }
| Effect::Learn
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::CollectEvidence { .. }
| Effect::Endure { .. }
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/ability_rw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3421,6 +3421,7 @@ fn legacy_effect(x: &Effect) -> bool {
| Effect::Learn
| Effect::NoteManaSpent
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::CollectEvidence { .. }
| Effect::Specialize
Expand Down Expand Up @@ -4758,6 +4759,7 @@ fn rw_effect(
p.writes_membership_external_zones.merge(ZoneSpan::Any);
(p, None)
}
Effect::CompletePlayerAction { .. } => (RwProfile::conservative(), None),
Effect::Connive { target, count } => {
let (mut p, sc) = obj(StateKind::ObjectCounters, target);
p.writes_external.set(StateKind::HandLibrary);
Expand Down
4 changes: 4 additions & 0 deletions crates/engine/src/game/ability_scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,7 @@ fn scan_effect(x: &Effect, mode: ScanMode) -> Axes {
}
Effect::Learn => Axes::NONE,
Effect::Forage => Axes::NONE,
Effect::CompletePlayerAction { .. } => Axes::NONE,
Effect::Harness => Axes::NONE,
Effect::CollectEvidence { amount: _ } => Axes::NONE,
Effect::Endure { amount, subject } => {
Expand Down Expand Up @@ -5568,6 +5569,7 @@ fn effect_target_ctx(e: &Effect, mode: ScanMode) -> FilterReadContext {
| Effect::Adapt { .. }
| Effect::Learn
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::CollectEvidence { .. }
| Effect::Endure { .. }
Expand Down Expand Up @@ -5946,6 +5948,7 @@ fn effect_census_role(e: &Effect) -> CensusRole {
| Effect::Adapt { .. }
| Effect::Learn
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::Endure { .. }
| Effect::BlightEffect { .. }
Expand Down Expand Up @@ -6206,6 +6209,7 @@ pub(crate) fn effect_is_randomness_bearing(e: &Effect) -> bool {
| Effect::Adapt { .. }
| Effect::Learn
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::CollectEvidence { .. }
| Effect::Endure { .. }
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/contraptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ pub(crate) fn continue_assemble_batch(
parent_targets: Vec::new(),
context: crate::types::ability::SpellContext::default(),
replacement_applied: Default::default(),
continuation: None,
players: vec![player],
},
);
Expand Down Expand Up @@ -421,6 +422,7 @@ fn prompt_reassemble_sprocket_choice(
parent_targets: ability.targets.clone(),
context: ability.context.clone(),
replacement_applied: ability.replacement_applied.clone(),
continuation: None,
players: vec![ability.controller],
},
);
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/game/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3712,6 +3712,7 @@ fn effect_details(effect: &Effect) -> Vec<(String, String)> {
| Effect::ChangeTargets { .. }
| Effect::ExchangeControl { .. }
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::Learn
| Effect::NoteManaSpent
Expand Down Expand Up @@ -6586,6 +6587,7 @@ fn visit_direct_effect_ability_payloads<'a>(
| Effect::Adapt { .. }
| Effect::Learn
| Effect::Forage
| Effect::CompletePlayerAction { .. }
| Effect::Harness
| Effect::CollectEvidence { .. }
| Effect::Endure { .. }
Expand Down
75 changes: 56 additions & 19 deletions crates/engine/src/game/effects/change_zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ use crate::game::game_object::AttachTarget;
#[cfg(test)]
use crate::game::zones;
use crate::types::ability::{
ControllerRef, Duration, Effect, EffectError, EffectKind, FilterProp, LibraryPosition,
QuantityExpr, ResolvedAbility, TargetChoiceTiming, TargetFilter, TargetRef,
ControllerRef, Duration, Effect, EffectError, EffectKind, EffectResolutionResult, FilterProp,
LibraryPosition, QuantityExpr, ResolvedAbility, TargetChoiceTiming, TargetFilter, TargetRef,
TargetSelectionMode, TypeFilter, TypedFilter,
};
#[cfg(test)]
Expand Down Expand Up @@ -410,7 +410,8 @@ pub fn resolve(
state: &mut GameState,
ability: &ResolvedAbility,
events: &mut Vec<GameEvent>,
) -> Result<(), EffectError> {
) -> Result<Option<EffectResolutionResult>, EffectError> {
let events_before = events.len();
let (
origin,
dest_zone,
Expand Down Expand Up @@ -482,6 +483,11 @@ pub fn resolve(
_ => return Err(EffectError::MissingParam("Destination".to_string())),
};

let completed_result = |count| {
super::this_way_cause_for_zone(dest_zone)
.map(|cause| EffectResolutionResult { cause, count })
};

let mut origin = origin;

let parsed_target = match &ability.effect {
Expand Down Expand Up @@ -605,7 +611,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

// CR 400.7: SelfRef resolves only to the exact source or, for a departure
Expand All @@ -618,7 +624,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

// CR 400.7 + CR 603.7c: a delayed ability whose pinned referent became a
Expand All @@ -639,7 +645,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

// CR 701.23b + CR 401.2: Interactive library-step fail-to-find guard.
Expand Down Expand Up @@ -678,7 +684,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

// CR 608.2c: A tracked-set filter ("from among the milled cards" / "X
Expand Down Expand Up @@ -757,7 +763,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

if eligible.is_empty() {
Expand All @@ -769,7 +775,7 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(0));
}

if matches!(ability.target_selection_mode, TargetSelectionMode::Random)
Expand Down Expand Up @@ -844,9 +850,9 @@ pub fn resolve(
ability.source_id,
);
crate::game::replacement::park_waiting_for(state, player);
return Ok(());
return Ok(None);
}
ZoneMoveResult::NeedsAuraAttachmentChoice => return Ok(()),
ZoneMoveResult::NeedsAuraAttachmentChoice => return Ok(None),
}

// CR 614.13a: single-pick entry completed (Done branch) — clear the
Expand All @@ -865,7 +871,11 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(count_selected_zone_arrivals(
&events[events_before..],
&[chosen],
dest_zone,
)));
}

if eligible.len() == 1 && !choice_up_to && choice_count == 1 {
Expand Down Expand Up @@ -936,9 +946,9 @@ pub fn resolve(
ability.source_id,
);
crate::game::replacement::park_waiting_for(state, player);
return Ok(());
return Ok(None);
}
ZoneMoveResult::NeedsAuraAttachmentChoice => return Ok(()),
ZoneMoveResult::NeedsAuraAttachmentChoice => return Ok(None),
}

// CR 614.13a: single-pick entry completed (Done branch) — clear the
Expand All @@ -957,7 +967,11 @@ pub fn resolve(
source_id: ability.source_id,
subject: None,
});
return Ok(());
return Ok(completed_result(count_selected_zone_arrivals(
&events[events_before..],
&[chosen],
dest_zone,
)));
}

state.waiting_for = WaitingFor::EffectZoneChoice {
Expand Down Expand Up @@ -998,7 +1012,7 @@ pub fn resolve(
};
// EffectResolved is emitted by the EffectZoneChoice handler after the player chooses
// (matching the DiscardChoice pattern — single authority for the event).
return Ok(());
return Ok(None);
}

let ctx = ChangeZoneIterationCtx {
Expand Down Expand Up @@ -1155,7 +1169,7 @@ pub fn resolve(
effect_kind: EffectKind::from(&ability.effect),
},
);
return Ok(());
return Ok(None);
}
crate::game::zone_pipeline::ZoneMoveTerminalResult::NeedsChoice(player) => {
// CR 614.12b + CR 614.1c + CR 614.13: stash the unprocessed targets
Expand Down Expand Up @@ -1220,7 +1234,7 @@ pub fn resolve(
crate::game::replacement::park_waiting_for(state, player);
// EffectResolved is emitted by the drain after the loop completes —
// do NOT emit here.
return Ok(());
return Ok(None);
}
}
}
Expand Down Expand Up @@ -1256,7 +1270,30 @@ pub fn resolve(
subject: None,
});

Ok(())
Ok(completed_result(count_selected_zone_arrivals(
&events[events_before..],
&targeted_objects,
dest_zone,
)))
}

/// CR 614.6 + CR 608.2c: Count only selected members that actually arrived in
/// the requested destination during this operation's exact event slice.
pub(crate) fn count_selected_zone_arrivals(
events: &[GameEvent],
selected: &[ObjectId],
destination: Zone,
) -> usize {
events
.iter()
.filter(|event| {
matches!(
event,
GameEvent::ZoneChanged { object_id, to, .. }
if *to == destination && selected.contains(object_id)
)
})
.count()
}

/// CR 122.1 + CR 614.1c: Merge unconditional and conditional entry-time counters
Expand Down
Loading
Loading