DISCLAIMER: Comments throughout this codebase were generated with AI assistance to help users find and understand code for reference while building FraGG 3.0. There may be mistakes in the comments. Please verify accuracy.
A CS2 demo parser that calculates advanced player performance ratings based on probability-based impact metrics, economic context, and round swing analysis.
This parser processes CS2 demo files and computes comprehensive player statistics including:
- Probability Swing: How much each action affected win probability
- Economic Impact: Equipment-adjusted kill values (export stat)
- 140+ tracked statistics: Opening kills, trades, clutches, utility, AWP stats, etc.
# Single demo
eco-rating -demo=path/to/demo.dem
# Cumulative mode (batch process from cloud bucket)
eco-rating -cumulative -tier=contendereco-rating/
├── main.go # Entry point, CLI handling
├── config/ # Configuration loading
├── bucket/ # Cloud storage client
├── downloader/ # Demo download & extraction
├── parser/ # Demo parsing (core logic)
│ ├── parser.go # Main DemoParser struct
│ ├── handlers.go # Event handlers (kills, damage, rounds)
│ ├── round.go # MatchState management
│ ├── side_stats.go # T/CT side stat updates
│ ├── trade_detector.go # Trade kill detection
│ ├── swing_tracker.go # Probability swing tracking
│ └── damage_tracker.go # Damage attribution
├── model/ # Data structures
│ ├── player_stats.go # PlayerStats struct (all tracked stats)
│ ├── round_stats.go # RoundStats struct (per-round data)
│ └── round_context_builder.go
├── rating/ # Rating calculations
│ ├── rating.go # Final rating computation
│ ├── weights.go # ALL constants and weights
│ ├── economy.go # Economic kill/death values
│ ├── hltv.go # HLTV 2.0 rating calculation
│ ├── probability/ # Win probability engine
│ └── swing/ # Swing calculation & attribution
├── output/ # Statistics aggregation
│ └── aggregator.go # Multi-game stat aggregation
└── export/ # Export to CSV/JSON
Edit model/player_stats.go to add your new stat:
type PlayerStats struct {
// ... existing fields ...
// Your new stat
MyNewStat int `json:"my_new_stat"`
MyNewStatPct float64 `json:"my_new_stat_pct"` // If it needs a percentage
}If your stat is tracked per-round, add it to model/round_stats.go:
type RoundStats struct {
// ... existing fields ...
MyNewStatThisRound int
}Edit parser/handlers.go to track your stat during parsing. Find the appropriate handler:
- Kill events:
handleKill()or create a newprocessMyNewStat()function - Damage events:
handlePlayerHurt() - Round events:
handleRoundEnd() - Bomb events:
handleBombPlanted(),handleBombDefused()
Example - tracking a new kill-related stat:
// In handlers.go, add to processKillerStats or create new function
func (d *DemoParser) processMyNewStat(ctx *killContext) {
if someCondition {
attacker := d.state.ensurePlayer(ctx.attacker)
round := d.state.ensureRound(ctx.attacker)
attacker.MyNewStat++
round.MyNewStatThisRound++
}
}
// Call it from handleKill()
func (d *DemoParser) handleKill(e events.Kill) {
// ... existing code ...
d.processMyNewStat(ctx)
}If your stat needs a per-round rate or percentage, add it to parser/parser.go in computeDerivedStats():
func (d *DemoParser) computeDerivedStats() {
for _, p := range d.state.Players {
if p.RoundsPlayed > 0 {
rounds := float64(p.RoundsPlayed)
// ... existing calculations ...
// Your new derived metric
p.MyNewStatPct = float64(p.MyNewStat) / rounds
}
}
}Edit output/aggregator.go:
- Add field to
AggregatedStatsstruct - Add accumulation in
AddGame():agg.MyNewStat += p.MyNewStat
- Add derived calculation in
Finalize()if needed
Edit export/file.go:
-
Add column to
getSingleGameHeader():return []string{ // ... existing headers ... "My New Stat", "My New Stat Pct", }
-
Add value to
getSingleGameRow():return []string{ // ... existing values ... strconv.Itoa(p.MyNewStat), formatFloat(p.MyNewStatPct), }
-
Repeat for
getAggregatedHeader()andgetAggregatedRow()if used in cumulative mode.
The final rating uses a probability-based system that measures how much each player's actions affected their team's win probability.
The eco-rating is computed in rating/rating.go:
rating = 1.0 // Baseline
+ adrContrib // ADR above/below 77
+ kastContrib // KAST above/below 72%
+ probSwingContrib // Probability swing (core metric)ProbSwingContribMultiplier = 2.5 // How much probability swing affects rating
ADRContribAbove = 0.005 // Bonus per ADR point above 77
ADRContribBelow = 0.004 // Penalty per ADR point below 77
KASTContribAbove = 0.20 // Bonus per KAST % above 72%
KASTContribBelow = 0.25 // Penalty per KAST % below 72%The probability engine (rating/probability/) calculates win probability based on:
- Players alive on each team
- Equipment values
- Bomb status
- Time remaining
Each action (kill, death, bomb plant/defuse) creates a swing:
- Before action: Calculate win probability (e.g., 45%)
- After action: Calculate new probability (e.g., 55%)
- Swing: The delta (+10%)
This is accumulated per player and becomes the primary rating driver.
Kill, Assist, Survive, or Traded. Percentage of rounds where player contributed.
A kill that avenges a teammate's death within 5 seconds.
Win probability delta from player actions. A kill that moves win probability from 30% to 50% = +20% swing.
Kill value adjusted for equipment advantage. Killing a rifle player with a pistol is worth 1.8x; killing a pistol player with a rifle is worth 0.7x.
| File | Purpose |
|---|---|
model/player_stats.go |
Add new stat fields |
model/round_stats.go |
Add per-round tracking fields |
parser/handlers.go |
Track stats during parsing |
parser/parser.go |
Calculate derived metrics |
output/aggregator.go |
Accumulate stats across games |
export/file.go |
Add to CSV export |
rating/weights.go |
Rating constants and baselines |
rating/rating.go |
Final rating formula |
rating/economy.go |
Economic kill/death values |
Review the inline comments in each file. Comments were generated with AI assistance to help explain the code, though there may be mistakes.