using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using FabiusTimer.Configs;
using FabiusTimer.Helpers;
using FabiusTimer.Models;
using FabiusTimer.Player;
using FabiusTimerApi.Enums;
using FabiusTimerApi.Models;
using Microsoft.Extensions.Localization;
using QAngle = CounterStrikeSharp.API.Modules.Utils.QAngle;

namespace FabiusTimer.Services;

public class Commands
{
    private readonly Plugin _plugin;
    private readonly ZoneInstance _zoneInstance;
    private readonly PlayersManager _playersManager;
    private readonly Config<StyleConfig> _styleConfig;
    private readonly IStringLocalizer _localizer;

    public Commands(Plugin plugin, ZoneInstance zoneInstance, PlayersManager playersManager, Config<StyleConfig> styleConfig)
    {
        _plugin = plugin;
        _zoneInstance = zoneInstance;
        _playersManager = playersManager;
        _styleConfig = styleConfig;
        _localizer = plugin.Localizer;
        
        RegisterCommands();
    }
    
    private CommandInfo.CommandCallback CallbackAnonymizer(Action<CCSPlayerController?, CommandInfo> action)
    {
        return new CommandInfo.CommandCallback(action);
    }
    
    private void RegisterCommands()
    {
        var commandMappings = new Dictionary<Route, (string Short, string Full)>
        {
            { Route.Main, ("m", "main") },
            { Route.Bonus1, ("b", "bonus") },
            { Route.Bonus2, ("b2", "bonus2") },
            { Route.Bonus3, ("b3", "bonus3") },
            { Route.Bonus4, ("b4", "bonus4") },
            { Route.Bonus5, ("b5", "bonus5") }
        };

        foreach (var (route, (shortName, fullName)) in commandMappings)
        {
            _plugin.AddCommand($"css_{shortName}", $"{route}",
                CallbackAnonymizer((controller, _) => OnRouteCommand(controller, route)));
            _plugin.AddCommand($"css_{fullName}", $"{route}",
                CallbackAnonymizer((controller, _) => OnRouteCommand(controller, route)));
        }

        var restartCommands = new List<string>
        {
            "css_restart",
            "css_r"
        };

        foreach (var restartCommand in restartCommands)
        {
            _plugin.AddCommand(restartCommand, restartCommand, CallbackAnonymizer(OnRestartCommand));
        }

        var styles = _styleConfig.Value.Styles;
        _plugin.AddCommand("css_style", "", (player, _) =>
        {
            if (player is null) return;

            if (!_playersManager.TryGetPlayer(player, out var timerPlayer)) return;
            
            var menu = new CenterHtmlMenu(_localizer.ForPlayer(player, "menu.wr.choose_style"), _plugin);
            foreach (var (key, value) in styles)
            {
                menu.AddMenuOption(_localizer.ForPlayer(player, key), (_, _) => timerPlayer.SetStyle(key, value));
            }

            menu.Open(player);
        });

        foreach (var (key, value) in styles)
        {
            var commands = value.Commands;
            if (commands is null) continue;

            foreach (var command in commands)
            {
                _plugin.AddCommand(command, command, CallbackAnonymizer((player, _) =>
                {
                    if (player is null) return;
                    if (!_playersManager.TryGetPlayer(player, out var timerPlayer)) return;

                    timerPlayer.SetStyle(key, value);
                }));
            }
        }

        var setStartCommands = new List<string> { "css_setstart", "css_set", "css_setspawn" };

        foreach (var startCommand in setStartCommands)
        {
            _plugin.AddCommand(startCommand, "Set start command", CallbackAnonymizer(OnSetStartCommand));
        }

        var unsetStartCommands = new List<string> { "css_unsetstart", "css_unset", "css_unsetspawn" };

        foreach (var startCommand in unsetStartCommands)
        {
            _plugin.AddCommand(startCommand, "Unset start command", CallbackAnonymizer(OnUnsetStartCommand));
        }
    }
    
    private void OnRouteCommand(CCSPlayerController? player, Route route)
    {
        if (player is null) return;

        if (!_playersManager.TryGetPlayer(player, out var timerPlayer)) return;
        
        var positions = _zoneInstance.GetZone((ZoneType)route, ZoneTrack.Start);
        if (positions is null)
        {
            timerPlayer.PrintToChat("command.route.zone_not_found", timerPlayer.Route);
            return;
        }

        if (timerPlayer.Data != null)
            timerPlayer.Data.TimerStarted = false;

        timerPlayer.Route = route;
        timerPlayer.Refresh();

        TeleportToZone(player, timerPlayer.GetStartPosition());
    }

    private void OnRestartCommand(CCSPlayerController? player, CommandInfo info)
    {
        if (player is null) return;

        if (!_playersManager.TryGetPlayer(player, out var timerPlayer)) return;
        
        var positions = _zoneInstance.GetZone((ZoneType)timerPlayer.Route, ZoneTrack.Start);
        if (positions is null)
        {
            timerPlayer.PrintToChat("command.route.zone_not_found", timerPlayer.Route);
            return;
        }

        TeleportToZone(player, timerPlayer.GetStartPosition());
    }

    private void TeleportToZone(CCSPlayerController player, (FVector Position, QAngle Angle)? pos)
    {
        if (pos is null) return;

        var playerPawn = player.PlayerPawn.Value;
        if (playerPawn is null) return;

        playerPawn.EyeAngles.X = pos.Value.Angle.X;
        playerPawn.EyeAngles.Y = pos.Value.Angle.Y;
        playerPawn.EyeAngles.Z = pos.Value.Angle.Z;
        playerPawn.Teleport(pos.Value.Position);
    }

    private void OnSetStartCommand(CCSPlayerController? player, CommandInfo info)
    {
        if (player == null || !_playersManager.TryGetPlayer(player, out var timerPlayer) 
                           || !_zoneInstance.ContainsKey(((ZoneType)timerPlayer.Route, ZoneTrack.Start))) return;
        if (player.Team is not (CsTeam.Terrorist or CsTeam.CounterTerrorist)) return;

        var data = timerPlayer.Data;
        if (data is null) return;

        var playerPawn = player.PlayerPawn.Value;
        if (playerPawn == null) return;

        if (!data.IsAtStartingLine)
        {
            timerPlayer.PrintToChat("is_not_a_launch_zone");
            return;
        }

        var pos = new FVector(playerPawn.AbsOrigin!.X, playerPawn.AbsOrigin.Y, playerPawn.AbsOrigin.Z);
        var angle = new QAngle(playerPawn.EyeAngles.X, playerPawn.EyeAngles.Y, playerPawn.EyeAngles.Z);

        if (!StartPositions.TryAdd(Route, (pos, angle)))
        {
            StartPositions[Route] = (pos, angle);
        }

        PrintToChat("successful_initial_positioning");
    }

    private void OnUnsetStartCommand(CCSPlayerController? player, CommandInfo info)
    {
        if (player == null || !_zoneInstance.ContainsKey(((ZoneType)Route, ZoneTrack.Start))) return;
        if (player.Team is not (CsTeam.Terrorist or CsTeam.CounterTerrorist)) return;

        var data = Data;
        if (data is null) return;

        if (!StartPositions.ContainsKey(Route))
        {
            PrintToChat("spawn_point_not_found");
            return;
        }

        StartPositions.Remove(Route);
        PrintToChat("start_point_reset_successfully");
    }
}

мне нужно реализовать метод для установки StartPositions типо

SetStartPosition

вот тут:

using System;
using CounterStrikeSharp.API.Core;
using CounterStrikeSharp.API.Core.Translations;
using CounterStrikeSharp.API.Modules.Commands;
using CounterStrikeSharp.API.Modules.Entities;
using CounterStrikeSharp.API.Modules.Menu;
using CounterStrikeSharp.API.Modules.Utils;
using FabiusTimer.Configs;
using FabiusTimer.Helpers;
using FabiusTimer.Managers;
using FabiusTimer.Models;
using FabiusTimer.Player.Replay;
using FabiusTimerApi;
using FabiusTimerApi.Enums;
using FabiusTimerApi.Models;
using Microsoft.Extensions.Localization;

namespace FabiusTimer.Player;

public class TimerPlayer : ITimerPlayer, IDisposable
{
    private readonly Plugin _plugin;
    private readonly Config<StyleConfig> _styleConfig;
    private readonly ZoneInstance _zoneInstance;
    private readonly IMapManager _mapManager;
    private readonly ICookieManager _cookieManager;
    private readonly Playback _playback;
    private readonly IStringLocalizer _localizer;
    public CCSPlayerController? Controller { get; set; }
    public SteamID? SteamId { get; set; }
    public Data? Data { get; set; }
    public ZoneCreationState CreationStage { get; set; } = ZoneCreationState.None;
    public ZoneType Type { get; set; } = ZoneType.None;
    public ZoneTrack Track { get; set; } = ZoneTrack.None;
    public Route Route { get; set; } = Route.Main;

    public event Action<TimerPlayer>? OnDisconnect;

    public string CurrentStyleName { get; set; }
    public Style? CurrentStyle { get; set; }
    public string? Position { get; set; }
    public int PersonalBestTime { get; set; }

    public Recorder Recorder { get; }
    public Dictionary<Route, Dictionary<Style, Record>> Records { get; set; } = new();
    private Dictionary<Route, (FVector Position, QAngle Angle)?> StartPositions { get; set; } = new();

    public TimerPlayer(
        Plugin plugin,
        Config<StyleConfig> styleConfig,
        ZoneInstance zoneInstance,
        IMapManager mapManager,
        ICookieManager cookieManager,
        FabiusTimerApi api,
        Playback playback)
    {
        _plugin = plugin;
        _styleConfig = styleConfig;
        _zoneInstance = zoneInstance;
        _mapManager = mapManager;
        _cookieManager = cookieManager;
        _playback = playback;
        Recorder = new Recorder(this, api);
        _localizer = plugin.Localizer;
    }

    public void Reset()
    {
        if (Data is null) return;

        Data.TimerStarted = false;
        Data.Jumps = 0;
        Data.Strafes = 0;
        Data.Time = 0;
    }

    public bool IsCreationZone()
    {
        return CreationStage != ZoneCreationState.None;
    }
    
    public void SetStyle(string styleId, Style style)
    {
        PrintToChat("style_selection", _localizer.ForPlayer(Controller, styleId));
        CurrentStyleName = styleId;
        CurrentStyle = style;
        Refresh();
    }

    public (FVector Position, QAngle Angle)? GetStartPosition()
    {
        if (StartPositions.TryGetValue(Route, out var result))
        {
            return result;
        }

        var mainZone = _zoneInstance.GetZone((ZoneType)Route, ZoneTrack.Start);
        if (mainZone is null) return null;

        return (Utils.CalculateCubeCenter(mainZone), QAngle.Zero);
    }

    public void UpdatePersonalBestRecord(Record newRecord)
    {
        if (CurrentStyle is null)
            return;

        if (!Records.TryGetValue(Route, out var styleRecords))
        {
            styleRecords = new Dictionary<Style, Record>();
            Records[Route] = styleRecords;
        }

        if (!styleRecords.TryAdd(CurrentStyle, newRecord))
        {
            styleRecords[CurrentStyle] = newRecord;
        }
    }

    public void Refresh()
    {
        if (Records.TryGetValue(Route, out var records))
        {
            PersonalBestTime = CurrentStyle == null ? 0 : records[CurrentStyle].Time;
        }
        else
        {
            PersonalBestTime = 0;
        }

        if (SteamId is not null)
        {
            Position = $"{_mapManager.GetPlayerPosition(SteamId.SteamId2, CurrentStyle, Route)}";
        }
    }

    public T GetPlayerCookie<T>(string key, T defaultValue = default!)
    {
        var steamId = SteamId?.SteamId64 ?? Controller?.SteamID ?? null;
        if (steamId is null) return defaultValue;

        return _cookieManager.GetPlayerCookie(steamId.Value, key, defaultValue);
    }

    public void SetPlayerCookie<T>(string key, T value)
    {
        var steamId = SteamId?.SteamId64 ?? Controller?.SteamID ?? null;
        if (steamId is null) return;

        _cookieManager.SetPlayerCookie(steamId.Value, key, value);
    }

    public void PrintToChat(string message, params object[] args)
    {
        if (Controller is null) return;

        _plugin.PrintToChat(Controller, message, args);
    }

    public bool StopTimer()
    {
        if (Data is null)
            return false;

        Data.TimerStarted = false;
        return true;
    }

    public void AdjustPlayerVelocity(float velocity)
    {
        var playerPawn = Controller?.PlayerPawn.Value;
        if (playerPawn == null) return;

        var currentX = playerPawn.AbsVelocity.X;
        var currentY = playerPawn.AbsVelocity.Y;
        var currentZ = playerPawn.AbsVelocity.Z;

        var currentSpeed3D = Math.Sqrt(currentX * currentX + currentY * currentY + currentZ * currentZ);

        playerPawn.AbsVelocity.X = (float)(currentX / currentSpeed3D) * velocity;
        playerPawn.AbsVelocity.Y = (float)(currentY / currentSpeed3D) * velocity;
        playerPawn.AbsVelocity.Z = (float)(currentZ / currentSpeed3D) * velocity;
    }

    public void Dispose()
    {
        Data = null;
        OnDisconnect?.Invoke(this);
    }
}