Skip to main content

libretro_core/
options.rs

1//! Core option builders and retained frontend registration storage.
2//!
3//! `CoreOptions` is the v2-first API. It can emit v2, v1, and legacy variable
4//! tables while preserving string storage for frontend-retained pointers.
5
6use crate::raw;
7use crate::sanitize_cstring;
8use std::ffi::{CString, c_char};
9use std::ptr;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct CoreOptionsVersion(u32);
13
14impl CoreOptionsVersion {
15    pub const LEGACY_VARIABLES: Self = Self(0);
16    pub const V1: Self = Self(1);
17    pub const V2: Self = Self(2);
18
19    pub fn new(value: u32) -> Self {
20        Self(value)
21    }
22
23    pub fn get(self) -> u32 {
24        self.0
25    }
26
27    pub fn supports_v1(self) -> bool {
28        self.0 >= 1
29    }
30
31    pub fn supports_v2(self) -> bool {
32        self.0 >= 2
33    }
34}
35
36#[derive(Clone, Debug)]
37pub struct VariableDefinition {
38    pub key: String,
39    pub value: String,
40}
41
42impl VariableDefinition {
43    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
44        Self {
45            key: key.into(),
46            value: value.into(),
47        }
48    }
49}
50
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub struct CoreOptionValue {
53    pub value: String,
54    pub label: Option<String>,
55}
56
57impl CoreOptionValue {
58    pub fn new(value: impl Into<String>) -> Self {
59        Self {
60            value: value.into(),
61            label: None,
62        }
63    }
64
65    pub fn with_label(mut self, label: impl Into<String>) -> Self {
66        self.label = Some(label.into());
67        self
68    }
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct CoreOptionDefinition {
73    pub key: String,
74    pub description: String,
75    pub description_categorized: Option<String>,
76    pub info: Option<String>,
77    pub info_categorized: Option<String>,
78    pub category_key: Option<String>,
79    pub values: Vec<CoreOptionValue>,
80    pub default_value: String,
81}
82
83impl CoreOptionDefinition {
84    pub fn new(
85        key: impl Into<String>,
86        description: impl Into<String>,
87        default_value: impl Into<String>,
88    ) -> Self {
89        Self {
90            key: key.into(),
91            description: description.into(),
92            description_categorized: None,
93            info: None,
94            info_categorized: None,
95            category_key: None,
96            values: Vec::new(),
97            default_value: default_value.into(),
98        }
99    }
100
101    pub fn with_value(mut self, value: CoreOptionValue) -> Self {
102        self.values.push(value);
103        self
104    }
105
106    pub fn with_values(mut self, values: impl IntoIterator<Item = CoreOptionValue>) -> Self {
107        self.values.extend(values);
108        self
109    }
110
111    pub fn with_info(mut self, info: impl Into<String>) -> Self {
112        self.info = Some(info.into());
113        self
114    }
115
116    pub fn with_categorized_description(mut self, description: impl Into<String>) -> Self {
117        self.description_categorized = Some(description.into());
118        self
119    }
120
121    pub fn with_categorized_info(mut self, info: impl Into<String>) -> Self {
122        self.info_categorized = Some(info.into());
123        self
124    }
125
126    pub fn with_category(mut self, category_key: impl Into<String>) -> Self {
127        self.category_key = Some(category_key.into());
128        self
129    }
130}
131
132#[derive(Clone, Debug, PartialEq, Eq)]
133pub struct CoreOptionCategory {
134    pub key: String,
135    pub description: String,
136    pub info: Option<String>,
137}
138
139impl CoreOptionCategory {
140    pub fn new(key: impl Into<String>, description: impl Into<String>) -> Self {
141        Self {
142            key: key.into(),
143            description: description.into(),
144            info: None,
145        }
146    }
147
148    pub fn with_info(mut self, info: impl Into<String>) -> Self {
149        self.info = Some(info.into());
150        self
151    }
152}
153
154#[derive(Clone, Debug, Default, PartialEq, Eq)]
155pub struct CoreOptions {
156    pub categories: Vec<CoreOptionCategory>,
157    pub definitions: Vec<CoreOptionDefinition>,
158}
159
160impl CoreOptions {
161    pub fn new(definitions: impl IntoIterator<Item = CoreOptionDefinition>) -> Self {
162        Self {
163            categories: Vec::new(),
164            definitions: definitions.into_iter().collect(),
165        }
166    }
167
168    pub fn with_categories(
169        mut self,
170        categories: impl IntoIterator<Item = CoreOptionCategory>,
171    ) -> Self {
172        self.categories.extend(categories);
173        self
174    }
175}
176
177#[derive(Clone, Debug, PartialEq, Eq)]
178pub struct CoreOptionDisplay {
179    pub key: String,
180    pub visible: bool,
181}
182
183impl CoreOptionDisplay {
184    pub fn new(key: impl Into<String>, visible: bool) -> Self {
185        Self {
186            key: key.into(),
187            visible,
188        }
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193pub enum CoreOptionsBuildError {
194    TooManyValues { max: usize },
195}
196
197#[derive(Default)]
198struct StringPool {
199    strings: Vec<CString>,
200}
201
202impl StringPool {
203    fn push(&mut self, value: impl AsRef<str>) -> *const c_char {
204        self.strings.push(sanitize_cstring(value.as_ref()));
205        self.strings
206            .last()
207            .expect("just pushed option string")
208            .as_ptr()
209    }
210
211    fn push_optional(&mut self, value: Option<&str>) -> *const c_char {
212        value.map_or(ptr::null(), |value| self.push(value))
213    }
214}
215
216#[derive(Default)]
217pub(crate) struct CoreOptionsStorage {
218    strings: StringPool,
219    variables: Vec<raw::retro_variable>,
220    v1_definitions: Vec<raw::retro_core_option_definition>,
221    local_v1_definitions: Vec<raw::retro_core_option_definition>,
222    v2_categories: Vec<raw::retro_core_option_v2_category>,
223    v2_definitions: Vec<raw::retro_core_option_v2_definition>,
224    local_v2_categories: Vec<raw::retro_core_option_v2_category>,
225    local_v2_definitions: Vec<raw::retro_core_option_v2_definition>,
226}
227
228impl CoreOptionsStorage {
229    pub(crate) fn variables(variables: &[VariableDefinition]) -> Self {
230        let mut storage = Self::default();
231        for variable in variables {
232            let key = storage.strings.push(&variable.key);
233            let value = storage.strings.push(&variable.value);
234            storage.variables.push(raw::retro_variable { key, value });
235        }
236        storage.variables.push(raw::retro_variable::default());
237        storage
238    }
239
240    pub(crate) fn legacy_from_options(
241        options: &CoreOptions,
242    ) -> Result<Self, CoreOptionsBuildError> {
243        let mut storage = Self::default();
244        for definition in &options.definitions {
245            ensure_values_fit(definition)?;
246            let key = storage.strings.push(&definition.key);
247            let value = storage.strings.push(legacy_definition_value(definition));
248            storage.variables.push(raw::retro_variable { key, value });
249        }
250        storage.variables.push(raw::retro_variable::default());
251        Ok(storage)
252    }
253
254    pub(crate) fn v1(definitions: &[CoreOptionDefinition]) -> Result<Self, CoreOptionsBuildError> {
255        let mut storage = Self::default();
256        storage.v1_definitions = storage.raw_v1_definitions(definitions)?;
257        storage
258            .v1_definitions
259            .push(raw::retro_core_option_definition::default());
260        Ok(storage)
261    }
262
263    pub(crate) fn v1_intl(
264        us: &[CoreOptionDefinition],
265        local: Option<&[CoreOptionDefinition]>,
266    ) -> Result<Self, CoreOptionsBuildError> {
267        let mut storage = Self::default();
268        storage.v1_definitions = storage.raw_v1_definitions(us)?;
269        storage
270            .v1_definitions
271            .push(raw::retro_core_option_definition::default());
272        if let Some(local) = local {
273            storage.local_v1_definitions = storage.raw_v1_definitions(local)?;
274            storage
275                .local_v1_definitions
276                .push(raw::retro_core_option_definition::default());
277        }
278        Ok(storage)
279    }
280
281    pub(crate) fn v2(options: &CoreOptions) -> Result<Self, CoreOptionsBuildError> {
282        let mut storage = Self::default();
283        storage.v2_categories = storage.raw_v2_categories(&options.categories);
284        storage
285            .v2_categories
286            .push(raw::retro_core_option_v2_category::default());
287        storage.v2_definitions = storage.raw_v2_definitions(&options.definitions)?;
288        storage
289            .v2_definitions
290            .push(raw::retro_core_option_v2_definition::default());
291        Ok(storage)
292    }
293
294    pub(crate) fn v2_intl(
295        us: &CoreOptions,
296        local: Option<&CoreOptions>,
297    ) -> Result<Self, CoreOptionsBuildError> {
298        let mut storage = Self::v2(us)?;
299        if let Some(local) = local {
300            storage.local_v2_categories = storage.raw_v2_categories(&local.categories);
301            storage
302                .local_v2_categories
303                .push(raw::retro_core_option_v2_category::default());
304            storage.local_v2_definitions = storage.raw_v2_definitions(&local.definitions)?;
305            storage
306                .local_v2_definitions
307                .push(raw::retro_core_option_v2_definition::default());
308        }
309        Ok(storage)
310    }
311
312    pub(crate) fn variables_ptr(&mut self) -> *mut raw::retro_variable {
313        self.variables.as_mut_ptr()
314    }
315
316    pub(crate) fn v1_definitions_ptr(&mut self) -> *mut raw::retro_core_option_definition {
317        self.v1_definitions.as_mut_ptr()
318    }
319
320    pub(crate) fn v1_intl_raw(&mut self) -> raw::retro_core_options_intl {
321        raw::retro_core_options_intl {
322            us: self.v1_definitions.as_mut_ptr(),
323            local: if self.local_v1_definitions.is_empty() {
324                ptr::null_mut()
325            } else {
326                self.local_v1_definitions.as_mut_ptr()
327            },
328        }
329    }
330
331    pub(crate) fn v2_raw(&mut self) -> raw::retro_core_options_v2 {
332        raw::retro_core_options_v2 {
333            categories: if self.v2_categories.is_empty() {
334                ptr::null_mut()
335            } else {
336                self.v2_categories.as_mut_ptr()
337            },
338            definitions: self.v2_definitions.as_mut_ptr(),
339        }
340    }
341
342    pub(crate) fn local_v2_raw(&mut self) -> Option<raw::retro_core_options_v2> {
343        (!self.local_v2_definitions.is_empty()).then(|| raw::retro_core_options_v2 {
344            categories: if self.local_v2_categories.is_empty() {
345                ptr::null_mut()
346            } else {
347                self.local_v2_categories.as_mut_ptr()
348            },
349            definitions: self.local_v2_definitions.as_mut_ptr(),
350        })
351    }
352
353    fn raw_v1_definitions(
354        &mut self,
355        definitions: &[CoreOptionDefinition],
356    ) -> Result<Vec<raw::retro_core_option_definition>, CoreOptionsBuildError> {
357        definitions
358            .iter()
359            .map(|definition| self.raw_v1_definition(definition))
360            .collect()
361    }
362
363    fn raw_v1_definition(
364        &mut self,
365        definition: &CoreOptionDefinition,
366    ) -> Result<raw::retro_core_option_definition, CoreOptionsBuildError> {
367        ensure_values_fit(definition)?;
368        Ok(raw::retro_core_option_definition {
369            key: self.strings.push(&definition.key),
370            desc: self.strings.push(&definition.description),
371            info: self.strings.push_optional(definition.info.as_deref()),
372            values: self.raw_values(definition),
373            default_value: self.strings.push(&definition.default_value),
374        })
375    }
376
377    fn raw_v2_categories(
378        &mut self,
379        categories: &[CoreOptionCategory],
380    ) -> Vec<raw::retro_core_option_v2_category> {
381        categories
382            .iter()
383            .map(|category| raw::retro_core_option_v2_category {
384                key: self.strings.push(&category.key),
385                desc: self.strings.push(&category.description),
386                info: self.strings.push_optional(category.info.as_deref()),
387            })
388            .collect()
389    }
390
391    fn raw_v2_definitions(
392        &mut self,
393        definitions: &[CoreOptionDefinition],
394    ) -> Result<Vec<raw::retro_core_option_v2_definition>, CoreOptionsBuildError> {
395        definitions
396            .iter()
397            .map(|definition| self.raw_v2_definition(definition))
398            .collect()
399    }
400
401    fn raw_v2_definition(
402        &mut self,
403        definition: &CoreOptionDefinition,
404    ) -> Result<raw::retro_core_option_v2_definition, CoreOptionsBuildError> {
405        ensure_values_fit(definition)?;
406        Ok(raw::retro_core_option_v2_definition {
407            key: self.strings.push(&definition.key),
408            desc: self.strings.push(&definition.description),
409            desc_categorized: self
410                .strings
411                .push_optional(definition.description_categorized.as_deref()),
412            info: self.strings.push_optional(definition.info.as_deref()),
413            info_categorized: self
414                .strings
415                .push_optional(definition.info_categorized.as_deref()),
416            category_key: self
417                .strings
418                .push_optional(definition.category_key.as_deref()),
419            values: self.raw_values(definition),
420            default_value: self.strings.push(&definition.default_value),
421        })
422    }
423
424    fn raw_values(
425        &mut self,
426        definition: &CoreOptionDefinition,
427    ) -> [raw::retro_core_option_value; raw::RETRO_NUM_CORE_OPTION_VALUES_MAX] {
428        let mut values =
429            [raw::retro_core_option_value::default(); raw::RETRO_NUM_CORE_OPTION_VALUES_MAX];
430        for (slot, value) in values.iter_mut().zip(&definition.values) {
431            *slot = raw::retro_core_option_value {
432                value: self.strings.push(&value.value),
433                label: self.strings.push_optional(value.label.as_deref()),
434            };
435        }
436        values
437    }
438}
439
440fn ensure_values_fit(definition: &CoreOptionDefinition) -> Result<(), CoreOptionsBuildError> {
441    if definition.values.len() > raw::RETRO_NUM_CORE_OPTION_VALUES_MAX {
442        return Err(CoreOptionsBuildError::TooManyValues {
443            max: raw::RETRO_NUM_CORE_OPTION_VALUES_MAX,
444        });
445    }
446    Ok(())
447}
448
449fn legacy_definition_value(definition: &CoreOptionDefinition) -> String {
450    let mut values = Vec::with_capacity(definition.values.len());
451    if definition
452        .values
453        .iter()
454        .any(|value| value.value == definition.default_value)
455    {
456        values.push(definition.default_value.clone());
457        values.extend(
458            definition
459                .values
460                .iter()
461                .filter(|value| value.value != definition.default_value)
462                .map(|value| value.value.clone()),
463        );
464    } else {
465        values.extend(definition.values.iter().map(|value| value.value.clone()));
466    }
467    format!("{}; {}", definition.description, values.join("|"))
468}
469
470#[cfg(test)]
471mod tests {
472    use super::*;
473
474    #[test]
475    fn legacy_options_put_default_value_first() {
476        let options =
477            CoreOptions::new([CoreOptionDefinition::new("demo_speed", "Speed", "normal")
478                .with_values([
479                    CoreOptionValue::new("slow"),
480                    CoreOptionValue::new("normal"),
481                    CoreOptionValue::new("fast"),
482                ])]);
483        let mut storage = CoreOptionsStorage::legacy_from_options(&options).expect("valid options");
484        let raw = storage.variables_ptr();
485
486        let value = unsafe { std::ffi::CStr::from_ptr((*raw).value) };
487        assert_eq!(value.to_string_lossy(), "Speed; normal|slow|fast");
488    }
489
490    #[test]
491    fn v2_options_retain_categories_and_labels() {
492        let options = CoreOptions::new([CoreOptionDefinition::new(
493            "demo_renderer",
494            "Renderer",
495            "gl",
496        )
497        .with_category("video")
498        .with_categorized_description("API")
499        .with_info("Selects the renderer")
500        .with_categorized_info("Rendering API")
501        .with_values([
502            CoreOptionValue::new("gl").with_label("OpenGL"),
503            CoreOptionValue::new("soft").with_label("Software"),
504        ])])
505        .with_categories([CoreOptionCategory::new("video", "Video").with_info("Video settings")]);
506        let mut storage = CoreOptionsStorage::v2(&options).expect("valid options");
507        let raw = storage.v2_raw();
508
509        let category = unsafe { &*raw.categories };
510        assert_eq!(
511            unsafe { std::ffi::CStr::from_ptr(category.desc) }.to_string_lossy(),
512            "Video"
513        );
514        let definition = unsafe { &*raw.definitions };
515        assert_eq!(
516            unsafe { std::ffi::CStr::from_ptr(definition.values[0].label) }.to_string_lossy(),
517            "OpenGL"
518        );
519        assert_eq!(
520            unsafe { std::ffi::CStr::from_ptr(definition.category_key) }.to_string_lossy(),
521            "video"
522        );
523    }
524}