2021-06-20 10:09:24 +02:00
using System.Diagnostics.CodeAnalysis ;
2022-05-19 14:44:24 +10:00
using System.Linq ;
2023-02-21 01:16:25 +01:00
using System.Threading.Tasks ;
2021-06-20 10:09:24 +02:00
using Content.Server.GameTicking.Presets ;
2023-08-01 17:11:50 -04:00
using Content.Server.Maps ;
2021-06-20 10:09:24 +02:00
using Content.Shared.CCVar ;
2021-12-21 21:23:29 +01:00
using Content.Shared.Damage ;
using Content.Shared.Damage.Prototypes ;
2023-01-04 00:49:15 -06:00
using Content.Shared.Database ;
2023-12-05 00:06:10 +01:00
using Content.Shared.FixedPoint ;
2023-08-25 18:50:46 +10:00
using Content.Shared.Ghost ;
2023-08-30 21:46:11 -07:00
using Content.Shared.Mind ;
2023-12-05 00:06:10 +01:00
using Content.Shared.Mobs ;
2023-01-13 16:57:10 -08:00
using Content.Shared.Mobs.Components ;
2023-12-05 00:06:10 +01:00
using Content.Shared.Mobs.Systems ;
2023-04-25 20:23:14 -04:00
using JetBrains.Annotations ;
2023-10-28 09:59:53 +11:00
using Robust.Shared.Player ;
2021-06-20 10:09:24 +02:00
namespace Content.Server.GameTicking
{
2022-02-16 00:23:23 -07:00
public sealed partial class GameTicker
2021-06-20 10:09:24 +02:00
{
2023-12-05 00:06:10 +01:00
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default ! ;
2021-06-20 10:09:24 +02:00
public const float PresetFailedCooldownIncrease = 30f ;
2023-08-01 17:11:50 -04:00
/// <summary>
/// The selected preset that will be used at the start of the next round.
/// </summary>
2022-05-19 14:44:24 +10:00
public GamePresetPrototype ? Preset { get ; private set ; }
2021-06-20 10:09:24 +02:00
2023-08-01 17:11:50 -04:00
/// <summary>
/// The preset that's currently active.
/// </summary>
public GamePresetPrototype ? CurrentPreset { get ; private set ; }
2023-10-28 09:59:53 +11:00
private bool StartPreset ( ICommonSession [ ] origReadyPlayers , bool force )
2022-02-21 14:11:39 -08:00
{
var startAttempt = new RoundStartAttemptEvent ( origReadyPlayers , force ) ;
RaiseLocalEvent ( startAttempt ) ;
if ( ! startAttempt . Cancelled )
return true ;
2023-08-01 17:11:50 -04:00
var presetTitle = CurrentPreset ! = null ? Loc . GetString ( CurrentPreset . ModeTitle ) : string . Empty ;
2022-02-21 14:11:39 -08:00
void FailedPresetRestart ( )
{
SendServerMessage ( Loc . GetString ( "game-ticker-start-round-cannot-start-game-mode-restart" ,
( "failedGameMode" , presetTitle ) ) ) ;
RestartRound ( ) ;
DelayStart ( TimeSpan . FromSeconds ( PresetFailedCooldownIncrease ) ) ;
}
if ( _configurationManager . GetCVar ( CCVars . GameLobbyFallbackEnabled ) )
{
2023-06-08 00:16:18 -07:00
var fallbackPresets = _configurationManager . GetCVar ( CCVars . GameLobbyFallbackPreset ) . Split ( "," ) ;
var startFailed = true ;
2022-02-21 14:11:39 -08:00
2023-06-08 00:16:18 -07:00
foreach ( var preset in fallbackPresets )
{
ClearGameRules ( ) ;
SetGamePreset ( preset ) ;
AddGamePresetRules ( ) ;
StartGamePresetRules ( ) ;
startAttempt . Uncancel ( ) ;
RaiseLocalEvent ( startAttempt ) ;
if ( ! startAttempt . Cancelled )
{
_chatManager . SendAdminAnnouncement (
Loc . GetString ( "game-ticker-start-round-cannot-start-game-mode-fallback" ,
( "failedGameMode" , presetTitle ) ,
( "fallbackMode" , Loc . GetString ( preset ) ) ) ) ;
RefreshLateJoinAllowed ( ) ;
startFailed = false ;
break ;
}
}
2022-02-21 14:11:39 -08:00
2023-06-08 00:16:18 -07:00
if ( startFailed )
2022-02-21 14:11:39 -08:00
{
FailedPresetRestart ( ) ;
2022-03-03 23:41:42 +01:00
return false ;
2022-02-21 14:11:39 -08:00
}
}
2023-06-08 00:16:18 -07:00
2022-02-21 14:11:39 -08:00
else
{
FailedPresetRestart ( ) ;
return false ;
}
return true ;
}
2021-12-21 21:23:29 +01:00
private void InitializeGamePreset ( )
2021-06-20 10:09:24 +02:00
{
2022-05-22 17:32:34 -07:00
SetGamePreset ( LobbyEnabled ? _configurationManager . GetCVar ( CCVars . GameLobbyDefaultPreset ) : "sandbox" ) ;
2021-06-20 10:09:24 +02:00
}
2024-04-24 17:38:43 +12:00
public void SetGamePreset ( GamePresetPrototype ? preset , bool force = false )
2021-12-21 18:56:47 +01:00
{
2021-12-21 21:23:29 +01:00
// Do nothing if this game ticker is a dummy!
if ( DummyTicker )
return ;
2021-06-20 10:09:24 +02:00
2022-05-19 14:44:24 +10:00
Preset = preset ;
2023-08-01 17:11:50 -04:00
ValidateMap ( ) ;
2023-12-30 21:51:36 +04:00
UpdateInfoText ( ) ;
2021-12-21 21:23:29 +01:00
if ( force )
2021-12-21 18:56:47 +01:00
{
2021-12-21 21:23:29 +01:00
StartRound ( true ) ;
}
}
2021-12-21 19:25:52 +01:00
2021-12-21 21:23:29 +01:00
public void SetGamePreset ( string preset , bool force = false )
{
var proto = FindGamePreset ( preset ) ;
if ( proto ! = null )
SetGamePreset ( proto , force ) ;
}
public GamePresetPrototype ? FindGamePreset ( string preset )
{
if ( _prototypeManager . TryIndex ( preset , out GamePresetPrototype ? presetProto ) )
return presetProto ;
2021-12-21 19:25:52 +01:00
2021-12-21 21:23:29 +01:00
foreach ( var proto in _prototypeManager . EnumeratePrototypes < GamePresetPrototype > ( ) )
{
foreach ( var alias in proto . Alias )
2021-06-20 10:09:24 +02:00
{
2021-12-21 21:23:29 +01:00
if ( preset . Equals ( alias , StringComparison . InvariantCultureIgnoreCase ) )
return proto ;
2021-06-20 10:09:24 +02:00
}
}
2021-12-21 21:23:29 +01:00
return null ;
2021-06-20 10:09:24 +02:00
}
2021-12-21 21:23:29 +01:00
public bool TryFindGamePreset ( string preset , [ NotNullWhen ( true ) ] out GamePresetPrototype ? prototype )
2021-06-20 10:09:24 +02:00
{
2021-12-21 21:23:29 +01:00
prototype = FindGamePreset ( preset ) ;
return prototype ! = null ;
2021-06-20 10:09:24 +02:00
}
2023-08-01 17:11:50 -04:00
public bool IsMapEligible ( GameMapPrototype map )
{
if ( Preset = = null )
return true ;
if ( Preset . MapPool = = null | | ! _prototypeManager . TryIndex < GameMapPoolPrototype > ( Preset . MapPool , out var pool ) )
return true ;
return pool . Maps . Contains ( map . ID ) ;
}
private void ValidateMap ( )
{
if ( Preset = = null | | _gameMapManager . GetSelectedMap ( ) is not { } map )
return ;
if ( Preset . MapPool = = null | |
! _prototypeManager . TryIndex < GameMapPoolPrototype > ( Preset . MapPool , out var pool ) )
return ;
if ( pool . Maps . Contains ( map . ID ) )
return ;
_gameMapManager . SelectMapRandom ( ) ;
}
2023-04-25 20:23:14 -04:00
[PublicAPI]
2021-12-21 21:23:29 +01:00
private bool AddGamePresetRules ( )
2021-06-20 10:09:24 +02:00
{
2022-05-19 14:44:24 +10:00
if ( DummyTicker | | Preset = = null )
2021-12-21 21:23:29 +01:00
return false ;
2023-08-01 17:11:50 -04:00
CurrentPreset = Preset ;
2022-05-19 14:44:24 +10:00
foreach ( var rule in Preset . Rules )
2021-12-21 21:23:29 +01:00
{
2023-04-25 20:23:14 -04:00
AddGameRule ( rule ) ;
2021-12-21 21:23:29 +01:00
}
return true ;
2021-06-20 10:09:24 +02:00
}
2024-01-30 22:52:35 -07:00
public void StartGamePresetRules ( )
2022-02-15 20:06:28 -07:00
{
2022-05-19 14:44:24 +10:00
// May be touched by the preset during init.
2023-04-25 20:23:14 -04:00
var rules = new List < EntityUid > ( GetAddedGameRules ( ) ) ;
foreach ( var rule in rules )
2022-02-15 20:06:28 -07:00
{
StartGameRule ( rule ) ;
}
}
2023-08-28 16:53:24 -07:00
public bool OnGhostAttempt ( EntityUid mindId , bool canReturnGlobal , bool viaCommand = false , MindComponent ? mind = null )
2021-06-20 10:09:24 +02:00
{
2023-08-28 16:53:24 -07:00
if ( ! Resolve ( mindId , ref mind ) )
return false ;
2023-01-04 00:49:15 -06:00
var playerEntity = mind . CurrentEntity ;
if ( playerEntity ! = null & & viaCommand )
_adminLogger . Add ( LogType . Mind , $"{EntityManager.ToPrettyString(playerEntity.Value):player} is attempting to ghost via command" ) ;
2021-12-21 21:23:29 +01:00
var handleEv = new GhostAttemptHandleEvent ( mind , canReturnGlobal ) ;
RaiseLocalEvent ( handleEv ) ;
2021-06-20 10:09:24 +02:00
2021-12-21 21:23:29 +01:00
// Something else has handled the ghost attempt for us! We return its result.
if ( handleEv . Handled )
return handleEv . Result ;
2021-12-21 18:56:47 +01:00
2022-12-19 21:55:45 -06:00
if ( mind . PreventGhosting )
{
2023-04-25 20:23:14 -04:00
if ( mind . Session ! = null ) // Logging is suppressed to prevent spam from ghost attempts caused by movement attempts
{
2023-10-28 09:59:53 +11:00
_chatManager . DispatchServerMessage ( mind . Session , Loc . GetString ( "comp-mind-ghosting-prevented" ) ,
2023-01-04 00:49:15 -06:00
true ) ;
2023-04-25 20:23:14 -04:00
}
2022-12-19 21:55:45 -06:00
return false ;
}
2023-01-06 20:39:27 +11:00
if ( HasComp < GhostComponent > ( playerEntity ) )
2021-12-21 21:23:29 +01:00
return false ;
if ( mind . VisitingEntity ! = default )
2021-12-21 18:56:47 +01:00
{
2023-08-28 16:53:24 -07:00
_mind . UnVisit ( mindId , mind : mind ) ;
2021-12-21 18:56:47 +01:00
}
2023-06-18 11:33:19 -07:00
var position = Exists ( playerEntity )
2021-12-21 21:23:29 +01:00
? Transform ( playerEntity . Value ) . Coordinates
: GetObserverSpawnPoint ( ) ;
2023-02-14 15:46:44 +13:00
if ( position = = default )
return false ;
2021-12-21 21:23:29 +01:00
// Ok, so, this is the master place for the logic for if ghosting is "too cheaty" to allow returning.
// There's no reason at this time to move it to any other place, especially given that the 'side effects required' situations would also have to be moved.
// + If CharacterDeadPhysically applies, we're physically dead. Therefore, ghosting OK, and we can return (this is critical for gibbing)
// Note that we could theoretically be ICly dead and still physically alive and vice versa.
// (For example, a zombie could be dead ICly, but may retain memories and is definitely physically active)
// + If we're in a mob that is critical, and we're supposed to be able to return if possible,
// we're succumbing - the mob is killed. Therefore, character is dead. Ghosting OK.
// (If the mob survives, that's a bug. Ghosting is kept regardless.)
2023-06-21 13:04:07 +12:00
var canReturn = canReturnGlobal & & _mind . IsCharacterDeadPhysically ( mind ) ;
2021-12-21 21:23:29 +01:00
if ( canReturnGlobal & & TryComp ( playerEntity , out MobStateComponent ? mobState ) )
{
2023-06-21 13:04:07 +12:00
if ( _mobState . IsCritical ( playerEntity . Value , mobState ) )
2021-12-21 21:23:29 +01:00
{
canReturn = true ;
//todo: what if they dont breathe lol
//cry deeply
2023-12-05 00:06:10 +01:00
FixedPoint2 dealtDamage = 200 ;
if ( TryComp < DamageableComponent > ( playerEntity , out var damageable )
& & TryComp < MobThresholdsComponent > ( playerEntity , out var thresholds ) )
{
var playerDeadThreshold = _mobThresholdSystem . GetThresholdForState ( playerEntity . Value , MobState . Dead , thresholds ) ;
dealtDamage = playerDeadThreshold - damageable . TotalDamage ;
}
DamageSpecifier damage = new ( _prototypeManager . Index < DamageTypePrototype > ( "Asphyxiation" ) , dealtDamage ) ;
2021-12-21 21:23:29 +01:00
_damageable . TryChangeDamage ( playerEntity , damage , true ) ;
}
}
2023-01-06 20:39:27 +11:00
var xformQuery = GetEntityQuery < TransformComponent > ( ) ;
var coords = _transform . GetMoverCoordinates ( position , xformQuery ) ;
2023-11-20 20:27:37 -08:00
var ghost = Spawn ( ObserverPrototypeName , coords ) ;
2021-12-21 21:23:29 +01:00
// Try setting the ghost entity name to either the character name or the player name.
// If all else fails, it'll default to the default entity prototype name, "observer".
// However, that should rarely happen.
2023-08-28 11:20:31 +02:00
if ( ! string . IsNullOrWhiteSpace ( mind . CharacterName ) )
_metaData . SetEntityName ( ghost , mind . CharacterName ) ;
2021-12-21 21:23:29 +01:00
else if ( ! string . IsNullOrWhiteSpace ( mind . Session ? . Name ) )
2023-08-28 11:20:31 +02:00
_metaData . SetEntityName ( ghost , mind . Session . Name ) ;
2021-12-21 21:23:29 +01:00
var ghostComponent = Comp < GhostComponent > ( ghost ) ;
if ( mind . TimeOfDeath . HasValue )
2021-06-20 10:09:24 +02:00
{
2023-08-25 18:50:46 +10:00
_ghost . SetTimeOfDeath ( ghost , mind . TimeOfDeath ! . Value , ghostComponent ) ;
2021-06-20 10:09:24 +02:00
}
2023-01-04 00:49:15 -06:00
if ( playerEntity ! = null )
_adminLogger . Add ( LogType . Mind , $"{EntityManager.ToPrettyString(playerEntity.Value):player} ghosted{(!canReturn ? " ( non - returnable ) " : " ")}" ) ;
2023-08-25 18:50:46 +10:00
_ghost . SetCanReturnToBody ( ghostComponent , canReturn ) ;
2021-12-21 21:23:29 +01:00
if ( canReturn )
2023-08-28 16:53:24 -07:00
_mind . Visit ( mindId , ghost , mind ) ;
2021-12-21 21:23:29 +01:00
else
2023-08-28 16:53:24 -07:00
_mind . TransferTo ( mindId , ghost , mind : mind ) ;
2021-12-21 21:23:29 +01:00
return true ;
2021-06-20 10:09:24 +02:00
}
2023-02-21 01:16:25 +01:00
private void IncrementRoundNumber ( )
{
var playerIds = _playerGameStatuses . Keys . Select ( player = > player . UserId ) . ToArray ( ) ;
var serverName = _configurationManager . GetCVar ( CCVars . AdminLogsServerName ) ;
// TODO FIXME AAAAAAAAAAAAAAAAAAAH THIS IS BROKEN
// Task.Run as a terrible dirty workaround to avoid synchronization context deadlock from .Result here.
// This whole setup logic should be made asynchronous so we can properly wait on the DB AAAAAAAAAAAAAH
var task = Task . Run ( async ( ) = >
{
2023-12-06 23:48:56 +01:00
var server = await _dbEntryManager . ServerEntity ;
2023-02-21 01:16:25 +01:00
return await _db . AddNewRound ( server , playerIds ) ;
} ) ;
_taskManager . BlockWaitOnTask ( task ) ;
RoundId = task . GetAwaiter ( ) . GetResult ( ) ;
}
2021-12-21 21:23:29 +01:00
}
2022-02-16 00:23:23 -07:00
public sealed class GhostAttemptHandleEvent : HandledEntityEventArgs
2021-12-21 21:23:29 +01:00
{
2023-08-28 16:53:24 -07:00
public MindComponent Mind { get ; }
2021-12-21 21:23:29 +01:00
public bool CanReturnGlobal { get ; }
public bool Result { get ; set ; }
2021-06-20 10:09:24 +02:00
2023-08-28 16:53:24 -07:00
public GhostAttemptHandleEvent ( MindComponent mind , bool canReturnGlobal )
2021-06-20 10:09:24 +02:00
{
2021-12-21 21:23:29 +01:00
Mind = mind ;
CanReturnGlobal = canReturnGlobal ;
2021-06-20 10:09:24 +02:00
}
}
}