Skip to content
255 changes: 255 additions & 0 deletions managed/src/SwiftlyS2.Core/Modules/Game/GameService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Core.Schemas;
using SwiftlyS2.Shared.Players;
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.Services;
using SwiftlyS2.Shared.EntitySystem;
using SwiftlyS2.Shared.SchemaDefinitions;

namespace SwiftlyS2.Core.Services;

internal class GameService : IGameService
{
private readonly IEntitySystemService entitySystemService;
// m_bMapHasBombZone (hash: 0x6295CF65D3F4FD4D) + 0x02
private static readonly Lazy<nint> MatchStructOffsetLazy = new(() => Schema.GetOffset(0x6295CF65D3F4FD4D) + 0x02, LazyThreadSafetyMode.None);
private static nint MatchStructOffset => MatchStructOffsetLazy.Value;

public GameService( IEntitySystemService entitySystemService )
{
this.entitySystemService = entitySystemService;
}

public unsafe ref readonly CCSMatch MatchData => ref *GetCCSMatchPtr();

public unsafe void Reset()
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->ActualRoundsPlayed = 0;
match->NOvertimePlaying = 0;
match->CTScoreFirstHalf = 0;
match->CTScoreSecondHalf = 0;
match->CTScoreOvertime = 0;
match->CTScoreTotal = 0;
match->TerroristScoreFirstHalf = 0;
match->TerroristScoreSecondHalf = 0;
match->TerroristScoreOvertime = 0;
match->TerroristScoreTotal = 0;
match->Phase = (int)GamePhase.GAMEPHASE_PLAYING_STANDARD;

gameRules.TotalRoundsPlayed = 0;
gameRules.OvertimePlaying = 0;
gameRules.GamePhaseEnum = GamePhase.GAMEPHASE_PLAYING_STANDARD;
gameRules.TotalRoundsPlayedUpdated();
gameRules.OvertimePlayingUpdated();
gameRules.GamePhaseUpdated();

UpdateTeamScores();
}

public unsafe void SetPhase( GamePhase phase )
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->Phase = (int)phase;

gameRules.GamePhaseEnum = phase;
gameRules.GamePhaseUpdated();
}

public unsafe void AddTerroristWins( int numWins )
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->ActualRoundsPlayed += (short)numWins;

gameRules.TotalRoundsPlayed = match->ActualRoundsPlayed;
gameRules.TotalRoundsPlayedUpdated();

AddTerroristScore(numWins);
}

public unsafe void AddCTWins( int numWins )
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->ActualRoundsPlayed += (short)numWins;

gameRules.TotalRoundsPlayed = match->ActualRoundsPlayed;
gameRules.TotalRoundsPlayedUpdated();

AddCTScore(numWins);
}

public unsafe void IncrementRound( int numRounds = 1 )
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->ActualRoundsPlayed += (short)numRounds;

gameRules.TotalRoundsPlayed = match->ActualRoundsPlayed;
gameRules.TotalRoundsPlayedUpdated();
}

public void AddTerroristBonusPoints( int points )
{
AddTerroristScore(points);
}

public void AddCTBonusPoints( int points )
{
AddCTScore(points);
}

public unsafe void AddTerroristScore( int score )
{
var match = GetCCSMatchPtr();

match->TerroristScoreTotal += (short)score;

if (match->NOvertimePlaying > 0)
{
match->TerroristScoreOvertime += (short)score;
}
else if (match->Phase == (int)GamePhase.GAMEPHASE_PLAYING_FIRST_HALF)
{
match->TerroristScoreFirstHalf += (short)score;
}
else if (match->Phase == (int)GamePhase.GAMEPHASE_PLAYING_SECOND_HALF)
{
match->TerroristScoreSecondHalf += (short)score;
}

UpdateTeamScores();
}

public unsafe void AddCTScore( int score )
{
var match = GetCCSMatchPtr();

match->CTScoreTotal += (short)score;

if (match->NOvertimePlaying > 0)
{
match->CTScoreOvertime += (short)score;
}
else if (match->Phase == (int)GamePhase.GAMEPHASE_PLAYING_FIRST_HALF)
{
match->CTScoreFirstHalf += (short)score;
}
else if (match->Phase == (int)GamePhase.GAMEPHASE_PLAYING_SECOND_HALF)
{
match->CTScoreSecondHalf += (short)score;
}

UpdateTeamScores();
}

public unsafe void GoToOvertime( int numOvertimesToAdd = 1 )
{
var match = GetCCSMatchPtr();
var gameRules = GetGameRules();

match->NOvertimePlaying += (short)numOvertimesToAdd;

gameRules.OvertimePlaying = match->NOvertimePlaying;
gameRules.OvertimePlayingUpdated();
}

public unsafe void SwapTeamScores()
{
var match = GetCCSMatchPtr();

(match->TerroristScoreFirstHalf, match->CTScoreFirstHalf) = (match->CTScoreFirstHalf, match->TerroristScoreFirstHalf);
(match->TerroristScoreSecondHalf, match->CTScoreSecondHalf) = (match->CTScoreSecondHalf, match->TerroristScoreSecondHalf);
(match->TerroristScoreOvertime, match->CTScoreOvertime) = (match->CTScoreOvertime, match->TerroristScoreOvertime);
(match->TerroristScoreTotal, match->CTScoreTotal) = (match->CTScoreTotal, match->TerroristScoreTotal);

UpdateTeamScores();
}

public unsafe int GetWinningTeam()
{
var match = GetCCSMatchPtr();
var teams = entitySystemService.GetAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");

foreach (var team in teams)
{
if (team.TeamNum == (int)Team.T && team.Surrendered)
{
return (int)Team.CT;
}

if (team.TeamNum == (int)Team.CT && team.Surrendered)
{
return (int)Team.T;
}
}

if (match->TerroristScoreTotal > match->CTScoreTotal)
{
return (int)Team.T;
}

if (match->TerroristScoreTotal < match->CTScoreTotal)
{
return (int)Team.CT;
}

return (int)Team.None;
}

private unsafe void UpdateTeamScores()
{
var match = GetCCSMatchPtr();
var teams = entitySystemService.GetAllEntitiesByDesignerName<CCSTeam>("cs_team_manager");

foreach (var team in teams)
{
switch (team.TeamNum)
{
case (int)Team.T:
UpdateTeamEntity(team, match->TerroristScoreTotal, match->TerroristScoreFirstHalf, match->TerroristScoreSecondHalf, match->TerroristScoreOvertime);
break;

case (int)Team.CT:
UpdateTeamEntity(team, match->CTScoreTotal, match->CTScoreFirstHalf, match->CTScoreSecondHalf, match->CTScoreOvertime);
break;
}
}
}

private CCSGameRules GetGameRules()
{
var gameRules = entitySystemService.GetGameRules();
if (gameRules?.IsValid ?? false)
{
return gameRules;
}
throw new InvalidOperationException("GameRules not found");
}

private unsafe CCSMatch* GetCCSMatchPtr()
{
var gameRules = GetGameRules();
return (CCSMatch*)(gameRules.Address + MatchStructOffset);
}

private static void UpdateTeamEntity( CCSTeam team, int totalScore, int firstHalfScore, int secondHalfScore, int overtimeScore )
{
team.Score = totalScore;
team.ScoreFirstHalf = firstHalfScore;
team.ScoreSecondHalf = secondHalfScore;
team.ScoreOvertime = overtimeScore;
team.ScoreUpdated();
team.ScoreFirstHalfUpdated();
team.ScoreSecondHalfUpdated();
team.ScoreOvertimeUpdated();
}
}
12 changes: 9 additions & 3 deletions managed/src/SwiftlyS2.Core/Modules/Plugins/SwiftlyCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ internal class SwiftlyCore : ISwiftlyCore, IDisposable
public MenuManagerAPI MenuManagerAPI { get; init; }
public CommandLineService CommandLineService { get; init; }
public HelpersService Helpers { get; init; }
public GameService GameService { get; init; }
public string ContextBasePath { get; init; }
public string PluginDataDirectory { get; init; }
public GameFileSystem GameFileSystem { get; init; }
Expand Down Expand Up @@ -120,6 +121,7 @@ public SwiftlyCore( string contextId, string contextBaseDirectory, PluginMetadat
.AddSingleton<MenuManagerAPI>()
.AddSingleton<CommandLineService>()
.AddSingleton<HelpersService>()
.AddSingleton<GameService>()
.AddSingleton<IPermissionManager>(provider => provider.GetRequiredService<PermissionManager>())

.AddSingleton<IEventSubscriber>(provider => provider.GetRequiredService<EventSubscriber>())
Expand All @@ -145,6 +147,7 @@ public SwiftlyCore( string contextId, string contextBaseDirectory, PluginMetadat
.AddSingleton<IMenuManagerAPI>(provider => provider.GetRequiredService<MenuManagerAPI>())
.AddSingleton<ICommandLine>(provider => provider.GetRequiredService<CommandLineService>())
.AddSingleton<IHelpers>(provider => provider.GetRequiredService<HelpersService>())
.AddSingleton<IGameService>(provider => provider.GetRequiredService<GameService>())
.AddSingleton<IGameFileSystem>(provider => provider.GetRequiredService<GameFileSystem>())

.AddLogging(builder => builder.AddProvider(new SwiftlyLoggerProvider(id.Name)))
Expand Down Expand Up @@ -176,6 +179,7 @@ public SwiftlyCore( string contextId, string contextBaseDirectory, PluginMetadat
MenuManagerAPI = serviceProvider.GetRequiredService<MenuManagerAPI>();
CommandLineService = serviceProvider.GetRequiredService<CommandLineService>();
Helpers = serviceProvider.GetRequiredService<HelpersService>();
GameService = serviceProvider.GetRequiredService<GameService>();
Logger = LoggerFactory.CreateLogger(contextType);
GameFileSystem = serviceProvider.GetRequiredService<GameFileSystem>();
}
Expand Down Expand Up @@ -220,10 +224,12 @@ public void Dispose()
// [Obsolete("MenuManager will be deprecared at the release of SwiftlyS2. Please use MenuManagerAPI instead")]
// IMenuManager ISwiftlyCore.Menus => MenuManager;
IMenuManagerAPI ISwiftlyCore.MenusAPI => MenuManagerAPI;
string ISwiftlyCore.PluginPath => ContextBasePath;
string ISwiftlyCore.CSGODirectory => NativeEngineHelpers.GetCSGODirectoryPath();
string ISwiftlyCore.GameDirectory => NativeEngineHelpers.GetGameDirectoryPath();
ICommandLine ISwiftlyCore.CommandLine => CommandLineService;
IHelpers ISwiftlyCore.Helpers => Helpers;
IGameService ISwiftlyCore.Game => GameService;
IGameFileSystem ISwiftlyCore.GameFileSystem => GameFileSystem;
string ISwiftlyCore.PluginPath => ContextBasePath;
string ISwiftlyCore.PluginDataDirectory => PluginDataDirectory;
string ISwiftlyCore.CSGODirectory => NativeEngineHelpers.GetCSGODirectoryPath();
string ISwiftlyCore.GameDirectory => NativeEngineHelpers.GetGameDirectoryPath();
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.SchemaDefinitions;
using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Shared.Schemas;
using SwiftlyS2.Shared.Natives;

namespace SwiftlyS2.Shared.SchemaDefinitions;

public partial interface CCSGameRules
{
public GamePhase GamePhaseEnum { get; set; }

/// <summary>
/// Find the player that the controller is targetting
/// </summary>
Expand All @@ -21,5 +22,12 @@ public partial interface CCSGameRules
/// <param name="delay">The delay before ending the round</param>
public void TerminateRound( RoundEndReason reason, float delay );

public GamePhase GamePhaseEnum { get; set; }
/// <summary>
/// Ends the current round with the specified reason after an optional delay
/// </summary>
/// <param name="reason">The reason for ending the round</param>
/// <param name="delay">The delay before ending the round</param>
/// <param name="teamId">The team id to end the round for</param>
/// <param name="unk01">Unknown parameter</param>
public void TerminateRound( RoundEndReason reason, float delay, uint teamId, uint unk01 = 0 );
}
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.Misc;
using SwiftlyS2.Shared.Natives;
using SwiftlyS2.Shared.SchemaDefinitions;
using SwiftlyS2.Core.Natives;
using SwiftlyS2.Shared.Schemas;
using SwiftlyS2.Shared.SchemaDefinitions;
using EndReason = SwiftlyS2.Shared.Natives.RoundEndReason;

namespace SwiftlyS2.Core.SchemaDefinitions;

internal partial class CCSGameRulesImpl : CCSGameRules
{
public GamePhase GamePhaseEnum {
get => (GamePhase)GamePhase;
set => GamePhase = (int)value;
}

public T? FindPickerEntity<T>( CBasePlayerController controller ) where T : ISchemaClass<T>
{
CBaseEntity ent = new CBaseEntityImpl(GameFunctions.FindPickerEntity(Address, controller.Address));
return ent.As<T>();
return ((CBaseEntity)new CBaseEntityImpl(GameFunctions.FindPickerEntity(Address, controller.Address))).As<T>();
}

public void TerminateRound( RoundEndReason reason, float delay )
public void TerminateRound( EndReason reason, float delay )
{
GameFunctions.TerminateRound(Address, (uint)reason, delay);
GameFunctions.TerminateRound(Address, (uint)reason, delay, 0, 0);
}

public GamePhase GamePhaseEnum {
get => (GamePhase)GamePhase;
set => GamePhase = (int)value;
public void TerminateRound( EndReason reason, float delay, uint teamId, uint unk01 )
{
GameFunctions.TerminateRound(Address, (uint)reason, delay, teamId, unk01);
}
}
Loading
Loading