11use crate :: utils:: constants;
22use std:: collections:: { HashMap , HashSet } ;
3- use std:: sync:: { Arc , Mutex } ;
3+ use std:: sync:: mpsc:: { self , Sender } ;
4+ use std:: sync:: { Arc , Mutex , OnceLock } ;
45use std:: time:: { SystemTime , UNIX_EPOCH } ;
56
67const CACHE_EXPIRY_MS : u64 = 5 * 60 * 1000 ;
@@ -34,6 +35,110 @@ pub struct BPTimerClient {
3435unsafe impl Send for BPTimerClient { }
3536unsafe impl Sync for BPTimerClient { }
3637
38+ struct HpReportTask {
39+ api_url : String ,
40+ api_key : String ,
41+ payload : serde_json:: Value ,
42+ cache : Arc < Mutex < HashMap < String , CacheEntry > > > ,
43+ cache_key : String ,
44+ rounded_hp_pct : f32 ,
45+ monster_id : u32 ,
46+ line : i32 ,
47+ rounded_pos_x : Option < f32 > ,
48+ rounded_pos_y : Option < f32 > ,
49+ rounded_pos_z : Option < f32 > ,
50+ }
51+
52+ static HP_REPORT_SENDER : OnceLock < Sender < HpReportTask > > = OnceLock :: new ( ) ;
53+
54+ fn get_hp_report_sender ( ) -> & ' static Sender < HpReportTask > {
55+ HP_REPORT_SENDER . get_or_init ( || {
56+ let ( tx, rx) = mpsc:: channel :: < HpReportTask > ( ) ;
57+
58+ std:: thread:: spawn ( move || {
59+ let client = reqwest:: blocking:: Client :: builder ( )
60+ . user_agent ( & crate :: utils:: constants:: user_agent ( ) )
61+ . tls_backend_rustls ( )
62+ . build ( )
63+ . unwrap_or_else ( |_| reqwest:: blocking:: Client :: new ( ) ) ;
64+
65+ while let Ok ( task) = rx. recv ( ) {
66+ match client
67+ . post ( & task. api_url )
68+ . header ( "X-API-Key" , & task. api_key )
69+ . header ( "Content-Type" , "application/json" )
70+ . json ( & task. payload )
71+ . send ( )
72+ {
73+ Ok ( resp) => {
74+ if resp. status ( ) . is_success ( ) {
75+ let mob_name =
76+ constants:: get_mob_name ( task. monster_id ) . unwrap_or_else ( || {
77+ format ! ( "Unknown Monster ({})" , task. monster_id)
78+ } ) ;
79+ let pos_info = match (
80+ task. rounded_pos_x ,
81+ task. rounded_pos_y ,
82+ task. rounded_pos_z ,
83+ ) {
84+ ( Some ( x) , Some ( y) , Some ( z) ) => {
85+ format ! ( " X: {:.2}, Y: {:.2}, Z: {:.2}" , x, y, z)
86+ }
87+ _ => String :: new ( ) ,
88+ } ;
89+ log:: info!(
90+ "[BPTimer] Reported {}% HP for {} ({}) on Line {}{}" ,
91+ task. rounded_hp_pct,
92+ mob_name,
93+ task. monster_id,
94+ task. line,
95+ pos_info
96+ ) ;
97+ } else {
98+ let status = resp. status ( ) ;
99+ if status. as_u16 ( ) == 409 {
100+ // 409 Conflict is expected when multiple clients are on the same line
101+ log:: info!(
102+ "[BPTimer] HP Report skipped: Already reported by another user."
103+ ) ;
104+ } else {
105+ let message = resp. text ( ) . ok ( ) . and_then ( |body| {
106+ serde_json:: from_str :: < serde_json:: Value > ( & body)
107+ . ok ( )
108+ . and_then ( |json| {
109+ json. get ( "message" )
110+ . or_else ( || json. get ( "error" ) )
111+ . and_then ( |v| v. as_str ( ) )
112+ . map ( |s| s. to_string ( ) )
113+ } )
114+ } ) ;
115+
116+ let error_msg = message
117+ . map ( |m| format ! ( "{} - {}" , status, m) )
118+ . unwrap_or_else ( || status. to_string ( ) ) ;
119+ log:: warn!( "[BPTimer] Failed to report HP: {}" , error_msg) ;
120+ }
121+ }
122+ }
123+ Err ( e) => {
124+ log:: warn!( "[BPTimer] Failed to report HP: {}" , e) ;
125+ }
126+ } ;
127+
128+ // Update cache on both success and error to prevent spam retries
129+ if let Ok ( mut cache_guard) = task. cache . lock ( ) {
130+ if let Some ( entry) = cache_guard. get_mut ( & task. cache_key ) {
131+ entry. is_pending = false ;
132+ entry. last_reported_hp = Some ( task. rounded_hp_pct ) ;
133+ }
134+ }
135+ }
136+ } ) ;
137+
138+ tx
139+ } )
140+ }
141+
37142impl BPTimerClient {
38143 pub fn new ( api_url : String , api_key : String ) -> Self {
39144 Self {
@@ -47,7 +152,7 @@ impl BPTimerClient {
47152 fn create_http_client ( ) -> reqwest:: blocking:: Client {
48153 reqwest:: blocking:: Client :: builder ( )
49154 . user_agent ( & crate :: utils:: constants:: user_agent ( ) )
50- . use_rustls_tls ( )
155+ . tls_backend_rustls ( )
51156 . build ( )
52157 . unwrap_or_else ( |_| reqwest:: blocking:: Client :: new ( ) )
53158 }
@@ -138,93 +243,41 @@ impl BPTimerClient {
138243 let rounded_pos_z = pos_z
139244 . map ( |z| ( z * POSITION_ROUNDING_MULTIPLIER ) . round ( ) / POSITION_ROUNDING_MULTIPLIER ) ;
140245
141- // Clone values for thread
142- let api_url = self . api_url . clone ( ) ;
143- let api_key = self . api_key . clone ( ) ;
144- let cache_key_clone = cache_key. clone ( ) ;
145- let cache = self . cache . clone ( ) ;
146-
147- // Spawn thread for blocking HTTP call
148- std:: thread:: spawn ( move || {
149- let client = Self :: create_http_client ( ) ;
246+ let payload = serde_json:: json!( {
247+ "monster_id" : monster_id as i32 ,
248+ "hp_pct" : rounded_hp_pct as i32 ,
249+ "line" : line,
250+ "pos_x" : rounded_pos_x,
251+ "pos_y" : rounded_pos_y,
252+ "pos_z" : rounded_pos_z,
253+ "account_id" : account_id,
254+ "uid" : uid,
255+ } ) ;
150256
151- let url = format ! ( "{}/api/create-hp-report" , api_url) ;
152-
153- let payload = serde_json:: json!( {
154- "monster_id" : monster_id as i32 ,
155- "hp_pct" : rounded_hp_pct as i32 ,
156- "line" : line,
157- "pos_x" : rounded_pos_x,
158- "pos_y" : rounded_pos_y,
159- "pos_z" : rounded_pos_z,
160- "account_id" : account_id,
161- "uid" : uid,
162- } ) ;
163-
164- match client
165- . post ( & url)
166- . header ( "X-API-Key" , & api_key)
167- . header ( "Content-Type" , "application/json" )
168- . json ( & payload)
169- . send ( )
170- {
171- Ok ( resp) => {
172- if resp. status ( ) . is_success ( ) {
173- let mob_name = constants:: get_mob_name ( monster_id)
174- . unwrap_or_else ( || format ! ( "Unknown Monster ({monster_id})" ) ) ;
175- let pos_info = match ( rounded_pos_x, rounded_pos_y, rounded_pos_z) {
176- ( Some ( x) , Some ( y) , Some ( z) ) => {
177- format ! ( " X: {:.2}, Y: {:.2}, Z: {:.2}" , x, y, z)
178- }
179- _ => String :: new ( ) ,
180- } ;
181- log:: info!(
182- "[BPTimer] Reported {}% HP for {} ({}) on Line {}{}" ,
183- rounded_hp_pct,
184- mob_name,
185- monster_id,
186- line,
187- pos_info
188- ) ;
189- } else {
190- let status = resp. status ( ) ;
191- if status. as_u16 ( ) == 409 {
192- // 409 Conflict is expected when multiple clients are on the same line
193- log:: info!(
194- "[BPTimer] HP Report skipped: Already reported by another user."
195- ) ;
196- } else {
197- let message = resp. text ( ) . ok ( ) . and_then ( |body| {
198- serde_json:: from_str :: < serde_json:: Value > ( & body)
199- . ok ( )
200- . and_then ( |json| {
201- json. get ( "message" )
202- . or_else ( || json. get ( "error" ) )
203- . and_then ( |v| v. as_str ( ) )
204- . map ( |s| s. to_string ( ) )
205- } )
206- } ) ;
207-
208- let error_msg = message
209- . map ( |m| format ! ( "{} - {}" , status, m) )
210- . unwrap_or_else ( || status. to_string ( ) ) ;
211- log:: warn!( "[BPTimer] Failed to report HP: {}" , error_msg) ;
212- }
213- }
214- }
215- Err ( e) => {
216- log:: warn!( "[BPTimer] Failed to report HP: {}" , e) ;
217- }
218- } ;
257+ let task = HpReportTask {
258+ api_url : format ! ( "{}/api/create-hp-report" , self . api_url) ,
259+ api_key : self . api_key . clone ( ) ,
260+ payload,
261+ cache : self . cache . clone ( ) ,
262+ cache_key : cache_key. clone ( ) ,
263+ rounded_hp_pct,
264+ monster_id,
265+ line,
266+ rounded_pos_x,
267+ rounded_pos_y,
268+ rounded_pos_z,
269+ } ;
219270
220- // Update cache on both success and error to prevent spam retries
221- if let Ok ( mut cache_guard) = cache. lock ( ) {
222- if let Some ( entry) = cache_guard. get_mut ( & cache_key_clone) {
223- entry. is_pending = false ;
271+ if let Err ( e) = get_hp_report_sender ( ) . send ( task) {
272+ log:: error!( "[BPTimer] Failed to queue HP report: {}" , e) ;
273+ // Worker thread died - reset cache to prevent blocking future reports
274+ if let Ok ( mut cache_guard) = self . cache . lock ( ) {
275+ if let Some ( entry) = cache_guard. get_mut ( & cache_key) {
224276 entry. last_reported_hp = Some ( rounded_hp_pct) ;
277+ entry. is_pending = false ;
225278 }
226279 }
227- } ) ;
280+ }
228281 }
229282
230283 /// Test API connection
0 commit comments