Skip to main content

foldit_plugin_sdk/
protocol.rs

1//! Native-Rust mirrors of the `proto::plugin` protocol types used by the
2//! [`crate::Plugin`] trait surface.
3
4use std::collections::HashMap;
5
6#[cfg(feature = "python")]
7use pyo3::prelude::*;
8
9use crate::proto::plugin::ScoreReport;
10
11/// Reference to a specific residue inside an entity. Mirror of
12/// `proto::plugin::ResidueRef`.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14#[cfg_attr(
15    feature = "python",
16    pyclass(name = "ResidueRef", module = "foldit_plugin_sdk", from_py_object)
17)]
18pub struct ResidueRef {
19    /// Entity the residue belongs to.
20    pub entity_id: molex::EntityId,
21    /// 0-indexed within entity.
22    pub residue_index: u32,
23}
24
25#[cfg(feature = "python")]
26#[pymethods]
27impl ResidueRef {
28    /// Build a residue ref from a raw entity id and 0-indexed residue
29    /// position. The host constructs these to populate a `DispatchContext`
30    /// on the receive path.
31    #[new]
32    #[must_use]
33    pub fn new(entity_id: u32, residue_index: u32) -> Self {
34        Self {
35            entity_id: molex::EntityId::from_raw(entity_id),
36            residue_index,
37        }
38    }
39
40    /// Raw entity id (`u32`) the residue belongs to.
41    #[must_use]
42    #[getter]
43    pub fn entity_id(&self) -> u32 {
44        self.entity_id.raw()
45    }
46
47    /// 0-indexed residue position within the entity.
48    #[must_use]
49    #[getter]
50    pub fn residue_index(&self) -> u32 {
51        self.residue_index
52    }
53}
54
55/// Captured by the orchestrator at op-trigger time and frozen on the wire.
56/// Streams do NOT receive an updated context mid-flight; only
57/// `UpdateStream(params)` can change values during a running stream.
58#[derive(Debug, Clone, Default)]
59#[cfg_attr(
60    feature = "python",
61    pyclass(name = "DispatchContext", module = "foldit_plugin_sdk", from_py_object)
62)]
63pub struct DispatchContext {
64    /// Entity the user has currently focused, or `None` for session
65    /// mode (no specific entity targeted).
66    pub focused_entity_id: Option<molex::EntityId>,
67    /// Residue selection at op-trigger time.
68    pub selection: Vec<ResidueRef>,
69    /// Residues the plugin may redesign (change identity at), the puzzle's
70    /// design mask. Carried alongside the selection so the engine can gate
71    /// identity changes; orthogonal to `selection`, which says where to
72    /// operate. Empty when the session gates no design.
73    pub designable: Vec<ResidueRef>,
74}
75
76#[cfg(feature = "python")]
77#[pymethods]
78impl DispatchContext {
79    /// Build a dispatch context from a raw focused entity id (or `None` for
80    /// session mode) plus the selection + design-mask residue refs. The host
81    /// constructs these on the receive path before dispatching an op.
82    #[new]
83    #[pyo3(signature = (focused_entity_id = None, selection = vec![], designable = vec![]))]
84    #[must_use]
85    pub fn new(
86        focused_entity_id: Option<u32>,
87        selection: Vec<ResidueRef>,
88        designable: Vec<ResidueRef>,
89    ) -> Self {
90        Self {
91            focused_entity_id: focused_entity_id.map(molex::EntityId::from_raw),
92            selection,
93            designable,
94        }
95    }
96
97    /// Raw id (`u32`) of the focused entity, or `None` in session mode.
98    #[must_use]
99    #[getter]
100    pub fn focused_entity_id(&self) -> Option<u32> {
101        self.focused_entity_id.map(molex::EntityId::raw)
102    }
103
104    /// Residue selection at op-trigger time.
105    #[must_use]
106    #[getter]
107    pub fn selection(&self) -> Vec<ResidueRef> {
108        self.selection.clone()
109    }
110
111    /// Residues the plugin may redesign (the puzzle's design mask).
112    #[must_use]
113    #[getter]
114    pub fn designable(&self) -> Vec<ResidueRef> {
115        self.designable.clone()
116    }
117}
118
119/// Native-Rust parameter value.
120///
121/// Mirrors `proto::plugin::ParamValue` (a oneof on the wire) but in
122/// ergonomic enum form for the `Plugin` trait surface. Wire-native
123/// conversion happens at the IPC boundary.
124#[derive(Debug, Clone, PartialEq)]
125pub enum ParamValue {
126    /// 32-bit signed integer.
127    Int(i32),
128    /// 32-bit float.
129    Float(f32),
130    /// Boolean.
131    Bool(bool),
132    /// Used for both `PARAM_TYPE_STRING` and `PARAM_TYPE_ENUM`.
133    String(String),
134    /// 3-component float vector (positions, axes, etc.).
135    Vec3([f32; 3]),
136}
137
138/// Outcome of a `PollStream` call. Native-Rust mirror of
139/// `proto::plugin::PollStreamResponse`'s oneof.
140#[derive(Debug, Clone)]
141pub enum PollOutcome {
142    /// Stream still running. `latest_assembly` is the working state at
143    /// poll time; not authoritative until promoted on `Final` /
144    /// `Cancelled`.
145    Pending {
146        /// Working assembly snapshot, if the plugin emits one.
147        latest_assembly: Option<Vec<u8>>,
148        /// Progress fraction in `[0.0, 1.0]`, if the plugin tracks it.
149        progress: Option<f32>,
150        /// Human-readable stage label, if provided.
151        stage: Option<String>,
152        /// Warm score of `latest_assembly`, if the plugin scores it.
153        score: Option<ScoreReport>,
154    },
155    /// Accepted intermediate the host should commit into canonical state
156    /// while the stream keeps running. Same payload as `Pending`, but the
157    /// host commits it rather than treating it as a discardable preview;
158    /// unlike a terminal it does not end the op (more checkpoints or a
159    /// terminal follow), so the poll loop keeps going.
160    Checkpoint {
161        /// Working assembly snapshot, if the plugin emits one.
162        latest_assembly: Option<Vec<u8>>,
163        /// Progress fraction in `[0.0, 1.0]`, if the plugin tracks it.
164        progress: Option<f32>,
165        /// Human-readable stage label, if provided.
166        stage: Option<String>,
167        /// Warm score of `latest_assembly`, if the plugin scores it.
168        score: Option<ScoreReport>,
169    },
170    /// Stream stopped at host request (the host sent `CancelStream` and
171    /// the plugin returned its working pose). Same downstream handling
172    /// as `Final`: the orchestrator promotes `assembly` into canonical
173    /// state. Distinguished from `Final` only so the host can tell
174    /// "the algorithm reached its endpoint" from "the user asked it
175    /// to stop"; for open-ended ops (wiggle, shake) this IS the
176    /// success terminal.
177    Cancelled {
178        /// Working assembly bytes for the orchestrator to promote.
179        assembly: Vec<u8>,
180        /// Warm score of `assembly`, if the plugin scores it.
181        score: Option<ScoreReport>,
182    },
183    /// Stream finished successfully. `assembly` is the definitive output
184    /// the orchestrator promotes into canonical state.
185    Final {
186        /// Definitive assembly bytes for the orchestrator to promote.
187        assembly: Vec<u8>,
188        /// Warm score of `assembly`, if the plugin scores it.
189        score: Option<ScoreReport>,
190    },
191    /// Op-level failure. Distinct from a transport-level error.
192    Error {
193        /// Machine-readable error code (e.g. `"STREAM_INTERNAL"`).
194        code: String,
195        /// Human-readable error message.
196        message: String,
197        /// Optional structured detail map.
198        details: HashMap<String, String>,
199    },
200}