1use 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}