Partial MatchController for a soccer match. Handles substitutions: a five-sub limit, goalkeeper-for-goalkeeper checks, available substitutes by catalog identity, pausing while the substitution menu is open, English and Russian notices, and saving/restoring substitution state around a preview.
using Socer;
using System.Collections.Generic;
using System.Linq;
public sealed partial class MatchController
{
public const int SubstitutionLimit=5;
public bool SubstitutionOpen { get; private set; }
public int SubstitutionsUsed { get; private set; }
public string SubstitutionNotice { get; private set; }="";
string substitutionNoticeRussian="";
public string LocalizedSubstitutionNotice=>GamePreferences.Russian?substitutionNoticeRussian:SubstitutionNotice;
int[] matchLineup;
readonly HashSet<int> substitutedOut=new();
bool pausedBeforeSubstitution;
int[] previewLineup;
int previewSubstitutionsUsed;
int[] previewSubstitutedOut;
string previewSubstitutionNotice;
string previewSubstitutionNoticeRussian;
public int MatchCardId(int slot)
{
var lineup=matchLineup??ActiveMatchRoster?.Lineup;
return slot>=0 && slot<11 && lineup?.Length==11?lineup[slot]:-1;
}
public PlayerCard MatchCard(int slot)=>ActiveMatchRoster?.Cards.FirstOrDefault(c=>c.Id==MatchCardId(slot));
public IEnumerable<PlayerCard> AvailableSubstitutes
{
get
{
var roster=ActiveMatchRoster;
if(roster==null)return Enumerable.Empty<PlayerCard>();
var playedIds=(matchLineup??roster.Lineup).Concat(substitutedOut).ToHashSet();
var playedPeople=roster.Cards.Where(c=>playedIds.Contains(c.Id)).Select(c=>c.CatalogId).ToHashSet();
// A second card is another development path, not a fresh physical
// copy of a footballer who is already playing or has been taken off.
return roster.Cards.Where(c=>!playedPeople.Contains(c.CatalogId));
}
}
public IReadOnlyCollection<int> MatchParticipants=>(matchLineup??ActiveMatchRoster.Lineup).Concat(substitutedOut).Distinct().ToArray();
void ResetMatchSubstitutions()
{
SubstitutionOpen=false;SubstitutionsUsed=0;SubstitutionNotice="";substitutionNoticeRussian="";
matchLineup=ActiveMatchRoster?.Lineup.ToArray();substitutedOut.Clear();
}
void SaveSubstitutionsBeforePreview()
{
previewLineup=matchLineup?.ToArray();previewSubstitutionsUsed=SubstitutionsUsed;
previewSubstitutedOut=substitutedOut.ToArray();previewSubstitutionNotice=SubstitutionNotice;
previewSubstitutionNoticeRussian=substitutionNoticeRussian;
ResetMatchSubstitutions();
}
void RestoreSubstitutionsAfterPreview()
{
SubstitutionOpen=false;matchLineup=previewLineup;SubstitutionsUsed=previewSubstitutionsUsed;
substitutedOut.Clear();foreach(var id in previewSubstitutedOut??System.Array.Empty<int>())substitutedOut.Add(id);
SubstitutionNotice=previewSubstitutionNotice??"";previewLineup=null;previewSubstitutedOut=null;
substitutionNoticeRussian=previewSubstitutionNoticeRussian??"";
}
public void OpenSubstitutions()
{
if(Match.MedicalActive || InjurySubstitutionRequired || MediaLeagueActive || SubstitutionOpen || ClubOpen || QuickMatchRunning || Match.Finished || Match.ShootoutActive || MatchIntroActive || Replay.Playing)return;
matchLineup??=ActiveMatchRoster?.Lineup.ToArray();
pausedBeforeSubstitution=Paused;Paused=true;accumulator=0;
SubstitutionNotice="Choose a player on the pitch and an available substitute.";
substitutionNoticeRussian="Выберите игрока на поле и доступного запасного.";
SubstitutionOpen=true;
}
public void CloseSubstitutions()
{
if(!SubstitutionOpen)return;
SubstitutionOpen=false;Paused=pausedBeforeSubstitution;
}
public bool CanSubstitute(int slot,int cardId)
{
if(Match.MedicalActive || !SubstitutionOpen || SubstitutionsUsed>=SubstitutionLimit || slot<0 || slot>=11 || !Match.Players[slot].InPlay)return false;
var card=ActiveMatchRoster?.Cards.FirstOrDefault(c=>c.Id==cardId);
return card!=null && (slot==0)==(card.Position=="ВР") && AvailableSubstitutes.Any(c=>c.Id==cardId);
}
public bool Substitute(int slot,int cardId)
{
if(!CanSubstitute(slot,cardId)){SubstitutionNotice="This substitution is unavailable. Goalkeepers must replace goalkeepers.";substitutionNoticeRussian="Замена недоступна. Вратаря может заменить только вратарь.";return false;}
var outgoing=Match.Players[slot];var oldCard=MatchCard(slot);var incoming=ActiveMatchRoster.Cards.First(c=>c.Id==cardId);
if(Match.Owner==slot){Match.Owner=-1;Match.Ball=outgoing.Position;Match.BallVelocity=new Point();}
if(Match.Passer==slot)Match.Passer=-1;
if(Match.PassReceiver==slot)Match.PassReceiver=-1;
var player=new Footballer{Id=slot,Team=0,Number=outgoing.Number,Home=outgoing.Home,
Position=outgoing.Position,Facing=outgoing.Facing,Instruction=outgoing.Instruction};
ActiveMatchRoster.ApplyCard(player,incoming,slot);
Match.Players[slot]=player;
Match.NotifyFootballSubstitution(0,slot);
substitutedOut.Add(oldCard.Id);matchLineup[slot]=cardId;SubstitutionsUsed++;
Match.AddEvent($"{Match.HomeName} · {incoming.Name} on for {oldCard.Name} ({Match.Minute}′)");
SubstitutionNotice=$"{incoming.Name} replaces {oldCard.Name}. {SubstitutionLimit-SubstitutionsUsed} substitutions left.";
substitutionNoticeRussian=$"{incoming.Name} вместо {oldCard.Name}. Осталось замен: {SubstitutionLimit-SubstitutionsUsed}.";
if(SelectedPlayer==slot || OrderedPlayer==slot){SelectedPlayer=OrderedPlayer=slot;AwaitingOrder=DirectOrderKind.None;}
return true;
}
}