Add arrivals (#14755)

* Arrivals

* More arrivals and shitty uhh preload

* cvar

* a

* clockin + maps

* shitter prevention

* Placement

* a

* cvar for tests and dev

* weh
This commit is contained in:
metalgearsloth
2023-03-22 20:29:55 +11:00
committed by GitHub
parent a26b284349
commit f3a06a0696
39 changed files with 9456 additions and 141 deletions

View File

@@ -34,6 +34,7 @@ namespace Content.Client.GameTicking.Managers
[ViewVariables] public bool DisallowedLateJoin { get; private set; }
[ViewVariables] public string? ServerInfoBlob { get; private set; }
[ViewVariables] public TimeSpan StartTime { get; private set; }
[ViewVariables] public TimeSpan PreloadTime { get; private set; }
[ViewVariables] public TimeSpan RoundStartTimeSpan { get; private set; }
[ViewVariables] public new bool Paused { get; private set; }
@@ -89,6 +90,7 @@ namespace Content.Client.GameTicking.Managers
private void LobbyStatus(TickerLobbyStatusEvent message)
{
StartTime = message.StartTime;
PreloadTime = message.PreloadTime;
RoundStartTimeSpan = message.RoundStartTimeSpan;
IsGameStarted = message.IsRoundStarted;
AreWeReady = message.YouAreReady;

View File

@@ -138,6 +138,10 @@ namespace Content.Client.Lobby
{
text = Loc.GetString("lobby-state-paused");
}
else if ((_gameTicker.StartTime - _gameTicker.PreloadTime) < _gameTiming.CurTime)
{
text = Loc.GetString("lobby-state-preloading");
}
else
{
var difference = _gameTicker.StartTime - _gameTiming.CurTime;

View File

@@ -53,6 +53,7 @@ public static class PoolManager
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
(CCVars.CargoShuttles.Name, "false"),
(CCVars.ArrivalsShuttles.Name, "false"),
(CCVars.EmergencyShuttleEnabled.Name, "false"),
(CCVars.ProcgenPreload.Name, "false"),
// @formatter:on

View File

@@ -1,6 +1,5 @@
using System.Linq;
using Content.Shared.GameTicking;
using Content.Server.Station.Systems;
using Content.Server.Station.Components;
using Robust.Server.Player;
using Robust.Shared.Network;
@@ -18,6 +17,12 @@ namespace Content.Server.GameTicking
[ViewVariables]
private TimeSpan _roundStartTime;
/// <summary>
/// How long before RoundStartTime do we load maps.
/// </summary>
[ViewVariables]
public TimeSpan RoundPreloadTime { get; } = TimeSpan.FromSeconds(15);
[ViewVariables]
private TimeSpan _pauseTime;
@@ -93,7 +98,7 @@ namespace Content.Server.GameTicking
private TickerLobbyStatusEvent GetStatusMsg(IPlayerSession session)
{
_playerGameStatuses.TryGetValue(session.UserId, out var status);
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == PlayerGameStatus.ReadyToPlay, _roundStartTime, _roundStartTimeSpan, Paused);
return new TickerLobbyStatusEvent(RunLevel != GameRunLevel.PreRoundLobby, LobbySong, LobbyBackground,status == PlayerGameStatus.ReadyToPlay, _roundStartTime, RoundPreloadTime, _roundStartTimeSpan, Paused);
}
private void SendStatusToAll()

View File

@@ -69,6 +69,16 @@ namespace Content.Server.GameTicking
[ViewVariables]
public int RoundId { get; private set; }
/// <summary>
/// Returns true if the round's map is eligible to be updated.
/// </summary>
/// <returns></returns>
public bool CanUpdateMap()
{
return RunLevel == GameRunLevel.PreRoundLobby &&
_roundStartTime - RoundPreloadTime > _gameTiming.CurTime;
}
/// <summary>
/// Loads all the maps for the given round.
/// </summary>
@@ -77,11 +87,13 @@ namespace Content.Server.GameTicking
/// </remarks>
private void LoadMaps()
{
if (_mapManager.MapExists(DefaultMap))
return;
AddGamePresetRules();
DefaultMap = _mapManager.CreateMap();
_mapManager.AddUninitializedMap(DefaultMap);
var startTime = _gameTiming.RealTime;
var maps = new List<GameMapPrototype>();
@@ -121,9 +133,6 @@ namespace Content.Server.GameTicking
LoadGameMap(map, toLoad, null);
}
var timeSpan = _gameTiming.RealTime - startTime;
_sawmill.Info($"Loaded maps in {timeSpan.TotalMilliseconds:N2}ms.");
}
@@ -170,6 +179,7 @@ namespace Content.Server.GameTicking
SendServerMessage(Loc.GetString("game-ticker-start-round"));
// Just in case it hasn't been loaded previously we'll try loading it.
LoadMaps();
// map has been selected so update the lobby info text
@@ -487,15 +497,24 @@ namespace Content.Server.GameTicking
RoundLengthMetric.Inc(frameTime);
}
if (RunLevel != GameRunLevel.PreRoundLobby || Paused ||
_roundStartTime > _gameTiming.CurTime ||
if (RunLevel != GameRunLevel.PreRoundLobby ||
Paused ||
_roundStartTime - RoundPreloadTime > _gameTiming.CurTime ||
_roundStartCountdownHasNotStartedYetDueToNoPlayers)
{
return;
}
if (_roundStartTime < _gameTiming.CurTime)
{
StartRound();
}
// Preload maps so we can start faster
else if (_roundStartTime - RoundPreloadTime < _gameTiming.CurTime)
{
LoadMaps();
}
}
public TimeSpan RoundDuration()
{

View File

@@ -3,6 +3,7 @@ using System.Linq;
using Content.Server.Ghost;
using Content.Server.Ghost.Components;
using Content.Server.Players;
using Content.Server.Shuttles.Systems;
using Content.Server.Spawners.Components;
using Content.Server.Speech.Components;
using Content.Server.Station.Components;
@@ -227,6 +228,13 @@ namespace Content.Server.GameTicking
Loc.GetString("job-greet-station-name", ("stationName", metaData.EntityName)));
}
// Arrivals is unable to do this during spawning as no actor is attached yet.
// We also want this message last.
if (lateJoin && _arrivals.Enabled)
{
_chatManager.DispatchServerMessage(player, Loc.GetString("latejoin-arrivals-direction"));
}
// We raise this event directed to the mob, but also broadcast it so game rules can do something now.
PlayersJoinedRoundNormally++;
var aev = new PlayerSpawnCompleteEvent(mob, player, jobId, lateJoin, PlayersJoinedRoundNormally, station, character);

View File

@@ -9,6 +9,7 @@ using Content.Server.Maps;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Preferences.Managers;
using Content.Server.ServerUpdates;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Chat;
using Content.Shared.Damage;
@@ -34,8 +35,8 @@ namespace Content.Server.GameTicking
{
public sealed partial class GameTicker : SharedGameTicker
{
[Dependency] private readonly ArrivalsSystem _arrivals = default!;
[Dependency] private readonly MapLoaderSystem _map = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[ViewVariables] private bool _initialized;

View File

@@ -264,7 +264,7 @@ namespace Content.Server.Physics.Controllers
// Reset inputs for non-piloted shuttles.
foreach (var (shuttle, _) in _shuttlePilots)
{
if (newPilots.ContainsKey(shuttle) || FTLLocked(shuttle)) continue;
if (newPilots.ContainsKey(shuttle) || CanPilot(shuttle)) continue;
_thruster.DisableLinearThrusters(shuttle);
}
@@ -275,7 +275,7 @@ namespace Content.Server.Physics.Controllers
// then do the movement input once for it.
foreach (var (shuttle, pilots) in _shuttlePilots)
{
if (Paused(shuttle.Owner) || FTLLocked(shuttle) || !TryComp(shuttle.Owner, out PhysicsComponent? body)) continue;
if (Paused(shuttle.Owner) || CanPilot(shuttle) || !TryComp(shuttle.Owner, out PhysicsComponent? body)) continue;
var shuttleNorthAngle = Transform(body.Owner).WorldRotation;
@@ -559,10 +559,11 @@ namespace Content.Server.Physics.Controllers
}
}
private bool FTLLocked(ShuttleComponent shuttle)
private bool CanPilot(ShuttleComponent shuttle)
{
return (TryComp<FTLComponent>(shuttle.Owner, out var ftl) &&
(ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0);
return TryComp<FTLComponent>(shuttle.Owner, out var ftl) &&
(ftl.State & (FTLState.Starting | FTLState.Travelling | FTLState.Arriving)) != 0x0 ||
HasComp<PreventPilotComponent>(shuttle.Owner);
}
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Shuttles.Components;
[RegisterComponent]
public sealed class ArrivalsShuttleComponent : Component
{
[DataField("station")]
public EntityUid Station;
[DataField("nextTransfer", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextTransfer;
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Added to a designated arrivals station for players to spawn at, if enabled.
/// </summary>
[RegisterComponent]
public sealed class ArrivalsSourceComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Added to players after having been spawned onto the station.
/// Prevents them from taking the arrivals shuttle.
/// </summary>
[RegisterComponent]
public sealed class ClockedInComponent : Component
{
}

View File

@@ -0,0 +1,10 @@
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Added to arrivals latejoins until they have arrived at the station.
/// </summary>
[RegisterComponent]
public sealed class PendingClockInComponent : Component
{
}

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Components;
/// <summary>
/// Added to a station that is available for arrivals shuttles.
/// </summary>
[RegisterComponent]
public sealed class StationArrivalsComponent : Component
{
[DataField("shuttle")]
public EntityUid Shuttle;
[DataField("shuttlePath")] public ResourcePath ShuttlePath = new("/Maps/Shuttles/arrivals.yml");
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Map;
namespace Content.Server.Shuttles.Events;
/// <summary>
/// Raised when a shuttle has moved to FTL space.
/// </summary>
[ByRefEvent]
public readonly record struct FTLStartedEvent(EntityUid? FromMapUid, Matrix3 FTLFrom, Angle FromRotation);

View File

@@ -0,0 +1,372 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Events;
using Content.Server.Shuttles.Components;
using Content.Server.Shuttles.Events;
using Content.Server.Spawners.Components;
using Content.Server.Spawners.EntitySystems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Shuttles.Components;
using Content.Shared.Spawners.Components;
using Content.Shared.Tiles;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Shuttles.Systems;
/// <summary>
/// If enabled spawns players on a separate arrivals station before they can transfer to the main station.
/// </summary>
public sealed class ArrivalsSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly MapLoaderSystem _loader = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly ShuttleSystem _shuttles = default!;
[Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
[Dependency] private readonly StationSystem _station = default!;
/// <summary>
/// If enabled then spawns players on an alternate map so they can take a shuttle to the station.
/// </summary>
public bool Enabled { get; private set; }
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerSpawningEvent>(OnPlayerSpawn, before: new []{typeof(SpawnPointSystem)});
SubscribeLocalEvent<StationArrivalsComponent, ComponentStartup>(OnArrivalsStartup);
SubscribeLocalEvent<ArrivalsShuttleComponent, ComponentStartup>(OnShuttleStartup);
SubscribeLocalEvent<ArrivalsShuttleComponent, EntityUnpausedEvent>(OnShuttleUnpaused);
SubscribeLocalEvent<StationInitializedEvent>(OnStationInit);
SubscribeLocalEvent<RoundStartingEvent>(OnRoundStarting);
SubscribeLocalEvent<ArrivalsShuttleComponent, FTLStartedEvent>(OnArrivalsFTL);
// Don't invoke immediately as it will get set in the natural course of things.
Enabled = _cfgManager.GetCVar(CCVars.ArrivalsShuttles);
_cfgManager.OnValueChanged(CCVars.ArrivalsShuttles, SetArrivals);
// Command so admins can set these for funsies
_console.RegisterCommand("arrivals", ArrivalsCommand, ArrivalsCompletion);
}
private CompletionResult ArrivalsCompletion(IConsoleShell shell, string[] args)
{
if (args.Length != 1)
return CompletionResult.Empty;
return new CompletionResult(new CompletionOption[]
{
// Enables and disable are separate comms in case you don't want to accidentally toggle it, compared to
// returns which doesn't have an immediate effect
new("enable", Loc.GetString("cmd-arrivals-enable-hint")),
new("disable", Loc.GetString("cmd-arrivals-disable-hint")),
new("returns", Loc.GetString("cmd-arrivals-returns-hint")),
new ("force", Loc.GetString("cmd-arrivals-force-hint"))
}, "Option");
}
[AdminCommand(AdminFlags.Fun)]
private void ArrivalsCommand(IConsoleShell shell, string argstr, string[] args)
{
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("cmd-arrivals-invalid"));
return;
}
switch (args[0])
{
case "enable":
_cfgManager.SetCVar(CCVars.ArrivalsShuttles, true);
break;
case "disable":
_cfgManager.SetCVar(CCVars.ArrivalsShuttles, false);
break;
case "returns":
var existing = _cfgManager.GetCVar(CCVars.ArrivalsReturns);
_cfgManager.SetCVar(CCVars.ArrivalsReturns, !existing);
shell.WriteLine(Loc.GetString("cmd-arrivals-returns", ("value", !existing)));
break;
case "force":
var query = AllEntityQuery<PendingClockInComponent, TransformComponent>();
var spawnPoints = EntityQuery<SpawnPointComponent, TransformComponent>().ToList();
TryGetArrivals(out var arrivalsUid);
while (query.MoveNext(out var uid, out _, out var pendingXform))
{
_random.Shuffle(spawnPoints);
foreach (var (point, xform) in spawnPoints)
{
if (point.SpawnType != SpawnPointType.LateJoin || xform.GridUid == arrivalsUid)
continue;
_transform.SetCoordinates(uid, pendingXform, xform.Coordinates);
break;
}
RemCompDeferred<PendingClockInComponent>(uid);
shell.WriteLine(Loc.GetString("cmd-arrivals-forced", ("uid", ToPrettyString(uid))));
}
break;
default:
shell.WriteError(Loc.GetString($"cmd-arrivals-invalid"));
break;
}
}
public override void Shutdown()
{
base.Shutdown();
_cfgManager.UnsubValueChanged(CCVars.ArrivalsShuttles, SetArrivals);
}
private void OnArrivalsFTL(EntityUid uid, ArrivalsShuttleComponent component, ref FTLStartedEvent args)
{
// Anyone already clocked in yeet them off the shuttle.
if (!_cfgManager.GetCVar(CCVars.ArrivalsReturns) && args.FromMapUid != null)
{
var clockedQuery = AllEntityQuery<ClockedInComponent, TransformComponent>();
// Clock them in when they FTL
while (clockedQuery.MoveNext(out var cUid, out _, out var xform))
{
if (xform.GridUid != uid)
continue;
var rotation = xform.LocalRotation;
_transform.SetCoordinates(cUid, new EntityCoordinates(args.FromMapUid.Value, args.FTLFrom.Transform(xform.LocalPosition)));
_transform.SetWorldRotation(cUid, args.FromRotation + rotation);
}
}
var pendingQuery = AllEntityQuery<PendingClockInComponent, TransformComponent>();
// Clock them in when they FTL
while (pendingQuery.MoveNext(out var pUid, out _, out var xform))
{
if (xform.GridUid != uid)
continue;
EnsureComp<ClockedInComponent>(pUid);
}
}
private void OnStationInit(StationInitializedEvent ev)
{
EnsureComp<StationArrivalsComponent>(ev.Station);
}
private void OnPlayerSpawn(PlayerSpawningEvent ev)
{
// Only works on latejoin even if enabled.
if (!Enabled || _ticker.RunLevel != GameRunLevel.InRound)
return;
var points = EntityQuery<SpawnPointComponent, TransformComponent>().ToList();
_random.Shuffle(points);
TryGetArrivals(out var arrivals);
if (TryComp<TransformComponent>(arrivals, out var arrivalsXform))
{
var mapId = arrivalsXform.MapID;
foreach (var (spawnPoint, xform) in points)
{
if (spawnPoint.SpawnType != SpawnPointType.LateJoin || xform.MapID != mapId)
continue;
ev.SpawnResult = _stationSpawning.SpawnPlayerMob(
xform.Coordinates,
ev.Job,
ev.HumanoidCharacterProfile,
ev.Station);
EnsureComp<PendingClockInComponent>(ev.SpawnResult.Value);
return;
}
}
}
private void OnShuttleStartup(EntityUid uid, ArrivalsShuttleComponent component, ComponentStartup args)
{
EnsureComp<PreventPilotComponent>(uid);
}
private void OnShuttleUnpaused(EntityUid uid, ArrivalsShuttleComponent component, ref EntityUnpausedEvent args)
{
component.NextTransfer += args.PausedTime;
}
private bool TryGetArrivals(out EntityUid uid)
{
var arrivalsQuery = EntityQueryEnumerator<ArrivalsSourceComponent>();
while (arrivalsQuery.MoveNext(out uid, out _))
{
return true;
}
return false;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ArrivalsShuttleComponent, ShuttleComponent, TransformComponent>();
var curTime = _timing.CurTime;
TryGetArrivals(out var arrivals);
// TODO: FTL fucker, if on an edge tile every N seconds check for wall or w/e
// TODO: Docking should be per-grid rather than per dock and bump off when undocking.
// TODO: Stop dispatch if emergency shuttle has arrived.
// TODO: Need server join message specifying shuttle wait time or smth.
// TODO: Need maps
// TODO: Need emergency suits on shuttle probs
// TODO: Need some kind of comp to shunt people off if they try to get on?
if (TryComp<TransformComponent>(arrivals, out var arrivalsXform))
{
while (query.MoveNext(out var comp, out var shuttle, out var xform))
{
if (comp.NextTransfer > curTime || !TryComp<StationDataComponent>(comp.Station, out var data))
continue;
// Go back to arrivals source
if (xform.MapUid != arrivalsXform.MapUid)
{
if (arrivals.IsValid())
_shuttles.FTLTravel(shuttle, arrivals, dock: true);
}
// Go to station
else
{
var targetGrid = _station.GetLargestGrid(data);
if (targetGrid != null)
_shuttles.FTLTravel(shuttle, targetGrid.Value, dock: true);
}
comp.NextTransfer += TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown));
}
}
}
private void OnRoundStarting(RoundStartingEvent ev)
{
// Setup arrivals station
if (!Enabled)
return;
SetupArrivalsStation();
}
private void SetupArrivalsStation()
{
var mapId = _mapManager.CreateMap();
if (!_loader.TryLoad(mapId, _cfgManager.GetCVar(CCVars.ArrivalsMap).ToString(), out var uids))
{
return;
}
foreach (var id in uids)
{
EnsureComp<ArrivalsSourceComponent>(id);
EnsureComp<ProtectedGridComponent>(id);
}
// Handle roundstart stations.
var query = AllEntityQuery<StationArrivalsComponent>();
while (query.MoveNext(out var uid, out var comp))
{
SetupShuttle(uid, comp);
}
}
private void SetArrivals(bool obj)
{
Enabled = obj;
if (Enabled)
{
SetupArrivalsStation();
var query = AllEntityQuery<StationArrivalsComponent>();
while (query.MoveNext(out var sUid, out var comp))
{
SetupShuttle(sUid, comp);
}
}
else
{
var sourceQuery = AllEntityQuery<ArrivalsSourceComponent>();
while (sourceQuery.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
var shuttleQuery = AllEntityQuery<ArrivalsShuttleComponent>();
while (shuttleQuery.MoveNext(out var uid, out _))
{
QueueDel(uid);
}
}
}
private void OnArrivalsStartup(EntityUid uid, StationArrivalsComponent component, ComponentStartup args)
{
if (!Enabled)
return;
// If it's a latespawn station then this will fail but that's okey
SetupShuttle(uid, component);
}
private void SetupShuttle(EntityUid uid, StationArrivalsComponent component)
{
if (!Deleted(component.Shuttle))
return;
// Spawn arrivals on a dummy map then dock it to the source.
var dummyMap = _mapManager.CreateMap();
if (TryGetArrivals(out var arrivals) &&
_loader.TryLoad(dummyMap, component.ShuttlePath.ToString(), out var shuttleUids))
{
component.Shuttle = shuttleUids[0];
var shuttleComp = Comp<ShuttleComponent>(component.Shuttle);
var arrivalsComp = EnsureComp<ArrivalsShuttleComponent>(component.Shuttle);
arrivalsComp.Station = uid;
EnsureComp<ProtectedGridComponent>(uid);
_shuttles.FTLTravel(shuttleComp, arrivals, hyperspaceTime: 10f, dock: true);
arrivalsComp.NextTransfer = _timing.CurTime + TimeSpan.FromSeconds(_cfgManager.GetCVar(CCVars.ArrivalsCooldown));
}
// Don't start the arrivals shuttle immediately docked so power has a time to stabilise?
var timer = AddComp<TimedDespawnComponent>(_mapManager.GetMapEntityId(dummyMap));
timer.Lifetime = 15f;
}
}

View File

@@ -71,7 +71,11 @@ public sealed partial class DockingSystem
// TODO: Validation
if (!TryComp<DockingComponent>(args.DockEntity, out var dock) ||
!dock.Docked) return;
!dock.Docked ||
HasComp<PreventPilotComponent>(Transform(uid).GridUid))
{
return;
}
Undock(dock);
}
@@ -81,7 +85,12 @@ public sealed partial class DockingSystem
_sawmill.Debug($"Received autodock request for {ToPrettyString(args.DockEntity)}");
var player = args.Session.AttachedEntity;
if (player == null || !HasComp<DockingComponent>(args.DockEntity)) return;
if (player == null ||
!HasComp<DockingComponent>(args.DockEntity) ||
HasComp<PreventPilotComponent>(Transform(uid).GridUid))
{
return;
}
// TODO: Validation
var comp = EnsureComp<AutoDockComponent>(args.DockEntity);

View File

@@ -63,7 +63,10 @@ namespace Content.Server.Shuttles.Systems
private void OnDestinationMessage(EntityUid uid, ShuttleConsoleComponent component, ShuttleConsoleDestinationMessage args)
{
if (!TryComp<FTLDestinationComponent>(args.Destination, out var dest)) return;
if (!TryComp<FTLDestinationComponent>(args.Destination, out var dest))
{
return;
}
if (!dest.Enabled) return;
@@ -77,10 +80,16 @@ namespace Content.Server.Shuttles.Systems
RaiseLocalEvent(entity.Value, ref getShuttleEv);
entity = getShuttleEv.Console;
if (entity == null || dest.Whitelist?.IsValid(entity.Value, EntityManager) == false) return;
if (entity == null || dest.Whitelist?.IsValid(entity.Value, EntityManager) == false)
{
return;
}
if (!TryComp<TransformComponent>(entity, out var xform) ||
!TryComp<ShuttleComponent>(xform.GridUid, out var shuttle)) return;
!TryComp<ShuttleComponent>(xform.GridUid, out var shuttle))
{
return;
}
if (HasComp<FTLComponent>(xform.GridUid))
{

View File

@@ -11,6 +11,7 @@ using Content.Server.Station.Systems;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Shuttles.Events;
using Content.Shared.Tiles;
using Robust.Server.GameObjects;
using Robust.Server.Maps;
using Robust.Server.Player;
@@ -448,6 +449,7 @@ public sealed partial class ShuttleSystem
_shuttleIndex += _mapManager.GetGrid(shuttle.Value).LocalAABB.Width + ShuttleSpawnBuffer;
component.EmergencyShuttle = shuttle;
EnsureComp<ProtectedGridComponent>(shuttle.Value);
}
private void CleanupEmergencyShuttle()

View File

@@ -15,6 +15,7 @@ using System.Diagnostics.CodeAnalysis;
using Content.Server.Shuttles.Events;
using Content.Shared.Buckle.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Shuttles.Components;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
@@ -40,7 +41,6 @@ public sealed partial class ShuttleSystem
private const float DefaultTravelTime = 30f;
private const float DefaultArrivalTime = 5f;
private const float FTLCooldown = 30f;
private const float ShuttleFTLRange = 100f;
/// <summary>
@@ -84,6 +84,12 @@ public sealed partial class ShuttleSystem
public bool CanFTL(EntityUid? uid, [NotNullWhen(false)] out string? reason, TransformComponent? xform = null)
{
if (HasComp<PreventPilotComponent>(uid))
{
reason = Loc.GetString("shuttle-console-prevent");
return false;
}
reason = null;
if (!TryComp<MapGridComponent>(uid, out var grid) ||
@@ -223,6 +229,9 @@ public sealed partial class ShuttleSystem
DoTheDinosaur(xform);
comp.State = FTLState.Travelling;
var fromMapUid = xform.MapUid;
var fromMatrix = _transform.GetWorldMatrix(xform);
var fromRotation = _transform.GetWorldRotation(xform);
var width = Comp<MapGridComponent>(uid).LocalAABB.Width;
xform.Coordinates = new EntityCoordinates(_mapManager.GetMapEntityId(_hyperSpaceMap!.Value), new Vector2(_index + width / 2f, 0f));
@@ -239,14 +248,16 @@ public sealed partial class ShuttleSystem
_physics.SetAngularDamping(body, 0f);
}
SetDockBolts(uid, true);
_console.RefreshShuttleConsoles(uid);
var ev = new FTLStartedEvent(fromMapUid, fromMatrix, fromRotation);
RaiseLocalEvent(uid, ref ev);
if (comp.TravelSound != null)
{
comp.TravelStream = SoundSystem.Play(comp.TravelSound.GetSound(),
Filter.Pvs(uid, 4f, entityManager: EntityManager), comp.TravelSound.Params);
}
SetDockBolts(uid, true);
_console.RefreshShuttleConsoles(uid);
break;
// Arriving, play effects
case FTLState.Travelling:

View File

@@ -1,5 +1,6 @@
using System.Linq;
using Content.Server.GameTicking;
using Content.Server.Shuttles.Components;
using Content.Server.Spawners.Components;
using Content.Server.Station.Systems;
using Robust.Shared.Random;
@@ -20,6 +21,9 @@ public sealed class SpawnPointSystem : EntitySystem
private void OnSpawnPlayer(PlayerSpawningEvent args)
{
if (args.SpawnResult != null)
return;
// TODO: Cache all this if it ends up important.
var points = EntityQuery<SpawnPointComponent>().ToList();
_random.Shuffle(points);
@@ -37,9 +41,11 @@ public sealed class SpawnPointSystem : EntitySystem
args.HumanoidCharacterProfile,
args.Station);
EnsureComp<ClockedInComponent>(args.SpawnResult.Value);
return;
}
else if (_gameTicker.RunLevel != GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.Job && (args.Job == null || spawnPoint.Job?.ID == args.Job.Prototype.ID))
if (_gameTicker.RunLevel != GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.Job && (args.Job == null || spawnPoint.Job?.ID == args.Job.Prototype.ID))
{
args.SpawnResult = _stationSpawning.SpawnPlayerMob(
xform.Coordinates,
@@ -47,6 +53,7 @@ public sealed class SpawnPointSystem : EntitySystem
args.HumanoidCharacterProfile,
args.Station);
EnsureComp<ClockedInComponent>(args.SpawnResult.Value);
return;
}
}
@@ -62,6 +69,7 @@ public sealed class SpawnPointSystem : EntitySystem
args.HumanoidCharacterProfile,
args.Station);
EnsureComp<ClockedInComponent>(args.SpawnResult.Value);
return;
}

View File

@@ -8,6 +8,7 @@ using Content.Server.PDA;
using Content.Server.Roles;
using Content.Server.Station.Components;
using Content.Server.Mind.Commands;
using Content.Server.Shuttles.Components;
using Content.Shared.Access.Systems;
using Content.Shared.CCVar;
using Content.Shared.Humanoid.Prototypes;

View File

@@ -1,110 +0,0 @@
using Content.Server.Stack;
using Content.Shared.Audio;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Stacks;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
namespace Content.Server.Tiles
{
public sealed class FloorTileSystem : EntitySystem
{
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FloorTileComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnAfterInteract(EntityUid uid, FloorTileComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Handled)
return;
if (!TryComp<StackComponent>(uid, out var stack))
return;
if (component.OutputTiles == null)
return;
// this looks a bit sussy but it might be because it needs to be able to place off of grids and expand them
var location = args.ClickLocation.AlignWithClosestGridTile();
var physics = GetEntityQuery<PhysicsComponent>();
foreach (var ent in location.GetEntitiesInTile(lookupSystem: _lookup))
{
// check that we the tile we're trying to access isn't blocked by a wall or something
if (physics.TryGetComponent(ent, out var phys) &&
phys.BodyType == BodyType.Static &&
phys.Hard &&
(phys.CollisionLayer & (int) CollisionGroup.Impassable) != 0)
return;
}
var locationMap = location.ToMap(EntityManager);
if (locationMap.MapId == MapId.Nullspace)
return;
_mapManager.TryGetGrid(location.EntityId, out var mapGrid);
foreach (var currentTile in component.OutputTiles)
{
var currentTileDefinition = (ContentTileDefinition) _tileDefinitionManager[currentTile];
if (mapGrid != null)
{
var tile = mapGrid.GetTileRef(location);
var baseTurf = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId];
if (HasBaseTurf(currentTileDefinition, baseTurf.ID))
{
if (!_stackSystem.Use(uid, 1, stack))
continue;
PlaceAt(mapGrid, location, currentTileDefinition.TileId, component.PlaceTileSound);
args.Handled = true;
return;
}
}
else if (HasBaseTurf(currentTileDefinition, ContentTileDefinition.SpaceID))
{
mapGrid = _mapManager.CreateGrid(locationMap.MapId);
var gridXform = Transform(mapGrid.Owner);
gridXform.WorldPosition = locationMap.Position;
location = new EntityCoordinates(mapGrid.Owner, Vector2.Zero);
PlaceAt(mapGrid, location, _tileDefinitionManager[component.OutputTiles[0]].TileId, component.PlaceTileSound, mapGrid.TileSize / 2f);
args.Handled = true;
return;
}
}
}
public bool HasBaseTurf(ContentTileDefinition tileDef, string baseTurf)
{
foreach (var tileBaseTurf in tileDef.BaseTurfs)
{
if (baseTurf == tileBaseTurf)
return true;
}
return false;
}
private void PlaceAt(MapGridComponent mapGrid, EntityCoordinates location, ushort tileId, SoundSpecifier placeSound, float offset = 0)
{
var variant = _random.Pick(((ContentTileDefinition) _tileDefinitionManager[tileId]).PlacementVariants);
mapGrid.SetTile(location.Offset(new Vector2(offset, offset)), new Tile(tileId, 0, variant));
_audio.Play(placeSound, Filter.Pvs(location), location, true, AudioHelpers.WithVariation(0.125f, _random));
}
}
}

View File

@@ -199,7 +199,7 @@ namespace Content.Server.Voting.Managers
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Map vote finished: {picked.MapName}");
var ticker = _entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
if (ticker.RunLevel == GameRunLevel.PreRoundLobby)
if (ticker.CanUpdateMap())
{
if (_gameMapManager.TrySelectMapIfEligible(picked.ID))
{
@@ -207,9 +207,16 @@ namespace Content.Server.Voting.Managers
}
}
else
{
if (ticker.RoundPreloadTime <= TimeSpan.Zero)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby"));
}
else
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("ui-vote-map-notlobby-time"));
}
}
};
}

View File

@@ -1055,6 +1055,30 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> CameraRotationLocked =
CVarDef.Create("shuttle.camera_rotation_locked", false, CVar.REPLICATED);
/// <summary>
/// Whether the arrivals shuttle is enabled.
/// </summary>
public static readonly CVarDef<bool> ArrivalsShuttles =
CVarDef.Create("shuttle.arrivals", true, CVar.SERVERONLY);
/// <summary>
/// The map to use for the arrivals station.
/// </summary>
public static readonly CVarDef<ResourcePath> ArrivalsMap =
CVarDef.Create("shuttle.arrivals_map", new ResourcePath("/Maps/Misc/terminal.yml"), CVar.SERVERONLY);
/// <summary>
/// Cooldown between arrivals departures. This should be longer than the FTL time or it will double cycle.
/// </summary>
public static readonly CVarDef<float> ArrivalsCooldown =
CVarDef.Create("shuttle.arrivals_cooldown", 90f, CVar.SERVERONLY);
/// <summary>
/// Are players allowed to return on the arrivals shuttle.
/// </summary>
public static readonly CVarDef<bool> ArrivalsReturns =
CVarDef.Create("shuttle.arrivals_returns", false, CVar.SERVERONLY);
/// <summary>
/// Whether cargo shuttles are enabled.
/// </summary>

View File

@@ -45,16 +45,18 @@ namespace Content.Shared.GameTicking
public bool YouAreReady { get; }
// UTC.
public TimeSpan StartTime { get; }
public TimeSpan PreloadTime { get; }
public TimeSpan RoundStartTimeSpan { get; }
public bool Paused { get; }
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, string? lobbyBackground, bool youAreReady, TimeSpan startTime, TimeSpan roundStartTimeSpan, bool paused)
public TickerLobbyStatusEvent(bool isRoundStarted, string? lobbySong, string? lobbyBackground, bool youAreReady, TimeSpan startTime, TimeSpan preloadTime, TimeSpan roundStartTimeSpan, bool paused)
{
IsRoundStarted = isRoundStarted;
LobbySong = lobbySong;
LobbyBackground = lobbyBackground;
YouAreReady = youAreReady;
StartTime = startTime;
PreloadTime = preloadTime;
RoundStartTimeSpan = roundStartTimeSpan;
Paused = paused;
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Shuttles.Components;
/// <summary>
/// Add to grids that you do not want manually piloted under any circumstances.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class PreventPilotComponent : Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Shared.Tiles;
/// <summary>
/// Raised directed on a grid when attempting a floor tile placement.
/// </summary>
[ByRefEvent]
public record struct FloorTileAttemptEvent(bool Cancelled);

View File

@@ -1,14 +1,15 @@
using Content.Shared.Maps;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Tiles
namespace Content.Shared.Tiles
{
/// <summary>
/// This gives items floor tile behavior, but it doesn't have to be a literal floor tile.
/// A lot of materials use this too. Note that the AfterInteract will fail without a stack component on the item.
/// </summary>
[RegisterComponent]
[RegisterComponent, NetworkedComponent]
public sealed class FloorTileComponent : Component
{
[DataField("outputs", customTypeSerializer: typeof(PrototypeIdListSerializer<ContentTileDefinition>))]

View File

@@ -0,0 +1,127 @@
using Content.Shared.Audio;
using Content.Shared.Interaction;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Stacks;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Tiles;
public sealed class FloorTileSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedStackSystem _stackSystem = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FloorTileComponent, AfterInteractEvent>(OnAfterInteract);
}
private void OnAfterInteract(EntityUid uid, FloorTileComponent component, AfterInteractEvent args)
{
if (!args.CanReach || args.Handled)
return;
if (!TryComp<StackComponent>(uid, out var stack))
return;
if (component.OutputTiles == null)
return;
// this looks a bit sussy but it might be because it needs to be able to place off of grids and expand them
var location = args.ClickLocation.AlignWithClosestGridTile();
var physics = GetEntityQuery<PhysicsComponent>();
foreach (var ent in location.GetEntitiesInTile(lookupSystem: _lookup))
{
// check that we the tile we're trying to access isn't blocked by a wall or something
if (physics.TryGetComponent(ent, out var phys) &&
phys.BodyType == BodyType.Static &&
phys.Hard &&
(phys.CollisionLayer & (int) CollisionGroup.Impassable) != 0)
return;
}
var locationMap = location.ToMap(EntityManager, _transform);
if (locationMap.MapId == MapId.Nullspace)
return;
_mapManager.TryGetGrid(location.EntityId, out var mapGrid);
foreach (var currentTile in component.OutputTiles)
{
var currentTileDefinition = (ContentTileDefinition) _tileDefinitionManager[currentTile];
if (mapGrid != null)
{
var ev = new FloorTileAttemptEvent();
RaiseLocalEvent(mapGrid);
if (HasComp<ProtectedGridComponent>(mapGrid.Owner) || ev.Cancelled)
{
if (_netManager.IsClient && _timing.IsFirstTimePredicted)
_popup.PopupEntity(Loc.GetString("invalid-floor-placement"), args.User);
return;
}
var tile = mapGrid.GetTileRef(location);
var baseTurf = (ContentTileDefinition) _tileDefinitionManager[tile.Tile.TypeId];
if (HasBaseTurf(currentTileDefinition, baseTurf.ID))
{
if (!_stackSystem.Use(uid, 1, stack))
continue;
PlaceAt(args.User, mapGrid, location, currentTileDefinition.TileId, component.PlaceTileSound);
args.Handled = true;
return;
}
}
else if (HasBaseTurf(currentTileDefinition, ContentTileDefinition.SpaceID))
{
mapGrid = _mapManager.CreateGrid(locationMap.MapId);
var gridXform = Transform(mapGrid.Owner);
_transform.SetWorldPosition(gridXform, locationMap.Position);
location = new EntityCoordinates(mapGrid.Owner, Vector2.Zero);
PlaceAt(args.User, mapGrid, location, _tileDefinitionManager[component.OutputTiles[0]].TileId, component.PlaceTileSound, mapGrid.TileSize / 2f);
args.Handled = true;
return;
}
}
}
public bool HasBaseTurf(ContentTileDefinition tileDef, string baseTurf)
{
foreach (var tileBaseTurf in tileDef.BaseTurfs)
{
if (baseTurf == tileBaseTurf)
return true;
}
return false;
}
private void PlaceAt(EntityUid user, MapGridComponent mapGrid, EntityCoordinates location, ushort tileId, SoundSpecifier placeSound, float offset = 0)
{
var variant = _random.Pick(((ContentTileDefinition) _tileDefinitionManager[tileId]).PlacementVariants);
mapGrid.SetTile(location.Offset(new Vector2(offset, offset)), new Tile(tileId, 0, variant));
_audio.PlayPredicted(placeSound, location, user, AudioHelpers.WithVariation(0.125f, _random));
}
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Tiles;
/// <summary>
/// Prevents floor tile updates when attached to a grid.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class ProtectedGridComponent : Component
{
}

View File

@@ -14,3 +14,4 @@ preload = false
auto_call_time = 0
cargo = false
emergency_enabled = false
arrivals = false

View File

@@ -34,3 +34,4 @@ player-leave-message = Player {$name} left.
latejoin-arrival-announcement = {$character} ({$job}) has arrived at the station!
latejoin-arrival-sender = Station
latejoin-arrivals-direction = A shuttle transferring you to your station will arrive shortly.

View File

@@ -1,4 +1,5 @@
lobby-state-paused = Paused
lobby-state-preloading = Soon
lobby-state-right-now-question = Right Now?
lobby-state-right-now-confirmation = Right Now
lobby-state-round-start-countdown-text = Round Starts In: {$timeLeft}

View File

@@ -0,0 +1,10 @@
cmd-arrivals-enable-hint = Enables arrivals
cmd-arrivals-disable-hint = Disables arrivals
cmd-arrivals-returns = Set arrivals returns to {$value}.
cmd-arrivals-returns-hint = Toggles allowing players to return via arrivals.
cmd-arrivals-invalid = Invalid arg supplied.
cmd-arrivals-force-hint = Forces players to arrive.
cmd-arrivals-forced = Forced {$uid} to arrive to the station.

View File

@@ -3,6 +3,7 @@ shuttle-pilot-end = Stopped piloting
shuttle-console-in-ftl = Can't FTL while in FTL!
shuttle-console-proximity = Too close to nearby objects
shuttle-console-prevent = You are unable to pilot this ship.
# Display
shuttle-console-display-label = Display

View File

@@ -0,0 +1 @@
invalid-floor-placement = Unable to place there

View File

@@ -17,4 +17,5 @@ ui-vote-gamemode-win = { $winner } won the gamemode vote!
ui-vote-map-title = Next map
ui-vote-map-tie = Tie for map vote! Picking... { $picked }
ui-vote-map-win = { $winner } won the map vote!
ui-vote-map-notlobby = Voting for maps is only valid in the pre-round lobby!
ui-vote-map-notlobby-time = Voting for maps is only valid in the pre-round lobby!
ui-vote-map-notlobby-time = Voting for maps is only valid in the pre-round lobby with { $time } remaining!

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff