game_manager_lib\services/
cf_aggregator.rs1use serde::Deserialize;
9use std::collections::HashMap;
10
11use once_cell::sync::OnceCell;
12
13use crate::services::recommendation::{calculate_game_weight, GameWithDetails};
14
15const COLLABORATIVE_INDEX_JSON: &str = include_str!("../../data/collaborative_index.json");
17
18#[derive(Debug, Deserialize)]
22struct CollaborativeIndexRaw {
23 index: HashMap<String, Vec<Similar>>,
24}
25
26#[derive(Debug, Deserialize, Clone)]
27pub struct Similar {
28 pub app_id: u32,
29 pub score: f32,
30}
31
32pub type CFIndex = HashMap<u32, Vec<Similar>>;
35
36static CF_INDEX: OnceCell<CFIndex> = OnceCell::new();
38
39pub fn init_cf_index() -> anyhow::Result<()> {
45 let raw: CollaborativeIndexRaw = serde_json::from_str(COLLABORATIVE_INDEX_JSON)?;
46
47 let index: CFIndex = raw
48 .index
49 .into_iter()
50 .filter_map(|(k, v)| k.parse::<u32>().ok().map(|id| (id, v)))
51 .collect();
52
53 let total_games = index.len();
54 let total_pairs: usize = index.values().map(|v| v.len()).sum();
55
56 CF_INDEX
57 .set(index)
58 .map_err(|_| anyhow::anyhow!("CF_INDEX já foi inicializado"))?;
59
60 log::info!(
61 "[CF] Índice carregado com sucesso | jogos={} pares={} média={:.2}",
62 total_games,
63 total_pairs,
64 total_pairs as f32 / total_games.max(1) as f32
65 );
66
67 Ok(())
68}
69
70#[inline]
72fn get_cf_index() -> Option<&'static CFIndex> {
73 CF_INDEX.get()
74}
75
76pub fn build_cf_candidates(user_games: &[GameWithDetails]) -> (HashMap<u32, f32>, CFStats) {
84 let mut scores: HashMap<u32, f32> = HashMap::new();
85
86 let Some(cf_index) = get_cf_index() else {
87 return (scores, CFStats::empty(user_games.len()));
88 };
89
90 let mut games_with_steam_id = 0;
91 let mut games_with_cf_match = 0;
92
93 for game in user_games {
94 let Some(steam_id) = game.steam_app_id else {
95 continue;
96 };
97 games_with_steam_id += 1;
98
99 let Some(similars) = cf_index.get(&steam_id) else {
100 continue;
101 };
102 games_with_cf_match += 1;
103
104 let source_weight = calculate_game_weight(&game.game);
105
106 for similar in similars {
107 if similar.score <= 0.0 {
108 continue;
109 }
110
111 *scores.entry(similar.app_id).or_insert(0.0) += similar.score * source_weight;
112 }
113 }
114
115 let stats = CFStats {
116 total_games: user_games.len(),
117 games_with_steam_id,
118 games_with_cf_match,
119 };
120
121 stats.log();
122
123 (scores, stats)
124}
125
126#[derive(Debug, Clone)]
129pub struct CFStats {
130 pub total_games: usize,
131 pub games_with_steam_id: usize,
132 pub games_with_cf_match: usize,
133}
134
135impl CFStats {
137 fn empty(total: usize) -> Self {
138 Self {
139 total_games: total,
140 games_with_steam_id: 0,
141 games_with_cf_match: 0,
142 }
143 }
144
145 pub fn log(&self) {
146 if self.total_games == 0 {
147 log::info!("[CF] Biblioteca vazia");
148 return;
149 }
150
151 let pct_steam = self.games_with_steam_id as f32 / self.total_games as f32 * 100.0;
152 let pct_cf = self.games_with_cf_match as f32 / self.total_games as f32 * 100.0;
153 let pct_fallback = 100.0 - pct_cf;
154
155 log::info!(
156 "[CF] Jogos na biblioteca={} | SteamID={:.1}% | CF ativo={:.1}% | Fallback CB={:.1}%",
157 self.total_games,
158 pct_steam,
159 pct_cf,
160 pct_fallback
161 );
162 }
163}