From 4a44eb54669f2c64ffcd755842bffad917617862 Mon Sep 17 00:00:00 2001 From: Filipe Veloso Date: Fri, 23 May 2025 02:42:12 -0300 Subject: [PATCH 1/2] add generic data structures --- Cargo.toml | 16 + README.md | 91 +- benches/benchmark.rs | 43 +- docs/generic_json_samples.md | 255 +++++ docs/generic_usage.md | 130 +++ docs/implementation_plan.md | 62 ++ examples/fps_game_analysis.rs | 336 +++++++ examples/generic_usage.rs | 98 ++ .../battle_royale_training_data.json | 194 ++++ examples/json_samples/fps_training_data.json | 110 +++ examples/json_samples/moba_training_data.json | 130 +++ .../json_samples/racing_training_data.json | 162 ++++ examples/json_samples/rts_training_data.json | 32 + .../json_samples/sports_training_data.json | 50 + examples/multi_game_analysis.rs | 877 ++++++++++++++++++ examples/train_model_example.rs | 53 ++ .../ThirdParty/NoCheatLib/include/nocheat.h | 186 ++-- models/custom_cheat_model.bin | Bin 11946 -> 11786 bytes models/default_cheat_model.bin | Bin 0 -> 126206 bytes rust-analyzer.json | 3 +- src/bin/train.rs | 14 +- src/lib.rs | 187 ++-- src/types.rs | 357 +++++-- tests/analisys_test.rs | 51 +- .../ThirdParty/NoCheatLib/include/nocheat.h | 95 ++ 25 files changed, 3244 insertions(+), 288 deletions(-) create mode 100644 docs/generic_json_samples.md create mode 100644 docs/generic_usage.md create mode 100644 docs/implementation_plan.md create mode 100644 examples/fps_game_analysis.rs create mode 100644 examples/generic_usage.rs create mode 100644 examples/json_samples/battle_royale_training_data.json create mode 100644 examples/json_samples/fps_training_data.json create mode 100644 examples/json_samples/moba_training_data.json create mode 100644 examples/json_samples/racing_training_data.json create mode 100644 examples/json_samples/rts_training_data.json create mode 100644 examples/json_samples/sports_training_data.json create mode 100644 examples/multi_game_analysis.rs create mode 100644 examples/train_model_example.rs create mode 100644 models/default_cheat_model.bin create mode 100644 ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h diff --git a/Cargo.toml b/Cargo.toml index 8044525..7462c30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,19 @@ once_cell = "1.17" [dev-dependencies] criterion = "0.4" + +[[example]] +name = "generic_usage" +path = "examples/generic_usage.rs" + +[[example]] +name = "fps_game_analysis" +path = "examples/fps_game_analysis.rs" + +[[example]] +name = "multi_game_analysis" +path = "examples/multi_game_analysis.rs" + +[[example]] +name = "train_model_example" +path = "examples/train_model_example.rs" diff --git a/README.md b/README.md index 9e47cb8..0505ad5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ NoCheat is a fast, machine learning-based anti-cheat library designed to detect - C-compatible FFI for integration with game engines - UE5 plugin integration ready - DataFrame-based feature engineering +- **Generic struct support for custom data analysis** ## How It Works @@ -43,7 +44,7 @@ For better results, you can train a model with your own labeled data: ```rust use nocheat::{train_model}; -use nocheat::types::PlayerStats; +use nocheat::types::{DefaultPlayerData, PlayerStats}; use std::collections::HashMap; // Prepare your training data @@ -56,14 +57,18 @@ shots.insert("rifle".to_string(), 100); let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 50); -training_data.push(PlayerStats { - player_id: "normal_player".to_string(), +let player_data = DefaultPlayerData { shots_fired: shots.clone(), hits: hits.clone(), headshots: 10, shot_timestamps_ms: None, training_label: None, -}); +}; + +training_data.push(PlayerStats::new( + "normal_player".to_string(), + player_data +)); labels.push(0.0); // Not a cheater // Example: Add a cheating player @@ -72,20 +77,90 @@ shots.insert("rifle".to_string(), 100); let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 95); // Suspiciously high accuracy -training_data.push(PlayerStats { - player_id: "cheater".to_string(), +let cheater_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots: 70, // Very high headshot ratio shot_timestamps_ms: None, training_label: None, -}); +}; + +training_data.push(PlayerStats::new( + "cheater".to_string(), + cheater_data +)); labels.push(1.0); // Labeled as a cheater // Train and save model train_model(training_data, labels, "cheat_model.bin").expect("Failed to train model"); ``` +## Using Generic Types for Custom Data Analysis + +NoCheat now supports generic data structures, giving you the flexibility to work with any JSON structure for player statistics. This is particularly useful when: + +1. Your game has unique metrics to track +2. You want to analyze different aspects of player behavior +3. You need to integrate with existing analytics systems + +### Example: Creating a Custom Data Structure + +```rust +use nocheat::types::{PlayerStats, PlayerResult, AnalysisResponse}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Define a custom data structure for your game +#[derive(Clone, Debug, Deserialize, Serialize)] +struct CustomPlayerData { + accuracy: f32, + reaction_time_ms: Vec, + movement_patterns: HashMap, + mouse_acceleration: Option, +} + +// Create stats with your custom data structure +let mut movement = HashMap::new(); +movement.insert("jumps".to_string(), 50); +movement.insert("crouches".to_string(), 30); + +let custom_data = CustomPlayerData { + accuracy: 0.75, + reaction_time_ms: vec![250, 220, 230, 210, 240], + movement_patterns: movement, + mouse_acceleration: Some(1.5), +}; + +// Create a PlayerStats instance with custom data +let custom_stats = PlayerStats::new("custom_player".to_string(), custom_data); +``` + +### Custom Result Types + +You can also define custom result types for your analysis: + +```rust +#[derive(Debug, PartialEq, Serialize)] +struct CustomAnalysisResult { + cheating_probability: f32, + abnormal_patterns: Vec, + confidence_score: f32, + recommended_action: String, +} + +// Create a custom result +let custom_result = CustomAnalysisResult { + cheating_probability: 0.85, + abnormal_patterns: vec!["AimSnap".to_string(), "RecoilControl".to_string()], + confidence_score: 0.92, + recommended_action: "Review gameplay footage".to_string(), +}; + +let player_result = PlayerResult::new("custom_player".to_string(), custom_result); +``` + +For more detailed examples, see the [Generic Usage Guide](docs/generic_usage.md). + ## Integration with Unreal Engine 5 ### Prerequisites @@ -149,7 +224,7 @@ train_model(training_data, labels, "cheat_model.bin").expect("Failed to train mo 2. Copy the compiled library to the appropriate platform folder in `ThirdParty/NoCheatLibrary/lib/` -3. Create a C interface header in `ThirdParty/NoCheatLibrary/include/nocheat.h`: +3. Create a C interface header in `ThirdParty/NoCheatLibrary/include/nocheat.h` or generate one with `cbindgen --config cbindgen.toml --crate nocheat --output ue_plugin\ThirdParty\NoCheatLib\include\nocheat.h`: ```c #pragma once diff --git a/benches/benchmark.rs b/benches/benchmark.rs index dbf7f6e..fd0b047 100644 --- a/benches/benchmark.rs +++ b/benches/benchmark.rs @@ -1,10 +1,10 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use nocheat::types::PlayerStats; +use nocheat::types::{DefaultPlayerData, LegacyPlayerStats, PlayerStats}; use nocheat::{build_dataframe, df_to_ndarray, generate_default_model, train_model}; use polars::prelude::{col, DataType, IntoLazy}; use std::collections::HashMap; -fn make_dummy_stats(n: usize) -> Vec { +fn make_dummy_stats(n: usize) -> Vec { let mut result = Vec::with_capacity(n); for i in 0..n { @@ -25,20 +25,21 @@ fn make_dummy_stats(n: usize) -> Vec { let total_hits = (150.0 * accuracy) as u32; let headshots = (total_hits as f32 * headshot_ratio) as u32; - result.push(PlayerStats { - player_id: format!("player_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, - hits: hits, - headshots: headshots, + hits, + headshots, shot_timestamps_ms: None, training_label: None, - }); + }; + + result.push(PlayerStats::new(format!("player_{}", i), player_data)); } result } -fn create_training_data(n: usize) -> (Vec, Vec) { +fn create_training_data(n: usize) -> (Vec, Vec) { let mut players = Vec::with_capacity(n); let mut labels = Vec::with_capacity(n); @@ -56,19 +57,18 @@ fn create_training_data(n: usize) -> (Vec, Vec) { let headshot_ratio = 0.1 + (i % 15) as f32 * 0.01; // 10-25% headshots let headshots = ((100.0 * accuracy) as f32 * headshot_ratio) as u32; - players.push(PlayerStats { - player_id: format!("normal_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, - hits: hits, - headshots: headshots, + hits, + headshots, shot_timestamps_ms: None, training_label: None, - }); + }; - labels.push(0.0); - } + players.push(PlayerStats::new(format!("normal_{}", i), player_data)); - // Create half cheaters + labels.push(0.0); + } // Create half cheaters for i in 0..(n / 2) { let mut shots = HashMap::new(); let mut hits = HashMap::new(); @@ -82,14 +82,15 @@ fn create_training_data(n: usize) -> (Vec, Vec) { let headshot_ratio = 0.4 + (i % 40) as f32 * 0.01; // 40-80% headshots let headshots = ((100.0 * accuracy) as f32 * headshot_ratio) as u32; - players.push(PlayerStats { - player_id: format!("cheater_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, - hits: hits, - headshots: headshots, + hits, + headshots, shot_timestamps_ms: None, training_label: None, - }); + }; + + players.push(PlayerStats::new(format!("cheater_{}", i), player_data)); labels.push(1.0); } diff --git a/docs/generic_json_samples.md b/docs/generic_json_samples.md new file mode 100644 index 0000000..ebb95eb --- /dev/null +++ b/docs/generic_json_samples.md @@ -0,0 +1,255 @@ +# Using Generic JSON Training Data with NoCheat + +This guide demonstrates how to use the generic JSON training data samples provided with NoCheat for different game types. The library's generic structure allows you to analyze any type of game data by adapting the player statistics format to your specific needs. + +## Available Sample Data + +We've included several JSON data samples for different game types: + +1. **FPS Game Data** (`fps_training_data.json`) + - Focuses on weapon accuracy, headshot ratios, and kill/death ratios + - Includes detailed weapon stats per weapon type + - Tracks movement patterns and camping behavior + +2. **MOBA Game Data** (`moba_training_data.json`) + - Focuses on champion performance, damage dealt, and skill usage patterns + - Includes detailed champion stats, gold earned, and item build orders + - Tracks skill usage timings and crowd control scores + +3. **Battle Royale Game Data** (`battle_royale_training_data.json`) + - Focuses on survival time, kills, and damage dealt + - Includes looting patterns, movement across the map + - Tracks weapon accuracy and placement statistics + +4. **Racing Game Data** (`racing_training_data.json`) + - Focuses on lap times, consistency, and racing patterns + - Includes detailed telemetry data such as speed, braking points + - Tracks sector times and racing line efficiency + +## Loading and Using the Generic JSON Data + +### 1. Define Your Custom Data Structure + +First, you need to define a Rust structure that matches your JSON format: + +```rust +// For FPS games +#[derive(Clone, Debug, Deserialize, Serialize)] +struct FpsPlayerData { + kills: u32, + deaths: u32, + assists: u32, + weapon_stats: HashMap, + average_position_change: f32, + camping_seconds: u32, + round_duration_seconds: u32, + team: String, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct WeaponStats { + shots_fired: u32, + hits: u32, + headshots: u32, + distance_meters: Vec, +} +``` + +### 2. Implement the Analyzable Trait + +```rust +impl Analyzable for FpsPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + let mut total_shots = 0; + let mut total_hits = 0; + + for weapon in self.weapon_stats.values() { + total_shots += weapon.shots_fired; + total_hits += weapon.hits; + } + + if total_shots == 0 { + return 0.0; + } + + total_hits as f32 / total_shots as f32 + } + + fn calculate_headshot_ratio(&self) -> f32 { + let mut total_hits = 0; + let mut total_headshots = 0; + + for weapon in self.weapon_stats.values() { + total_hits += weapon.hits; + total_headshots += weapon.headshots; + } + + if total_hits == 0 { + return 0.0; + } + + total_headshots as f32 / total_hits as f32 + } + + fn extract_features(&self) -> Vec { + // Return relevant features for model training + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + self.kills as f32 / self.deaths.max(1) as f32, + ] + } + + fn is_suspicious(&self) -> bool { + // Define suspicious behavior + self.calculate_accuracy_rate() > 0.8 || + self.calculate_headshot_ratio() > 0.7 || + (self.kills > 30 && self.deaths < 5) + } +} +``` + +### 3. Load JSON Data and Create PlayerStats Instances + +```rust +use std::fs::File; +use std::io::Read; +use serde_json::from_str; +use nocheat::types::PlayerStats; + +fn load_fps_training_data(path: &str) -> Vec> { + let mut file = File::open(path).expect("Failed to open file"); + let mut contents = String::new(); + file.read_to_string(&mut contents).expect("Failed to read file"); + + from_str(&contents).expect("Failed to parse JSON") +} + +// Load the training data +let fps_players = load_fps_training_data("examples/json_samples/fps_training_data.json"); +``` + +### 4. Create a Game-Specific Analyzer + +```rust +fn analyze_fps_data(players: Vec>) -> AnalysisResponse { + let mut results = Vec::new(); + + for player in players { + // Extract features specific to FPS games + let accuracy = player.data.calculate_accuracy_rate(); + let headshot_ratio = player.data.calculate_headshot_ratio(); + let kd_ratio = player.data.kills as f32 / player.data.deaths.max(1) as f32; + + // Build a list of suspected cheats + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + + if accuracy > 0.8 { + suspected_cheats.push("Aimbot".to_string()); + anomaly_details.insert("High Accuracy".to_string(), + format!("{:.1}% hit rate", accuracy * 100.0)); + } + + if headshot_ratio > 0.7 { + suspected_cheats.push("Headshot Hack".to_string()); + anomaly_details.insert("Headshot Anomaly".to_string(), + format!("{:.1}% headshot ratio", headshot_ratio * 100.0)); + } + + if kd_ratio > 5.0 { + suspected_cheats.push("Skill Anomaly".to_string()); + anomaly_details.insert("K/D Ratio".to_string(), + format!("K/D ratio of {:.1}", kd_ratio)); + } + + // Calculate overall cheating probability + let cheating_probability = (accuracy * 0.4 + headshot_ratio * 0.4 + (kd_ratio / 10.0) * 0.2) + .min(1.0); + + // Create the result + let result = FpsAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength: if cheating_probability > 0.8 { "High" } + else if cheating_probability > 0.5 { "Medium" } + else { "Low" }.to_string(), + anomaly_details, + }; + + results.push(PlayerResult::new(player.player_id, result)); + } + + AnalysisResponse { results } +} +``` + +### 5. Train a Custom Model Using Game-Specific Features + +```rust +use nocheat::train_model; + +fn train_fps_cheat_model(fps_players: Vec>, output_path: &str) -> Result<()> { + // Extract features for the model + let mut features = Vec::new(); + let mut labels = Vec::new(); + + for player in &fps_players { + // Create feature vector + let player_features = vec![ + player.data.calculate_accuracy_rate() as f64, + player.data.calculate_headshot_ratio() as f64, + (player.data.kills as f64) / (player.data.deaths.max(1) as f64), + player.data.average_position_change as f64, + (player.data.camping_seconds as f64) / (player.data.round_duration_seconds as f64) + ]; + + features.push(player_features); + + // Get the training label + if let Some(label) = player.data.training_label { + labels.push(label); + } else { + // Skip players without labels + features.pop(); + } + } + + // Train the model using the nocheat training API + // This is a simplified example - you would need to adapt this to work with your feature vectors + let _result = train_custom_model(features, labels, output_path); + + Ok(()) +} +``` + +## Adapting to Other Game Types + +The same pattern can be applied to the other game types: + +1. Define a Rust structure matching your JSON structure +2. Implement the `Analyzable` trait for your game-specific data +3. Load the JSON data and create `PlayerStats` instances +4. Create a game-specific analyzer that extracts relevant features +5. Train a custom model tailored to your game + +For each game type, focus on the metrics that are most relevant for cheat detection: + +- **MOBA Games**: Look for abnormal damage output, unrealistic skill timing patterns, and suspiciously high gold/xp rates +- **Battle Royale Games**: Focus on hit accuracy, headshot ratios, and unusual movement/looting patterns +- **Racing Games**: Check for impossible lap times, unrealistic cornering speeds, and perfect racing lines + +## Using Different Models for Different Games + +You can maintain separate trained models for each game type: + +```rust +// Load game-specific models +let fps_model = load_model("models/fps_cheat_model.bin").expect("Failed to load FPS model"); +let moba_model = load_model("models/moba_cheat_model.bin").expect("Failed to load MOBA model"); +let br_model = load_model("models/battle_royale_cheat_model.bin").expect("Failed to load BR model"); +let racing_model = load_model("models/racing_cheat_model.bin").expect("Failed to load Racing model"); +``` + +This approach allows you to have specialized cheat detection tailored to each game's unique mechanics and cheating patterns. diff --git a/docs/generic_usage.md b/docs/generic_usage.md new file mode 100644 index 0000000..ee55377 --- /dev/null +++ b/docs/generic_usage.md @@ -0,0 +1,130 @@ +# Guide: Using Generic PlayerStats in NoCheat + +The NoCheat library now supports generic data structures for player stats, allowing you to customize the data that is analyzed for cheat detection. + +## Key Components + +1. **PlayerStats**: A generic structure that can work with any type of data +2. **PlayerResult**: A generic structure for analysis results +3. **AnalysisResponse**: A generic response wrapper for multiple player results +4. **DefaultPlayerData**: The original data structure (for backward compatibility) +5. **DefaultAnalysisResult**: The original result structure (for backward compatibility) + +## Type Aliases for Backward Compatibility + +- **LegacyPlayerStats** = PlayerStats +- **LegacyPlayerResult** = PlayerResult +- **LegacyAnalysisResponse** = AnalysisResponse + +## Examples + +Run examples: +- `cargo run --example generic_usage` +- `cargo run --example fps_game_analysis` +- `cargo run --example multi_game_analysis` +- `cargo run --example train_model_example` + + +### Using the Default Structure (Backward Compatible) + +```rust +use nocheat::types::{PlayerStats, DefaultPlayerData}; +use std::collections::HashMap; + +// Create default data +let mut shots = HashMap::new(); +shots.insert("rifle".to_string(), 100); + +let mut hits = HashMap::new(); +hits.insert("rifle".to_string(), 50); + +let default_data = DefaultPlayerData { + shots_fired: shots, + hits: hits, + headshots: 10, + shot_timestamps_ms: None, + training_label: None, +}; + +// Create a PlayerStats instance +let player_stats = PlayerStats::new("player123".to_string(), default_data); +``` + +### Creating Custom Data Structures + +```rust +use nocheat::types::PlayerStats; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct CustomPlayerData { + accuracy: f32, + reaction_time_ms: Vec, + movement_patterns: HashMap, + mouse_acceleration: Option, +} + +// Create custom data +let mut movement = HashMap::new(); +movement.insert("jumps".to_string(), 50); +movement.insert("crouches".to_string(), 30); + +let custom_data = CustomPlayerData { + accuracy: 0.75, + reaction_time_ms: vec![250, 220, 230, 210, 240], + movement_patterns: movement, + mouse_acceleration: Some(1.5), +}; + +// Create a PlayerStats instance with custom data +let custom_stats = PlayerStats::new("custom_player".to_string(), custom_data); +``` + +### Custom Analysis Results + +```rust +use nocheat::types::{PlayerResult, AnalysisResponse}; +use serde::Serialize; + +#[derive(Debug, PartialEq, Serialize)] +struct CustomAnalysisResult { + cheating_probability: f32, + abnormal_patterns: Vec, + confidence_score: f32, + recommended_action: String, +} + +let custom_result = CustomAnalysisResult { + cheating_probability: 0.85, + abnormal_patterns: vec!["AimSnap".to_string(), "RecoilControl".to_string()], + confidence_score: 0.92, + recommended_action: "Review gameplay footage".to_string(), +}; + +let player_result = PlayerResult::new("custom_player".to_string(), custom_result); + +// Group multiple results +let response = AnalysisResponse { + results: vec![player_result], +}; +``` + +## Best Practices + +1. Ensure your custom data and result types implement the necessary traits: + - Data types: `Clone + Serialize + DeserializeOwned` + - Result types: `Serialize + PartialEq` + +2. Consider implementing serialization/deserialization for your custom types if you need to parse JSON or other formats. + +3. When defining custom analysis functions, specify the generic types explicitly: + ```rust + fn analyze_custom_data(stats: Vec>) -> Result> { + // Your analysis logic here + } + ``` + +## Complete Example + +See the file `examples/generic_usage.rs` for a complete working example of custom data structures and analysis results. diff --git a/docs/implementation_plan.md b/docs/implementation_plan.md new file mode 100644 index 0000000..e88217c --- /dev/null +++ b/docs/implementation_plan.md @@ -0,0 +1,62 @@ +# Making PlayerStats Generic: Summary and Next Steps + +## What we've accomplished + +1. **Generic Structure Design**: We've successfully refactored the `PlayerStats`, `PlayerResult`, and `AnalysisResponse` structures to be generic. + +2. **Backward Compatibility**: We've added type aliases for backward compatibility: + - `LegacyPlayerStats` + - `LegacyPlayerResult` + - `LegacyAnalysisResponse` + +3. **Documentation**: We've created a guide on how to use the generic structures, including examples. + +4. **Example Code**: We've provided full working examples demonstrating how to use custom data structures: + - Basic generic usage example + - FPS game-specific analysis example + - Multi-game analysis example covering FPS, MOBA, Battle Royale, and Racing games + +5. **Enhanced Flexibility**: Added new capabilities: + - `Analyzable` trait for common analysis operations across data types + - Conversion methods to transform between different data structures + - Game-specific JSON training data samples + - Documentation on how to use JSON data with generic types + +## Completed Tasks + +1. **API Updates**: The core functions like `analyze_stats`, `build_dataframe`, and others have been updated to work with the generic types: + - Updated function signatures to include generic type parameters where appropriate + - Modified internal logic to work with the new nested data structure + - Maintained backward compatibility through type aliases + +2. **Test Updates**: All tests have been updated to use the new generic structure and are passing. + +3. **Practical Examples**: Added diverse examples showing how to use generic types: + - FPS game analysis example + - Multi-game analysis demonstrating different data structures for different game genres + +## Remaining Tasks + +1. **FFI Interface**: The C FFI functions may need further updates to handle the generic types properly, particularly when integrating with game engines. + +2. **Further Documentation**: More detailed documentation on how to use the generic API with existing game telemetry systems. + +3. **Performance Optimization**: Potential optimization of generic code to ensure no performance regression from the previous implementation. + +## Implemented Approach + +We used a combination of approaches: + +1. **Core API Using Legacy Types**: The main API functions continue to use legacy types but we've added: + - Type conversions between legacy and custom data types + - The `Analyzable` trait for standardizing analysis operations + - Game-specific analyzers for different data types + +2. **Generic Internal Functions**: Internal functions have been updated to work with generics where appropriate. + +3. **Trait-Based Approach**: The implementation of the `Analyzable` trait provides a standardized way to perform common analysis operations across different data types. + +This approach successfully provides: +1. **Backward compatibility**: Existing code continues to work +2. **Flexibility**: New code can use custom data structures +3. **Incremental adoption**: Users can migrate to the generic API at their own pace diff --git a/examples/fps_game_analysis.rs b/examples/fps_game_analysis.rs new file mode 100644 index 0000000..5a84c19 --- /dev/null +++ b/examples/fps_game_analysis.rs @@ -0,0 +1,336 @@ +// Example of using generic types with custom analysis logic +use nocheat::types::{AnalysisResponse, Analyzable, DefaultPlayerData, PlayerResult, PlayerStats}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Define a custom data structure specifically for a first-person shooter game +#[derive(Clone, Debug, Deserialize, Serialize)] +struct FpsPlayerData { + // Basic statistics + kills: u32, + deaths: u32, + assists: u32, + + // Weapon-specific stats + weapon_stats: HashMap, + + // Movement and positioning data + average_position_change: f32, // How much the player moves + camping_seconds: u32, // Time spent in one location + + // Round information + round_duration_seconds: u32, + team: String, // "CT" or "T" for counter-strike like games +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct WeaponStats { + shots_fired: u32, + hits: u32, + headshots: u32, + distance_meters: Vec, // Distance for each kill +} + +// Define custom analysis results +#[derive(Debug, PartialEq, Serialize)] +struct FpsAnalysisResult { + cheating_probability: f32, + suspected_cheats: Vec, + evidence_strength: String, // "Low", "Medium", "High" + anomaly_details: HashMap, +} + +// Implement the Analyzable trait for our custom data +impl Analyzable for FpsPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + let mut total_shots = 0; + let mut total_hits = 0; + + for weapon in self.weapon_stats.values() { + total_shots += weapon.shots_fired; + total_hits += weapon.hits; + } + + if total_shots == 0 { + return 0.0; + } + + total_hits as f32 / total_shots as f32 + } + + fn calculate_headshot_ratio(&self) -> f32 { + let mut total_hits = 0; + let mut total_headshots = 0; + + for weapon in self.weapon_stats.values() { + total_hits += weapon.hits; + total_headshots += weapon.headshots; + } + + if total_hits == 0 { + return 0.0; + } + + total_headshots as f32 / total_hits as f32 + } + + fn extract_features(&self) -> Vec { + let kd_ratio = if self.deaths == 0 { + self.kills as f32 + } else { + self.kills as f32 / self.deaths as f32 + }; + + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + kd_ratio, + self.average_position_change, + self.camping_seconds as f32 / self.round_duration_seconds as f32, + ] + } + + fn is_suspicious(&self) -> bool { + let accuracy = self.calculate_accuracy_rate(); + let headshot_ratio = self.calculate_headshot_ratio(); + let kd_ratio = if self.deaths == 0 { + self.kills as f32 + } else { + self.kills as f32 / self.deaths as f32 + }; + + // Check for suspicious patterns + accuracy > 0.8 || headshot_ratio > 0.7 || kd_ratio > 5.0 + } +} + +// Custom analyzer function for FPS games +fn analyze_fps_players( + players: Vec>, +) -> AnalysisResponse { + let mut results = Vec::new(); + + for player in players { + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + + // Check accuracy (aimbot detection) + let accuracy = player.data.calculate_accuracy_rate(); + if accuracy > 0.8 { + suspected_cheats.push("Aimbot".to_string()); + anomaly_details.insert( + "High Accuracy".to_string(), + format!("{:.1}% hit rate is suspiciously high", accuracy * 100.0), + ); + } + + // Check headshot ratio (aimbot detection) + let headshot_ratio = player.data.calculate_headshot_ratio(); + if headshot_ratio > 0.7 { + suspected_cheats.push("Aimbot (Headshot)".to_string()); + anomaly_details.insert( + "Headshot Anomaly".to_string(), + format!( + "{:.1}% headshot ratio is suspiciously high", + headshot_ratio * 100.0 + ), + ); + } + + // Check KD ratio (general skill anomaly) + let kd_ratio = if player.data.deaths == 0 { + player.data.kills as f32 + } else { + player.data.kills as f32 / player.data.deaths as f32 + }; + if kd_ratio > 5.0 { + suspected_cheats.push("Skill Anomaly".to_string()); + anomaly_details.insert( + "K/D Ratio".to_string(), + format!("K/D ratio of {:.1} is unusually high", kd_ratio), + ); + } + + // Calculate overall cheating probability + let mut cheating_probability = 0.0; + if !suspected_cheats.is_empty() { + cheating_probability = + (accuracy * 0.3 + headshot_ratio * 0.5 + (kd_ratio / 10.0) * 0.2).min(1.0); + } + + // Determine evidence strength + let evidence_strength = if cheating_probability > 0.8 { + "High".to_string() + } else if cheating_probability > 0.5 { + "Medium".to_string() + } else { + "Low".to_string() + }; + + // Create the analysis result + let result = FpsAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength, + anomaly_details, + }; + + results.push(PlayerResult::new(player.player_id, result)); + } + + AnalysisResponse { results } +} + +fn main() { + // Create sample weapon stats + let rifle_stats = WeaponStats { + shots_fired: 100, + hits: 90, + headshots: 65, + distance_meters: vec![15.5, 20.2, 18.7, 25.0, 10.2], + }; + + let pistol_stats = WeaponStats { + shots_fired: 30, + hits: 25, + headshots: 20, + distance_meters: vec![8.5, 12.2, 7.3], + }; + + // Create a weapon stats map + let mut weapon_stats = HashMap::new(); + weapon_stats.insert("rifle".to_string(), rifle_stats); + weapon_stats.insert("pistol".to_string(), pistol_stats); + + // Create FPS player data for a suspected cheater + let cheater_data = FpsPlayerData { + kills: 35, + deaths: 5, + assists: 8, + weapon_stats: weapon_stats.clone(), + average_position_change: 6.5, + camping_seconds: 12, + round_duration_seconds: 300, + team: "CT".to_string(), + }; + + // Modify the weapon stats for a legitimate player + let mut legitimate_weapon_stats = weapon_stats.clone(); + if let Some(rifle) = legitimate_weapon_stats.get_mut("rifle") { + rifle.hits = 62; + rifle.headshots = 20; + } + if let Some(pistol) = legitimate_weapon_stats.get_mut("pistol") { + pistol.hits = 15; + pistol.headshots = 5; + } + + // Create FPS player data for a legitimate player + let legitimate_data = FpsPlayerData { + kills: 15, + deaths: 12, + assists: 7, + weapon_stats: weapon_stats, + average_position_change: 8.2, + camping_seconds: 45, + round_duration_seconds: 300, + team: "T".to_string(), + }; + + // Create player stats objects + let players = vec![ + PlayerStats::new("suspicious_player".to_string(), cheater_data), + PlayerStats::new("legitimate_player".to_string(), legitimate_data), + ]; + + // Analyze the players + let analysis = analyze_fps_players(players); + + // Print the results + println!("Analysis Results:"); + for result in &analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + + if !result.data.anomaly_details.is_empty() { + println!("Anomaly Details:"); + for (key, value) in &result.data.anomaly_details { + println!(" - {}: {}", key, value); + } + } + } + + // Serialize to JSON + let json = serde_json::to_string_pretty(&analysis).unwrap(); + println!("\nJSON Output:\n{}", json); + + // Show how to convert from DefaultPlayerData to our custom format + println!("\nConverting from DefaultPlayerData to FpsPlayerData:"); + + // Create default player data + let mut shots = HashMap::new(); + shots.insert("rifle".to_string(), 100); + + let mut hits = HashMap::new(); + hits.insert("rifle".to_string(), 75); + + let default_data = DefaultPlayerData { + shots_fired: shots, + hits: hits, + headshots: 30, + shot_timestamps_ms: Some(vec![1000, 1200, 1500, 1800, 2100]), + training_label: None, + }; + + let legacy_stats = PlayerStats::new("legacy_player".to_string(), default_data); + // Convert to our custom format + let converted_stats = legacy_stats.convert(|data| { + // Create weapon stats for rifle + let rifle_shots = *data.shots_fired.get("rifle").unwrap_or(&0); + let rifle_hits = *data.hits.get("rifle").unwrap_or(&0); + let rifle_stats = WeaponStats { + shots_fired: rifle_shots, + hits: rifle_hits, + headshots: data.headshots, + distance_meters: vec![15.0, 20.0, 25.0], // Dummy data + }; + + // Add to weapon stats map + let mut weapon_stats = HashMap::new(); + weapon_stats.insert("rifle".to_string(), rifle_stats); + + // Create FPS player data + FpsPlayerData { + kills: rifle_hits, // Assume each hit is a kill for simplicity + deaths: 10, // Dummy value + assists: 5, // Dummy value + weapon_stats, + average_position_change: 7.5, // Dummy value + camping_seconds: 20, // Dummy value + round_duration_seconds: 300, // Dummy value + team: "CT".to_string(), // Dummy value + } + }); + + println!("Original Legacy Player ID: {}", legacy_stats.player_id); + println!("Converted Player ID: {}", converted_stats.player_id); + println!( + "Converted Accuracy: {:.1}%", + converted_stats.data.calculate_accuracy_rate() * 100.0 + ); + println!( + "Converted Headshot Ratio: {:.1}%", + converted_stats.data.calculate_headshot_ratio() * 100.0 + ); +} diff --git a/examples/generic_usage.rs b/examples/generic_usage.rs new file mode 100644 index 0000000..8490aac --- /dev/null +++ b/examples/generic_usage.rs @@ -0,0 +1,98 @@ +use nocheat::types::{AnalysisResponse, DefaultPlayerData, PlayerResult, PlayerStats}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +// Define a custom data structure for player statistics +#[derive(Clone, Debug, Deserialize, Serialize)] +struct CustomPlayerData { + // Game-specific statistics that differ from the default structure + accuracy: f32, + reaction_time_ms: Vec, + movement_patterns: HashMap, + mouse_acceleration: Option, +} + +// Define a custom analysis result structure +#[derive(Debug, PartialEq, Serialize)] +struct CustomAnalysisResult { + cheating_probability: f32, + abnormal_patterns: Vec, + confidence_score: f32, + recommended_action: String, +} + +fn main() { + // Example 1: Using the default data structure (backward compatible) + let mut shots = HashMap::new(); + shots.insert("rifle".to_string(), 100); + + let mut hits = HashMap::new(); + hits.insert("rifle".to_string(), 50); + + let default_data = DefaultPlayerData { + shots_fired: shots, + hits: hits, + headshots: 10, + shot_timestamps_ms: None, + training_label: None, + }; + + // Create a PlayerStats instance with the default data structure + let player_stats = PlayerStats::new("player123".to_string(), default_data); + + println!( + "Default PlayerStats: {}, headshots: {}", + player_stats.player_id, player_stats.data.headshots + ); + + // Example 2: Using a custom data structure + let mut movement = HashMap::new(); + movement.insert("jumps".to_string(), 50); + movement.insert("crouches".to_string(), 30); + + let custom_data = CustomPlayerData { + accuracy: 0.75, + reaction_time_ms: vec![250, 220, 230, 210, 240], + movement_patterns: movement, + mouse_acceleration: Some(1.5), + }; + + // Create a PlayerStats instance with custom data + let custom_stats = PlayerStats::new("custom_player".to_string(), custom_data); + + println!( + "Custom PlayerStats: {}, accuracy: {}", + custom_stats.player_id, custom_stats.data.accuracy + ); + + // Example 3: Creating custom analysis results + let custom_result = CustomAnalysisResult { + cheating_probability: 0.85, + abnormal_patterns: vec!["AimSnap".to_string(), "RecoilControl".to_string()], + confidence_score: 0.92, + recommended_action: "Review gameplay footage".to_string(), + }; + + let player_result = PlayerResult::new("custom_player".to_string(), custom_result); + + println!( + "Custom analysis result - Player: {}, Cheating probability: {}, Recommended action: {}", + player_result.player_id, + player_result.data.cheating_probability, + player_result.data.recommended_action + ); + + // Example 4: Creating an AnalysisResponse with custom results + let response = AnalysisResponse { + results: vec![player_result], + }; + + println!( + "Analysis response contains {} result(s)", + response.results.len() + ); + + // Example 5: Serializing to JSON + let json = serde_json::to_string_pretty(&response).unwrap(); + println!("JSON output:\n{}", json); +} diff --git a/examples/json_samples/battle_royale_training_data.json b/examples/json_samples/battle_royale_training_data.json new file mode 100644 index 0000000..f2050e1 --- /dev/null +++ b/examples/json_samples/battle_royale_training_data.json @@ -0,0 +1,194 @@ +[ + { + "player_id": "br_legit_player_1", + "data": { + "match_id": "BR-2025-05-23-001", + "placement": 5, + "survival_time_seconds": 1320, + "kills": 6, + "damage_dealt": 1250, + "damage_taken": 850, + "revives": 2, + "distance_traveled": { + "walking": 2500, + "swimming": 150, + "driving": 4200, + "flying": 0 + }, + "loot_collected": { + "weapons": 5, + "ammo": 250, + "healing": 12, + "armor": 3, + "attachments": 8 + }, + "weapon_stats": { + "assault_rifle": { + "shots_fired": 120, + "hits": 48, + "headshots": 8, + "damage": 720 + }, + "shotgun": { + "shots_fired": 18, + "hits": 12, + "headshots": 3, + "damage": 380 + }, + "sniper": { + "shots_fired": 10, + "hits": 4, + "headshots": 2, + "damage": 150 + } + }, + "hot_drop": true, + "team_size": 3, + "training_label": 0.0 + } + }, + { + "player_id": "br_legit_player_2", + "data": { + "match_id": "BR-2025-05-23-002", + "placement": 1, + "survival_time_seconds": 1620, + "kills": 10, + "damage_dealt": 1850, + "damage_taken": 620, + "revives": 3, + "distance_traveled": { + "walking": 3200, + "swimming": 0, + "driving": 5800, + "flying": 1200 + }, + "loot_collected": { + "weapons": 7, + "ammo": 320, + "healing": 18, + "armor": 4, + "attachments": 12 + }, + "weapon_stats": { + "assault_rifle": { + "shots_fired": 180, + "hits": 72, + "headshots": 15, + "damage": 1080 + }, + "smg": { + "shots_fired": 120, + "hits": 55, + "headshots": 8, + "damage": 440 + }, + "sniper": { + "shots_fired": 15, + "hits": 8, + "headshots": 4, + "damage": 330 + } + }, + "hot_drop": false, + "team_size": 3, + "training_label": 0.0 + } + }, + { + "player_id": "br_cheater_player_1", + "data": { + "match_id": "BR-2025-05-23-003", + "placement": 1, + "survival_time_seconds": 1580, + "kills": 28, + "damage_dealt": 4850, + "damage_taken": 350, + "revives": 0, + "distance_traveled": { + "walking": 4800, + "swimming": 0, + "driving": 8500, + "flying": 0 + }, + "loot_collected": { + "weapons": 4, + "ammo": 280, + "healing": 15, + "armor": 3, + "attachments": 10 + }, + "weapon_stats": { + "assault_rifle": { + "shots_fired": 250, + "hits": 225, + "headshots": 180, + "damage": 3375 + }, + "pistol": { + "shots_fired": 45, + "hits": 40, + "headshots": 35, + "damage": 600 + }, + "sniper": { + "shots_fired": 25, + "hits": 22, + "headshots": 20, + "damage": 875 + } + }, + "hot_drop": true, + "team_size": 3, + "training_label": 1.0 + } + }, + { + "player_id": "br_cheater_player_2", + "data": { + "match_id": "BR-2025-05-23-004", + "placement": 1, + "survival_time_seconds": 1680, + "kills": 32, + "damage_dealt": 5200, + "damage_taken": 280, + "revives": 1, + "distance_traveled": { + "walking": 5200, + "swimming": 300, + "driving": 7800, + "flying": 2000 + }, + "loot_collected": { + "weapons": 6, + "ammo": 350, + "healing": 20, + "armor": 4, + "attachments": 15 + }, + "weapon_stats": { + "assault_rifle": { + "shots_fired": 280, + "hits": 252, + "headshots": 210, + "damage": 3780 + }, + "smg": { + "shots_fired": 150, + "hits": 135, + "headshots": 110, + "damage": 1080 + }, + "sniper": { + "shots_fired": 12, + "hits": 10, + "headshots": 9, + "damage": 340 + } + }, + "hot_drop": false, + "team_size": 3, + "training_label": 1.0 + } + } +] diff --git a/examples/json_samples/fps_training_data.json b/examples/json_samples/fps_training_data.json new file mode 100644 index 0000000..0781895 --- /dev/null +++ b/examples/json_samples/fps_training_data.json @@ -0,0 +1,110 @@ +[ + { + "player_id": "fps_legit_player_1", + "data": { + "kills": 18, + "deaths": 15, + "assists": 7, + "weapon_stats": { + "rifle": { + "shots_fired": 120, + "hits": 55, + "headshots": 12, + "distance_meters": [15.2, 22.7, 18.3, 30.1, 12.5] + }, + "pistol": { + "shots_fired": 45, + "hits": 18, + "headshots": 5, + "distance_meters": [8.3, 10.1, 5.7] + } + }, + "average_position_change": 7.8, + "camping_seconds": 35, + "round_duration_seconds": 300, + "team": "CT", + "training_label": 0.0 + } + }, + { + "player_id": "fps_legit_player_2", + "data": { + "kills": 22, + "deaths": 17, + "assists": 4, + "weapon_stats": { + "rifle": { + "shots_fired": 150, + "hits": 70, + "headshots": 18, + "distance_meters": [17.3, 25.6, 20.1, 15.7, 30.5, 22.8] + }, + "sniper": { + "shots_fired": 15, + "hits": 10, + "headshots": 8, + "distance_meters": [45.2, 38.7, 50.1, 42.3] + } + }, + "average_position_change": 8.2, + "camping_seconds": 25, + "round_duration_seconds": 300, + "team": "T", + "training_label": 0.0 + } + }, + { + "player_id": "fps_cheater_player_1", + "data": { + "kills": 38, + "deaths": 4, + "assists": 5, + "weapon_stats": { + "rifle": { + "shots_fired": 100, + "hits": 92, + "headshots": 75, + "distance_meters": [20.3, 25.7, 30.2, 18.9, 35.4] + }, + "pistol": { + "shots_fired": 35, + "hits": 32, + "headshots": 28, + "distance_meters": [12.5, 15.3, 8.7, 9.2] + } + }, + "average_position_change": 5.4, + "camping_seconds": 10, + "round_duration_seconds": 300, + "team": "T", + "training_label": 1.0 + } + }, + { + "player_id": "fps_cheater_player_2", + "data": { + "kills": 42, + "deaths": 5, + "assists": 3, + "weapon_stats": { + "sniper": { + "shots_fired": 50, + "hits": 48, + "headshots": 45, + "distance_meters": [60.2, 55.7, 48.3, 70.1, 65.4] + }, + "pistol": { + "shots_fired": 20, + "hits": 19, + "headshots": 17, + "distance_meters": [10.5, 8.7, 12.3] + } + }, + "average_position_change": 6.2, + "camping_seconds": 15, + "round_duration_seconds": 300, + "team": "CT", + "training_label": 1.0 + } + } +] diff --git a/examples/json_samples/moba_training_data.json b/examples/json_samples/moba_training_data.json new file mode 100644 index 0000000..55b6663 --- /dev/null +++ b/examples/json_samples/moba_training_data.json @@ -0,0 +1,130 @@ +[ + { + "player_id": "moba_legit_player_1", + "data": { + "champion": "Warrior", + "level": 18, + "gold_earned": 12500, + "gold_spent": 11800, + "kills": 7, + "deaths": 4, + "assists": 12, + "damage_stats": { + "physical_damage_dealt": 28500, + "magic_damage_dealt": 5200, + "true_damage_dealt": 1200, + "damage_to_champions": 15600 + }, + "healing": 3500, + "damage_mitigated": 22000, + "vision_score": 25, + "crowd_control_score": 35, + "skill_timings": [ + {"skill": "Q", "timestamps_ms": [4500, 9200, 15600, 22300, 28700]}, + {"skill": "W", "timestamps_ms": [3200, 10500, 17800, 25100, 32400]}, + {"skill": "E", "timestamps_ms": [6800, 12100, 18400, 24700, 31000]}, + {"skill": "R", "timestamps_ms": [16200, 42500]} + ], + "item_build_order": [2003, 1055, 3047, 6333, 3068, 3143], + "team": "blue", + "match_duration_seconds": 1850, + "training_label": 0.0 + } + }, + { + "player_id": "moba_legit_player_2", + "data": { + "champion": "Mage", + "level": 18, + "gold_earned": 14200, + "gold_spent": 13700, + "kills": 12, + "deaths": 6, + "assists": 8, + "damage_stats": { + "physical_damage_dealt": 8500, + "magic_damage_dealt": 45200, + "true_damage_dealt": 2800, + "damage_to_champions": 32600 + }, + "healing": 2500, + "damage_mitigated": 9000, + "vision_score": 35, + "crowd_control_score": 28, + "skill_timings": [ + {"skill": "Q", "timestamps_ms": [3800, 8500, 13200, 17900, 22600]}, + {"skill": "W", "timestamps_ms": [5100, 9800, 14500, 19200, 23900]}, + {"skill": "E", "timestamps_ms": [6400, 11100, 15800, 20500, 25200]}, + {"skill": "R", "timestamps_ms": [12500, 38900]} + ], + "item_build_order": [1056, 3020, 3165, 3089, 3135, 3157], + "team": "red", + "match_duration_seconds": 1850, + "training_label": 0.0 + } + }, + { + "player_id": "moba_cheater_player_1", + "data": { + "champion": "Marksman", + "level": 18, + "gold_earned": 18500, + "gold_spent": 17200, + "kills": 25, + "deaths": 1, + "assists": 5, + "damage_stats": { + "physical_damage_dealt": 85000, + "magic_damage_dealt": 3500, + "true_damage_dealt": 4200, + "damage_to_champions": 52000 + }, + "healing": 4800, + "damage_mitigated": 12000, + "vision_score": 18, + "crowd_control_score": 5, + "skill_timings": [ + {"skill": "Q", "timestamps_ms": [2550, 5050, 7550, 10050, 12550]}, + {"skill": "W", "timestamps_ms": [3300, 5800, 8300, 10800, 13300]}, + {"skill": "E", "timestamps_ms": [1800, 4300, 6800, 9300, 11800]}, + {"skill": "R", "timestamps_ms": [8200, 23500, 38800]} + ], + "item_build_order": [1055, 3006, 3031, 3094, 3036, 3072], + "team": "blue", + "match_duration_seconds": 1850, + "training_label": 1.0 + } + }, + { + "player_id": "moba_cheater_player_2", + "data": { + "champion": "Assassin", + "level": 18, + "gold_earned": 17800, + "gold_spent": 16900, + "kills": 22, + "deaths": 3, + "assists": 3, + "damage_stats": { + "physical_damage_dealt": 62000, + "magic_damage_dealt": 18000, + "true_damage_dealt": 7500, + "damage_to_champions": 48000 + }, + "healing": 6200, + "damage_mitigated": 8500, + "vision_score": 12, + "crowd_control_score": 8, + "skill_timings": [ + {"skill": "Q", "timestamps_ms": [2200, 4700, 7200, 9700, 12200]}, + {"skill": "W", "timestamps_ms": [3500, 6000, 8500, 11000, 13500]}, + {"skill": "E", "timestamps_ms": [1500, 4000, 6500, 9000, 11500]}, + {"skill": "R", "timestamps_ms": [7500, 22800, 38100]} + ], + "item_build_order": [1036, 3142, 3147, 3071, 3814, 3074], + "team": "red", + "match_duration_seconds": 1850, + "training_label": 1.0 + } + } +] diff --git a/examples/json_samples/racing_training_data.json b/examples/json_samples/racing_training_data.json new file mode 100644 index 0000000..b31ed01 --- /dev/null +++ b/examples/json_samples/racing_training_data.json @@ -0,0 +1,162 @@ +[ + { + "player_id": "racing_legit_player_1", + "data": { + "track_id": "Monaco", + "car_model": "Formula X", + "finish_position": 3, + "finish_time_seconds": 5423.82, + "best_lap_time_seconds": 84.32, + "average_lap_time_seconds": 85.78, + "lap_times": [88.45, 86.21, 85.67, 84.32, 84.98, 85.12], + "penalties": { + "corner_cutting": 1, + "speeding": 0, + "collision": 2 + }, + "telemetry": { + "top_speed_kmh": 332.5, + "average_speed_kmh": 215.8, + "gear_changes": 487, + "braking_points": 378, + "acceleration_events": 382, + "drift_angles": [12.5, 15.2, 8.7, 18.3, 10.5] + }, + "sectors": [ + {"sector_id": 1, "times": [28.34, 28.12, 27.98, 27.45, 27.89, 28.01]}, + {"sector_id": 2, "times": [31.21, 30.78, 30.45, 30.12, 30.34, 30.28]}, + {"sector_id": 3, "times": [28.90, 27.31, 27.24, 26.75, 26.75, 26.83]} + ], + "pit_stops": 1, + "fuel_consumption": 75.8, + "tire_wear": { + "front_left": 68.5, + "front_right": 72.3, + "rear_left": 65.7, + "rear_right": 70.2 + }, + "weather_conditions": "Dry", + "training_label": 0.0 + } + }, + { + "player_id": "racing_legit_player_2", + "data": { + "track_id": "Silverstone", + "car_model": "GT Racer", + "finish_position": 1, + "finish_time_seconds": 6215.45, + "best_lap_time_seconds": 90.15, + "average_lap_time_seconds": 91.42, + "lap_times": [93.25, 92.10, 91.85, 90.15, 90.82, 91.37, 90.38], + "penalties": { + "corner_cutting": 0, + "speeding": 0, + "collision": 1 + }, + "telemetry": { + "top_speed_kmh": 295.8, + "average_speed_kmh": 187.3, + "gear_changes": 528, + "braking_points": 420, + "acceleration_events": 426, + "drift_angles": [10.2, 12.8, 8.4, 11.7, 9.2] + }, + "sectors": [ + {"sector_id": 1, "times": [30.12, 29.85, 29.62, 29.18, 29.35, 29.58, 29.20]}, + {"sector_id": 2, "times": [32.45, 31.98, 31.75, 31.22, 31.43, 31.65, 31.28]}, + {"sector_id": 3, "times": [30.68, 30.27, 30.48, 29.75, 30.04, 30.14, 29.90]} + ], + "pit_stops": 1, + "fuel_consumption": 82.3, + "tire_wear": { + "front_left": 72.8, + "front_right": 78.5, + "rear_left": 68.2, + "rear_right": 75.1 + }, + "weather_conditions": "Wet", + "training_label": 0.0 + } + }, + { + "player_id": "racing_cheater_player_1", + "data": { + "track_id": "Monza", + "car_model": "Formula X", + "finish_position": 1, + "finish_time_seconds": 4205.32, + "best_lap_time_seconds": 72.18, + "average_lap_time_seconds": 72.51, + "lap_times": [73.25, 72.48, 72.18, 72.20, 72.45, 72.50], + "penalties": { + "corner_cutting": 0, + "speeding": 0, + "collision": 0 + }, + "telemetry": { + "top_speed_kmh": 372.5, + "average_speed_kmh": 265.8, + "gear_changes": 412, + "braking_points": 328, + "acceleration_events": 332, + "drift_angles": [5.2, 4.8, 3.7, 5.5, 4.2] + }, + "sectors": [ + {"sector_id": 1, "times": [23.85, 23.70, 23.52, 23.55, 23.58, 23.62]}, + {"sector_id": 2, "times": [26.25, 26.08, 25.98, 25.95, 26.12, 26.08]}, + {"sector_id": 3, "times": [23.15, 22.70, 22.68, 22.70, 22.75, 22.80]} + ], + "pit_stops": 0, + "fuel_consumption": 58.2, + "tire_wear": { + "front_left": 45.2, + "front_right": 48.5, + "rear_left": 42.8, + "rear_right": 46.3 + }, + "weather_conditions": "Dry", + "training_label": 1.0 + } + }, + { + "player_id": "racing_cheater_player_2", + "data": { + "track_id": "Spa", + "car_model": "GT Racer", + "finish_position": 1, + "finish_time_seconds": 5123.75, + "best_lap_time_seconds": 78.35, + "average_lap_time_seconds": 78.82, + "lap_times": [79.85, 79.12, 78.65, 78.35, 78.42, 78.53], + "penalties": { + "corner_cutting": 0, + "speeding": 0, + "collision": 0 + }, + "telemetry": { + "top_speed_kmh": 338.2, + "average_speed_kmh": 242.5, + "gear_changes": 452, + "braking_points": 365, + "acceleration_events": 368, + "drift_angles": [4.8, 5.3, 3.2, 4.7, 3.8] + }, + "sectors": [ + {"sector_id": 1, "times": [25.45, 25.28, 25.12, 25.05, 25.10, 25.15]}, + {"sector_id": 2, "times": [28.85, 28.52, 28.33, 28.20, 28.22, 28.28]}, + {"sector_id": 3, "times": [25.55, 25.32, 25.20, 25.10, 25.10, 25.10]} + ], + "pit_stops": 0, + "fuel_consumption": 65.8, + "tire_wear": { + "front_left": 52.3, + "front_right": 56.7, + "rear_left": 48.5, + "rear_right": 53.2 + }, + "weather_conditions": "Dry", + "training_label": 1.0 + } + } +] diff --git a/examples/json_samples/rts_training_data.json b/examples/json_samples/rts_training_data.json new file mode 100644 index 0000000..6a36179 --- /dev/null +++ b/examples/json_samples/rts_training_data.json @@ -0,0 +1,32 @@ +[ + { + "player_id": "rts_legit_player_1", + "data": { + "build_order": ["Barracks", "Factory", "Starport"], + "units_trained": 30, + "units_destroyed": 25, + "resources_gathered": { + "minerals": 15000, + "vespene": 3500 + }, + "average_apm": 200, + "game_duration_seconds": 1200, + "training_label": 0.0 + } + }, + { + "player_id": "rts_cheater_player_1", + "data": { + "build_order": ["GhostAcademy", "NukeSilo", "TechLab", "Starport"], + "units_trained": 80, + "units_destroyed": 75, + "resources_gathered": { + "minerals": 50000, + "vespene": 16000 + }, + "average_apm": 400, + "game_duration_seconds": 1200, + "training_label": 1.0 + } + } +] diff --git a/examples/json_samples/sports_training_data.json b/examples/json_samples/sports_training_data.json new file mode 100644 index 0000000..b691451 --- /dev/null +++ b/examples/json_samples/sports_training_data.json @@ -0,0 +1,50 @@ +[ + { + "player_id": "soccer_legit_player_1", + "data": { + "sport": "Soccer", + "goals": 1, + "assists": 0, + "passes_completed": 35, + "tackles": 2, + "distance_covered_km": 9.8, + "training_label": 0.0 + } + }, + { + "player_id": "soccer_legit_player_2", + "data": { + "sport": "Soccer", + "goals": 2, + "assists": 1, + "passes_completed": 50, + "tackles": 1, + "distance_covered_km": 10.4, + "training_label": 0.0 + } + }, + { + "player_id": "soccer_cheater_player_1", + "data": { + "sport": "Soccer", + "goals": 7, + "assists": 3, + "passes_completed": 15, + "tackles": 0, + "distance_covered_km": 5.2, + "training_label": 1.0 + } + }, + { + "player_id": "soccer_cheater_player_2", + "data": { + "sport": "Soccer", + "goals": 8, + "assists": 0, + "passes_completed": 10, + "tackles": 0, + "distance_covered_km": 4.8, + "training_label": 1.0 + } + } +] diff --git a/examples/multi_game_analysis.rs b/examples/multi_game_analysis.rs new file mode 100644 index 0000000..0b1f86f --- /dev/null +++ b/examples/multi_game_analysis.rs @@ -0,0 +1,877 @@ +// Example showing how to load and use different types of generic training data +use nocheat::types::{AnalysisResponse, Analyzable, PlayerResult, PlayerStats}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::fs::File; +use std::io::Read; + +// --- FPS GAME DATA STRUCTURES --- +#[derive(Clone, Debug, Deserialize, Serialize)] +struct FpsPlayerData { + kills: u32, + deaths: u32, + assists: u32, + weapon_stats: HashMap, + average_position_change: f32, + camping_seconds: u32, + round_duration_seconds: u32, + team: String, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct WeaponStats { + shots_fired: u32, + hits: u32, + headshots: u32, + distance_meters: Vec, +} + +// --- MOBA GAME DATA STRUCTURES --- +#[derive(Clone, Debug, Deserialize, Serialize)] +struct MobaPlayerData { + champion: String, + level: u32, + gold_earned: u32, + gold_spent: u32, + kills: u32, + deaths: u32, + assists: u32, + damage_stats: DamageStats, + healing: u32, + damage_mitigated: u32, + vision_score: u32, + crowd_control_score: u32, + skill_timings: Vec, + item_build_order: Vec, + team: String, + match_duration_seconds: u32, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct DamageStats { + physical_damage_dealt: u32, + magic_damage_dealt: u32, + true_damage_dealt: u32, + damage_to_champions: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct SkillTiming { + skill: String, + timestamps_ms: Vec, +} + +// --- BATTLE ROYALE GAME DATA STRUCTURES --- +#[derive(Clone, Debug, Deserialize, Serialize)] +struct BattleRoyalePlayerData { + match_id: String, + placement: u32, + survival_time_seconds: u32, + kills: u32, + damage_dealt: u32, + damage_taken: u32, + revives: u32, + distance_traveled: DistanceTraveled, + loot_collected: LootCollected, + weapon_stats: HashMap, + hot_drop: bool, + team_size: u32, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct DistanceTraveled { + walking: u32, + swimming: u32, + driving: u32, + flying: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct LootCollected { + weapons: u32, + ammo: u32, + healing: u32, + armor: u32, + attachments: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct BrWeaponStats { + shots_fired: u32, + hits: u32, + headshots: u32, + damage: u32, +} + +// --- RACING GAME DATA STRUCTURES --- +#[derive(Clone, Debug, Deserialize, Serialize)] +struct RacingPlayerData { + track_id: String, + car_model: String, + finish_position: u32, + finish_time_seconds: f32, + best_lap_time_seconds: f32, + average_lap_time_seconds: f32, + lap_times: Vec, + penalties: Penalties, + telemetry: Telemetry, + sectors: Vec, + pit_stops: u32, + fuel_consumption: f32, + tire_wear: TireWear, + weather_conditions: String, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Penalties { + corner_cutting: u32, + speeding: u32, + collision: u32, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Telemetry { + top_speed_kmh: f32, + average_speed_kmh: f32, + gear_changes: u32, + braking_points: u32, + acceleration_events: u32, + drift_angles: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct SectorTime { + sector_id: u32, + times: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct TireWear { + front_left: f32, + front_right: f32, + rear_left: f32, + rear_right: f32, +} + +// --- RTS GAME DATA STRUCTURES --- +#[derive(Clone, Debug, Deserialize, Serialize)] +struct RtsPlayerData { + build_order: Vec, + units_trained: u32, + units_destroyed: u32, + resources_gathered: Resources, + average_apm: u32, + game_duration_seconds: u32, + training_label: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct Resources { + minerals: u32, + vespene: u32, +} + +// --- ANALYSIS RESULT STRUCTURES --- +#[derive(Debug, PartialEq, Serialize)] +struct GenericAnalysisResult { + cheating_probability: f32, + suspected_cheats: Vec, + evidence_strength: String, + anomaly_details: HashMap, +} + +// --- IMPLEMENT ANALYZABLE TRAIT --- +// For FPS data +impl Analyzable for FpsPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + let mut total_shots = 0; + let mut total_hits = 0; + + for weapon in self.weapon_stats.values() { + total_shots += weapon.shots_fired; + total_hits += weapon.hits; + } + + if total_shots == 0 { + return 0.0; + } + + total_hits as f32 / total_shots as f32 + } + + fn calculate_headshot_ratio(&self) -> f32 { + let mut total_hits = 0; + let mut total_headshots = 0; + + for weapon in self.weapon_stats.values() { + total_hits += weapon.hits; + total_headshots += weapon.headshots; + } + + if total_hits == 0 { + return 0.0; + } + + total_headshots as f32 / total_hits as f32 + } + + fn extract_features(&self) -> Vec { + let kd_ratio = if self.deaths == 0 { + self.kills as f32 + } else { + self.kills as f32 / self.deaths as f32 + }; + + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + kd_ratio, + self.average_position_change, + self.camping_seconds as f32 / self.round_duration_seconds as f32, + ] + } + + fn is_suspicious(&self) -> bool { + let accuracy = self.calculate_accuracy_rate(); + let headshot_ratio = self.calculate_headshot_ratio(); + let kd_ratio = if self.deaths == 0 { + self.kills as f32 + } else { + self.kills as f32 / self.deaths as f32 + }; + + accuracy > 0.8 || headshot_ratio > 0.7 || kd_ratio > 5.0 + } +} + +// For Battle Royale data +impl Analyzable for BattleRoyalePlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + let mut total_shots = 0; + let mut total_hits = 0; + + for weapon in self.weapon_stats.values() { + total_shots += weapon.shots_fired; + total_hits += weapon.hits; + } + + if total_shots == 0 { + return 0.0; + } + + total_hits as f32 / total_shots as f32 + } + + fn calculate_headshot_ratio(&self) -> f32 { + let mut total_hits = 0; + let mut total_headshots = 0; + + for weapon in self.weapon_stats.values() { + total_hits += weapon.hits; + total_headshots += weapon.headshots; + } + + if total_hits == 0 { + return 0.0; + } + + total_headshots as f32 / total_hits as f32 + } + + fn extract_features(&self) -> Vec { + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + self.kills as f32, + self.damage_dealt as f32 / self.damage_taken.max(1) as f32, + self.placement as f32, + self.survival_time_seconds as f32, + ] + } + + fn is_suspicious(&self) -> bool { + self.calculate_accuracy_rate() > 0.9 + || self.calculate_headshot_ratio() > 0.8 + || (self.kills > 25 && self.placement <= 3) + } +} + +// For Racing data +impl Analyzable for RacingPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + // Racing games don't have a direct accuracy metric + // Instead, we'll use consistency as a proxy + if self.lap_times.is_empty() { + return 0.0; + } + + let average = self.average_lap_time_seconds; + let deviation: f32 = self + .lap_times + .iter() + .map(|&t| (t - average).abs()) + .sum::() + / self.lap_times.len() as f32; + + // Return consistency (1.0 means perfect consistency) + 1.0 - (deviation / average).min(1.0) + } + + fn calculate_headshot_ratio(&self) -> f32 { + // Racing games don't have headshots + // Instead, we'll use sector perfectness as a proxy + if self.sectors.is_empty() { + return 0.0; + } + + let mut perfect_sectors = 0; + let mut total_sectors = 0; + + for sector in &self.sectors { + if !sector.times.is_empty() { + let min_time = sector.times.iter().fold(f32::INFINITY, |a, &b| a.min(b)); + let avg_time: f32 = sector.times.iter().sum::() / sector.times.len() as f32; + + // If minimum time is within 1% of average, count as "perfect" + if (avg_time - min_time) / avg_time < 0.01 { + perfect_sectors += 1; + } + + total_sectors += 1; + } + } + + if total_sectors == 0 { + return 0.0; + } + + perfect_sectors as f32 / total_sectors as f32 + } + + fn extract_features(&self) -> Vec { + vec![ + self.best_lap_time_seconds, + self.average_lap_time_seconds, + self.calculate_accuracy_rate(), // consistency + self.telemetry.top_speed_kmh, + self.telemetry.average_speed_kmh, + self.penalties.corner_cutting as f32, + self.penalties.collision as f32, + ] + } + + fn is_suspicious(&self) -> bool { + // Detect suspiciously fast times or perfect consistency + if self.calculate_accuracy_rate() > 0.98 { + return true; // Too perfect consistency + } + + // Check if lap times are suspiciously fast (depends on the track) + match self.track_id.as_str() { + "Monaco" => self.best_lap_time_seconds < 75.0, + "Silverstone" => self.best_lap_time_seconds < 85.0, + "Monza" => self.best_lap_time_seconds < 75.0, + "Spa" => self.best_lap_time_seconds < 80.0, + _ => false, + } + } +} + +// For RTS data +impl Analyzable for RtsPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + if self.units_trained == 0 { + return 0.0; + } + self.units_destroyed as f32 / self.units_trained as f32 + } + fn calculate_headshot_ratio(&self) -> f32 { + // Use normalized APM as a proxy + let duration = self.game_duration_seconds as f32; + if duration == 0.0 { + return 0.0; + } + self.average_apm as f32 / duration + } + fn extract_features(&self) -> Vec { + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + self.average_apm as f32, + self.resources_gathered.minerals as f32, + self.resources_gathered.vespene as f32, + ] + } + fn is_suspicious(&self) -> bool { + self.calculate_accuracy_rate() > 1.5 || self.calculate_headshot_ratio() > 2.0 + } +} + +// --- IMPLEMENT ANALYZABLE TRAIT FOR MOBA DATA --- +impl Analyzable for MobaPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + if self.deaths == 0 { + return self.kills as f32; + } + self.kills as f32 / self.deaths as f32 + } + fn calculate_headshot_ratio(&self) -> f32 { + if self.kills + self.assists == 0 { + return 0.0; + } + self.assists as f32 / (self.kills + self.assists) as f32 + } + fn extract_features(&self) -> Vec { + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + self.level as f32, + self.gold_earned as f32 / self.gold_spent.max(1) as f32, + ] + } + fn is_suspicious(&self) -> bool { + self.calculate_accuracy_rate() > 5.0 || self.calculate_headshot_ratio() > 0.5 + } +} + +// Custom wrapper struct for JSON deserialization +#[derive(Deserialize)] +struct PlayerDataWrapper { + player_id: String, + data: T, +} + +// Convert from wrapper to PlayerStats +fn convert_to_player_stats(wrappers: Vec>) -> Vec> +where + T: Clone + Serialize, +{ + wrappers + .into_iter() + .map(|w| PlayerStats::new(w.player_id, w.data)) + .collect() +} + +// Functions to load different data types +fn load_json_data(file_path: &str) -> Result>, Box> +where + T: for<'de> Deserialize<'de> + Clone + Serialize, +{ + let mut file = File::open(file_path)?; + let mut contents = String::new(); + file.read_to_string(&mut contents)?; + let wrappers: Vec> = serde_json::from_str(&contents)?; + Ok(convert_to_player_stats(wrappers)) +} + +/// Analyze any Analyzable player data into GenericAnalysisResult +fn analyze_generic_data( + players: &[PlayerStats], +) -> AnalysisResponse { + let mut results = Vec::new(); + for player in players { + let accuracy = player.data.calculate_accuracy_rate(); + let headshot = player.data.calculate_headshot_ratio(); + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + if accuracy > 0.85 { + suspected_cheats.push("High Efficiency".to_string()); + anomaly_details.insert( + "Accuracy Rate".to_string(), + format!("{:.1}%", accuracy * 100.0), + ); + } + if headshot > 0.75 { + suspected_cheats.push("High APM or Consistency".to_string()); + anomaly_details.insert("Normalized APM".to_string(), format!("{:.1}", headshot)); + } + let mut cheating_probability = 0.0; + if !suspected_cheats.is_empty() { + cheating_probability = (accuracy * 0.5 + headshot * 0.5).min(1.0); + } + let evidence_strength = if cheating_probability > 0.8 { + "High".to_string() + } else if cheating_probability > 0.5 { + "Medium".to_string() + } else { + "Low".to_string() + }; + let result = GenericAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength, + anomaly_details, + }; + results.push(PlayerResult::new(player.player_id.clone(), result)); + } + AnalysisResponse { results } +} + +fn analyze_fps_data( + players: &[PlayerStats], +) -> AnalysisResponse { + let mut results = Vec::new(); + + for player in players { + let accuracy = player.data.calculate_accuracy_rate(); + let headshot_ratio = player.data.calculate_headshot_ratio(); + let kd_ratio = if player.data.deaths == 0 { + player.data.kills as f32 + } else { + player.data.kills as f32 / player.data.deaths as f32 + }; + + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + + // Check for suspicious patterns + if accuracy > 0.8 { + suspected_cheats.push("Aimbot".to_string()); + anomaly_details.insert( + "High Accuracy".to_string(), + format!("{:.1}% hit rate is suspiciously high", accuracy * 100.0), + ); + } + + if headshot_ratio > 0.7 { + suspected_cheats.push("Aimbot (Headshot)".to_string()); + anomaly_details.insert( + "Headshot Anomaly".to_string(), + format!( + "{:.1}% headshot ratio is suspiciously high", + headshot_ratio * 100.0 + ), + ); + } + + if kd_ratio > 5.0 { + suspected_cheats.push("Skill Anomaly".to_string()); + anomaly_details.insert( + "K/D Ratio".to_string(), + format!("K/D ratio of {:.1} is unusually high", kd_ratio), + ); + } + + // Calculate overall cheating probability + let mut cheating_probability = 0.0; + if !suspected_cheats.is_empty() { + cheating_probability = + (accuracy * 0.3 + headshot_ratio * 0.5 + (kd_ratio / 10.0) * 0.2).min(1.0); + } + + // Determine evidence strength + let evidence_strength = if cheating_probability > 0.8 { + "High".to_string() + } else if cheating_probability > 0.5 { + "Medium".to_string() + } else { + "Low".to_string() + }; + + // Create the analysis result + let result = GenericAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength, + anomaly_details, + }; + + results.push(PlayerResult::new(player.player_id.clone(), result)); + } + + AnalysisResponse { results } +} + +fn analyze_battle_royale_data( + players: &[PlayerStats], +) -> AnalysisResponse { + let mut results = Vec::new(); + + for player in players { + let accuracy = player.data.calculate_accuracy_rate(); + let headshot_ratio = player.data.calculate_headshot_ratio(); + let kills = player.data.kills as f32; + let placement = player.data.placement as f32; + + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + + // Check for suspicious patterns + if accuracy > 0.9 { + suspected_cheats.push("Aimbot".to_string()); + anomaly_details.insert( + "High Accuracy".to_string(), + format!("{:.1}% hit rate is suspiciously high", accuracy * 100.0), + ); + } + + if headshot_ratio > 0.8 { + suspected_cheats.push("Headshot Hack".to_string()); + anomaly_details.insert( + "Headshot Anomaly".to_string(), + format!( + "{:.1}% headshot ratio is suspiciously high", + headshot_ratio * 100.0 + ), + ); + } + + if kills > 25.0 && placement <= 3.0 { + suspected_cheats.push("Kill Anomaly".to_string()); + anomaly_details.insert( + "High Kill Count".to_string(), + format!("{} kills with placement {} is unusual", kills, placement), + ); + } + + // Calculate overall cheating probability + let mut cheating_probability = 0.0; + if !suspected_cheats.is_empty() { + cheating_probability = (accuracy * 0.3 + + headshot_ratio * 0.3 + + (1.0 - placement / 100.0) * 0.2 + + (kills / 40.0) * 0.2) + .min(1.0); + } + + // Determine evidence strength + let evidence_strength = if cheating_probability > 0.8 { + "High".to_string() + } else if cheating_probability > 0.5 { + "Medium".to_string() + } else { + "Low".to_string() + }; + + // Create the analysis result + let result = GenericAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength, + anomaly_details, + }; + + results.push(PlayerResult::new(player.player_id.clone(), result)); + } + + AnalysisResponse { results } +} + +fn analyze_racing_data( + players: &[PlayerStats], +) -> AnalysisResponse { + let mut results = Vec::new(); + + for player in players { + let consistency = player.data.calculate_accuracy_rate(); // Using accuracy as consistency + let perfect_sectors = player.data.calculate_headshot_ratio(); // Using headshot ratio as perfect sectors + let best_lap = player.data.best_lap_time_seconds; + + let mut suspected_cheats = Vec::new(); + let mut anomaly_details = HashMap::new(); + + // Check for suspicious patterns based on track + let too_fast = match player.data.track_id.as_str() { + "Monaco" => best_lap < 75.0, + "Silverstone" => best_lap < 85.0, + "Monza" => best_lap < 75.0, + "Spa" => best_lap < 80.0, + _ => false, + }; + + if too_fast { + suspected_cheats.push("Speed Hack".to_string()); + anomaly_details.insert( + "Impossible Lap Time".to_string(), + format!( + "Lap time of {:.2}s on {} is unrealistically fast", + best_lap, player.data.track_id + ), + ); + } + + if consistency > 0.98 { + suspected_cheats.push("Bot Driver".to_string()); + anomaly_details.insert( + "Perfect Consistency".to_string(), + format!( + "Consistency of {:.1}% is unrealistically perfect", + consistency * 100.0 + ), + ); + } + + if perfect_sectors > 0.9 { + suspected_cheats.push("Perfect Racing Line".to_string()); + anomaly_details.insert( + "Sector Perfection".to_string(), + format!( + "{:.1}% of sectors were perfect - likely automated driving", + perfect_sectors * 100.0 + ), + ); + } + + // Calculate overall cheating probability + let mut cheating_probability = 0.0; + if !suspected_cheats.is_empty() { + // Different weights for racing games + cheating_probability = + (consistency * 0.4 + perfect_sectors * 0.4 + (too_fast as u8 as f32) * 0.2) + .min(1.0); + } + + // Determine evidence strength + let evidence_strength = if cheating_probability > 0.8 { + "High".to_string() + } else if cheating_probability > 0.5 { + "Medium".to_string() + } else { + "Low".to_string() + }; + + // Create the analysis result + let result = GenericAnalysisResult { + cheating_probability, + suspected_cheats, + evidence_strength, + anomaly_details, + }; + + results.push(PlayerResult::new(player.player_id.clone(), result)); + } + + AnalysisResponse { results } +} + +// Main function to demonstrate loading and analyzing different game data types +#[allow(unused_variables)] +pub fn main() -> Result<(), Box> { + println!("Loading and analyzing different game data types using generic structures\n"); + + // --- FPS GAME ANALYSIS --- + println!("==== FPS GAME ANALYSIS ===="); + let fps_data: Vec> = + load_json_data("examples/json_samples/fps_training_data.json")?; + println!("Loaded {} FPS player records", fps_data.len()); + + let fps_analysis = analyze_fps_data(&fps_data); + + for result in &fps_analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + } + + // --- BATTLE ROYALE GAME ANALYSIS --- + println!("\n==== BATTLE ROYALE GAME ANALYSIS ===="); + let br_data: Vec> = + load_json_data("examples/json_samples/battle_royale_training_data.json")?; + println!("Loaded {} Battle Royale player records", br_data.len()); + + let br_analysis = analyze_battle_royale_data(&br_data); + + for result in &br_analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + } + + // --- RACING GAME ANALYSIS --- + println!("\n==== RACING GAME ANALYSIS ===="); + let racing_data: Vec> = + load_json_data("examples/json_samples/racing_training_data.json")?; + println!("Loaded {} Racing player records", racing_data.len()); + + let racing_analysis = analyze_racing_data(&racing_data); + + for result in &racing_analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + } + + // --- MOBA GAME ANALYSIS --- + println!("\n==== MOBA GAME ANALYSIS ===="); + let moba_data: Vec> = + load_json_data("examples/json_samples/moba_training_data.json")?; + println!("Loaded {} MOBA player records", moba_data.len()); + + let moba_analysis = analyze_generic_data(&moba_data); + + for result in &moba_analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + } + + // --- RTS GAME ANALYSIS --- + println!("\n==== RTS GAME ANALYSIS ===="); + let rts_data: Vec> = + load_json_data("examples/json_samples/rts_training_data.json")?; + println!("Loaded {} RTS player records", rts_data.len()); + + let rts_analysis = analyze_generic_data(&rts_data); + + for result in &rts_analysis.results { + println!("\nPlayer: {}", result.player_id); + println!( + "Cheating Probability: {:.1}%", + result.data.cheating_probability * 100.0 + ); + println!("Evidence Strength: {}", result.data.evidence_strength); + + if !result.data.suspected_cheats.is_empty() { + println!("Suspected Cheats:"); + for cheat in &result.data.suspected_cheats { + println!(" - {}", cheat); + } + } + } + + Ok(()) +} diff --git a/examples/train_model_example.rs b/examples/train_model_example.rs new file mode 100644 index 0000000..26d1dab --- /dev/null +++ b/examples/train_model_example.rs @@ -0,0 +1,53 @@ +// filepath: examples/train_model_example.rs +// Example demonstrating how to generate a default model and train a custom model +use nocheat::types::LegacyPlayerStats; +use nocheat::{generate_default_model, train_model}; +use serde_json; +use std::error::Error; +use std::fs; +use std::path::Path; + +fn main() { + if let Err(e) = try_main() { + eprintln!("Error: {}", e); + std::process::exit(1); + } +} + +// Real example code moved into try_main +fn try_main() -> Result<(), Box> { + // Generate a default model + let default_model_path = "default_cheat_model.bin"; + println!("Generating default model at '{}'...", default_model_path); + generate_default_model(default_model_path)?; + assert!(Path::new(default_model_path).exists()); + println!("Default model generated successfully.\n"); + + // Train a custom model using flat training_data.json (DefaultPlayerData schema) + let training_data_path = "examples/json_samples/training_data.json"; + let custom_model_path = "custom_cheat_model.bin"; + println!( + "Training custom model from '{}' into '{}'...", + training_data_path, custom_model_path + ); + + // Read and parse training data + let contents = fs::read_to_string(training_data_path)?; + let training_stats: Vec = serde_json::from_str(&contents)?; + let labels: Vec = training_stats + .iter() + .filter_map(|stat| stat.data.training_label) + .collect(); + + // Verify data-label alignment + if training_stats.len() != labels.len() { + return Err("Mismatch between training data and labels".into()); + } + + // Train and save the model + train_model(training_stats, labels, custom_model_path)?; + assert!(Path::new(custom_model_path).exists()); + println!("Custom model generated successfully."); + + Ok(()) +} diff --git a/examples/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h b/examples/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h index 08d09ed..9844ff2 100644 --- a/examples/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h +++ b/examples/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h @@ -1,94 +1,94 @@ -#pragma once - -#ifdef __cplusplus -extern "C" { - /// FFI: analyze a JSON buffer of PlayerStats; returns JSON buffer -/// -/// This function provides a C-compatible interface for the cheat detection system. -/// It takes a JSON buffer containing player statistics, analyzes them, and returns -/// the results as a JSON buffer. -/// -/// # Safety -/// -/// This function is unsafe because it deals with raw pointers and memory allocation -/// across the FFI boundary. The caller is responsible for: -/// -/// - Ensuring the input pointers are valid and properly aligned -/// - Freeing the returned buffer using the `free_buffer` function -/// -/// # Arguments -/// -/// * `stats_json_ptr` - Pointer to a UTF-8 encoded JSON buffer -/// * `stats_json_len` - Length of the JSON buffer in bytes -/// * `out_json_ptr` - Pointer to a location where the output buffer pointer will be stored -/// * `out_json_len` - Pointer to a location where the output buffer length will be stored -/// -/// # Returns -/// -/// * `0` on success -/// * Negative values on various errors: -/// * `-1` - Null pointer provided -/// * `-2` - JSON parsing error -/// * `-3` - Analysis error -/// * `-4` - Serialization error -/// * `-5` - Memory allocation error -#endif - -#ifdef _WIN32 - #define NOCHEAT_API __declspec(dllimport) -#else - #define NOCHEAT_API -#endif - -typedef struct PlayerStats { - const char* player_id; - const char* stats_json; // JSON string with weapon stats, hits, etc. -} PlayerStats; - -typedef struct AnalysisResult { - const char* result_json; // JSON string with suspicion score and flags -} AnalysisResult; - -/** - * Analyzes player statistics for suspicious behavior - * @param stats_json_ptr Pointer to UTF-8 encoded JSON buffer containing player stats - * @param stats_json_len Length of the JSON buffer in bytes - * @param out_json_ptr Pointer to a location where output buffer pointer will be stored - * @param out_json_len Pointer to a location where output buffer length will be stored - * @return 0 on success, negative values on error - */ -NOCHEAT_API int analyze_round( - const unsigned char* stats_json_ptr, - size_t stats_json_len, - unsigned char** out_json_ptr, - size_t* out_json_len -); - -/** - * Frees memory allocated by analyze_round - * @param ptr Pointer to the buffer to free - * @param len Length of the buffer - */ -NOCHEAT_API void free_buffer( - unsigned char* ptr, - size_t len -); - -/** - * Set a custom path to load the model from - * @param path_ptr Pointer to a UTF-8 encoded path string - * @param path_len Length of the path string in bytes - * @return 0 on success, negative values on error: - * -1: Null path provided - * -2: Invalid UTF-8 path - * -3: File doesn't exist - * -4: Model couldn't be deserialized - */ -NOCHEAT_API int set_model_path( - const unsigned char* path_ptr, - size_t path_len -); - -#ifdef __cplusplus -} +#pragma once + +#ifdef __cplusplus +extern "C" { + /// FFI: analyze a JSON buffer of PlayerStats; returns JSON buffer +/// +/// This function provides a C-compatible interface for the cheat detection system. +/// It takes a JSON buffer containing player statistics, analyzes them, and returns +/// the results as a JSON buffer. +/// +/// # Safety +/// +/// This function is unsafe because it deals with raw pointers and memory allocation +/// across the FFI boundary. The caller is responsible for: +/// +/// - Ensuring the input pointers are valid and properly aligned +/// - Freeing the returned buffer using the `free_buffer` function +/// +/// # Arguments +/// +/// * `stats_json_ptr` - Pointer to a UTF-8 encoded JSON buffer +/// * `stats_json_len` - Length of the JSON buffer in bytes +/// * `out_json_ptr` - Pointer to a location where the output buffer pointer will be stored +/// * `out_json_len` - Pointer to a location where the output buffer length will be stored +/// +/// # Returns +/// +/// * `0` on success +/// * Negative values on various errors: +/// * `-1` - Null pointer provided +/// * `-2` - JSON parsing error +/// * `-3` - Analysis error +/// * `-4` - Serialization error +/// * `-5` - Memory allocation error +#endif + +#ifdef _WIN32 + #define NOCHEAT_API __declspec(dllimport) +#else + #define NOCHEAT_API +#endif + +typedef struct PlayerStats { + const char* player_id; + const char* stats_json; // JSON string with weapon stats, hits, etc. +} PlayerStats; + +typedef struct AnalysisResult { + const char* result_json; // JSON string with suspicion score and flags +} AnalysisResult; + +/** + * Analyzes player statistics for suspicious behavior + * @param stats_json_ptr Pointer to UTF-8 encoded JSON buffer containing player stats + * @param stats_json_len Length of the JSON buffer in bytes + * @param out_json_ptr Pointer to a location where output buffer pointer will be stored + * @param out_json_len Pointer to a location where output buffer length will be stored + * @return 0 on success, negative values on error + */ +NOCHEAT_API int analyze_round( + const unsigned char* stats_json_ptr, + size_t stats_json_len, + unsigned char** out_json_ptr, + size_t* out_json_len +); + +/** + * Frees memory allocated by analyze_round + * @param ptr Pointer to the buffer to free + * @param len Length of the buffer + */ +NOCHEAT_API void free_buffer( + unsigned char* ptr, + size_t len +); + +/** + * Set a custom path to load the model from + * @param path_ptr Pointer to a UTF-8 encoded path string + * @param path_len Length of the path string in bytes + * @return 0 on success, negative values on error: + * -1: Null path provided + * -2: Invalid UTF-8 path + * -3: File doesn't exist + * -4: Model couldn't be deserialized + */ +NOCHEAT_API int set_model_path( + const unsigned char* path_ptr, + size_t path_len +); + +#ifdef __cplusplus +} #endif \ No newline at end of file diff --git a/models/custom_cheat_model.bin b/models/custom_cheat_model.bin index 879fef4fb153bd401b8455a9d6e393ac16038dd0..525cf4512df26f4429f283a8c02400e7c1266aa9 100644 GIT binary patch literal 11786 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 diff --git a/models/default_cheat_model.bin b/models/default_cheat_model.bin new file mode 100644 index 0000000000000000000000000000000000000000..c0098c84cbd51ea9028ba253ee8837e11ae25a10 GIT binary patch literal 126206 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 HJ~;muGI7@T literal 0 HcmV?d00001 diff --git a/rust-analyzer.json b/rust-analyzer.json index 39472ab..9cd5058 100644 --- a/rust-analyzer.json +++ b/rust-analyzer.json @@ -1,7 +1,8 @@ { "cargo": { "excludeDir": [ - "examples" + "examples", + "docs" ] } } diff --git a/src/bin/train.rs b/src/bin/train.rs index 37a2a93..852594f 100644 --- a/src/bin/train.rs +++ b/src/bin/train.rs @@ -1,4 +1,4 @@ -use nocheat::types::PlayerStats; +use nocheat::types::LegacyPlayerStats; use nocheat::{generate_default_model, train_model}; use std::env; use std::fs::File; @@ -59,21 +59,17 @@ fn main() -> io::Result<()> { let file = File::open(training_data_path)?; let mut reader = BufReader::new(file); let mut contents = String::new(); - reader.read_to_string(&mut contents)?; - - // Parse the JSON into PlayerStats and labels - let training_data: Vec = match serde_json::from_str(&contents) { + reader.read_to_string(&mut contents)?; // Parse the JSON into PlayerStats and labels + let training_data: Vec = match serde_json::from_str(&contents) { Ok(data) => data, Err(e) => { eprintln!("Error parsing training data: {}", e); process::exit(1); } - }; - - // Extract labels from the training data + }; // Extract labels from the training data let labels: Vec = training_data .iter() - .filter_map(|stat| stat.training_label) + .filter_map(|stat| stat.data.training_label) .collect(); // Verify we have valid training data diff --git a/src/lib.rs b/src/lib.rs index ad3436f..3eb9d0a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,14 +10,15 @@ This library uses a RandomForest classifier to analyze player statistics and ide - Machine learning-based detection - C-compatible FFI for integration with game engines - DataFrame-based feature engineering +- Generic struct support for custom data analysis ## Usage Examples -Simple usage from Rust: +Simple usage with the default data structures: ```rust -use nocheat::{analyze_stats}; -use nocheat::types::{PlayerStats, AnalysisResponse}; +use nocheat::analyze_stats; +use nocheat::types::{DefaultPlayerData, PlayerStats}; use std::collections::HashMap; // Prepare player statistics @@ -26,8 +27,7 @@ shots.insert("rifle".to_string(), 100); let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 80); // Unusually high accuracy -let player_stats = PlayerStats { - player_id: "player123".to_string(), +let player_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots: 60, @@ -35,6 +35,8 @@ let player_stats = PlayerStats { training_label: None, }; +let player_stats = PlayerStats::new("player123".to_string(), player_data); + // Analyze the stats let analysis = analyze_stats(vec![player_stats]); if let Ok(response) = analysis { @@ -54,7 +56,10 @@ use std::{fs::File, ptr}; use std::collections::HashMap; pub mod types; -use types::{AnalysisResponse, PlayerResult, PlayerStats}; +use types::{ + DefaultAnalysisResult, DefaultPlayerData, LegacyAnalysisResponse, LegacyPlayerResult, + LegacyPlayerStats, PlayerStats, +}; /// Public wrapper for statistical analysis of player data to detect cheating. /// @@ -64,17 +69,17 @@ use types::{AnalysisResponse, PlayerResult, PlayerStats}; /// /// # Arguments /// -/// * `stats` - A vector of PlayerStats structures containing data to analyze +/// * `stats` - A vector of LegacyPlayerStats structures containing data to analyze /// /// # Returns /// -/// * `Result` - The analysis results wrapped in a Result +/// * `Result` - The analysis results wrapped in a Result /// /// # Example /// /// ```no_run /// use nocheat::{analyze_stats}; -/// use nocheat::types::PlayerStats; +/// use nocheat::types::{DefaultPlayerData, PlayerStats}; /// use std::collections::HashMap; /// /// // Create player statistics @@ -83,19 +88,20 @@ use types::{AnalysisResponse, PlayerResult, PlayerStats}; /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 50); /// -/// let stats = vec![PlayerStats { -/// player_id: "player123".to_string(), +/// let player_data = DefaultPlayerData { /// shots_fired: shots, /// hits: hits, /// headshots: 10, /// shot_timestamps_ms: None, /// training_label: None, -/// }]; +/// }; +/// +/// let stats = vec![PlayerStats::new("player123".to_string(), player_data)]; /// /// let results = analyze_stats(stats).expect("Analysis failed"); /// assert_eq!(results.results.len(), 1); /// ``` -pub fn analyze_stats(stats: Vec) -> Result { +pub fn analyze_stats(stats: Vec) -> Result { do_analysis(stats) } @@ -131,7 +137,7 @@ fn load_model(path: &str) -> Result { /// /// ``` /// use nocheat::{build_dataframe}; -/// use nocheat::types::PlayerStats; +/// use nocheat::types::{DefaultPlayerData, PlayerStats}; /// use std::collections::HashMap; /// /// // Create test player statistics @@ -140,23 +146,27 @@ fn load_model(path: &str) -> Result { /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 50); /// -/// let stats = vec![PlayerStats { -/// player_id: "player123".to_string(), +/// let player_data = DefaultPlayerData { /// shots_fired: shots, /// hits: hits, /// headshots: 10, /// shot_timestamps_ms: None, /// training_label: None, -/// }]; +/// }; +/// +/// let stats = vec![PlayerStats::new("player123".to_string(), player_data)]; /// /// let df = build_dataframe(&stats).expect("DataFrame creation failed"); /// assert_eq!(df.height(), 1); /// ``` -pub fn build_dataframe(stats: &[PlayerStats]) -> Result { +pub fn build_dataframe(stats: &[LegacyPlayerStats]) -> Result { let ids: Vec<&str> = stats.iter().map(|p| p.player_id.as_str()).collect(); - let shots: Vec = stats.iter().map(|p| p.shots_fired.values().sum()).collect(); - let hits: Vec = stats.iter().map(|p| p.hits.values().sum()).collect(); - let headshots: Vec = stats.iter().map(|p| p.headshots).collect(); + let shots: Vec = stats + .iter() + .map(|p| p.data.shots_fired.values().sum()) + .collect(); + let hits: Vec = stats.iter().map(|p| p.data.hits.values().sum()).collect(); + let headshots: Vec = stats.iter().map(|p| p.data.headshots).collect(); let df = df! { "player_id" => ids, @@ -186,7 +196,7 @@ pub fn build_dataframe(stats: &[PlayerStats]) -> Result { /// ```no_run /// // Note: This example is marked as no_run to avoid compilation issues in doctests /// use nocheat::{build_dataframe, df_to_ndarray}; -/// use nocheat::types::PlayerStats; +/// use nocheat::types::{DefaultPlayerData, PlayerStats}; /// use std::collections::HashMap; /// use polars::prelude::{col, IntoLazy, DataType}; /// @@ -196,14 +206,15 @@ pub fn build_dataframe(stats: &[PlayerStats]) -> Result { /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 50); /// -/// let stats = vec![PlayerStats { -/// player_id: "player123".to_string(), +/// let player_data = DefaultPlayerData { /// shots_fired: shots, /// hits: hits, /// headshots: 10, /// shot_timestamps_ms: None, /// training_label: None, -/// }]; +/// }; +/// +/// let stats = vec![PlayerStats::new("player123".to_string(), player_data)]; /// /// let df = build_dataframe(&stats).expect("DataFrame creation failed"); /// @@ -232,7 +243,7 @@ pub fn df_to_ndarray(df: &DataFrame, cols: &[&str]) -> Result> { } /// Core analysis function: feature engineering + RF inference -fn do_analysis(stats: Vec) -> Result { +fn do_analysis(stats: Vec) -> Result { // Check if we can load the model (for debugging) if !std::path::Path::new(unsafe { CURRENT_MODEL_PATH }).exists() { return Err(anyhow::anyhow!("{} does not exist", unsafe { @@ -279,14 +290,16 @@ fn do_analysis(stats: Vec) -> Result { flags.push("HighHitRate".to_string()); } - results.push(PlayerResult { - player_id: stat.player_id, - suspicion_score: score, - flags, - }); + results.push(LegacyPlayerResult::new( + stat.player_id, + DefaultAnalysisResult { + suspicion_score: score, + flags, + }, + )); } - Ok(AnalysisResponse { results }) + Ok(LegacyAnalysisResponse { results }) } /// Train a new cheat detection model and save it to disk. @@ -307,8 +320,8 @@ fn do_analysis(stats: Vec) -> Result { /// # Example /// /// ```no_run -/// use nocheat::{train_model}; -/// use nocheat::types::PlayerStats; +/// use nocheat::train_model; +/// use nocheat::types::{DefaultPlayerData, PlayerStats}; /// use std::collections::HashMap; /// /// // Create training data @@ -321,14 +334,18 @@ fn do_analysis(stats: Vec) -> Result { /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 50); // 50% accuracy is normal /// -/// training_data.push(PlayerStats { -/// player_id: "normal_player".to_string(), +/// let normal_player_data = DefaultPlayerData { /// shots_fired: shots.clone(), /// hits: hits.clone(), /// headshots: 10, // 20% headshot ratio is normal /// shot_timestamps_ms: None, /// training_label: None, -/// }); +/// }; +/// +/// training_data.push(PlayerStats::new( +/// "normal_player".to_string(), +/// normal_player_data +/// )); /// labels.push(0.0); // Not a cheater /// /// // Example of a cheating player @@ -337,21 +354,25 @@ fn do_analysis(stats: Vec) -> Result { /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 95); // 95% accuracy is suspicious /// -/// training_data.push(PlayerStats { -/// player_id: "cheater".to_string(), +/// let cheater_player_data = DefaultPlayerData { /// shots_fired: shots, /// hits: hits, /// headshots: 70, // 70% headshot ratio is very suspicious /// shot_timestamps_ms: None, /// training_label: None, -/// }); +/// }; +/// +/// training_data.push(PlayerStats::new( +/// "cheater".to_string(), +/// cheater_player_data +/// )); /// labels.push(1.0); // Labeled as a cheater /// /// // Train and save model /// train_model(training_data, labels, "cheat_model.bin").expect("Failed to train model"); /// ``` pub fn train_model( - training_data: Vec, + training_data: Vec, labels: Vec, output_path: &str, ) -> Result<()> { @@ -467,14 +488,18 @@ pub fn generate_default_model(output_path: &str) -> Result<()> { let headshot_ratio = 0.1 + (i % 15) as f32 * 0.01; let headshots = (hit_count as f32 * headshot_ratio) as u32; - training_data.push(PlayerStats { - player_id: format!("normal_player_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, hits, headshots, shot_timestamps_ms: None, training_label: Some(0.0), - }); + }; + + training_data.push(PlayerStats::new( + format!("normal_player_{}", i), + player_data, + )); labels.push(0.0); // Not a cheater } @@ -498,14 +523,16 @@ pub fn generate_default_model(output_path: &str) -> Result<()> { let headshot_ratio = 0.4 + (i % 40) as f32 * 0.01; let headshots = (hit_count as f32 * headshot_ratio) as u32; - training_data.push(PlayerStats { - player_id: format!("cheater_{}", i), - shots_fired: shots, - hits, - headshots, - shot_timestamps_ms: None, - training_label: Some(1.0), - }); + training_data.push(PlayerStats::new( + format!("cheater_{}", i), + DefaultPlayerData { + shots_fired: shots, + hits, + headshots, + shot_timestamps_ms: None, + training_label: Some(1.0), + }, + )); labels.push(1.0); // Labeled as a cheater } @@ -556,7 +583,7 @@ pub unsafe extern "C" fn analyze_round( return -1; } let input = std::slice::from_raw_parts(stats_json_ptr, stats_json_len); - let stats: Vec = match serde_json::from_slice(input) { + let stats: Vec = match serde_json::from_slice(input) { Ok(v) => v, Err(_) => return -2, }; @@ -593,7 +620,7 @@ pub unsafe extern "C" fn free_buffer(ptr: *mut c_uchar, len: size_t) { /// Serialize response and allocate C buffer fn write_buffer( - resp: &AnalysisResponse, + resp: &LegacyAnalysisResponse, out_json_ptr: *mut *mut c_uchar, out_json_len: *mut size_t, ) -> c_int { @@ -677,7 +704,7 @@ mod tests { use std::collections::HashMap; use std::fs; - fn create_test_stats() -> Vec { + fn create_test_stats() -> Vec { let mut shots1 = HashMap::new(); shots1.insert("rifle".to_string(), 100); let mut hits1 = HashMap::new(); @@ -691,22 +718,26 @@ mod tests { hits2.insert("pistol".to_string(), 45); // suspicious hit rate vec![ - PlayerStats { - player_id: "normal_player".to_string(), - shots_fired: shots1, - hits: hits1, - headshots: 10, - shot_timestamps_ms: None, - training_label: None, - }, - PlayerStats { - player_id: "suspicious_player".to_string(), - shots_fired: shots2, - hits: hits2, - headshots: 50, // suspicious headshot count - shot_timestamps_ms: None, - training_label: None, - }, + PlayerStats::new( + "normal_player".to_string(), + DefaultPlayerData { + shots_fired: shots1, + hits: hits1, + headshots: 10, + shot_timestamps_ms: None, + training_label: None, + }, + ), + PlayerStats::new( + "suspicious_player".to_string(), + DefaultPlayerData { + shots_fired: shots2, + hits: hits2, + headshots: 50, // suspicious headshot count + shot_timestamps_ms: None, + training_label: None, + }, + ), ] } @@ -796,14 +827,15 @@ mod tests { let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 50); - training_data.push(PlayerStats { - player_id: "normal_player".to_string(), + let player_data = DefaultPlayerData { shots_fired: shots, hits, headshots: 10, shot_timestamps_ms: None, training_label: None, - }); + }; + + training_data.push(PlayerStats::new("normal_player".to_string(), player_data)); labels.push(0.0); // Add a cheating player @@ -812,14 +844,15 @@ mod tests { let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 95); - training_data.push(PlayerStats { - player_id: "cheater".to_string(), + let player_data = DefaultPlayerData { shots_fired: shots, hits, headshots: 70, shot_timestamps_ms: None, training_label: None, - }); + }; + + training_data.push(PlayerStats::new("cheater".to_string(), player_data)); labels.push(1.0); // Train the model diff --git a/src/types.rs b/src/types.rs index afea069..83a64ae 100644 --- a/src/types.rs +++ b/src/types.rs @@ -3,24 +3,22 @@ use std::collections::HashMap; /// Represents player statistics from a game round. /// -/// This structure contains all the statistics for a single player that are -/// needed to analyze whether the player might be cheating. +/// This structure is generic over `T`, which allows it to work with any JSON structure. /// /// # Example /// /// ```no_run -/// use nocheat::types::PlayerStats; +/// use nocheat::types::{PlayerStats, DefaultPlayerData}; /// use std::collections::HashMap; /// -/// // Create stats for a player +/// // Create stats for a player using the default data structure /// let mut shots = HashMap::new(); /// shots.insert("rifle".to_string(), 100); /// /// let mut hits = HashMap::new(); /// hits.insert("rifle".to_string(), 50); /// -/// let player_stats = PlayerStats { -/// player_id: "player123".to_string(), +/// let player_data = DefaultPlayerData { /// shots_fired: shots, /// hits: hits, /// headshots: 10, @@ -28,12 +26,50 @@ use std::collections::HashMap; /// training_label: None, /// }; /// +/// let player_stats = PlayerStats::new("player123".to_string(), player_data); +/// /// assert_eq!(player_stats.player_id, "player123"); /// ``` -#[derive(Deserialize, Clone)] -pub struct PlayerStats { +#[derive(Clone, Debug, Serialize)] +pub struct PlayerStats +where + T: Clone + Serialize, +{ /// Unique identifier for the player pub player_id: String, + /// Generic data structure containing player statistics + pub data: T, +} + +impl PlayerStats +where + T: Clone + Serialize, +{ + /// Creates a new PlayerStats instance with the given player ID and data + pub fn new(player_id: String, data: T) -> Self { + PlayerStats { player_id, data } + } + + /// Converts this PlayerStats instance into one with a different data type + /// using the provided conversion function + pub fn convert(&self, converter: F) -> PlayerStats + where + U: Clone + Serialize, + F: FnOnce(&T) -> U, + { + PlayerStats { + player_id: self.player_id.clone(), + data: converter(&self.data), + } + } +} + +/// Default data structure for player statistics +/// +/// This provides the original functionality of PlayerStats but now as a separate +/// data structure that can be used with the generic PlayerStats +#[derive(Deserialize, Clone, Debug, Serialize)] +pub struct DefaultPlayerData { /// Number of shots fired per weapon type pub shots_fired: HashMap, /// Number of successful hits registered per weapon type @@ -50,26 +86,52 @@ pub struct PlayerStats { /// Analysis result for a single player. /// /// Contains the suspicion score and a list of flags indicating -/// suspicious behaviors detected for the player. +/// suspicious behaviors detected for the player. This struct is generic +/// to allow for different types of analysis based on the data type. /// /// # Example /// /// ```no_run -/// use nocheat::types::PlayerResult; +/// use nocheat::types::{PlayerResult, DefaultAnalysisResult}; /// -/// let result = PlayerResult { -/// player_id: "player123".to_string(), -/// suspicion_score: 0.75, -/// flags: vec!["HighHeadshotRatio".to_string()], -/// }; +/// let result = PlayerResult::new( +/// "player123".to_string(), +/// DefaultAnalysisResult { +/// suspicion_score: 0.75, +/// flags: vec!["HighHeadshotRatio".to_string()], +/// } +/// ); /// -/// assert!(result.suspicion_score > 0.7); -/// assert!(result.flags.contains(&"HighHeadshotRatio".to_string())); +/// assert!(result.data.suspicion_score > 0.7); +/// assert!(result.data.flags.contains(&"HighHeadshotRatio".to_string())); /// ``` -#[derive(Serialize, Debug, PartialEq)] -pub struct PlayerResult { +#[derive(Debug, PartialEq, Serialize)] +pub struct PlayerResult +where + R: Serialize + PartialEq, +{ /// Unique identifier for the player (same as in PlayerStats) pub player_id: String, + /// Generic result data + pub data: R, +} + +impl PlayerResult +where + R: Serialize + PartialEq, +{ + /// Creates a new PlayerResult with the given player ID and result data + pub fn new(player_id: String, data: R) -> Self { + PlayerResult { player_id, data } + } +} + +/// Default analysis result data structure +/// +/// This provides the original functionality of PlayerResult but now as a separate +/// data structure that can be used with the generic PlayerResult +#[derive(Debug, PartialEq, Serialize)] +pub struct DefaultAnalysisResult { /// Score between 0.0 and 1.0 indicating likelihood of cheating pub suspicion_score: f32, /// List of flags indicating specific suspicious behaviors @@ -77,34 +139,139 @@ pub struct PlayerResult { } /// Response wrapper containing analysis results for multiple players. +/// This struct is generic to work with different analysis result types. /// /// # Example /// /// ```no_run -/// use nocheat::types::{AnalysisResponse, PlayerResult}; +/// use nocheat::types::{AnalysisResponse, PlayerResult, DefaultAnalysisResult}; /// /// let response = AnalysisResponse { /// results: vec![ -/// PlayerResult { -/// player_id: "player123".to_string(), -/// suspicion_score: 0.75, -/// flags: vec!["HighHeadshotRatio".to_string()], -/// }, -/// PlayerResult { -/// player_id: "player456".to_string(), -/// suspicion_score: 0.2, -/// flags: vec![], -/// } +/// PlayerResult::new( +/// "player123".to_string(), +/// DefaultAnalysisResult { +/// suspicion_score: 0.75, +/// flags: vec!["HighHeadshotRatio".to_string()], +/// } +/// ), +/// PlayerResult::new( +/// "player456".to_string(), +/// DefaultAnalysisResult { +/// suspicion_score: 0.2, +/// flags: vec![], +/// } +/// ) /// ], /// }; /// /// assert_eq!(response.results.len(), 2); -/// assert!(response.results[0].suspicion_score > response.results[1].suspicion_score); +/// assert!(response.results[0].data.suspicion_score > response.results[1].data.suspicion_score); /// ``` -#[derive(Serialize, Debug, PartialEq)] -pub struct AnalysisResponse { +#[derive(Debug, PartialEq, Serialize)] +pub struct AnalysisResponse +where + R: Serialize + PartialEq, +{ /// List of analysis results for all players - pub results: Vec, + pub results: Vec>, +} + +/// Type alias for backward compatibility with the original PlayerStats struct +pub type LegacyPlayerStats = PlayerStats; + +/// Type alias for backward compatibility with the original PlayerResult struct +pub type LegacyPlayerResult = PlayerResult; + +/// Type alias for backward compatibility with the original AnalysisResponse struct +pub type LegacyAnalysisResponse = AnalysisResponse; + +/// Implementation for deserialization of PlayerStats with a DefaultPlayerData structure +impl<'de> Deserialize<'de> for PlayerStats { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Custom struct that handles flattened fields + #[derive(Deserialize)] + struct FlatPlayerStats { + player_id: String, + shots_fired: HashMap, + hits: HashMap, + headshots: u32, + shot_timestamps_ms: Option>, + #[serde(default)] + training_label: Option, + } + + let flat = FlatPlayerStats::deserialize(deserializer)?; + + // Create the nested structure + let data = DefaultPlayerData { + shots_fired: flat.shots_fired, + hits: flat.hits, + headshots: flat.headshots, + shot_timestamps_ms: flat.shot_timestamps_ms, + training_label: flat.training_label, + }; + + Ok(PlayerStats { + player_id: flat.player_id, + data, + }) + } +} + +/// A trait for data types that can be analyzed for cheating behavior +pub trait Analyzable { + /// Calculate the accuracy rate for this player + fn calculate_accuracy_rate(&self) -> f32; + + /// Calculate the headshot ratio for this player + fn calculate_headshot_ratio(&self) -> f32; + + /// Extract features for machine learning models + fn extract_features(&self) -> Vec; + + /// Check if this player's behavior is suspicious + fn is_suspicious(&self) -> bool; +} + +impl Analyzable for DefaultPlayerData { + fn calculate_accuracy_rate(&self) -> f32 { + let total_shots: u32 = self.shots_fired.values().sum(); + let total_hits: u32 = self.hits.values().sum(); + + if total_shots == 0 { + return 0.0; + } + + total_hits as f32 / total_shots as f32 + } + + fn calculate_headshot_ratio(&self) -> f32 { + let total_hits: u32 = self.hits.values().sum(); + + if total_hits == 0 { + return 0.0; + } + + self.headshots as f32 / total_hits as f32 + } + + fn extract_features(&self) -> Vec { + vec![ + self.calculate_accuracy_rate(), + self.calculate_headshot_ratio(), + ] + } + + fn is_suspicious(&self) -> bool { + let accuracy = self.calculate_accuracy_rate(); + let headshot_ratio = self.calculate_headshot_ratio(); + + accuracy > 0.8 || headshot_ratio > 0.5 + } } #[cfg(test)] @@ -121,8 +288,7 @@ mod tests { hits.insert("rifle".to_string(), 50); hits.insert("pistol".to_string(), 15); - let stats = PlayerStats { - player_id: "player123".to_string(), + let player_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots: 10, @@ -130,41 +296,48 @@ mod tests { training_label: None, }; + let stats = PlayerStats::new("player123".to_string(), player_data); + assert_eq!(stats.player_id, "player123"); - assert_eq!(*stats.shots_fired.get("rifle").unwrap(), 100); - assert_eq!(*stats.hits.get("pistol").unwrap(), 15); - assert_eq!(stats.headshots, 10); - assert_eq!(stats.shot_timestamps_ms.unwrap().len(), 3); + assert_eq!(*stats.data.shots_fired.get("rifle").unwrap(), 100); + assert_eq!(*stats.data.hits.get("pistol").unwrap(), 15); + assert_eq!(stats.data.headshots, 10); + assert_eq!(stats.data.shot_timestamps_ms.unwrap().len(), 3); } #[test] fn test_player_result_creation() { - let result = PlayerResult { - player_id: "player123".to_string(), + let result_data = DefaultAnalysisResult { suspicion_score: 0.75, flags: vec!["HighHeadshotRatio".to_string(), "AimSnap".to_string()], }; + let result = PlayerResult::new("player123".to_string(), result_data); + assert_eq!(result.player_id, "player123"); - assert_eq!(result.suspicion_score, 0.75); - assert_eq!(result.flags.len(), 2); - assert!(result.flags.contains(&"HighHeadshotRatio".to_string())); + assert_eq!(result.data.suspicion_score, 0.75); + assert_eq!(result.data.flags.len(), 2); + assert!(result.data.flags.contains(&"HighHeadshotRatio".to_string())); } #[test] fn test_analysis_response_creation() { let response = AnalysisResponse { results: vec![ - PlayerResult { - player_id: "player123".to_string(), - suspicion_score: 0.75, - flags: vec!["HighHeadshotRatio".to_string()], - }, - PlayerResult { - player_id: "player456".to_string(), - suspicion_score: 0.2, - flags: vec![], - }, + PlayerResult::new( + "player123".to_string(), + DefaultAnalysisResult { + suspicion_score: 0.75, + flags: vec!["HighHeadshotRatio".to_string()], + }, + ), + PlayerResult::new( + "player456".to_string(), + DefaultAnalysisResult { + suspicion_score: 0.2, + flags: vec![], + }, + ), ], }; @@ -172,4 +345,78 @@ mod tests { assert_eq!(response.results[0].player_id, "player123"); assert_eq!(response.results[1].player_id, "player456"); } + + #[test] + fn test_convert_player_stats() { + let mut shots = HashMap::new(); + shots.insert("rifle".to_string(), 100); + + let mut hits = HashMap::new(); + hits.insert("rifle".to_string(), 50); + + let player_data = DefaultPlayerData { + shots_fired: shots, + hits: hits, + headshots: 10, + shot_timestamps_ms: None, + training_label: None, + }; + + let legacy_stats = PlayerStats::new("player123".to_string(), player_data); + + // Define a simple custom data type + #[derive(Clone, Debug, Serialize)] + struct SimpleData { + accuracy: f32, + headshot_ratio: f32, + } + + // Convert to the custom data type + let custom_stats = legacy_stats.convert(|data| { + let accuracy = data.calculate_accuracy_rate(); + let headshot_ratio = data.calculate_headshot_ratio(); + + SimpleData { + accuracy, + headshot_ratio, + } + }); + + assert_eq!(custom_stats.player_id, "player123"); + assert_eq!(custom_stats.data.accuracy, 0.5); + assert_eq!(custom_stats.data.headshot_ratio, 0.2); + } + #[test] + fn test_analyzable_trait() { + let mut shots = HashMap::new(); + shots.insert("rifle".to_string(), 100); + shots.insert("pistol".to_string(), 50); + + let mut hits = HashMap::new(); + hits.insert("rifle".to_string(), 90); // 90% accuracy for rifle (suspicious) + hits.insert("pistol".to_string(), 40); // 80% accuracy for pistol (suspicious) + + let player_data = DefaultPlayerData { + shots_fired: shots, + hits: hits, + headshots: 70, // Very high headshot count (suspicious) + shot_timestamps_ms: None, + training_label: None, + }; + + // Calculate accuracy rate + assert_eq!(player_data.calculate_accuracy_rate(), 130.0 / 150.0); + + // Calculate headshot ratio + assert_eq!(player_data.calculate_headshot_ratio(), 70.0 / 130.0); + + // Check if suspicious - should be true with these values + assert!(player_data.is_suspicious()); + + // Extract features + let features = player_data.extract_features(); + assert_eq!(features.len(), 2); + assert_eq!(features[0], 130.0 / 150.0); + assert_eq!(features[1], 70.0 / 130.0); + } } diff --git a/tests/analisys_test.rs b/tests/analisys_test.rs index 02d018a..d875fce 100644 --- a/tests/analisys_test.rs +++ b/tests/analisys_test.rs @@ -1,23 +1,24 @@ -use nocheat::types::PlayerStats; +use nocheat::types::{DefaultPlayerData, LegacyPlayerStats, PlayerStats}; use nocheat::{analyze_stats, build_dataframe, df_to_ndarray, generate_default_model, train_model}; use polars::prelude::{col, DataType, IntoLazy}; use std::collections::HashMap; use std::fs; -fn make_dummy_stats() -> Vec { +fn make_dummy_stats() -> Vec { let mut shots = HashMap::new(); shots.insert("rifle".to_string(), 100); let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 50); - vec![PlayerStats { - player_id: "player1".to_string(), + let player_data = DefaultPlayerData { shots_fired: shots, hits, headshots: 10, shot_timestamps_ms: None, training_label: None, - }] + }; + + vec![PlayerStats::new("player1".to_string(), player_data)] } #[test] @@ -84,14 +85,15 @@ fn test_training_workflow() { let headshot_ratio = 0.1 + (i % 10) as f32 * 0.01; let headshots = (hit_count as f32 * headshot_ratio) as u32; - training_data.push(PlayerStats { - player_id: format!("normal_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots, shot_timestamps_ms: None, training_label: Some(0.0), - }); + }; + + training_data.push(PlayerStats::new(format!("normal_{}", i), player_data)); labels.push(0.0); // Not a cheater } @@ -113,14 +115,15 @@ fn test_training_workflow() { let headshot_ratio = 0.4 + (i % 30) as f32 * 0.01; let headshots = (hit_count as f32 * headshot_ratio) as u32; - training_data.push(PlayerStats { - player_id: format!("cheater_{}", i), + let player_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots, shot_timestamps_ms: None, training_label: Some(1.0), - }); + }; + + training_data.push(PlayerStats::new(format!("cheater_{}", i), player_data)); labels.push(1.0); // Labeled as a cheater } @@ -141,8 +144,7 @@ fn test_training_workflow() { let mut hits_normal = HashMap::new(); hits_normal.insert("rifle".to_string(), 50); // 50% accuracy - let normal_player = PlayerStats { - player_id: "test_normal".to_string(), + let normal_data = DefaultPlayerData { shots_fired: test_normal, hits: hits_normal, headshots: 10, // 20% headshot ratio @@ -150,13 +152,14 @@ fn test_training_workflow() { training_label: None, }; + let normal_player = PlayerStats::new("test_normal".to_string(), normal_data); + let mut test_suspicious = HashMap::new(); test_suspicious.insert("rifle".to_string(), 100); let mut hits_suspicious = HashMap::new(); hits_suspicious.insert("rifle".to_string(), 90); // 90% accuracy - let suspicious_player = PlayerStats { - player_id: "test_suspicious".to_string(), + let suspicious_data = DefaultPlayerData { shots_fired: test_suspicious, hits: hits_suspicious, headshots: 70, // 78% headshot ratio @@ -164,6 +167,8 @@ fn test_training_workflow() { training_label: None, }; + let suspicious_player = PlayerStats::new("test_suspicious".to_string(), suspicious_data); + // Save the original model file path if it exists, so we can restore it after the test let original_model_exists = std::path::Path::new("cheat_model.bin").exists(); let backup_path = temp_dir.join("backup_cheat_model.bin"); @@ -197,11 +202,9 @@ fn test_training_workflow() { assert!(result.is_ok(), "Analysis failed"); let analysis = result.unwrap(); - assert_eq!(analysis.results.len(), 2, "Expected 2 analysis results"); - - // 5. Verify the results - normal player should have low score, suspicious high score - let normal_score = analysis.results[0].suspicion_score; - let suspicious_score = analysis.results[1].suspicion_score; + assert_eq!(analysis.results.len(), 2, "Expected 2 analysis results"); // 5. Verify the results - normal player should have low score, suspicious high score + let normal_score = analysis.results[0].data.suspicion_score; + let suspicious_score = analysis.results[1].data.suspicion_score; println!("Normal player score: {}", normal_score); println!("Suspicious player score: {}", suspicious_score); @@ -254,8 +257,7 @@ fn test_generate_default_model() { let mut hits = HashMap::new(); hits.insert("rifle".to_string(), 95); // 95% accuracy - let suspicious_player = PlayerStats { - player_id: "suspicious".to_string(), + let suspicious_data = DefaultPlayerData { shots_fired: shots, hits: hits, headshots: 80, // 84% headshot ratio (very suspicious) @@ -263,6 +265,8 @@ fn test_generate_default_model() { training_label: None, }; + let suspicious_player = PlayerStats::new("suspicious".to_string(), suspicious_data); + // Save the original model file path if it exists, so we can restore it after the test let original_model_exists = std::path::Path::new("cheat_model.bin").exists(); let backup_path = temp_dir.join("backup_cheat_model.bin"); @@ -293,9 +297,8 @@ fn test_generate_default_model() { } assert!(analysis.is_ok(), "Analysis with default model failed"); - let result = analysis.unwrap(); - let score = result.results[0].suspicion_score; + let score = result.results[0].data.suspicion_score; println!("Suspicious player score: {}", score); // With such suspicious stats, the score should be high diff --git a/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h b/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h new file mode 100644 index 0000000..f7bc0c6 --- /dev/null +++ b/ue_plugin/ThirdParty/NoCheatLib/include/nocheat.h @@ -0,0 +1,95 @@ +/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ + +#include +#include +#include +#include +#include + +namespace nocheat { + +extern "C" { + +/// FFI: analyze a JSON buffer of PlayerStats; returns JSON buffer +/// +/// This function provides a C-compatible interface for the cheat detection system. +/// It takes a JSON buffer containing player statistics, analyzes them, and returns +/// the results as a JSON buffer. +/// +/// # Safety +/// +/// This function is unsafe because it deals with raw pointers and memory allocation +/// across the FFI boundary. The caller is responsible for: +/// +/// - Ensuring the input pointers are valid and properly aligned +/// - Freeing the returned buffer using the `free_buffer` function +/// +/// # Arguments +/// +/// * `stats_json_ptr` - Pointer to a UTF-8 encoded JSON buffer +/// * `stats_json_len` - Length of the JSON buffer in bytes +/// * `out_json_ptr` - Pointer to a location where the output buffer pointer will be stored +/// * `out_json_len` - Pointer to a location where the output buffer length will be stored +/// +/// # Returns +/// +/// * `0` on success +/// * Negative values on various errors: +/// * `-1` - Null pointer provided +/// * `-2` - JSON parsing error +/// * `-3` - Analysis error +/// * `-4` - Serialization error +/// * `-5` - Memory allocation error +int analyze_round(const unsigned char *stats_json_ptr, + size_t stats_json_len, + unsigned char **out_json_ptr, + size_t *out_json_len); + +/// Companion to free allocated buffer +/// +/// This function must be called to free the memory allocated by `analyze_round`. +/// +/// # Safety +/// +/// This function is unsafe because it deals with raw pointers and memory deallocation. +/// The caller must ensure that: +/// +/// - The pointer was previously allocated by `analyze_round` +/// - The pointer has not already been freed +/// - The length matches what was given in `out_json_len` +/// +/// # Arguments +/// +/// * `ptr` - Pointer to the buffer to free +/// * `len` - Length of the buffer in bytes +void free_buffer(unsigned char *ptr, size_t len); + +/// Set the path to load a custom model +/// +/// This function allows loading a custom model from a specified path. +/// It's particularly useful when integrating with game engines like Unreal Engine +/// where the default path may not be accessible or when you want to load different models. +/// +/// # Safety +/// +/// This function is unsafe because it: +/// - Modifies a global static variable that affects all future model loading +/// - Takes a raw pointer that must be valid UTF-8 encoded path string +/// +/// # Arguments +/// +/// * `path_ptr` - Pointer to a null-terminated UTF-8 encoded string containing the model path +/// * `path_len` - Length of the path string in bytes (not including null terminator) +/// +/// # Returns +/// +/// * `0` on success +/// * `-1` if the path pointer is null +/// * `-2` if the path is not valid UTF-8 +/// * `-3` if the model file doesn't exist or can't be opened +/// * `-4` if the model couldn't be deserialized (invalid format) +int set_model_path(const unsigned char *path_ptr, size_t path_len); + +} // extern "C" + +} // namespace nocheat From 719cdb0af415343209160b374d85bedbe73b6ab2 Mon Sep 17 00:00:00 2001 From: Filipe Veloso Date: Fri, 23 May 2025 09:14:11 -0300 Subject: [PATCH 2/2] Add license and coc --- CODE_OF_CONDUCT.md | 14 ++++++++++++++ LICENSE | 21 +++++++++++++++++++++ README.md | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..2af2b95 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,14 @@ +# Code of Conduct + +We want this project to be a welcoming and collaborative space for everyone who contributes code, documentation, or discussion. To keep interactions positive and focused on the project, please follow these guidelines: + +1. Be respectful and courteous. +2. Use clear, constructive language when giving feedback. +3. Avoid personal attacks, insults, or derogatory remarks. +4. Keep discussions and contributions relevant to the project; avoid political or ideological topics. +5. Do not politicize issues; proposals should focus on technical merit and project goals. +6. If you disagree, discuss ideas, not people. + +Any behavior that violates these principles—harassment, hate speech, trolling, or attempts to push unrelated political agendas—may result in removal of comments, denial of contributions, or blocking from the project. + +Thank you for helping maintain a collaborative and focused community. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3103639 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0505ad5..bb64393 100644 --- a/README.md +++ b/README.md @@ -591,8 +591,39 @@ To customize the detection for your specific game, you can: ## License -[Include your license information here] +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) ## Contributing -[Include contribution guidelines here] \ No newline at end of file +We welcome contributions from the community! By participating, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md). + +### Reporting Issues + +- Please search existing issues before opening a new one. +- Provide a clear, descriptive title and detailed steps to reproduce. +- Include rustc version, OS, and any relevant logs or outputs. + +### Pull Requests + +1. Fork the repository and create your branch from `main`. +2. Follow the Rust style guidelines (run `cargo fmt` and `cargo clippy`). +3. Write tests for your changes and ensure all existing tests pass (`cargo test`). +4. Update documentation in `README.md` or `docs/` as needed. +5. Submit a pull request with a clear description and link to any related issue. + +### Development Setup + +1. Install Rust via [rustup](https://rustup.rs/). +2. Clone the repo: `git clone https://github.com/yourusername/nocheat.git`. +3. Navigate into the project: `cd nocheat`. +4. Build and test: `cargo build && cargo test`. +5. Generate documentation: `cargo doc --open`. + +### Code Style + +- We use `rustfmt` for formatting and `clippy` for linting. +- Please adhere to the existing code patterns and naming conventions. + +### License + +All contributions will be made under the MIT License, ensuring that the project remains free and open source. \ No newline at end of file