flashpoint_archive/game/
mod.rs

1use chrono::Utc;
2use rusqlite::{
3    params,
4    types::{FromSql, FromSqlError, Value, ValueRef},
5    Connection, OptionalExtension, Result,
6};
7use serde::{Deserialize, Serialize};
8use unicase::UniCase;
9use uuid::Uuid;
10use std::{collections::{HashMap, HashSet}, fmt::Display, ops::{Deref, DerefMut}, rc::Rc, vec::Vec};
11
12use crate::{game_data::{GameData, PartialGameData}, platform::{self, PlatformAppPath}, tag::{self, Tag}, MAX_SEARCH};
13
14use self::search::{mark_index_dirty, GameSearch, GameSearchRelations};
15
16pub mod search;
17pub mod ext;
18
19#[cfg(feature = "napi")]
20use napi::bindgen_prelude::{ToNapiValue, FromNapiValue};
21
22#[derive(Debug, Clone)]
23pub struct TagVec (Vec<String>);
24
25impl serde::Serialize for TagVec {
26    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
27    where
28        S: serde::Serializer,
29    {
30        let combined = self.0.join(";");
31        serializer.serialize_str(&combined)
32    }
33}
34
35impl<'de> serde::Deserialize<'de> for TagVec {
36    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
37    where
38        D: serde::Deserializer<'de>,
39    {
40        struct TagVecVisitor;
41
42        impl<'de> serde::de::Visitor<'de> for TagVecVisitor {
43            type Value = TagVec;
44
45            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
46                formatter.write_str("a string separated by ;")
47            }
48
49            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
50            where
51                E: serde::de::Error,
52            {
53                let parts: Vec<String> = value.split(';').map(String::from).collect();
54                Ok(TagVec(parts))
55            }
56        }
57
58        deserializer.deserialize_str(TagVecVisitor)
59    }
60}
61
62impl Deref for TagVec {
63    type Target = Vec<String>;
64
65    fn deref(&self) -> &Self::Target {
66        &self.0
67    }
68}
69
70impl DerefMut for TagVec {
71    fn deref_mut(&mut self) -> &mut Self::Target {
72        &mut self.0
73    }
74}
75
76impl Default for TagVec {
77    fn default() -> Self {
78        TagVec (vec![])
79    }
80}
81
82impl Display for TagVec {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        f.write_str(self.join("; ").as_str())?;
85        Ok(())
86    }
87}
88
89#[cfg(feature = "napi")]
90impl FromNapiValue for TagVec {
91    unsafe fn from_napi_value(env: napi::sys::napi_env, napi_val: napi::sys::napi_value) -> napi::Result<Self> {
92        let mut len = 0;
93        napi::sys::napi_get_array_length(env, napi_val, &mut len);
94
95        let mut result = Vec::with_capacity(len as usize);
96
97        for i in 0..len {
98            let mut element_value: napi::sys::napi_value = std::ptr::null_mut();
99            napi::sys::napi_get_element(env, napi_val, i, &mut element_value);
100
101            // Assuming the elements are N-API strings, we use a utility function to convert them
102            let str_length = {
103                let mut str_length = 0;
104                napi::sys::napi_get_value_string_utf8(
105                    env, 
106                    element_value, 
107                    std::ptr::null_mut(), 
108                    0, 
109                    &mut str_length
110                );
111                str_length
112            };
113
114            let mut buffer = Vec::with_capacity(str_length as usize + 1);
115            let buffer_ptr = buffer.as_mut_ptr() as *mut _;
116            napi::sys::napi_get_value_string_utf8(
117                env, 
118                element_value, 
119                buffer_ptr, 
120                buffer.capacity(), 
121                std::ptr::null_mut()
122            );
123
124            buffer.set_len(str_length as usize);
125            let string = String::from_utf8_lossy(&buffer).to_string();
126            result.push(string);
127        }
128
129        Ok(TagVec(result))
130    }
131}
132
133#[cfg(feature = "napi")]
134impl ToNapiValue for TagVec {
135    unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result<napi::sys::napi_value> {
136        let len = val.len();
137        let mut js_array: napi::sys::napi_value = std::ptr::null_mut();
138        napi::sys::napi_create_array_with_length(env, len, &mut js_array);
139
140        for (i, item) in val.iter().enumerate() {
141            let mut js_string: napi::sys::napi_value = std::ptr::null_mut();
142            napi::sys::napi_create_string_utf8(env, item.as_ptr() as *const _, item.len(), &mut js_string);
143
144            napi::sys::napi_set_element(env, js_array, i as u32, js_string);
145        }
146
147        Ok(js_array)
148    }
149}
150
151impl IntoIterator for TagVec {
152    type Item = String;
153    type IntoIter = std::vec::IntoIter<Self::Item>;
154
155    fn into_iter(self) -> Self::IntoIter {
156        self.0.into_iter()
157    }
158}
159
160impl From<Vec<&str>> for TagVec {
161    fn from(vec: Vec<&str>) -> Self {
162        let strings: Vec<String> = vec.iter().map(|&s| s.to_string()).collect();
163        TagVec (strings)
164    }
165}
166
167// impl From<Vec<_>> for TagVec {
168//     fn from(vec: Vec<_>) -> Self {
169//         TagVec(Vec::nmew
170//     }
171// }
172
173// Custom trait for splitting a string by ";" and removing whitespace
174trait FromDelimitedString: Sized {
175    fn from_delimited_string(s: &str) -> Result<Self, Box<dyn std::error::Error>>;
176}
177
178impl FromDelimitedString for TagVec {
179    fn from_delimited_string(s: &str) -> Result<Self, Box<dyn std::error::Error>> {
180        let values: Vec<String> = s
181            .split(';')
182            .map(|part| part.trim().to_string())
183            .filter(|part| !part.is_empty())
184            .collect();
185
186        Ok(TagVec(values))
187    }
188}
189
190// Implement FromSql for Vec<String>
191impl FromSql for TagVec {
192    fn column_result(value: ValueRef) -> Result<Self, FromSqlError> {
193        match value {
194            ValueRef::Text(_) => {
195                let s = value.as_str()?;
196                FromDelimitedString::from_delimited_string(s)
197                    .map_err(|_| FromSqlError::OutOfRange(0))
198            }
199            _ => Err(FromSqlError::InvalidType),
200        }
201    }
202}
203
204#[cfg_attr(feature = "napi", napi(object))]
205#[derive(Debug, Clone, Deserialize, Serialize)]
206pub struct AdditionalApp {
207    pub id: String,
208    pub name: String,
209    pub application_path: String,
210    pub launch_command: String,
211    pub auto_run_before: bool,
212    pub wait_for_exit: bool,
213    pub parent_game_id: String,
214}
215
216#[cfg_attr(feature = "napi", napi(object))]
217#[derive(Debug, Clone, Deserialize, Serialize)]
218pub struct Game {
219    pub id: String,
220    pub owner: String,
221    pub library: String,
222    pub title: String,
223    pub alternate_titles: String,
224    pub series: String,
225    pub developer: String,
226    pub publisher: String,
227    pub primary_platform: String,
228    pub platforms: TagVec,
229    pub date_added: String,
230    pub date_modified: String,
231    pub detailed_platforms: Option<Vec<Tag>>,
232    pub legacy_broken: bool,
233    pub legacy_extreme: bool,
234    pub play_mode: String,
235    pub status: String,
236    pub notes: String,
237    pub tags: TagVec,
238    pub detailed_tags: Option<Vec<Tag>>,
239    pub source: String,
240    pub legacy_application_path: String,
241    pub legacy_launch_command: String,
242    pub release_date: String,
243    pub version: String,
244    pub original_description: String,
245    pub language: String,
246    pub active_data_id: Option<i64>,
247    pub active_data_on_disk: bool,
248    pub last_played: Option<String>,
249    pub playtime: i64,
250    pub play_counter: i64,
251    pub active_game_config_id: Option<i64>,
252    pub active_game_config_owner: Option<String>,
253    pub archive_state: i64,
254    pub game_data: Option<Vec<GameData>>,
255    pub add_apps: Option<Vec<AdditionalApp>>,
256    pub ruffle_support: String,
257    pub logo_path: String,
258    pub screenshot_path: String,
259    pub ext_data: Option<HashMap<String, serde_json::Value>>,
260}
261
262#[cfg_attr(feature = "napi", napi(object))]
263#[derive(Debug, Clone, Deserialize, Serialize)]
264pub struct PartialGame {
265    pub id: String,
266    pub owner: Option<String>,
267    pub library: Option<String>,
268    pub title: Option<String>,
269    pub alternate_titles: Option<String>,
270    pub series: Option<String>,
271    pub developer: Option<String>,
272    pub publisher: Option<String>,
273    pub primary_platform: Option<String>,
274    pub platforms: Option<TagVec>,
275    pub date_added: Option<String>,
276    pub date_modified: Option<String>,
277    pub legacy_broken: Option<bool>,
278    pub legacy_extreme: Option<bool>,
279    pub play_mode: Option<String>,
280    pub status: Option<String>,
281    pub notes: Option<String>,
282    pub tags: Option<TagVec>,
283    pub detailed_tags: Option<Vec<Tag>>,
284    pub source: Option<String>,
285    pub legacy_application_path: Option<String>,
286    pub legacy_launch_command: Option<String>,
287    pub release_date: Option<String>,
288    pub version: Option<String>,
289    pub original_description: Option<String>,
290    pub language: Option<String>,
291    pub active_data_id: Option<i64>,
292    pub active_data_on_disk: Option<bool>,
293    pub last_played: Option<String>,
294    pub playtime: Option<i64>,
295    pub play_counter: Option<i64>,
296    pub active_game_config_id: Option<i64>,
297    pub active_game_config_owner: Option<String>,
298    pub archive_state: Option<i64>,
299    pub add_apps: Option<Vec<AdditionalApp>>,
300    pub ruffle_support: Option<String>,
301    pub logo_path: Option<String>,
302    pub screenshot_path: Option<String>,
303    pub ext_data: Option<HashMap<String, serde_json::Value>>,
304}
305
306#[cfg_attr(feature = "napi", napi(object))]
307#[derive(Debug, Clone, Deserialize, Serialize)]
308pub struct GameRedirect {
309    pub source_id: String,
310    pub dest_id: String,
311}
312
313pub fn find_all_ids(conn: &Connection) -> Result<Vec<String>> {
314    let mut stmt = conn.prepare("SELECT id FROM game")?;
315
316    let ids = stmt.query_map([], |row| {
317        row.get(0)
318    })?
319    .collect::<Result<Vec<String>>>()?;
320
321    Ok(ids)
322}
323
324pub fn find(conn: &Connection, id: &str) -> Result<Option<Game>> {
325    let mut stmt = conn.prepare(
326        "SELECT id, title, alternateTitles, series, developer, publisher, platformsStr, \
327        platformName, dateAdded, dateModified, broken, extreme, playMode, status, notes, \
328        tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
329        originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
330        activeGameConfigId, activeGameConfigOwner, archiveState, library, playCounter, \
331        logoPath, screenshotPath, ruffleSupport, owner \
332        FROM game WHERE id = COALESCE((SELECT id FROM game_redirect WHERE sourceId = ?), ?)",
333    )?;
334
335    let game_result = stmt
336        .query_row(params![id, id], |row| {
337            Ok(Game {
338                id: row.get(0)?,
339                title: row.get(1)?,
340                alternate_titles: row.get(2)?,
341                series: row.get(3)?,
342                developer: row.get(4)?,
343                publisher: row.get(5)?,
344                platforms: row.get(6)?,
345                primary_platform: row.get(7)?,
346                date_added: row.get(8)?,
347                date_modified: row.get(9)?,
348                legacy_broken: row.get(10)?,
349                legacy_extreme: row.get(11)?,
350                play_mode: row.get(12)?,
351                status: row.get(13)?,
352                notes: row.get(14)?,
353                tags: row.get(15)?,
354                source: row.get(16)?,
355                legacy_application_path: row.get(17)?,
356                legacy_launch_command: row.get(18)?,
357                release_date: row.get(19)?,
358                version: row.get(20)?,
359                original_description: row.get(21)?,
360                language: row.get(22)?,
361                active_data_id: row.get(23)?,
362                active_data_on_disk: row.get(24)?,
363                last_played: row.get(25)?,
364                playtime: row.get(26)?,
365                active_game_config_id: row.get(27)?,
366                active_game_config_owner: row.get(28)?,
367                archive_state: row.get(29)?,
368                library: row.get(30)?,
369                play_counter: row.get(31)?,
370                detailed_platforms: None,
371                detailed_tags: None,
372                game_data: None,
373                add_apps: None,
374                logo_path: row.get(32)?,
375                screenshot_path: row.get(33)?,
376                ruffle_support: row.get(34)?,
377                owner: row.get(35)?,
378                ext_data: None,
379            })
380        })
381        .optional()?; // Converts rusqlite::Error::QueryReturnedNoRows to None
382
383    if let Some(mut game) = game_result {
384        game.detailed_platforms = Some(get_game_platforms(conn, id)?);
385        game.detailed_tags = Some(get_game_tags(conn, id)?);
386        game.game_data = Some(get_game_data(conn, id)?);
387        game.add_apps = Some(get_game_add_apps(conn, id)?);
388        game.ext_data = Some(find_ext_data(conn, &game.id)?);
389        Ok(Some(game))
390    } else {
391        Ok(None)
392    }
393}
394
395fn find_ext_data(conn: &Connection, id: &str) -> Result<HashMap<String, serde_json::Value>> {
396    let mut stmt = conn.prepare("SELECT extId, data FROM ext_data WHERE gameId = ?")?;
397    let rows = stmt.query_map(params![id], |row| {
398        let ext_id: String = row.get(0)?;
399        let data: serde_json::Value = row.get(1)?;
400        Ok((ext_id, data))
401    })?;
402
403    let mut res = HashMap::new();
404
405    for result in rows {
406        let (ext_id, data) = result?;
407        res.insert(ext_id, data);
408    }
409
410    Ok(res)
411}
412
413pub fn create(conn: &Connection, partial: &PartialGame) -> Result<Game> {
414    let mut game: Game = partial.into();
415
416    let tags_copy = game.tags.clone();
417    let platforms_copy = game.platforms.clone();
418    game.tags = vec![].into();
419    game.platforms = vec![].into();
420
421    let mut detailed_tags = vec![];
422
423    match game.detailed_tags.as_deref() {
424        Some(dtags) if !dtags.is_empty() => {
425            for tag in dtags {
426                detailed_tags.push(tag.id);
427            }
428        },
429        _ => {
430            for name in tags_copy {
431                let detailed_tag = tag::find_or_create(conn, &name)?;
432                game.tags.push(detailed_tag.name);
433                detailed_tags.push(detailed_tag.id);
434            }
435        }
436    }
437
438    let mut detailed_platforms = vec![];
439
440    for name in platforms_copy {
441        let detailed_platform = platform::find_or_create(conn, &name, None)?;
442        game.platforms.push(detailed_platform.name);
443        detailed_platforms.push(detailed_platform.id);
444    }
445
446    conn.execute(
447        "INSERT INTO game (id, owner, library, title, alternateTitles, series, developer, publisher, \
448         platformName, platformsStr, dateAdded, dateModified, broken, extreme, playMode, status, \
449         notes, tagsStr, source, applicationPath, launchCommand, releaseDate, version, \
450         originalDescription, language, activeDataId, activeDataOnDisk, lastPlayed, playtime, \
451         activeGameConfigId, activeGameConfigOwner, archiveState, orderTitle, logoPath, screenshotPath, ruffleSupport) VALUES (?, ?, ?, ?, ?, ?, ?, \
452         ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', ?, ?, ?)",
453        params![
454            &game.id,
455            &game.owner,
456            &game.library,
457            &game.title,
458            &game.alternate_titles,
459            &game.series,
460            &game.developer,
461            &game.publisher,
462            &game.primary_platform,
463            &game.platforms.join("; "),
464            &game.date_added,
465            &game.date_modified,
466            &game.legacy_broken,
467            &game.legacy_extreme,
468            &game.play_mode,
469            &game.status,
470            &game.notes,
471            &game.tags.join("; "),
472            &game.source,
473            &game.legacy_application_path,
474            &game.legacy_launch_command,
475            &game.release_date,
476            &game.version,
477            &game.original_description,
478            &game.language,
479            &game.active_data_id,
480            &game.active_data_on_disk,
481            &game.last_played,
482            &game.playtime,
483            &game.active_game_config_id,
484            &game.active_game_config_owner,
485            &game.archive_state,
486            &game.logo_path,
487            &game.screenshot_path,
488            &game.ruffle_support,
489        ],
490    )?;
491
492    for tag in detailed_tags {
493        conn.execute("INSERT OR IGNORE INTO game_tags_tag (gameId, tagId) VALUES (?, ?)", params![game.id, tag])?;
494    }
495
496    for platform in detailed_platforms {
497        conn.execute("INSERT OR IGNORE INTO game_platforms_platform (gameId, platformId) VALUES (?, ?)", params![game.id, platform])?;
498    }
499
500    if let Some(ext_data) = &game.ext_data {
501        for (ext_id, data) in ext_data {
502            conn.execute("INSERT INTO ext_data (extId, gameId, data) VALUES(?, ?, ?)
503              ON CONFLICT(extId, gameId)
504              DO UPDATE SET data = ?", params![ext_id, game.id, data, data])?;
505        }
506    }
507
508    mark_index_dirty(conn)?;
509
510    Ok(game)
511}
512
513pub fn save(conn: &Connection, game: &PartialGame) -> Result<Game> {
514    // Allow use of rarray() in SQL queries
515    rusqlite::vtab::array::load_module(conn)?;
516
517    let existing_game_result = find(conn, game.id.as_str())?;
518    if let Some(mut existing_game) = existing_game_result {
519        existing_game.apply_partial(game);
520
521        // Process  any tag and platform changes
522        let tags_copy = existing_game.tags.clone();
523        let platforms_copy = existing_game.platforms.clone();
524        let mut detailed_tags_copy: Vec<Tag> = vec![];
525        let mut detailed_platforms_copy: Vec<Tag> = vec![];
526        existing_game.tags = vec![].into();
527        existing_game.platforms = vec![].into();
528
529        for name in tags_copy {
530            let detailed_tag = tag::find_or_create(conn, &name)?;
531            detailed_tags_copy.push(detailed_tag.clone());
532            existing_game.tags.push(detailed_tag.name);
533        }
534
535        for name in platforms_copy {
536            let detailed_platform = platform::find_or_create(conn, &name, None)?;
537            detailed_platforms_copy.push(detailed_platform.clone());
538            existing_game.platforms.push(detailed_platform.name);
539        }
540
541        // Update relations in database
542        let tag_ids: Vec<i64> = detailed_tags_copy.iter().map(|t| t.id).collect::<Vec<i64>>();
543        let tag_values = Rc::new(tag_ids.iter().copied().map(Value::from).collect::<Vec<Value>>());
544        let mut stmt = conn.prepare("DELETE FROM game_tags_tag WHERE gameId = ? AND tagId NOT IN rarray(?)")?;
545        stmt.execute(params![existing_game.id.as_str(), tag_values]).map(|changes| changes as usize)?;
546        for tag_id in tag_ids {
547            stmt = conn.prepare("INSERT OR IGNORE INTO game_tags_tag (gameId, tagId) VALUES (?, ?)")?;
548            stmt.execute(params![existing_game.id.as_str(), tag_id])?;
549        }
550
551        let platform_ids: Vec<i64> = detailed_platforms_copy.iter().map(|t| t.id).collect::<Vec<i64>>();
552        let platform_values = Rc::new(platform_ids.iter().copied().map(Value::from).collect::<Vec<Value>>());
553        let mut stmt = conn.prepare("DELETE FROM game_platforms_platform WHERE gameId = ? AND platformId NOT IN rarray(?)")?;
554        stmt.execute(params![existing_game.id.as_str(), platform_values]).map(|changes| changes as usize)?;
555        for platform_id in platform_ids {
556            stmt = conn.prepare("INSERT OR IGNORE INTO game_platforms_platform (gameId, platformId) VALUES (?, ?)")?;
557            stmt.execute(params![existing_game.id.as_str(), platform_id])?;
558        }
559
560        // Write back any ext data changes to the database
561        if let Some(ext_data) = &game.ext_data {
562            for (ext_id, data) in ext_data {
563                conn.execute("INSERT INTO ext_data (extId, gameId, data) VALUES(?, ?, ?)
564                  ON CONFLICT(extId, gameId)
565                  DO UPDATE SET data = ?", params![ext_id, game.id, data, data])?;
566            }
567        }
568
569        // Write back the changes to the database
570        conn.execute(
571            "UPDATE game SET owner = ?, library = ?, title = ?, alternateTitles = ?, series = ?, developer = ?, publisher = ?, \
572             platformName = ?, platformsStr = ?, dateAdded = ?, dateModified = ?, broken = ?, \
573             extreme = ?, playMode = ?, status = ?, notes = ?, tagsStr = ?, source = ?, \
574             applicationPath = ?, launchCommand = ?, releaseDate = ?, version = ?, \
575             originalDescription = ?, language = ?, activeDataId = ?, activeDataOnDisk = ?, \
576             lastPlayed = ?, playtime = ?, playCounter = ?, activeGameConfigId = ?, activeGameConfigOwner = ?, \
577             archiveState = ?, logoPath = ?, screenshotPath = ?, ruffleSupport = ? WHERE id = ?",
578            params![
579                &existing_game.owner,
580                &existing_game.library,
581                &existing_game.title,
582                &existing_game.alternate_titles,
583                &existing_game.series,
584                &existing_game.developer,
585                &existing_game.publisher,
586                &existing_game.primary_platform,
587                &existing_game.platforms.join("; "),
588                &existing_game.date_added,
589                &existing_game.date_modified,
590                &existing_game.legacy_broken,
591                &existing_game.legacy_extreme,
592                &existing_game.play_mode,
593                &existing_game.status,
594                &existing_game.notes,
595                &existing_game.tags.join("; "),
596                &existing_game.source,
597                &existing_game.legacy_application_path,
598                &existing_game.legacy_launch_command,
599                &existing_game.release_date,
600                &existing_game.version,
601                &existing_game.original_description,
602                &existing_game.language,
603                &existing_game.active_data_id,
604                &existing_game.active_data_on_disk,
605                &existing_game.last_played,
606                &existing_game.playtime,
607                &existing_game.play_counter,
608                &existing_game.active_game_config_id,
609                &existing_game.active_game_config_owner,
610                &existing_game.archive_state,
611                &existing_game.logo_path,
612                &existing_game.screenshot_path,
613                &existing_game.ruffle_support,
614                &existing_game.id,
615            ],
616        )?;
617
618        existing_game.detailed_platforms = get_game_platforms(conn, &existing_game.id)?.into();
619        existing_game.detailed_tags = get_game_tags(conn, &existing_game.id)?.into();
620        existing_game.game_data = get_game_data(conn, &existing_game.id)?.into();
621
622        mark_index_dirty(conn)?;
623
624        Ok(existing_game)
625    } else {
626        Err(rusqlite::Error::QueryReturnedNoRows)
627    }
628}
629
630pub fn delete(conn: &Connection, id: &str) -> Result<()> {    
631    let mut stmt = "DELETE FROM game WHERE id = ?";
632    conn.execute(stmt, params![id])?;
633
634    stmt = "DELETE FROM additional_app WHERE parentGameId = ?";
635    conn.execute(stmt, params![id])?;
636
637    stmt = "DELETE FROM game_tags_tag WHERE gameId = ?";
638    conn.execute(stmt, params![id])?;
639
640    stmt = "DELETE FROM game_platforms_platform WHERE gameId = ?";
641    conn.execute(stmt, params![id])?;
642
643    stmt = "DELETE FROM ext_data WHERE gameId = ?";
644    conn.execute(stmt, params![id])?;
645
646    Ok(())
647}
648
649pub fn count(conn: &Connection) -> Result<i64> {
650    conn.query_row("SELECT COUNT(*) FROM game", (), |row| row.get::<_, i64>(0))
651}
652
653fn get_game_platforms(conn: &Connection, id: &str) -> Result<Vec<Tag>> {
654    let mut platform_stmt = conn.prepare(
655        "SELECT p.id, p.description, pa.name, p.dateModified FROM platform p
656         INNER JOIN game_platforms_platform gpp ON gpp.platformId = p.id
657         INNER JOIN platform_alias pa ON p.primaryAliasId = pa.id
658         WHERE gpp.gameId = ?",
659    )?;
660
661    let platform_iter = platform_stmt.query_map(params![id], |row| {
662        Ok(Tag {
663            id: row.get(0)?,
664            description: row.get(1)?,
665            name: row.get(2)?,
666            date_modified: row.get(3)?,
667            category: None,
668            aliases: vec![],
669        })
670    })?;
671
672    let mut platforms: Vec<Tag> = vec![];
673
674    for platform_result in platform_iter {
675        let mut platform = platform_result?;
676
677        // Query for the aliases of the platform
678        let mut platform_aliases_stmt =
679            conn.prepare("SELECT pa.name FROM platform_alias pa WHERE pa.platformId = ?")?;
680
681        let aliases_iter = platform_aliases_stmt
682            .query_map(params![platform.id], |row| Ok(row.get::<_, String>(0)?))?;
683
684        // Collect aliases into the platform's aliases vector
685        for alias_result in aliases_iter {
686            platform.aliases.push(alias_result?);
687        }
688
689        platforms.push(platform);
690    }
691
692    Ok(platforms)
693}
694
695fn get_game_tags(conn: &Connection, id: &str) -> Result<Vec<Tag>> {
696    let mut tag_stmt = conn.prepare(
697        "SELECT t.id, t.description, ta.name, t.dateModified, tc.name FROM tag t
698         INNER JOIN game_tags_tag gtt ON gtt.tagId = t.id
699         INNER JOIN tag_alias ta ON t.primaryAliasId = ta.id
700         INNER JOIN tag_category tc ON t.categoryId = tc.id
701         WHERE gtt.gameId = ?",
702    )?;
703
704    let tag_iter = tag_stmt.query_map(params![id], |row| {
705        Ok(Tag {
706            id: row.get(0)?,
707            description: row.get(1)?,
708            name: row.get(2)?,
709            date_modified: row.get(3)?,
710            category: row.get(4)?,
711            aliases: vec![],
712        })
713    })?;
714
715    let mut tags: Vec<Tag> = vec![];
716
717    for tag_result in tag_iter {
718        let mut tag = tag_result?;
719
720        // Query for the aliases of the platform
721        let mut tag_aliases_stmt =
722            conn.prepare("SELECT ta.name FROM tag_alias ta WHERE ta.tagId = ?")?;
723
724        let aliases_iter =
725            tag_aliases_stmt.query_map(params![tag.id], |row| Ok(row.get::<_, String>(0)?))?;
726
727        // Collect aliases into the platform's aliases vector
728        for alias_result in aliases_iter {
729            tag.aliases.push(alias_result?);
730        }
731
732        tags.push(tag);
733    }
734
735    Ok(tags)
736}
737
738pub fn get_game_data(conn: &Connection, id: &str) -> Result<Vec<GameData>> {
739    let mut game_data: Vec<GameData> = vec![];
740
741    let mut game_data_stmt = conn.prepare("
742        SELECT id, title, dateAdded, sha256, crc32, presentOnDisk,
743        path, size, parameters, applicationPath, launchCommand
744        FROM game_data
745        WHERE gameId = ?
746    ")?;
747
748    let rows = game_data_stmt.query_map(params![id], |row| {
749        Ok(GameData {
750            id: row.get(0)?,
751            game_id: id.to_owned(),
752            title: row.get(1)?,
753            date_added: row.get(2)?,
754            sha256: row.get(3)?,
755            crc32: row.get(4)?,
756            present_on_disk: row.get(5)?,
757            path: row.get(6)?,
758            size: row.get(7)?,
759            parameters: row.get(8)?,
760            application_path: row.get(9)?,
761            launch_command: row.get(10)?,
762        })
763    })?;
764
765    for result in rows {
766        game_data.push(result?);
767    }
768
769    Ok(game_data)
770}
771
772fn get_game_add_apps(conn: &Connection, game_id: &str) -> Result<Vec<AdditionalApp>> {
773    let mut add_app_stmt = conn.prepare(
774        "SELECT id, name, applicationPath, launchCommand, autoRunBefore, waitForExit
775        FROM additional_app WHERE parentGameId = ?"
776    )?;
777
778    let mut add_apps: Vec<AdditionalApp> = vec![];
779
780    let add_app_iter = add_app_stmt.query_map(params![game_id], |row| {
781        Ok(AdditionalApp {
782            id: row.get(0)?,
783            parent_game_id: game_id.to_owned(),
784            name: row.get(1)?,
785            application_path: row.get(2)?,
786            launch_command: row.get(3)?,
787            auto_run_before: row.get(4)?,
788            wait_for_exit: row.get(5)?,
789        })
790    })?;
791
792    for add_app in add_app_iter {
793        add_apps.push(add_app?);
794    }
795
796    Ok(add_apps)
797}
798
799pub fn find_game_data_by_id(conn: &Connection, id: i64) -> Result<Option<GameData>> {
800    let mut game_data_stmt = conn.prepare("
801        SELECT gameId, title, dateAdded, sha256, crc32, presentOnDisk,
802        path, size, parameters, applicationPath, launchCommand
803        FROM game_data
804        WHERE id = ?
805    ")?;
806
807    Ok(game_data_stmt.query_row(params![id], |row| {
808        Ok(GameData {
809            id: id.to_owned(),
810            game_id: row.get(0)?,
811            title: row.get(1)?,
812            date_added: row.get(2)?,
813            sha256: row.get(3)?,
814            crc32: row.get(4)?,
815            present_on_disk: row.get(5)?,
816            path: row.get(6)?,
817            size: row.get(7)?,
818            parameters: row.get(8)?,
819            application_path: row.get(9)?,
820            launch_command: row.get(10)?,
821        })
822    }).optional()?)
823}
824
825pub fn create_game_data(conn: &Connection, partial: &PartialGameData) -> Result<GameData> {
826    // Make sure game exists
827    let game = find(conn, &partial.game_id)?;
828    if game.is_none() {
829        println!("{} missing", &partial.game_id);
830        return Err(rusqlite::Error::QueryReturnedNoRows);
831    }
832
833    let mut game_data: GameData = partial.into();
834    
835    let mut stmt = conn.prepare("INSERT INTO game_data (gameId, title, dateAdded, sha256, crc32, presentOnDisk
836        , path, size, parameters, applicationPath, launchCommand)
837        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id")?;
838    let game_data_id: i64 = stmt.query_row(params![
839        &game_data.game_id,
840        &game_data.title,
841        &game_data.date_added,
842        &game_data.sha256,
843        &game_data.crc32,
844        &game_data.present_on_disk,
845        &game_data.path,
846        &game_data.size,
847        &game_data.parameters,
848        &game_data.application_path,
849        &game_data.launch_command,
850    ], |row| row.get(0))?;
851
852    game_data.id = game_data_id;
853    Ok(game_data)
854}
855
856pub fn save_game_data(conn: &Connection, partial: &PartialGameData) -> Result<GameData> {
857    let game_data: GameData = partial.into();
858    
859    let mut stmt = conn.prepare("UPDATE game_data
860        SET gameId = ?, title = ?, dateAdded = ?, sha256 = ?, crc32 = ?, presentOnDisk = ?,
861        path = ?, size = ?, parameters = ?, applicationPath = ?, launchCommand = ? WHERE id = ?")?;
862    stmt.execute(params![
863        &game_data.game_id,
864        &game_data.title,
865        &game_data.date_added,
866        &game_data.sha256,
867        &game_data.crc32,
868        &game_data.present_on_disk,
869        &game_data.path,
870        &game_data.size,
871        &game_data.parameters,
872        &game_data.application_path,
873        &game_data.launch_command,
874        &game_data.id,
875    ])?;
876
877    let res = find_game_data_by_id(conn, game_data.id)?;
878    match res {
879        Some(r) => Ok(r),
880        None => Err(rusqlite::Error::QueryReturnedNoRows),
881    }
882}
883
884pub fn find_with_tag(conn: &Connection, tag: &str) -> Result<Vec<Game>> {
885    let mut search = GameSearch::default();
886    search.load_relations = GameSearchRelations {
887        tags: true,
888        platforms: true,
889        game_data: true,
890        add_apps: true,
891        ext_data: true,
892    };
893    search.filter.exact_whitelist.tags = Some(vec![tag.to_owned()]);
894    search.limit = MAX_SEARCH;
895    search::search(conn, &search)
896}
897
898pub fn find_split_string_col(conn: &Connection, column: &str, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
899    let map_closure = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
900    let selection= format!("SELECT DISTINCT {} FROM game", column);
901    let dev_iter: Vec<String> = match search_opt {
902        None => {
903            let mut stmt = conn.prepare(&selection)?;
904            let mapped_rows = stmt.query_map((), map_closure)?;
905            mapped_rows
906                .map(|result| result.map_err(|e| e.into()))
907                .collect::<Result<Vec<String>>>()?
908        },
909        Some(mut search_data) => {
910            search_data.limit = MAX_SEARCH;
911            search_data.load_relations = GameSearchRelations::default();
912            search::search_custom(conn, &search_data, &selection, map_closure)?
913        }
914    };
915
916    let mut data_set: HashSet<UniCase<String>> = HashSet::new();
917        
918    for row in dev_iter {
919        for data in row.split(|c| c == ';' || c == ',') {
920            data_set.insert(UniCase::new(data.trim().to_string()));
921        }
922    }
923
924    let data: Vec<String> = data_set.into_iter().map(|unicase| unicase.into_inner()).collect();
925
926    Ok(data)
927}
928
929pub fn find_developers(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
930    find_split_string_col(conn, "developer", search_opt)
931}
932
933pub fn find_publishers(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
934    find_split_string_col(conn, "publisher", search_opt)
935}
936
937pub fn find_series(conn: &Connection, search_opt: Option<GameSearch>) -> Result<Vec<String>> {
938    let map_closure = |row: &rusqlite::Row<'_>| row.get::<_, String>(0);
939    let selection = "SELECT DISTINCT series FROM game";
940    match search_opt {
941        None => {
942            let mut stmt = conn.prepare(selection)?;
943            let mapped_rows = stmt.query_map((), map_closure)?;
944            Ok(mapped_rows
945                .map(|result| result.map_err(|e| e.into()))
946                .collect::<Result<Vec<String>>>()?)
947        },
948        Some(mut search_data) => {
949            search_data.limit = MAX_SEARCH;
950            search_data.load_relations = GameSearchRelations::default();
951            Ok(search::search_custom(conn, &search_data, selection, map_closure)?)
952        }
953    }
954}
955
956pub fn find_libraries(conn: &Connection) -> Result<Vec<String>> {
957    let mut stmt = conn.prepare("SELECT DISTINCT library FROM game")?;
958    let libraries_iter = stmt.query_map((), |row| row.get(0))?;
959
960    let mut libraries = vec![];
961
962    for library in libraries_iter {
963        libraries.push(library?);
964    }
965
966    Ok(libraries)
967}
968
969pub fn find_statuses(conn: &Connection) -> Result<Vec<String>> {
970    let mut stmt = conn.prepare("SELECT DISTINCT status FROM game")?;
971    let status_iter = stmt.query_map((), |row| {
972        let value: String = row.get(0)?;
973        Ok(value)
974    })?;
975
976    let mut statuses = HashSet::new();
977
978    for status in status_iter {
979        if let Ok(status) = status {
980
981            status.split(';').for_each(|v| { statuses.insert(v.trim().to_string()); });
982        }
983    }
984
985    Ok(statuses.into_iter().collect())
986}
987
988pub fn find_play_modes(conn: &Connection) -> Result<Vec<String>> {
989    let mut stmt = conn.prepare("SELECT DISTINCT playMode FROM game")?;
990    let play_modes_iter = stmt.query_map((), |row| {
991        let value: String = row.get(0)?;
992        Ok(value)
993    })?;
994
995    let mut play_modes = HashSet::new();
996
997    for play_mode in play_modes_iter {
998        if let Ok(play_mode) = play_mode {
999
1000            play_mode.split(';').for_each(|v| { play_modes.insert(v.trim().to_string()); });
1001        }
1002    }
1003
1004    Ok(play_modes.into_iter().collect())
1005}
1006
1007pub fn find_application_paths(conn: &Connection) -> Result<Vec<String>> {
1008    let mut stmt = conn.prepare("
1009    SELECT COUNT(*) as games_count, applicationPath FROM (
1010        SELECT applicationPath FROM game WHERE applicationPath != ''
1011        UNION ALL
1012        SELECT applicationPath FROM game_data WHERE applicationPath != ''
1013    ) GROUP BY applicationPath ORDER BY games_count DESC")?;
1014    let ap_iter = stmt.query_map((), |row| row.get(1))?;
1015
1016    let mut app_paths = vec![];
1017
1018    for app_path in ap_iter {
1019        app_paths.push(app_path?);
1020    }
1021
1022    Ok(app_paths)
1023}
1024
1025pub fn find_platform_app_paths(conn: &Connection) -> Result<HashMap<String, Vec<PlatformAppPath>>> {
1026    let mut suggestions = HashMap::new();
1027    let platforms = platform::find(conn)?;
1028
1029    for platform in platforms {
1030        let mut stmt = conn.prepare("
1031        SELECT COUNT(*) as games_count, applicationPath FROM (
1032            SELECT applicationPath FROM game WHERE applicationPath != '' AND game.id IN (
1033                SELECT gameId FROM game_platforms_platform WHERE platformId = ?
1034            )
1035            UNION ALL
1036            SELECT applicationPath FROM game_data WHERE applicationPath != '' AND game_data.gameId IN (
1037                SELECT gameId FROM game_platforms_platform WHERE platformId = ?
1038            )
1039        ) GROUP BY applicationPath ORDER BY games_count DESC")?;
1040
1041        let results = stmt.query_map(params![platform.id, platform.id], |row| {
1042            Ok(PlatformAppPath {
1043                app_path: row.get(1)?,
1044                count: row.get(0)?,
1045            })
1046        })?;
1047
1048        let mut platform_list = vec![];
1049
1050        for app_path in results {
1051            platform_list.push(app_path?);
1052        }
1053
1054        suggestions.insert(platform.name, platform_list);
1055    }
1056
1057    Ok(suggestions)
1058}
1059
1060pub fn find_add_app_by_id(conn: &Connection, id: &str) -> Result<Option<AdditionalApp>> {
1061    let mut stmt = conn.prepare("SELECT name, applicationPath, launchCommand, autoRunBefore,
1062        waitForExit, parentGameId FROM additional_app WHERE id = ?")?;
1063
1064    stmt.query_row(params![id], |row| {
1065        Ok(AdditionalApp{
1066            id: id.to_owned(),
1067            name: row.get(0)?,
1068            application_path: row.get(1)?,
1069            launch_command: row.get(2)?,
1070            auto_run_before: row.get(3)?,
1071            wait_for_exit: row.get(4)?,
1072            parent_game_id: row.get(5)?
1073        })
1074    }).optional()
1075}
1076
1077pub fn create_add_app(conn: &Connection, add_app: &mut AdditionalApp) -> Result<()> {
1078    let id = conn.query_row("INSERT INTO additional_app (
1079        id, applicationPath, launchCommand, name, parentGameId, autoRunBefore, waitForExit
1080    ) VALUES (?, ?, ?, ?, ?, ? , ?) RETURNING id", params![add_app.id, add_app.application_path, add_app.launch_command,
1081    add_app.name, add_app.parent_game_id, add_app.auto_run_before, add_app.wait_for_exit], |row| row.get::<_, String>(0))?;
1082    add_app.id = id;
1083    Ok(())
1084}
1085
1086pub fn add_playtime(conn: &Connection, game_id: &str, seconds: i64) -> Result<()> {
1087    let mut game = match find(conn, game_id)? {
1088        Some(g) => g,
1089        None => return Err(rusqlite::Error::QueryReturnedNoRows)
1090    };
1091
1092    game.play_counter += 1;
1093    game.playtime += seconds;
1094    game.last_played = Some(Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string());
1095
1096    save(conn, &(game.into()))?;
1097    Ok(())
1098}
1099
1100pub fn clear_playtime_tracking(conn: &Connection) -> Result<()> {
1101    let mut stmt = conn.prepare("UPDATE game SET playtime = 0, playCounter = 0, lastPlayed = NULL")?;
1102    stmt.execute(())?;
1103    Ok(())
1104}
1105
1106pub fn clear_playtime_tracking_by_id(conn: &Connection, game_id: &str) -> Result<()> {
1107    let mut stmt = conn.prepare("UPDATE game SET playtime = 0, playCounter = 0, lastPlayed = NULL WHERE id = ?")?;
1108    stmt.execute(params![game_id])?;
1109    Ok(())
1110}
1111
1112pub fn force_active_data_most_recent(conn: &Connection) -> Result<()> {
1113    conn.execute("UPDATE game
1114    SET activeDataId = (SELECT game_data.id FROM game_data WHERE game.id = game_data.gameId ORDER BY game_data.dateAdded DESC LIMIT 1)
1115    WHERE game.activeDataId = -1", ())?;
1116    Ok(())
1117}
1118
1119pub fn find_redirects(conn: &Connection) -> Result<Vec<GameRedirect>> {
1120    let mut redirects = vec![];
1121
1122    let mut stmt = conn.prepare("SELECT sourceId, id, dateAdded FROM game_redirect")?;
1123    let redirects_iter = stmt.query_map((), |row| Ok(GameRedirect{
1124        source_id: row.get(0)?,
1125        dest_id: row.get(1)?
1126    }))?;
1127
1128    for r in redirects_iter {
1129        redirects.push(r?);
1130    }
1131
1132    Ok(redirects)
1133}
1134
1135pub fn create_redirect(conn: &Connection, src_id: &str, dest_id: &str) -> Result<()> {
1136    conn.execute("INSERT OR IGNORE INTO game_redirect (sourceId, id) VALUES (?, ?)", params![src_id, dest_id])?;
1137    Ok(())
1138}
1139
1140pub fn delete_redirect(conn: &Connection, src_id: &str, dest_id: &str) -> Result<()> {
1141    conn.execute("DELETE FROM game_redirect WHERE sourceId = ? AND id = ?", params![src_id, dest_id])?;
1142    Ok(())
1143}
1144
1145impl Default for PartialGame {
1146    fn default() -> Self {
1147        PartialGame {
1148            id: String::from(""),
1149            owner: None,
1150            library: None,
1151            title: None,
1152            alternate_titles: None,
1153            series: None,
1154            developer: None,
1155            publisher: None,
1156            primary_platform: None,
1157            platforms: None,
1158            date_added: None,
1159            date_modified: None,
1160            legacy_broken: None,
1161            legacy_extreme: None,
1162            play_mode: None,
1163            status: None,
1164            notes: None,
1165            tags: None,
1166            detailed_tags: None,
1167            source: None,
1168            legacy_application_path: None,
1169            legacy_launch_command: None,
1170            release_date: None,
1171            version: None,
1172            original_description: None,
1173            language: None,
1174            active_data_id: None,
1175            active_data_on_disk: None,
1176            last_played: None,
1177            playtime: None,
1178            play_counter: None,
1179            active_game_config_id: None,
1180            active_game_config_owner: None,
1181            archive_state: None,
1182            add_apps: None,
1183            logo_path: None,
1184            screenshot_path: None,
1185            ruffle_support: None,
1186            ext_data: None,
1187        }
1188    }
1189}
1190
1191impl Default for Game {
1192    fn default() -> Self {
1193        Game {
1194            id: Uuid::new_v4().to_string(),
1195            owner: String::from("local"),
1196            library: String::from("arcade"),
1197            title: String::default(),
1198            alternate_titles: String::default(),
1199            series: String::default(),
1200            developer: String::default(),
1201            publisher: String::default(),
1202            primary_platform: String::default(),
1203            platforms: TagVec::default(),
1204            date_added: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1205            date_modified: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1206            detailed_platforms: None,
1207            legacy_broken: false,
1208            legacy_extreme: false,
1209            play_mode: String::default(),
1210            status: String::default(),
1211            notes: String::default(),
1212            tags: TagVec::default(),
1213            detailed_tags: None,
1214            source: String::default(),
1215            legacy_application_path: String::default(),
1216            legacy_launch_command: String::default(),
1217            release_date: String::default(),
1218            version: String::default(),
1219            original_description: String::default(),
1220            language: String::default(),
1221            active_data_id: None,
1222            active_data_on_disk: false,
1223            last_played: None,
1224            playtime: 0,
1225            play_counter: 0,
1226            active_game_config_id: None,
1227            active_game_config_owner: None,
1228            archive_state: 0,
1229            game_data: None,
1230            add_apps: None,
1231            logo_path: String::default(),
1232            screenshot_path: String::default(),
1233            ruffle_support: String::default(),
1234            ext_data: None,
1235        }
1236    }
1237}
1238
1239impl Game {
1240    fn apply_partial(&mut self, source: &PartialGame) {
1241        if source.id == "" {
1242            self.id = Uuid::new_v4().to_string();
1243        } else {
1244            self.id = source.id.clone();
1245        }
1246
1247        if let Some(owner) = source.owner.clone() {
1248            self.owner = owner;
1249        }
1250
1251        if let Some(library) = source.library.clone() {
1252            self.library = library;
1253        }
1254
1255        if let Some(title) = source.title.clone() {
1256            self.title = title;
1257        }
1258    
1259        if let Some(alternate_titles) = source.alternate_titles.clone() {
1260            self.alternate_titles = alternate_titles;
1261        }
1262    
1263        if let Some(series) = source.series.clone() {
1264            self.series = series;
1265        }
1266    
1267        if let Some(developer) = source.developer.clone() {
1268            self.developer = developer;
1269        }
1270    
1271        if let Some(publisher) = source.publisher.clone() {
1272            self.publisher = publisher;
1273        }
1274
1275        if let Some(platforms) = source.platforms.clone() {
1276            self.platforms = platforms;
1277        }
1278    
1279        if let Some(platform) = source.primary_platform.clone() {
1280            // Make sure platforms always includes the primary platform
1281            if !self.platforms.contains(&platform) {
1282                self.platforms.push(platform.clone());
1283            }
1284
1285            self.primary_platform = platform;
1286        }
1287    
1288        if let Some(date_added) = source.date_added.clone() {
1289            self.date_added = date_added;
1290        }
1291    
1292        if let Some(date_modified) = source.date_modified.clone() {
1293            self.date_modified = date_modified;
1294        }
1295    
1296        if let Some(legacy_broken) = source.legacy_broken {
1297            self.legacy_broken = legacy_broken;
1298        }
1299    
1300        if let Some(legacy_extreme) = source.legacy_extreme {
1301            self.legacy_extreme = legacy_extreme;
1302        }
1303    
1304        if let Some(play_mode) = source.play_mode.clone() {
1305            self.play_mode = play_mode;
1306        }
1307    
1308        if let Some(status) = source.status.clone() {
1309            self.status = status;
1310        }
1311    
1312        if let Some(notes) = source.notes.clone() {
1313            self.notes = notes;
1314        }
1315    
1316        if let Some(tags) = source.tags.clone() {
1317            self.tags = tags;
1318        }
1319
1320        if let Some(detailed_tags) = source.detailed_tags.clone() {
1321            self.detailed_tags = Some(detailed_tags);
1322        }
1323    
1324        if let Some(source) = source.source.clone() {
1325            self.source = source;
1326        }
1327    
1328        if let Some(legacy_application_path) = source.legacy_application_path.clone() {
1329            self.legacy_application_path = legacy_application_path;
1330        }
1331    
1332        if let Some(legacy_launch_command) = source.legacy_launch_command.clone() {
1333            self.legacy_launch_command = legacy_launch_command;
1334        }
1335    
1336        if let Some(release_date) = source.release_date.clone() {
1337            self.release_date = release_date;
1338        }
1339    
1340        if let Some(version) = source.version.clone() {
1341            self.version = version;
1342        }
1343    
1344        if let Some(original_description) = source.original_description.clone() {
1345            self.original_description = original_description;
1346        }
1347    
1348        if let Some(language) = source.language.clone() {
1349            self.language = language;
1350        }
1351    
1352        if let Some(active_data_id) = source.active_data_id {
1353            self.active_data_id = Some(active_data_id);
1354        }
1355    
1356        if let Some(active_data_on_disk) = source.active_data_on_disk {
1357            self.active_data_on_disk = active_data_on_disk;
1358        }
1359    
1360        if let Some(last_played) = source.last_played.clone() {
1361            self.last_played = Some(last_played);
1362        }
1363    
1364        if let Some(playtime) = source.playtime {
1365            self.playtime = playtime;
1366        }
1367
1368        if let Some(play_counter) = source.play_counter {
1369            self.play_counter = play_counter;
1370        }
1371    
1372        if let Some(active_game_config_id) = source.active_game_config_id {
1373            self.active_game_config_id = Some(active_game_config_id);
1374        }
1375    
1376        if let Some(active_game_config_owner) = source.active_game_config_owner.clone() {
1377            self.active_game_config_owner = Some(active_game_config_owner);
1378        }
1379    
1380        if let Some(archive_state) = source.archive_state {
1381            self.archive_state = archive_state;
1382        }
1383
1384        if let Some(ruffle_support) = source.ruffle_support.clone() {
1385            self.ruffle_support = ruffle_support;
1386        }
1387
1388        if let Some(logo_path) = &source.logo_path {
1389            self.logo_path = logo_path.clone();
1390        }
1391
1392        if let Some(screenshot_path) = &source.screenshot_path {
1393            self.screenshot_path = screenshot_path.clone();
1394        }
1395
1396        if let Some(ext_data) = source.ext_data.clone() {
1397            self.ext_data = Some(ext_data);
1398        }
1399    }
1400}
1401
1402impl From<&PartialGame> for Game {
1403    fn from(source: &PartialGame) -> Self {
1404        let mut game = Game::default();
1405        game.apply_partial(source);
1406        game
1407    }
1408}
1409
1410impl From<Game> for PartialGame {
1411    fn from(game: Game) -> Self {
1412        let mut new_plats = game.platforms.clone();
1413        // Make sure game.platform is present in the vec. If not, add it
1414        if !new_plats.contains(&game.primary_platform) {
1415            new_plats.push(game.primary_platform.clone());
1416        }
1417
1418        PartialGame {
1419            id: game.id,
1420            owner: Some(game.owner),
1421            library: Some(game.library),
1422            title: Some(game.title),
1423            alternate_titles: Some(game.alternate_titles),
1424            series: Some(game.series),
1425            developer: Some(game.developer),
1426            publisher: Some(game.publisher),
1427            primary_platform: Some(game.primary_platform),
1428            platforms: Some(new_plats),
1429            date_added: Some(game.date_added),
1430            date_modified: Some(game.date_modified),
1431            legacy_broken: Some(game.legacy_broken),
1432            legacy_extreme: Some(game.legacy_extreme),
1433            play_mode: Some(game.play_mode),
1434            status: Some(game.status),
1435            notes: Some(game.notes),
1436            tags: Some(game.tags),
1437            detailed_tags: game.detailed_tags,
1438            source: Some(game.source),
1439            legacy_application_path: Some(game.legacy_application_path),
1440            legacy_launch_command: Some(game.legacy_launch_command),
1441            release_date: Some(game.release_date),
1442            version: Some(game.version),
1443            original_description: Some(game.original_description),
1444            language: Some(game.language),
1445            active_data_id: game.active_data_id,
1446            active_data_on_disk: Some(game.active_data_on_disk),
1447            last_played: game.last_played,
1448            playtime: Some(game.playtime),
1449            play_counter: Some(game.play_counter),
1450            active_game_config_id: game.active_game_config_id,
1451            active_game_config_owner: game.active_game_config_owner,
1452            archive_state: Some(game.archive_state),
1453            add_apps: game.add_apps,
1454            ruffle_support: Some(game.ruffle_support),
1455            logo_path: Some(game.logo_path),
1456            screenshot_path: Some(game.screenshot_path),
1457            ext_data: game.ext_data,
1458        }
1459    }
1460}
1461
1462impl GameData {
1463    fn apply_partial(&mut self, value: &PartialGameData) {
1464        if let Some(id) = value.id {
1465            self.id = id;
1466        }
1467
1468        if let Some(title) = value.title.clone() {
1469            self.title = title;
1470        }
1471
1472        if let Some(data_added) = value.date_added.clone() {
1473            self.date_added = data_added;
1474        }
1475
1476        if let Some(sha256) = value.sha256.clone() {
1477            self.sha256 = sha256;
1478        }
1479
1480        if let Some(crc32) = value.crc32 {
1481            self.crc32 = crc32;
1482        }
1483
1484        if let Some(size) = value.size {
1485            self.size = size;
1486        }
1487
1488        if let Some(present_on_disk) = value.present_on_disk {
1489            self.present_on_disk = present_on_disk;
1490        }
1491
1492        if let Some(path) = value.path.clone() {
1493            self.path = Some(path);
1494        }
1495        
1496        if let Some(parameters) = value.parameters.clone() {
1497            self.parameters = Some(parameters);
1498        }
1499    
1500        if let Some(application_path) = value.application_path.clone() {
1501            self.application_path = application_path;
1502        }
1503
1504        if let Some(launch_command) = value.launch_command.clone() {
1505            self.launch_command = launch_command;
1506        }
1507
1508    }
1509}
1510
1511impl Default for GameData {
1512    fn default() -> Self {
1513        GameData {
1514            id: -1,
1515            game_id: "".to_owned(),
1516            title: "".to_owned(),
1517            date_added: Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
1518            sha256: "".to_owned(),
1519            crc32: 0,
1520            size: 0,
1521            present_on_disk: false,
1522            path: None,
1523            parameters: None,
1524            application_path: "".to_owned(),
1525            launch_command: "".to_owned(),
1526        }
1527    }
1528}
1529
1530impl From<&PartialGameData> for GameData {
1531    fn from(value: &PartialGameData) -> Self {
1532        let mut data = GameData {
1533            id: -1,
1534            game_id: value.game_id.clone(),
1535            ..Default::default()
1536        };
1537
1538        data.apply_partial(value);
1539
1540        data
1541    }
1542}