Manager.Stats.cs, part of the Manager class. Handles collecting and submitting player run statistics, leaderboard score computation, achievement unlocking stubs, and formatting stat keys for the game services.
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Services;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Xml.Linq;
public enum StatType { LeaderboardRun, NumRuns, NumVictory, NumDefeat, NumKills, NumMinibossKills, PerkChosen, PerkIgnored, SurvivalTime }
public enum DateRangeMode { All, Day, Week, Month }
public enum PlayerStatDifficultyMode { All, Normal, Expert, Cursed }
public enum LeaderboardMode { FastestWin, MostWins, MostRuns, MostEnemyKills, MostMinibossKills, LongestSurvival }
public class RunInfo
{
public string displayName;
public long steamId;
public float score;
public string dateString;
public Dictionary<string, object> data;
public RunInfo( string displayName, long steamId, float score, string dateString, Dictionary<string, object> data )
{
this.displayName = displayName;
this.steamId = steamId;
this.score = score;
this.data = data;
this.dateString = dateString;
}
}
public class PlayerInfo
{
public string displayName;
public long steamId;
public int level;
public PlayerInfo( string displayName, long steamId, int level = 0)
{
this.displayName = displayName;
this.steamId = steamId;
this.level = level;
}
}
public partial class Manager
{
public const float LEADERBOARD_VERSION = 8;
public RunInfo RunEntryToShow { get; set; }
public PlayerInfo PlayerProfileToShow { get; set; }
public int NumPlayersLeaderboardToShow { get; set; }
public DateRangeMode DateRangeToShow { get; set; }
public LeaderboardMode LeaderboardModeToShow { get; set; }
public bool LeaderboardShowFriends { get; set; }
public bool ShowQuestPanel { get; set; }
public bool ShowShopPanel { get; set; }
public bool ShowLoadoutPanel { get; set; }
public bool ShowPerkUnlockPanel { get; set; }
public void RestartStats()
{
}
public void SubmitGameOverStats()
{
if( !(LocalPlayer.IsValid() && !LocalPlayer.IsProxy) )
return;
if ( !LocalPlayer.HasSubmittedRunPlayedStat )
{
Sandbox.Services.Stats.Increment( GetStatString(StatType.NumRuns, NumPlayersThisRun, Difficulty), 1 );
LocalPlayer.HasSubmittedRunPlayedStat = true;
}
}
public void SubmitScore( bool victory )
{
if ( !(LocalPlayer.IsValid() && !LocalPlayer.IsProxy) )
return;
var elapsedTime = ElapsedTime.Relative;
var data = new Dictionary<string, object>();
data.Add( "run_time", elapsedTime );
Dictionary<int, int> importantPerks = LocalPlayer.GetImportantPerks();
var importantPerksJson = JsonSerializer.Serialize( importantPerks );
data.Add( "important_perks", importantPerksJson );
Dictionary<int, int> allPerks = new();
// add all perks to data
foreach ( var pair in LocalPlayer.Perks )
{
int perkIdent = pair.Key;
int perkLevel = pair.Value.Level;
//Log.Info( $"perk: {perkIdent} level: {perkLevel}" );
allPerks.Add( perkIdent, perkLevel );
}
var allPerksJson = JsonSerializer.Serialize( allPerks );
data.Add( "perks", allPerksJson );
data.Add( "player_level", LocalPlayer.Level);
data.Add( "num_kills", LocalPlayer.Stats[PlayerStat.NumEnemiesKilled] );
data.Add( "hp_healed", MathX.CeilToInt( LocalPlayer.Stats[PlayerStat.TotalHealed] ) );
data.Add( "num_deaths", LocalPlayer.Stats[PlayerStat.NumTimesDied] );
data.Add( "num_players", NumPlayersThisRun );
if ( NumPlayersThisRun > 1 )
{
int num = 0;
foreach( var player in Manager.Instance.Players )
{
if( player == LocalPlayer )
continue;
//data.Add( $"other_player_id_{num}", JsonSerializer.Serialize( player.Network.Owner.SteamId ) );
data.Add( $"other_player_id_{num}", (long)player.Network.Owner.SteamId );
Log.Info( $"submitting - other_player_id_{num}: {(long)player.Network.Owner.SteamId}" );
data.Add( $"other_player_name_{num}", player.Network.Owner.DisplayName );
Log.Info( $"submitting - other_player_name_{num}: {player.Network.Owner.DisplayName}" );
data.Add( $"other_player_level_{num}", player.Level );
Log.Info( $"submitting - other_player_level_{num}: {player.Level}" );
num++;
}
}
float bossLifeRemovedPct = -1f;
if ( !victory && HasSpawnedBoss && Boss.IsValid() )
{
bossLifeRemovedPct = (1f - Boss.HpPercent) * 100f;
data.Add( "boss_life_removed_pct", bossLifeRemovedPct );
}
if( !Game.IsEditor )
{
Sandbox.Services.Stats.Increment( GetStatString( victory ? StatType.NumVictory : StatType.NumDefeat, NumPlayersThisRun, Difficulty ), 1 );
var score = GetScore( elapsedTime, victory, bossLifeRemovedPct );
Log.Info( $"submitting run for {GetStatString( StatType.LeaderboardRun, NumPlayersThisRun, Difficulty )} - victory: {victory} score: {score} time: {elapsedTime} bossLifeRemovedPct: {bossLifeRemovedPct} data: {data}" );
//Sandbox.Services.Stats.SetValue( GetStatString( StatType.LeaderboardRun, NumPlayersThisRun, Difficulty ), score, data );
Sandbox.Services.Stats.SetValue( GetStatString( StatType.LeaderboardRun, NumPlayersThisRun, Difficulty ), score );
Sandbox.Services.Stats.SetValue( GetStatString( StatType.SurvivalTime, NumPlayersThisRun, Difficulty ), elapsedTime );
}
if( victory )
{
switch( Difficulty )
{
case 0: UnlockAchievement( "victory_normal" ); break;
case 1: UnlockAchievement( "victory_expert" ); break;
case 2: UnlockAchievement( "victory_cursed" ); break;
}
var timesDashed = LocalPlayer.ResultStats.ContainsKey( ResultStat.TimesDashed ) ? LocalPlayer.ResultStats[ResultStat.TimesDashed] : 0;
if( timesDashed == 0 )
UnlockAchievement( "no_dashing" );
bool isJackOfAllTradesValid = true;
// check if any perk is above level 1 - if so, not valid for achievement
foreach ( var pair in LocalPlayer.Perks )
{
if ( pair.Value.Level > 1 )
{
isJackOfAllTradesValid = false;
break;
}
}
if ( isJackOfAllTradesValid )
UnlockAchievement( "jack_of_all_trades" );
if( (int)LocalPlayer.Stats[PlayerStat.NumBulletsHitGround] == 0 )
UnlockAchievement( "no_bullets_hit_ground" );
if( !LocalPlayer.HasLostHp )
UnlockAchievement( "no_hp_lost" );
if( LocalPlayer.NumItemsCollected == 0 )
UnlockAchievement( "no_items_collected" );
if ( LocalPlayer.Perks.Count <= 12 )
UnlockAchievement( "limited_different_perks" );
}
}
public const float VICTORY_OFFSET = 2000000f;
/// <summary>
/// Defeat score offset for runs where the boss was reached. Must be higher than max possible
/// pre-boss survival time (~780s). Boss defeat scores range from BOSS_DEFEAT_OFFSET to BOSS_DEFEAT_OFFSET+10000.
/// </summary>
public const float BOSS_DEFEAT_OFFSET = 1000f;
/// <summary>
/// If victory, lower time is better. If not victory and boss was reached, higher boss damage % is better.
/// If not victory and boss was not reached, higher survival time is better.
/// </summary>
public static float GetScore( float elapsedTime, bool victory, float bossLifeRemovedPct = -1f )
{
if ( victory )
return VICTORY_OFFSET - elapsedTime;
if ( bossLifeRemovedPct >= 0f )
return BOSS_DEFEAT_OFFSET + bossLifeRemovedPct * 100f;
return elapsedTime;
}
private void LogData( Dictionary<string, object> data, float value )
{
var sb = new System.Text.StringBuilder();
sb.AppendLine( $"**** run - time: {value} data:" );
foreach ( var pair in data )
{
if ( pair.Value is Dictionary<int, int> intDict )
{
sb.AppendLine( $"{pair.Key}:" );
foreach ( var subPair in intDict )
{
sb.AppendLine( $" {subPair.Key}: {subPair.Value}" );
}
}
else if ( pair.Value is Dictionary<int, object> objDict )
{
sb.AppendLine( $"{pair.Key}:" );
foreach ( var subPair in objDict )
{
sb.AppendLine( $" {subPair.Key}: {subPair.Value}" );
}
}
else
{
sb.AppendLine( $"{pair.Key}: {pair.Value}" );
}
}
Log.Info( sb.ToString() );
}
public static string GetStatString( StatType statType, int numPlayers, int difficulty )
{
if( statType == StatType.PerkChosen || statType == StatType.PerkIgnored )
throw new Exception( "GetStatString - use GetPerkStatString for perk stats" );
// run time scores include the number of players, other stats don't
if( statType == StatType.LeaderboardRun || statType == StatType.SurvivalTime )
return $"{GetStatPrefix( statType )}_p{numPlayers}_diff{difficulty}_v{LEADERBOARD_VERSION}";
return $"{GetStatPrefix( statType )}_diff{difficulty}_v{LEADERBOARD_VERSION}";
}
public static string GetPerkStatString( StatType statType, TypeDescription perkType )
{
return $"{GetStatPrefix( statType )}_{perkType.ToString()}_v{LEADERBOARD_VERSION}";
}
static string GetStatPrefix( StatType statType )
{
switch( statType )
{
case StatType.LeaderboardRun: return "run";
case StatType.NumRuns: return "num_runs";
case StatType.NumVictory: return "num_victory";
case StatType.NumDefeat: return "num_defeat";
case StatType.NumKills: return "num_kills";
case StatType.NumMinibossKills: return "num_miniboss_kills";
case StatType.SurvivalTime: return "survival_time";
case StatType.PerkChosen: return "chosen";
case StatType.PerkIgnored: return "ignored";
default: throw new Exception( $"GetStatPrefix - unhandled statType {statType}" );
}
}
public void UnlockAchievement( string achievementIdent )
{
Log.Info( $"unlocking achievement {achievementIdent}" );
//Sandbox.Services.Achievements.Unlock( achievementIdent );
}
}