@@ -1698,6 +1698,56 @@ fn is_likely_steamodded(path: &Path) -> Result<bool, String> {
16981698 Ok ( false )
16991699}
17001700
1701+ /// Deserialize a number field that may be an integer or float, coercing to i64.
1702+ /// Many mod authors write floats (e.g., -1000000.0 or 2.7e+27) where i64 is expected.
1703+ fn deserialize_lenient_i64 < ' de , D > ( deserializer : D ) -> Result < i64 , D :: Error >
1704+ where
1705+ D : serde:: Deserializer < ' de > ,
1706+ {
1707+ use serde:: de:: { self , Visitor } ;
1708+
1709+ struct LenientI64Visitor ;
1710+
1711+ impl < ' de > Visitor < ' de > for LenientI64Visitor {
1712+ type Value = i64 ;
1713+
1714+ fn expecting ( & self , formatter : & mut std:: fmt:: Formatter ) -> std:: fmt:: Result {
1715+ formatter. write_str ( "an integer or float" )
1716+ }
1717+
1718+ fn visit_i64 < E > ( self , value : i64 ) -> Result < i64 , E >
1719+ where
1720+ E : de:: Error ,
1721+ {
1722+ Ok ( value)
1723+ }
1724+
1725+ fn visit_u64 < E > ( self , value : u64 ) -> Result < i64 , E >
1726+ where
1727+ E : de:: Error ,
1728+ {
1729+ // Clamp to i64::MAX if too large
1730+ Ok ( value. min ( i64:: MAX as u64 ) as i64 )
1731+ }
1732+
1733+ fn visit_f64 < E > ( self , value : f64 ) -> Result < i64 , E >
1734+ where
1735+ E : de:: Error ,
1736+ {
1737+ // Clamp to i64 range and convert
1738+ if value >= i64:: MAX as f64 {
1739+ Ok ( i64:: MAX )
1740+ } else if value <= i64:: MIN as f64 {
1741+ Ok ( i64:: MIN )
1742+ } else {
1743+ Ok ( value as i64 )
1744+ }
1745+ }
1746+ }
1747+
1748+ deserializer. deserialize_any ( LenientI64Visitor )
1749+ }
1750+
17011751/// JSON schema for mod configuration
17021752#[ derive( Debug , Serialize , Deserialize ) ]
17031753struct ModJson {
@@ -1708,7 +1758,7 @@ struct ModJson {
17081758 description : String ,
17091759 prefix : String ,
17101760 main_file : String ,
1711- #[ serde( default ) ]
1761+ #[ serde( default , deserialize_with = "deserialize_lenient_i64" ) ]
17121762 priority : i64 ,
17131763 #[ serde( default = "default_badge_color" ) ]
17141764 badge_colour : String ,
@@ -1749,17 +1799,31 @@ fn default_text_color() -> String {
17491799 "FFFFFF" . to_string ( )
17501800}
17511801
1802+ /// Strip trailing commas from JSON content to handle lenient JSON authored by mod creators.
1803+ /// Matches `,` followed by optional whitespace and then `]` or `}`.
1804+ fn strip_trailing_commas ( json : & str ) -> String {
1805+ // Use lazy_static regex for efficiency
1806+ use regex:: Regex ;
1807+ lazy_static:: lazy_static! {
1808+ static ref TRAILING_COMMA : Regex = Regex :: new( r",(\s*[}\]])" ) . unwrap( ) ;
1809+ }
1810+ TRAILING_COMMA . replace_all ( json, "$1" ) . into_owned ( )
1811+ }
1812+
17521813/// Parse mod info from JSON file
17531814fn parse_mod_json ( json_path : & Path , mod_path : & Path ) -> Result < Option < DetectedMod > , String > {
1754- let file = match File :: open ( json_path) {
1755- Ok ( file ) => file ,
1815+ let content = match std :: fs :: read_to_string ( json_path) {
1816+ Ok ( s ) => s ,
17561817 Err ( e) => {
1757- log:: debug!( "Failed to open JSON file {}: {}" , json_path. display( ) , e) ;
1818+ log:: debug!( "Failed to read JSON file {}: {}" , json_path. display( ) , e) ;
17581819 return Ok ( None ) ;
17591820 }
17601821 } ;
17611822
1762- let mod_json: ModJson = match serde_json:: from_reader ( file) {
1823+ // Strip trailing commas (common in hand-authored JSON)
1824+ let sanitized = strip_trailing_commas ( & content) ;
1825+
1826+ let mod_json: ModJson = match serde_json:: from_str ( & sanitized) {
17631827 Ok ( json) => json,
17641828 Err ( e) => {
17651829 log:: debug!( "Failed to parse JSON file {}: {}" , json_path. display( ) , e) ;
0 commit comments