CareerPersistence.cs

Partial MatchController that persists the club career. Wraps FileSystem.Data in a size-limited save file reader, loads the career on start, tracks block/recovery/unsaved states for the HUD, retries failed writes on a 15 second timer, and lets the player recover a validated backup.

Rough Code
using Sandbox;
using Socer;
using System;
using System.Linq;

namespace Socer
{
    public enum CareerPersistenceState { Healthy, Recovered, Unsaved, RecoveryRequired }

    public sealed class SandboxCareerSaveFiles : ICareerSaveFiles
    {
        public bool Exists(string path)=>FileSystem.Data.FileExists(path);
        public string Read(string path)
        {
            if(FileSystem.Data.FileSize(path)>32L*1024*1024)
                throw new InvalidOperationException("The save exceeds the 32 MiB file safety limit.");
            return FileSystem.Data.ReadAllText(path);
        }
        public void Write(string path,string text)=>FileSystem.Data.WriteAllText(path,text);
    }
}

public sealed partial class MatchController
{
    CareerSaveStore careerSaveStore;
    bool careerLoadBlocked,careerWriteFailed,careerRecoveredNotice;
    float nextCareerSaveRetry;
    CareerSaveHud careerSaveHud;
    public string CareerSaveError {get;private set;}="";
    public int CareerSaveRevision {get;private set;}
    public CareerPersistenceState PersistenceState=>careerLoadBlocked?CareerPersistenceState.RecoveryRequired:
        careerWriteFailed?CareerPersistenceState.Unsaved:careerRecoveredNotice?CareerPersistenceState.Recovered:CareerPersistenceState.Healthy;
    public bool CanRetryCareerSave=>!saveBlocked && Career!=null && (!careerLoadBlocked ||
        (ClubOpen && Career.Pending==null && !PreparingMatch && !MatchModeSelection && !QuickMatchRunning &&
        !IsRulesPreview && !StrategyLabOpen && !MediaLeagueActive));

    CareerSaveStore SaveStore=>careerSaveStore??=new CareerSaveStore(new SandboxCareerSaveFiles());
    void EnsureCareerSaveInterface()
    {
        if(careerSaveHud?.GameObject?.IsValid==true)return;
        careerSaveHud=Scene.GetAllComponents<CareerSaveHud>().FirstOrDefault();
        if(careerSaveHud!=null)return;
        var ui=Scene.GetAllComponents<MatchHud>().FirstOrDefault()?.GameObject;
        if(ui!=null)careerSaveHud=ui.Components.Create<CareerSaveHud>();
    }
    void LoadCareerForStart()
    {
        var loaded=SaveStore.Load();
        Career=loaded.Career??ClubCareer.Create(MatchSimulation.CreateSeed());
        careerLoadBlocked=loaded.Blocked;careerWriteFailed=false;careerRecoveredNotice=loaded.Recovered;
        CareerSaveError=loaded.Error??"";CareerSaveRevision++;
        if(loaded.Blocked)Log.Warning("Socer career recovery is blocked: "+CareerSaveError);
        else if(loaded.Recovered)Log.Warning("Socer loaded the validated career backup: "+CareerSaveError);
    }
    void CareerWriteFailed(string error)
    {
        nextCareerSaveRetry=Time.Now+15f;
        if(careerWriteFailed && CareerSaveError==error)return;
        careerWriteFailed=true;CareerSaveError=error;CareerSaveRevision++;
        Log.Warning("Socer progress is not being saved: "+error);
    }
    void CareerWriteSucceeded()
    {
        if(!careerWriteFailed)return;
        careerWriteFailed=false;CareerSaveError="";CareerSaveRevision++;
    }
    void TickCareerSaveRetry()
    {
        if(!careerWriteFailed || careerLoadBlocked || saveBlocked || IsRulesPreview || Time.Now<nextCareerSaveRetry)return;
        SaveCareer();
    }
    public void AcknowledgeCareerRecovery()
    {
        if(PersistenceState!=CareerPersistenceState.Recovered)return;
        careerRecoveredNotice=false;CareerSaveError="";CareerSaveRevision++;
    }
    public void RetryCareerSave()
    {
        if(!CanRetryCareerSave)return;
        if(!careerLoadBlocked){SaveCareer();return;}
        var loaded=SaveStore.Load();
        if(loaded.Career==null || loaded.Blocked)
        {
            CareerSaveError=string.IsNullOrWhiteSpace(loaded.Error)?"No valid saved career was found.":loaded.Error;
            CareerSaveRevision++;return;
        }

        // An explicit recovery replaces the temporary, unsaved session only in
        // the club. It restores the saved attempt, never a partial live result.
        Career=loaded.Career;careerLoadBlocked=false;careerWriteFailed=false;careerRecoveredNotice=true;
        CareerSaveError="";CareerSaveRevision++;
        RevealedCard=null;RevealedPack=null;appliedClubPlan=null;attemptBound=false;
        PreparingMatch=false;MatchModeSelection=false;QuickBroadcastOpen=false;
        Director=new();Broadcast=new();Match=new MatchSimulation(MatchSimulation.CreateSeed());
        careerMatch=false;
        if(Career.Pending!=null)
        {
            if(Career.HasCompleteLineup)StartCareer(Career.Pending.Cup);
            else{Match=new MatchSimulation(Career.Pending.Seed);careerMatch=true;}
        }
        else{friendlyOpponent=MatchSimulation.FriendlyOpponent(Match.Seed);Career.Apply(Match,friendlyOpponent);}
        ResetMatchSubstitutions();Replay.Reset();Replay.Capture(Match);
        observedGoals=Match.Goals[0]+Match.Goals[1];replayAfterGoal=0;accumulator=0;
        SkipMatchIntro();ClubOpen=Career.Pending==null || !Career.HasCompleteLineup;
        ClubPage="Главная";Paused=ClubOpen;Revision++;
        SaveCareer();
        if(Career.HasRelegationChoice)OpenRelegationChoice();
    }
}