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.
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();
}
}