Merge remote-tracking branch 'refs/remotes/upstream/master' into ed-08-05-2024-upstream
# Conflicts: # Content.Server/GameTicking/GameTicker.Spawning.cs # Content.Shared/Preferences/HumanoidCharacterProfile.cs # Resources/Prototypes/lobbyscreens.yml
This commit is contained in:
@@ -33,8 +33,8 @@ public sealed class PresetIdCardSystem : EntitySystem
|
||||
var station = _stationSystem.GetOwningStation(uid);
|
||||
|
||||
// If we're not on an extended access station, the ID is already configured correctly from MapInit.
|
||||
if (station == null || !Comp<StationJobsComponent>(station.Value).ExtendedAccess)
|
||||
return;
|
||||
if (station == null || !TryComp<StationJobsComponent>(station.Value, out var jobsComp) || !jobsComp.ExtendedAccess)
|
||||
continue;
|
||||
|
||||
SetupIdAccess(uid, card, true);
|
||||
SetupIdName(uid, card);
|
||||
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Antag;
|
||||
@@ -63,15 +64,17 @@ public sealed partial class AntagSelectionSystem
|
||||
/// </summary>
|
||||
public int GetTargetAntagCount(Entity<AntagSelectionComponent> ent, AntagSelectionPlayerPool? pool, AntagSelectionDefinition def)
|
||||
{
|
||||
var poolSize = pool?.Count ?? _playerManager.Sessions.Length;
|
||||
var poolSize = pool?.Count ?? _playerManager.Sessions
|
||||
.Count(s => s.State.Status is not SessionStatus.Disconnected and not SessionStatus.Zombie);
|
||||
|
||||
// factor in other definitions' affect on the count.
|
||||
var countOffset = 0;
|
||||
foreach (var otherDef in ent.Comp.Definitions)
|
||||
{
|
||||
countOffset += Math.Clamp(poolSize / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
|
||||
countOffset += Math.Clamp((poolSize - countOffset) / otherDef.PlayerRatio, otherDef.Min, otherDef.Max) * otherDef.PlayerRatio;
|
||||
}
|
||||
// make sure we don't double-count the current selection
|
||||
countOffset -= Math.Clamp((poolSize + countOffset) / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||
countOffset -= Math.Clamp(poolSize / def.PlayerRatio, def.Min, def.Max) * def.PlayerRatio;
|
||||
|
||||
return Math.Clamp((poolSize - countOffset) / def.PlayerRatio, def.Min, def.Max);
|
||||
}
|
||||
|
||||
@@ -280,11 +280,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
_transform.SetMapCoordinates((player, playerXform), pos);
|
||||
}
|
||||
|
||||
// If we want to just do a ghost role spawner, set up data here and then return early.
|
||||
// This could probably be an event in the future if we want to be more refined about it.
|
||||
if (isSpawner)
|
||||
{
|
||||
if (!TryComp<GhostRoleAntagSpawnerComponent>(player, out var spawnerComp))
|
||||
{
|
||||
Log.Error("Antag spawner with GhostRoleAntagSpawnerComponent.");
|
||||
Log.Error($"Antag spawner {player} does not have a GhostRoleAntagSpawnerComponent.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -293,6 +295,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
return;
|
||||
}
|
||||
|
||||
// The following is where we apply components, equipment, and other changes to our antagonist entity.
|
||||
EntityManager.AddComponents(player, def.Components);
|
||||
_stationSpawning.EquipStartingGear(player, def.StartingGear);
|
||||
|
||||
@@ -308,11 +311,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
||||
_role.MindAddRoles(curMind.Value, def.MindComponents);
|
||||
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
|
||||
}
|
||||
|
||||
if (def.Briefing is { } briefing)
|
||||
{
|
||||
SendBriefing(session, briefing);
|
||||
SendBriefing(session, def.Briefing);
|
||||
}
|
||||
|
||||
var afterEv = new AfterAntagEntitySelectedEvent(session, player, ent, def);
|
||||
@@ -325,7 +324,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
public AntagSelectionPlayerPool GetPlayerPool(Entity<AntagSelectionComponent> ent, List<ICommonSession> sessions, AntagSelectionDefinition def)
|
||||
{
|
||||
var preferredList = new List<ICommonSession>();
|
||||
var secondBestList = new List<ICommonSession>();
|
||||
var fallbackList = new List<ICommonSession>();
|
||||
var unwantedList = new List<ICommonSession>();
|
||||
var invalidList = new List<ICommonSession>();
|
||||
foreach (var session in sessions)
|
||||
@@ -344,7 +343,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
}
|
||||
else if (def.FallbackRoles.Count != 0 && pref.AntagPreferences.Any(p => def.FallbackRoles.Contains(p)))
|
||||
{
|
||||
secondBestList.Add(session);
|
||||
fallbackList.Add(session);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -352,7 +351,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
}
|
||||
}
|
||||
|
||||
return new AntagSelectionPlayerPool(new() { preferredList, secondBestList, unwantedList, invalidList });
|
||||
return new AntagSelectionPlayerPool(new() { preferredList, fallbackList, unwantedList, invalidList });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,5 +17,5 @@ public sealed partial class MobReplacementRuleComponent : Component
|
||||
/// Chance per-entity.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float Chance = 0.001f;
|
||||
public float Chance = 0.004f;
|
||||
}
|
||||
|
||||
@@ -9,180 +9,37 @@ using System.Numerics;
|
||||
|
||||
namespace Content.Server.Chemistry.Containers.EntitySystems;
|
||||
|
||||
[Obsolete("This is being depreciated. Use SharedSolutionContainerSystem instead!")]
|
||||
public sealed partial class SolutionContainerSystem : SharedSolutionContainerSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SolutionContainerManagerComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<SolutionContainerManagerComponent, ComponentShutdown>(OnComponentShutdown);
|
||||
SubscribeLocalEvent<ContainedSolutionComponent, ComponentShutdown>(OnComponentShutdown);
|
||||
}
|
||||
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name)
|
||||
=> EnsureSolution(entity, name, out _);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, out bool existed)
|
||||
=> EnsureSolution(entity, name, FixedPoint2.Zero, out existed);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, out bool existed)
|
||||
=> EnsureSolution(entity, name, maxVol, null, out existed);
|
||||
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Solution EnsureSolution(Entity<MetaDataComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
|
||||
{
|
||||
var (uid, meta) = entity;
|
||||
if (!Resolve(uid, ref meta))
|
||||
throw new InvalidOperationException("Attempted to ensure solution on invalid entity.");
|
||||
|
||||
var manager = EnsureComp<SolutionContainerManagerComponent>(uid);
|
||||
if (meta.EntityLifeStage >= EntityLifeStage.MapInitialized)
|
||||
return EnsureSolutionEntity((uid, manager), name, maxVol, prototype, out existed).Comp.Solution;
|
||||
else
|
||||
return EnsureSolutionPrototype((uid, manager), name, maxVol, prototype, out existed);
|
||||
EnsureSolution(entity, name, maxVol, prototype, out existed, out var solution);
|
||||
return solution!;//solution is only ever null on the client, so we can suppress this
|
||||
}
|
||||
|
||||
public void EnsureAllSolutions(Entity<SolutionContainerManagerComponent> entity)
|
||||
[Obsolete("This is being depreciated. Use the ensure methods in SharedSolutionContainerSystem instead!")]
|
||||
public Entity<SolutionComponent> EnsureSolutionEntity(
|
||||
Entity<SolutionContainerManagerComponent?> entity,
|
||||
string name,
|
||||
FixedPoint2 maxVol,
|
||||
Solution? prototype,
|
||||
out bool existed)
|
||||
{
|
||||
if (entity.Comp.Solutions is not { } prototypes)
|
||||
return;
|
||||
|
||||
foreach (var (name, prototype) in prototypes)
|
||||
{
|
||||
EnsureSolutionEntity((entity.Owner, entity.Comp), name, prototype.MaxVolume, prototype, out _);
|
||||
}
|
||||
|
||||
entity.Comp.Solutions = null;
|
||||
Dirty(entity);
|
||||
EnsureSolutionEntity(entity, name, out existed, out var solEnt, maxVol, prototype);
|
||||
return solEnt!.Value;//solEnt is only ever null on the client, so we can suppress this
|
||||
}
|
||||
|
||||
public Entity<SolutionComponent> EnsureSolutionEntity(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
|
||||
{
|
||||
existed = true;
|
||||
|
||||
var (uid, container) = entity;
|
||||
|
||||
var solutionSlot = ContainerSystem.EnsureContainer<ContainerSlot>(uid, $"solution@{name}", out existed);
|
||||
if (!Resolve(uid, ref container, logMissing: false))
|
||||
{
|
||||
existed = false;
|
||||
container = AddComp<SolutionContainerManagerComponent>(uid);
|
||||
container.Containers.Add(name);
|
||||
}
|
||||
else if (!existed)
|
||||
{
|
||||
container.Containers.Add(name);
|
||||
Dirty(uid, container);
|
||||
}
|
||||
|
||||
var needsInit = false;
|
||||
SolutionComponent solutionComp;
|
||||
if (solutionSlot.ContainedEntity is not { } solutionId)
|
||||
{
|
||||
prototype ??= new() { MaxVolume = maxVol };
|
||||
prototype.Name = name;
|
||||
(solutionId, solutionComp, _) = SpawnSolutionUninitialized(solutionSlot, name, maxVol, prototype);
|
||||
existed = false;
|
||||
needsInit = true;
|
||||
Dirty(uid, container);
|
||||
}
|
||||
else
|
||||
{
|
||||
solutionComp = Comp<SolutionComponent>(solutionId);
|
||||
DebugTools.Assert(TryComp(solutionId, out ContainedSolutionComponent? relation) && relation.Container == uid && relation.ContainerName == name);
|
||||
DebugTools.Assert(solutionComp.Solution.Name == name);
|
||||
|
||||
var solution = solutionComp.Solution;
|
||||
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
|
||||
|
||||
// Depending on MapInitEvent order some systems can ensure solution empty solutions and conflict with the prototype solutions.
|
||||
// We want the reagents from the prototype to exist even if something else already created the solution.
|
||||
if (prototype is { Volume.Value: > 0 })
|
||||
solution.AddSolution(prototype, PrototypeManager);
|
||||
|
||||
Dirty(solutionId, solutionComp);
|
||||
}
|
||||
|
||||
if (needsInit)
|
||||
EntityManager.InitializeAndStartEntity(solutionId, Transform(solutionId).MapID);
|
||||
|
||||
return (solutionId, solutionComp);
|
||||
}
|
||||
|
||||
private Solution EnsureSolutionPrototype(Entity<SolutionContainerManagerComponent?> entity, string name, FixedPoint2 maxVol, Solution? prototype, out bool existed)
|
||||
{
|
||||
existed = true;
|
||||
|
||||
var (uid, container) = entity;
|
||||
if (!Resolve(uid, ref container, logMissing: false))
|
||||
{
|
||||
container = AddComp<SolutionContainerManagerComponent>(uid);
|
||||
existed = false;
|
||||
}
|
||||
|
||||
if (container.Solutions is null)
|
||||
container.Solutions = new(SolutionContainerManagerComponent.DefaultCapacity);
|
||||
|
||||
if (!container.Solutions.TryGetValue(name, out var solution))
|
||||
{
|
||||
solution = prototype ?? new() { Name = name, MaxVolume = maxVol };
|
||||
container.Solutions.Add(name, solution);
|
||||
existed = false;
|
||||
}
|
||||
else
|
||||
solution.MaxVolume = FixedPoint2.Max(solution.MaxVolume, maxVol);
|
||||
|
||||
Dirty(uid, container);
|
||||
return solution;
|
||||
}
|
||||
|
||||
|
||||
private Entity<SolutionComponent, ContainedSolutionComponent> SpawnSolutionUninitialized(ContainerSlot container, string name, FixedPoint2 maxVol, Solution prototype)
|
||||
{
|
||||
var coords = new EntityCoordinates(container.Owner, Vector2.Zero);
|
||||
var uid = EntityManager.CreateEntityUninitialized(null, coords, null);
|
||||
|
||||
var solution = new SolutionComponent() { Solution = prototype };
|
||||
AddComp(uid, solution);
|
||||
|
||||
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
|
||||
AddComp(uid, relation);
|
||||
|
||||
MetaData.SetEntityName(uid, $"solution - {name}");
|
||||
ContainerSystem.Insert(uid, container, force: true);
|
||||
|
||||
return (uid, solution, relation);
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)
|
||||
{
|
||||
EnsureAllSolutions(entity);
|
||||
}
|
||||
|
||||
private void OnComponentShutdown(Entity<SolutionContainerManagerComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
foreach (var name in entity.Comp.Containers)
|
||||
{
|
||||
if (ContainerSystem.TryGetContainer(entity, $"solution@{name}", out var solutionContainer))
|
||||
ContainerSystem.ShutdownContainer(solutionContainer);
|
||||
}
|
||||
entity.Comp.Containers.Clear();
|
||||
}
|
||||
|
||||
private void OnComponentShutdown(Entity<ContainedSolutionComponent> entity, ref ComponentShutdown args)
|
||||
{
|
||||
if (TryComp(entity.Comp.Container, out SolutionContainerManagerComponent? container))
|
||||
{
|
||||
container.Containers.Remove(entity.Comp.ContainerName);
|
||||
Dirty(entity.Comp.Container, container);
|
||||
}
|
||||
|
||||
if (ContainerSystem.TryGetContainer(entity, $"solution@{entity.Comp.ContainerName}", out var solutionContainer))
|
||||
ContainerSystem.ShutdownContainer(solutionContainer);
|
||||
}
|
||||
|
||||
#endregion Event Handlers
|
||||
}
|
||||
|
||||
@@ -33,9 +33,11 @@ namespace Content.Server.Database
|
||||
}
|
||||
|
||||
#region Preferences
|
||||
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
|
||||
public async Task<PlayerPreferences?> GetPlayerPreferencesAsync(
|
||||
NetUserId userId,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var prefs = await db.DbContext
|
||||
.Preference
|
||||
@@ -47,7 +49,7 @@ namespace Content.Server.Database
|
||||
.ThenInclude(l => l.Groups)
|
||||
.ThenInclude(group => group.Loadouts)
|
||||
.AsSingleQuery()
|
||||
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
|
||||
.SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel);
|
||||
|
||||
if (prefs is null)
|
||||
return null;
|
||||
@@ -515,13 +517,13 @@ namespace Content.Server.Database
|
||||
#endregion
|
||||
|
||||
#region Playtime
|
||||
public async Task<List<PlayTime>> GetPlayTimes(Guid player)
|
||||
public async Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
return await db.DbContext.PlayTime
|
||||
.Where(p => p.PlayerId == player)
|
||||
.ToListAsync();
|
||||
.ToListAsync(cancel);
|
||||
}
|
||||
|
||||
public async Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
|
||||
@@ -673,7 +675,7 @@ namespace Content.Server.Database
|
||||
*/
|
||||
public async Task<Admin?> GetAdminDataForAsync(NetUserId userId, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
return await db.DbContext.Admin
|
||||
.Include(p => p.Flags)
|
||||
@@ -688,7 +690,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task<AdminRank?> GetAdminRankDataForAsync(int id, CancellationToken cancel = default)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
return await db.DbContext.AdminRank
|
||||
.Include(r => r.Flags)
|
||||
@@ -697,7 +699,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task RemoveAdminAsync(NetUserId userId, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var admin = await db.DbContext.Admin.SingleAsync(a => a.UserId == userId.UserId, cancel);
|
||||
db.DbContext.Admin.Remove(admin);
|
||||
@@ -707,7 +709,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task AddAdminAsync(Admin admin, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
db.DbContext.Admin.Add(admin);
|
||||
|
||||
@@ -716,7 +718,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task UpdateAdminAsync(Admin admin, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var existing = await db.DbContext.Admin.Include(a => a.Flags).SingleAsync(a => a.UserId == admin.UserId, cancel);
|
||||
existing.Flags = admin.Flags;
|
||||
@@ -728,7 +730,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task RemoveAdminRankAsync(int rankId, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var admin = await db.DbContext.AdminRank.SingleAsync(a => a.Id == rankId, cancel);
|
||||
db.DbContext.AdminRank.Remove(admin);
|
||||
@@ -738,7 +740,7 @@ namespace Content.Server.Database
|
||||
|
||||
public async Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
db.DbContext.AdminRank.Add(rank);
|
||||
|
||||
@@ -811,7 +813,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
public async Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var existing = await db.DbContext.AdminRank
|
||||
.Include(r => r.Flags)
|
||||
@@ -1594,7 +1596,9 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
return db.DbContext.Database.HasPendingModelChanges();
|
||||
}
|
||||
|
||||
protected abstract Task<DbGuard> GetDb([CallerMemberName] string? name = null);
|
||||
protected abstract Task<DbGuard> GetDb(
|
||||
CancellationToken cancel = default,
|
||||
[CallerMemberName] string? name = null);
|
||||
|
||||
protected void LogDbOp(string? name)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,11 @@ namespace Content.Server.Database
|
||||
void Shutdown();
|
||||
|
||||
#region Preferences
|
||||
Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile);
|
||||
Task<PlayerPreferences> InitPrefsAsync(
|
||||
NetUserId userId,
|
||||
ICharacterProfile defaultProfile,
|
||||
CancellationToken cancel);
|
||||
|
||||
Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index);
|
||||
|
||||
Task SaveCharacterSlotAsync(NetUserId userId, ICharacterProfile? profile, int slot);
|
||||
@@ -38,7 +42,7 @@ namespace Content.Server.Database
|
||||
|
||||
// Single method for two operations for transaction.
|
||||
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
|
||||
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId);
|
||||
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
|
||||
#endregion
|
||||
|
||||
#region User Ids
|
||||
@@ -157,8 +161,9 @@ namespace Content.Server.Database
|
||||
/// Look up a player's role timers.
|
||||
/// </summary>
|
||||
/// <param name="player">The player to get the role timer information from.</param>
|
||||
/// <param name="cancel"></param>
|
||||
/// <returns>All role timers belonging to the player.</returns>
|
||||
Task<List<PlayTime>> GetPlayTimes(Guid player);
|
||||
Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel = default);
|
||||
|
||||
/// <summary>
|
||||
/// Update play time information in bulk.
|
||||
@@ -346,7 +351,10 @@ namespace Content.Server.Database
|
||||
_sqliteInMemoryConnection?.Dispose();
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences> InitPrefsAsync(NetUserId userId, ICharacterProfile defaultProfile)
|
||||
public Task<PlayerPreferences> InitPrefsAsync(
|
||||
NetUserId userId,
|
||||
ICharacterProfile defaultProfile,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.InitPrefsAsync(userId, defaultProfile));
|
||||
@@ -376,10 +384,10 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId)
|
||||
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId));
|
||||
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel));
|
||||
}
|
||||
|
||||
public Task AssignUserIdAsync(string name, NetUserId userId)
|
||||
@@ -487,10 +495,10 @@ namespace Content.Server.Database
|
||||
|
||||
#region Playtime
|
||||
|
||||
public Task<List<PlayTime>> GetPlayTimes(Guid player)
|
||||
public Task<List<PlayTime>> GetPlayTimes(Guid player, CancellationToken cancel)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.GetPlayTimes(player));
|
||||
return RunDbCommand(() => _db.GetPlayTimes(player, cancel));
|
||||
}
|
||||
|
||||
public Task UpdatePlayTimes(IReadOnlyCollection<PlayTimeUpdate> updates)
|
||||
|
||||
@@ -527,22 +527,26 @@ WHERE to_tsvector('english'::regconfig, a.message) @@ websearch_to_tsquery('engl
|
||||
return time;
|
||||
}
|
||||
|
||||
private async Task<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
|
||||
private async Task<DbGuardImpl> GetDbImpl(
|
||||
CancellationToken cancel = default,
|
||||
[CallerMemberName] string? name = null)
|
||||
{
|
||||
LogDbOp(name);
|
||||
|
||||
await _dbReadyTask;
|
||||
await _prefsSemaphore.WaitAsync();
|
||||
await _prefsSemaphore.WaitAsync(cancel);
|
||||
|
||||
if (_msLag > 0)
|
||||
await Task.Delay(_msLag);
|
||||
await Task.Delay(_msLag, cancel);
|
||||
|
||||
return new DbGuardImpl(this, new PostgresServerDbContext(_options));
|
||||
}
|
||||
|
||||
protected override async Task<DbGuard> GetDb([CallerMemberName] string? name = null)
|
||||
protected override async Task<DbGuard> GetDb(
|
||||
CancellationToken cancel = default,
|
||||
[CallerMemberName] string? name = null)
|
||||
{
|
||||
return await GetDbImpl(name);
|
||||
return await GetDbImpl(cancel, name);
|
||||
}
|
||||
|
||||
private sealed class DbGuardImpl : DbGuard
|
||||
|
||||
@@ -439,7 +439,7 @@ namespace Content.Server.Database
|
||||
public override async Task<((Admin, string? lastUserName)[] admins, AdminRank[])> GetAllAdminAndRanksAsync(
|
||||
CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
await using var db = await GetDbImpl(cancel);
|
||||
|
||||
var admins = await db.SqliteDbContext.Admin
|
||||
.Include(a => a.Flags)
|
||||
@@ -514,23 +514,27 @@ namespace Content.Server.Database
|
||||
return DateTime.SpecifyKind(time, DateTimeKind.Utc);
|
||||
}
|
||||
|
||||
private async Task<DbGuardImpl> GetDbImpl([CallerMemberName] string? name = null)
|
||||
private async Task<DbGuardImpl> GetDbImpl(
|
||||
CancellationToken cancel = default,
|
||||
[CallerMemberName] string? name = null)
|
||||
{
|
||||
LogDbOp(name);
|
||||
await _dbReadyTask;
|
||||
if (_msDelay > 0)
|
||||
await Task.Delay(_msDelay);
|
||||
await Task.Delay(_msDelay, cancel);
|
||||
|
||||
await _prefsSemaphore.WaitAsync();
|
||||
await _prefsSemaphore.WaitAsync(cancel);
|
||||
|
||||
var dbContext = new SqliteServerDbContext(_options());
|
||||
|
||||
return new DbGuardImpl(this, dbContext);
|
||||
}
|
||||
|
||||
protected override async Task<DbGuard> GetDb([CallerMemberName] string? name = null)
|
||||
protected override async Task<DbGuard> GetDb(
|
||||
CancellationToken cancel = default,
|
||||
[CallerMemberName] string? name = null)
|
||||
{
|
||||
return await GetDbImpl(name).ConfigureAwait(false);
|
||||
return await GetDbImpl(cancel, name).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private sealed class DbGuardImpl : DbGuard
|
||||
@@ -569,9 +573,9 @@ namespace Content.Server.Database
|
||||
_semaphore = new SemaphoreSlim(maxCount, maxCount);
|
||||
}
|
||||
|
||||
public Task WaitAsync()
|
||||
public Task WaitAsync(CancellationToken cancel = default)
|
||||
{
|
||||
var task = _semaphore.WaitAsync();
|
||||
var task = _semaphore.WaitAsync(cancel);
|
||||
|
||||
if (_synchronous)
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Players.PlayTimeTracking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -16,17 +17,22 @@ namespace Content.Server.Database;
|
||||
/// Actual loading code is handled by separate managers such as <see cref="IServerPreferencesManager"/>.
|
||||
/// This manager is simply a centralized "is loading done" controller for other code to rely on.
|
||||
/// </remarks>
|
||||
public sealed class UserDbDataManager
|
||||
public sealed class UserDbDataManager : IPostInjectInit
|
||||
{
|
||||
[Dependency] private readonly IServerPreferencesManager _prefs = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTimeTracking = default!;
|
||||
|
||||
private readonly Dictionary<NetUserId, UserData> _users = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
// TODO: Ideally connected/disconnected would be subscribed to IPlayerManager directly,
|
||||
// but this runs into ordering issues with game ticker.
|
||||
public void ClientConnected(ICommonSession session)
|
||||
{
|
||||
_sawmill.Verbose($"Initiating load for user {session}");
|
||||
|
||||
DebugTools.Assert(!_users.ContainsKey(session.UserId), "We should not have any cached data on client connect.");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
@@ -51,11 +57,52 @@ public sealed class UserDbDataManager
|
||||
|
||||
private async Task Load(ICommonSession session, CancellationToken cancel)
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_prefs.LoadData(session, cancel),
|
||||
_playTimeTracking.LoadData(session, cancel));
|
||||
// The task returned by this function is only ever observed by callers of WaitLoadComplete,
|
||||
// which doesn't even happen currently if the lobby is enabled.
|
||||
// As such, this task must NOT throw a non-cancellation error!
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
_prefs.LoadData(session, cancel),
|
||||
_playTimeTracking.LoadData(session, cancel));
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
_prefs.FinishLoad(session);
|
||||
|
||||
_sawmill.Verbose($"Load complete for user {session}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_sawmill.Debug($"Load cancelled for user {session}");
|
||||
|
||||
// We can rethrow the cancellation.
|
||||
// This will make the task returned by WaitLoadComplete() also return a cancellation.
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Must catch all exceptions here, otherwise task may go unobserved.
|
||||
_sawmill.Error($"Load of user data failed: {e}");
|
||||
|
||||
// Kick them from server, since something is hosed. Let them try again I guess.
|
||||
session.Channel.Disconnect("Loading of server user data failed, this is a bug.");
|
||||
|
||||
// We throw a OperationCanceledException so users of WaitLoadComplete() always see cancellation here.
|
||||
throw new OperationCanceledException("Load of user data cancelled due to unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wait for all on-database data for a user to be loaded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The task returned by this function may end up in a cancelled state
|
||||
/// (throwing <see cref="OperationCanceledException"/>) if the user disconnects while loading or an error occurs.
|
||||
/// </remarks>
|
||||
/// <param name="session"></param>
|
||||
/// <returns>
|
||||
/// A task that completes when all on-database data for a user has finished loading.
|
||||
/// </returns>
|
||||
public Task WaitLoadComplete(ICommonSession session)
|
||||
{
|
||||
return _users[session.UserId].Task;
|
||||
@@ -63,7 +110,7 @@ public sealed class UserDbDataManager
|
||||
|
||||
public bool IsLoadComplete(ICommonSession session)
|
||||
{
|
||||
return GetLoadTask(session).IsCompleted;
|
||||
return GetLoadTask(session).IsCompletedSuccessfully;
|
||||
}
|
||||
|
||||
public Task GetLoadTask(ICommonSession session)
|
||||
@@ -71,5 +118,10 @@ public sealed class UserDbDataManager
|
||||
return _users[session.UserId].Task;
|
||||
}
|
||||
|
||||
void IPostInjectInit.PostInject()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("userdb");
|
||||
}
|
||||
|
||||
private sealed record UserData(CancellationTokenSource Cancel, Task Task);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,13 @@ public sealed partial class ExplosiveComponent : Component
|
||||
[DataField("deleteAfterExplosion")]
|
||||
public bool? DeleteAfterExplosion;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to not set <see cref="Exploded"/> to true, allowing it to explode multiple times.
|
||||
/// This should never be used if it is damageable.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Repeatable;
|
||||
|
||||
/// <summary>
|
||||
/// Avoid somehow double-triggering this explosion (e.g. by damaging this entity from its own explosion.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
using Content.Server.Explosion.EntitySystems;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Server.Explosion.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Constantly triggers after being added to an entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(TriggerSystem))]
|
||||
[AutoGenerateComponentPause]
|
||||
public sealed partial class RepeatingTriggerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// How long to wait between triggers.
|
||||
/// The first trigger starts this long after the component is added.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Delay = TimeSpan.FromSeconds(1);
|
||||
|
||||
/// <summary>
|
||||
/// When the next trigger will be.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan NextTrigger;
|
||||
}
|
||||
@@ -160,7 +160,7 @@ public sealed partial class ExplosionSystem : EntitySystem
|
||||
if (explosive.Exploded)
|
||||
return;
|
||||
|
||||
explosive.Exploded = true;
|
||||
explosive.Exploded = !explosive.Repeatable;
|
||||
|
||||
// Override the explosion intensity if optional arguments were provided.
|
||||
if (radius != null)
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
|
||||
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
|
||||
SubscribeLocalEvent<TriggerWhenEmptyComponent, OnEmptyGunShotEvent>(OnEmptyTriggered);
|
||||
SubscribeLocalEvent<RepeatingTriggerComponent, MapInitEvent>(OnRepeatInit);
|
||||
|
||||
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(OnSpawnTrigger);
|
||||
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
|
||||
@@ -241,6 +242,11 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
Trigger(uid, args.EmptyGun);
|
||||
}
|
||||
|
||||
private void OnRepeatInit(Entity<RepeatingTriggerComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay;
|
||||
}
|
||||
|
||||
public bool Trigger(EntityUid trigger, EntityUid? user = null)
|
||||
{
|
||||
var triggerEvent = new TriggerEvent(trigger, user);
|
||||
@@ -323,6 +329,7 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
UpdateProximity();
|
||||
UpdateTimer(frameTime);
|
||||
UpdateTimedCollide(frameTime);
|
||||
UpdateRepeat();
|
||||
}
|
||||
|
||||
private void UpdateTimer(float frameTime)
|
||||
@@ -357,5 +364,19 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Unprimed, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateRepeat()
|
||||
{
|
||||
var now = _timing.CurTime;
|
||||
var query = EntityQueryEnumerator<RepeatingTriggerComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.NextTrigger > now)
|
||||
continue;
|
||||
|
||||
comp.NextTrigger = now + comp.Delay;
|
||||
Trigger(uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,14 +144,33 @@ namespace Content.Server.GameTicking
|
||||
|
||||
async void SpawnWaitDb()
|
||||
{
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
try
|
||||
{
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Bail, user must've disconnected or something.
|
||||
Log.Debug($"Database load cancelled while waiting to spawn {session}");
|
||||
return;
|
||||
}
|
||||
|
||||
SpawnPlayer(session, EntityUid.Invalid);
|
||||
}
|
||||
|
||||
async void SpawnObserverWaitDb()
|
||||
{
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
try
|
||||
{
|
||||
await _userDb.WaitLoadComplete(session);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Bail, user must've disconnected or something.
|
||||
Log.Debug($"Database load cancelled while waiting to spawn {session}");
|
||||
return;
|
||||
}
|
||||
|
||||
JoinAsObserver(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,9 @@ namespace Content.Server.GameTicking
|
||||
return spawnableStations;
|
||||
}
|
||||
|
||||
private void SpawnPlayers(List<ICommonSession> readyPlayers, Dictionary<NetUserId, HumanoidCharacterProfile> profiles, bool force)
|
||||
private void SpawnPlayers(List<ICommonSession> readyPlayers,
|
||||
Dictionary<NetUserId, HumanoidCharacterProfile> profiles,
|
||||
bool force)
|
||||
{
|
||||
// Allow game rules to spawn players by themselves if needed. (For example, nuke ops or wizard)
|
||||
RaiseLocalEvent(new RulePlayerSpawningEvent(readyPlayers, profiles, force));
|
||||
@@ -94,7 +96,8 @@ namespace Content.Server.GameTicking
|
||||
if (job == null)
|
||||
{
|
||||
var playerSession = _playerManager.GetSessionById(netUser);
|
||||
_chatManager.DispatchServerMessage(playerSession, Loc.GetString("job-not-available-wait-in-lobby"));
|
||||
_chatManager.DispatchServerMessage(playerSession,
|
||||
Loc.GetString("job-not-available-wait-in-lobby"));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -116,10 +119,17 @@ namespace Content.Server.GameTicking
|
||||
RefreshLateJoinAllowed();
|
||||
|
||||
// Allow rules to add roles to players who have been spawned in. (For example, on-station traitors)
|
||||
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(assignedJobs.Keys.Select(x => _playerManager.GetSessionById(x)).ToArray(), profiles, force));
|
||||
RaiseLocalEvent(new RulePlayerJobsAssignedEvent(
|
||||
assignedJobs.Keys.Select(x => _playerManager.GetSessionById(x)).ToArray(),
|
||||
profiles,
|
||||
force));
|
||||
}
|
||||
|
||||
private void SpawnPlayer(ICommonSession player, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
|
||||
private void SpawnPlayer(ICommonSession player,
|
||||
EntityUid station,
|
||||
string? jobId = null,
|
||||
bool lateJoin = true,
|
||||
bool silent = false)
|
||||
{
|
||||
var character = GetPlayerProfile(player);
|
||||
|
||||
@@ -132,7 +142,12 @@ namespace Content.Server.GameTicking
|
||||
SpawnPlayer(player, character, station, jobId, lateJoin, silent);
|
||||
}
|
||||
|
||||
private void SpawnPlayer(ICommonSession player, HumanoidCharacterProfile character, EntityUid station, string? jobId = null, bool lateJoin = true, bool silent = false)
|
||||
private void SpawnPlayer(ICommonSession player,
|
||||
HumanoidCharacterProfile character,
|
||||
EntityUid station,
|
||||
string? jobId = null,
|
||||
bool lateJoin = true,
|
||||
bool silent = false)
|
||||
{
|
||||
// Can't spawn players with a dummy ticker!
|
||||
if (DummyTicker)
|
||||
@@ -176,7 +191,9 @@ namespace Content.Server.GameTicking
|
||||
restrictedRoles.UnionWith(jobBans);
|
||||
|
||||
// Pick best job best on prefs.
|
||||
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station, character.JobPriorities, true,
|
||||
jobId ??= _stationJobs.PickBestAvailableJobWithPriority(station,
|
||||
character.JobPriorities,
|
||||
true,
|
||||
restrictedRoles);
|
||||
// If no job available, stay in lobby, or if no lobby spawn as observer
|
||||
if (jobId is null)
|
||||
@@ -185,7 +202,9 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
JoinAsObserver(player);
|
||||
}
|
||||
_chatManager.DispatchServerMessage(player, Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
||||
|
||||
_chatManager.DispatchServerMessage(player,
|
||||
Loc.GetString("game-ticker-player-no-jobs-available-when-joining"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -199,7 +218,7 @@ namespace Content.Server.GameTicking
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
var job = new JobComponent { Prototype = jobId };
|
||||
var job = new JobComponent {Prototype = jobId};
|
||||
_roles.MindAddRole(newMind, job, silent: silent);
|
||||
var jobName = _jobs.MindTryGetJobName(newMind);
|
||||
|
||||
@@ -214,12 +233,11 @@ namespace Content.Server.GameTicking
|
||||
if (lateJoin && !silent)
|
||||
{
|
||||
_chatSystem.DispatchStationAnnouncement(station,
|
||||
Loc.GetString(
|
||||
"latejoin-arrival-announcement",
|
||||
("character", MetaData(mob).EntityName),
|
||||
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName)),
|
||||
("gender", character.Gender) // CrystallPunk-LastnameGender
|
||||
), Loc.GetString("latejoin-arrival-sender"),
|
||||
Loc.GetString("latejoin-arrival-announcement",
|
||||
("character", MetaData(mob).EntityName),
|
||||
("gender", character.Gender), // CrystallPunk-LastnameGender
|
||||
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))),
|
||||
Loc.GetString("latejoin-arrival-sender"),
|
||||
playDefaultSound: false);
|
||||
}
|
||||
|
||||
@@ -231,14 +249,17 @@ namespace Content.Server.GameTicking
|
||||
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
|
||||
|
||||
if (lateJoin)
|
||||
_adminLogger.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
_adminLogger.Add(LogType.LateJoin,
|
||||
LogImpact.Medium,
|
||||
$"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
else
|
||||
_adminLogger.Add(LogType.RoundStartJoin, LogImpact.Medium, $"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
_adminLogger.Add(LogType.RoundStartJoin,
|
||||
LogImpact.Medium,
|
||||
$"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
|
||||
// Make sure they're aware of extended access.
|
||||
if (Comp<StationJobsComponent>(station).ExtendedAccess
|
||||
&& (jobPrototype.ExtendedAccess.Count > 0
|
||||
|| jobPrototype.ExtendedAccessGroups.Count > 0))
|
||||
&& (jobPrototype.ExtendedAccess.Count > 0 || jobPrototype.ExtendedAccessGroups.Count > 0))
|
||||
{
|
||||
_chatManager.DispatchServerMessage(player, Loc.GetString("job-greet-crew-shortages"));
|
||||
}
|
||||
@@ -260,14 +281,20 @@ namespace Content.Server.GameTicking
|
||||
}
|
||||
else
|
||||
{
|
||||
_chatManager.DispatchServerMessage(player, Loc.GetString("latejoin-arrivals-direction-time",
|
||||
("time", $"{arrival:mm\\:ss}")));
|
||||
_chatManager.DispatchServerMessage(player,
|
||||
Loc.GetString("latejoin-arrivals-direction-time", ("time", $"{arrival:mm\\:ss}")));
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
var aev = new PlayerSpawnCompleteEvent(mob,
|
||||
player,
|
||||
jobId,
|
||||
lateJoin,
|
||||
PlayersJoinedRoundNormally,
|
||||
station,
|
||||
character);
|
||||
RaiseLocalEvent(mob, aev, true);
|
||||
}
|
||||
|
||||
@@ -289,7 +316,10 @@ namespace Content.Server.GameTicking
|
||||
/// <param name="station">The station they're spawning on</param>
|
||||
/// <param name="jobId">An optional job for them to spawn as</param>
|
||||
/// <param name="silent">Whether or not the player should be greeted upon joining</param>
|
||||
public void MakeJoinGame(ICommonSession player, EntityUid station, string? jobId = null, bool silent = false)
|
||||
public void MakeJoinGame(ICommonSession player,
|
||||
EntityUid station,
|
||||
string? jobId = null,
|
||||
bool silent = false)
|
||||
{
|
||||
if (!_playerGameStatuses.ContainsKey(player.UserId))
|
||||
return;
|
||||
@@ -335,23 +365,29 @@ namespace Content.Server.GameTicking
|
||||
_metaData.SetEntityName(ghost, name);
|
||||
_ghost.SetCanReturnToBody(ghost, false);
|
||||
_mind.TransferTo(mind.Value, ghost);
|
||||
_adminLogger.Add(LogType.LateJoin, LogImpact.Low, $"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
|
||||
_adminLogger.Add(LogType.LateJoin,
|
||||
LogImpact.Low,
|
||||
$"{player.Name} late joined the round as an Observer with {ToPrettyString(ghost):entity}.");
|
||||
}
|
||||
|
||||
#region Mob Spawning Helpers
|
||||
|
||||
private EntityUid SpawnObserverMob()
|
||||
{
|
||||
var coordinates = GetObserverSpawnPoint();
|
||||
return EntityManager.SpawnEntity(ObserverPrototypeName, coordinates);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Spawn Points
|
||||
|
||||
public EntityCoordinates GetObserverSpawnPoint()
|
||||
{
|
||||
_possiblePositions.Clear();
|
||||
|
||||
foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
foreach (var (point, transform) in EntityManager
|
||||
.EntityQuery<SpawnPointComponent, TransformComponent>(true))
|
||||
{
|
||||
if (point.SpawnType != SpawnPointType.Observer)
|
||||
continue;
|
||||
@@ -367,8 +403,7 @@ namespace Content.Server.GameTicking
|
||||
var query = AllEntityQuery<MapGridComponent>();
|
||||
while (query.MoveNext(out var uid, out var grid))
|
||||
{
|
||||
if (!metaQuery.TryGetComponent(uid, out var meta) ||
|
||||
meta.EntityPaused)
|
||||
if (!metaQuery.TryGetComponent(uid, out var meta) || meta.EntityPaused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -389,8 +424,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
var gridXform = Transform(gridUid);
|
||||
|
||||
return new EntityCoordinates(gridUid,
|
||||
gridXform.InvWorldMatrix.Transform(toMap.Position));
|
||||
return new EntityCoordinates(gridUid, gridXform.InvWorldMatrix.Transform(toMap.Position));
|
||||
}
|
||||
|
||||
return spawn;
|
||||
@@ -406,8 +440,7 @@ namespace Content.Server.GameTicking
|
||||
{
|
||||
var mapUid = _mapManager.GetMapEntityId(map);
|
||||
|
||||
if (!metaQuery.TryGetComponent(mapUid, out var meta) ||
|
||||
meta.EntityPaused)
|
||||
if (!metaQuery.TryGetComponent(mapUid, out var meta) || meta.EntityPaused)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -420,6 +453,7 @@ namespace Content.Server.GameTicking
|
||||
_sawmill.Warning("Found no observer spawn points!");
|
||||
return EntityCoordinates.Invalid;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -437,7 +471,11 @@ namespace Content.Server.GameTicking
|
||||
public bool LateJoin { get; }
|
||||
public EntityUid Station { get; }
|
||||
|
||||
public PlayerBeforeSpawnEvent(ICommonSession player, HumanoidCharacterProfile profile, string? jobId, bool lateJoin, EntityUid station)
|
||||
public PlayerBeforeSpawnEvent(ICommonSession player,
|
||||
HumanoidCharacterProfile profile,
|
||||
string? jobId,
|
||||
bool lateJoin,
|
||||
EntityUid station)
|
||||
{
|
||||
Player = player;
|
||||
Profile = profile;
|
||||
@@ -465,7 +503,13 @@ namespace Content.Server.GameTicking
|
||||
// Ex. If this is the 27th person to join, this will be 27.
|
||||
public int JoinOrder { get; }
|
||||
|
||||
public PlayerSpawnCompleteEvent(EntityUid mob, ICommonSession player, string? jobId, bool lateJoin, int joinOrder, EntityUid station, HumanoidCharacterProfile profile)
|
||||
public PlayerSpawnCompleteEvent(EntityUid mob,
|
||||
ICommonSession player,
|
||||
string? jobId,
|
||||
bool lateJoin,
|
||||
int joinOrder,
|
||||
EntityUid station,
|
||||
HumanoidCharacterProfile profile)
|
||||
{
|
||||
Mob = mob;
|
||||
Player = player;
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -82,17 +85,23 @@ public abstract partial class GameRuleSystem<T> where T: IComponent
|
||||
targetCoords = EntityCoordinates.Invalid;
|
||||
targetGrid = EntityUid.Invalid;
|
||||
|
||||
var possibleTargets = station.Comp.Grids;
|
||||
if (possibleTargets.Count == 0)
|
||||
// Weight grid choice by tilecount
|
||||
var weights = new Dictionary<Entity<MapGridComponent>, float>();
|
||||
foreach (var possibleTarget in station.Comp.Grids)
|
||||
{
|
||||
if (!TryComp<MapGridComponent>(possibleTarget, out var comp))
|
||||
continue;
|
||||
|
||||
weights.Add((possibleTarget, comp), _map.GetAllTiles(possibleTarget, comp).Count());
|
||||
}
|
||||
|
||||
if (weights.Count == 0)
|
||||
{
|
||||
targetGrid = EntityUid.Invalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
targetGrid = RobustRandom.Pick(possibleTargets);
|
||||
|
||||
if (!TryComp<MapGridComponent>(targetGrid, out var gridComp))
|
||||
return false;
|
||||
(targetGrid, var gridComp) = RobustRandom.Pick(weights);
|
||||
|
||||
var found = false;
|
||||
var aabb = gridComp.LocalAABB;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Components
|
||||
@@ -87,5 +88,12 @@ namespace Content.Server.Ghost.Roles.Components
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("reregister")]
|
||||
public bool ReregisterOnGhost { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
||||
/// </summary>
|
||||
[DataField("raffle")]
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a ghost role is currently being raffled, and stores data about the raffle in progress.
|
||||
/// Raffles start when the first player joins a raffle.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[Access(typeof(GhostRoleSystem))]
|
||||
public sealed partial class GhostRoleRaffleComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Identifier of the <see cref="GhostRoleComponent">Ghost Role</see> this raffle is for.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField]
|
||||
public uint Identifier { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of sessions that are currently in the raffle.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public HashSet<ICommonSession> CurrentMembers = [];
|
||||
|
||||
/// <summary>
|
||||
/// List of sessions that are currently or were previously in the raffle.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public HashSet<ICommonSession> AllMembers = [];
|
||||
|
||||
/// <summary>
|
||||
/// Time left in the raffle in seconds. This must be initialized to a positive value.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField]
|
||||
public TimeSpan Countdown = TimeSpan.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The cumulative time, i.e. how much time the raffle will take in total. Added to when the time is extended
|
||||
/// by someone joining the raffle.
|
||||
/// Must be set to the same value as <see cref="Countdown"/> on initialization.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("cumulativeTime")]
|
||||
public TimeSpan CumulativeTime = TimeSpan.MaxValue;
|
||||
|
||||
/// <inheritdoc cref="GhostRoleRaffleSettings.JoinExtendsDurationBy"/>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("joinExtendsDurationBy")]
|
||||
public TimeSpan JoinExtendsDurationBy { get; set; }
|
||||
|
||||
/// <inheritdoc cref="GhostRoleRaffleSettings.MaxDuration"/>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
[DataField("maxDuration")]
|
||||
public TimeSpan MaxDuration { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Events;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Server.Ghost.Roles.UI;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Shared.Administration;
|
||||
@@ -21,7 +24,9 @@ using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
@@ -41,12 +46,16 @@ namespace Content.Server.Ghost.Roles
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private uint _nextRoleIdentifier;
|
||||
private bool _needsUpdateGhostRoleCount = true;
|
||||
|
||||
private readonly Dictionary<uint, Entity<GhostRoleComponent>> _ghostRoles = new();
|
||||
private readonly Dictionary<uint, Entity<GhostRoleRaffleComponent>> _ghostRoleRaffles = new();
|
||||
|
||||
private readonly Dictionary<ICommonSession, GhostRolesEui> _openUis = new();
|
||||
private readonly Dictionary<ICommonSession, MakeGhostRoleEui> _openMakeGhostRoleUis = new();
|
||||
|
||||
@@ -63,10 +72,12 @@ namespace Content.Server.Ghost.Roles
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MindRemovedMessage>(OnMindRemoved);
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<GhostRoleComponent, MapInitEvent>(OnMapInit);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnStartup);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentStartup>(OnRoleStartup);
|
||||
SubscribeLocalEvent<GhostRoleComponent, ComponentShutdown>(OnRoleShutdown);
|
||||
SubscribeLocalEvent<GhostRoleComponent, EntityPausedEvent>(OnPaused);
|
||||
SubscribeLocalEvent<GhostRoleComponent, EntityUnpausedEvent>(OnUnpaused);
|
||||
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentInit>(OnRaffleInit);
|
||||
SubscribeLocalEvent<GhostRoleRaffleComponent, ComponentShutdown>(OnRaffleShutdown);
|
||||
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, TakeGhostRoleEvent>(OnSpawnerTakeRole);
|
||||
SubscribeLocalEvent<GhostTakeoverAvailableComponent, TakeGhostRoleEvent>(OnTakeoverTakeRole);
|
||||
SubscribeLocalEvent<GhostRoleMobSpawnerComponent, GetVerbsEvent<Verb>>(OnVerb);
|
||||
@@ -165,17 +176,118 @@ namespace Content.Server.Ghost.Roles
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
if (_needsUpdateGhostRoleCount)
|
||||
|
||||
UpdateGhostRoleCount();
|
||||
UpdateRaffles(frameTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles sending count update for the ghost role button in ghost UI, if ghost role count changed.
|
||||
/// </summary>
|
||||
private void UpdateGhostRoleCount()
|
||||
{
|
||||
if (!_needsUpdateGhostRoleCount)
|
||||
return;
|
||||
|
||||
_needsUpdateGhostRoleCount = false;
|
||||
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRoleCount());
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
_needsUpdateGhostRoleCount = false;
|
||||
var response = new GhostUpdateGhostRoleCountEvent(GetGhostRolesInfo().Length);
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
RaiseNetworkEvent(response, player.Channel);
|
||||
}
|
||||
RaiseNetworkEvent(response, player.Channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ghost role raffle logic.
|
||||
/// </summary>
|
||||
private void UpdateRaffles(float frameTime)
|
||||
{
|
||||
var query = EntityQueryEnumerator<GhostRoleRaffleComponent, MetaDataComponent>();
|
||||
while (query.MoveNext(out var entityUid, out var raffle, out var meta))
|
||||
{
|
||||
if (meta.EntityPaused)
|
||||
continue;
|
||||
|
||||
// if all participants leave/were removed from the raffle, the raffle is canceled.
|
||||
if (raffle.CurrentMembers.Count == 0)
|
||||
{
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
raffle.Countdown = raffle.Countdown.Subtract(TimeSpan.FromSeconds(frameTime));
|
||||
if (raffle.Countdown.Ticks > 0)
|
||||
continue;
|
||||
|
||||
// the raffle is over! find someone to take over the ghost role
|
||||
if (!TryComp(entityUid, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
Log.Warning($"Ghost role raffle finished on {entityUid} but {nameof(GhostRoleComponent)} is missing");
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ghostRole.RaffleConfig is null)
|
||||
{
|
||||
Log.Warning($"Ghost role raffle finished on {entityUid} but RaffleConfig became null");
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
continue;
|
||||
}
|
||||
|
||||
var foundWinner = false;
|
||||
var deciderPrototype = _prototype.Index(ghostRole.RaffleConfig.Decider);
|
||||
|
||||
// use the ghost role's chosen winner picker to find a winner
|
||||
deciderPrototype.Decider.PickWinner(
|
||||
raffle.CurrentMembers.AsEnumerable(),
|
||||
session =>
|
||||
{
|
||||
var success = TryTakeover(session, raffle.Identifier);
|
||||
foundWinner |= success;
|
||||
return success;
|
||||
}
|
||||
);
|
||||
|
||||
if (!foundWinner)
|
||||
{
|
||||
Log.Warning($"Ghost role raffle for {entityUid} ({ghostRole.RoleName}) finished without " +
|
||||
$"{ghostRole.RaffleConfig?.Decider} finding a winner");
|
||||
}
|
||||
|
||||
// raffle over
|
||||
RemoveRaffleAndUpdateEui(entityUid, raffle);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryTakeover(ICommonSession player, uint identifier)
|
||||
{
|
||||
// TODO: the following two checks are kind of redundant since they should already be removed
|
||||
// from the raffle
|
||||
// can't win if you are disconnected (although you shouldn't be a candidate anyway)
|
||||
if (player.Status != SessionStatus.InGame)
|
||||
return false;
|
||||
|
||||
// can't win if you are no longer a ghost (e.g. if you returned to your body)
|
||||
if (player.AttachedEntity == null || !HasComp<GhostComponent>(player.AttachedEntity))
|
||||
return false;
|
||||
|
||||
if (Takeover(player, identifier))
|
||||
{
|
||||
// takeover successful, we have a winner! remove the winner from other raffles they might be in
|
||||
LeaveAllRaffles(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RemoveRaffleAndUpdateEui(EntityUid entityUid, GhostRoleRaffleComponent raffle)
|
||||
{
|
||||
_ghostRoleRaffles.Remove(raffle.Identifier);
|
||||
RemComp(entityUid, raffle);
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
private void PlayerStatusChanged(object? blah, SessionStatusEventArgs args)
|
||||
{
|
||||
if (args.NewStatus == SessionStatus.InGame)
|
||||
@@ -183,6 +295,11 @@ namespace Content.Server.Ghost.Roles
|
||||
var response = new GhostUpdateGhostRoleCountEvent(_ghostRoles.Count);
|
||||
RaiseNetworkEvent(response, args.Session.Channel);
|
||||
}
|
||||
else
|
||||
{
|
||||
// people who disconnect are removed from ghost role raffles
|
||||
LeaveAllRaffles(args.Session);
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterGhostRole(Entity<GhostRoleComponent> role)
|
||||
@@ -201,24 +318,170 @@ namespace Content.Server.Ghost.Roles
|
||||
return;
|
||||
|
||||
_ghostRoles.Remove(comp.Identifier);
|
||||
if (TryComp(role.Owner, out GhostRoleRaffleComponent? raffle))
|
||||
{
|
||||
// if a raffle is still running, get rid of it
|
||||
RemoveRaffleAndUpdateEui(role.Owner, raffle);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateAllEui();
|
||||
}
|
||||
}
|
||||
|
||||
// probably fine to be init because it's never added during entity initialization, but much later
|
||||
private void OnRaffleInit(Entity<GhostRoleRaffleComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp(ent, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
// can't have a raffle for a ghost role that doesn't exist
|
||||
RemComp<GhostRoleRaffleComponent>(ent);
|
||||
return;
|
||||
}
|
||||
|
||||
var config = ghostRole.RaffleConfig;
|
||||
if (config is null)
|
||||
return; // should, realistically, never be reached but you never know
|
||||
|
||||
var settings = config.SettingsOverride
|
||||
?? _prototype.Index<GhostRoleRaffleSettingsPrototype>(config.Settings).Settings;
|
||||
|
||||
if (settings.MaxDuration < settings.InitialDuration)
|
||||
{
|
||||
Log.Error($"Ghost role on {ent} has invalid raffle settings (max duration shorter than initial)");
|
||||
ghostRole.RaffleConfig = null; // make it a non-raffle role so stuff isn't entirely broken
|
||||
RemComp<GhostRoleRaffleComponent>(ent);
|
||||
return;
|
||||
}
|
||||
|
||||
var raffle = ent.Comp;
|
||||
raffle.Identifier = ghostRole.Identifier;
|
||||
raffle.Countdown = TimeSpan.FromSeconds(settings.InitialDuration);
|
||||
raffle.CumulativeTime = TimeSpan.FromSeconds(settings.InitialDuration);
|
||||
// we copy these settings into the component because they would be cumbersome to access otherwise
|
||||
raffle.JoinExtendsDurationBy = TimeSpan.FromSeconds(settings.JoinExtendsDurationBy);
|
||||
raffle.MaxDuration = TimeSpan.FromSeconds(settings.MaxDuration);
|
||||
}
|
||||
|
||||
private void OnRaffleShutdown(Entity<GhostRoleRaffleComponent> ent, ref ComponentShutdown args)
|
||||
{
|
||||
_ghostRoleRaffles.Remove(ent.Comp.Identifier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Joins the given player onto a ghost role raffle, or creates it if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="identifier">The ID that represents the ghost role or ghost role raffle.
|
||||
/// (A raffle will have the same ID as the ghost role it's for.)</param>
|
||||
private void JoinRaffle(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
|
||||
return;
|
||||
|
||||
// get raffle or create a new one if it doesn't exist
|
||||
var raffle = _ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt)
|
||||
? raffleEnt.Comp
|
||||
: EnsureComp<GhostRoleRaffleComponent>(roleEnt.Owner);
|
||||
|
||||
_ghostRoleRaffles.TryAdd(identifier, (roleEnt.Owner, raffle));
|
||||
|
||||
if (!raffle.CurrentMembers.Add(player))
|
||||
{
|
||||
Log.Warning($"{player.Name} tried to join raffle for ghost role {identifier} but they are already in the raffle");
|
||||
return;
|
||||
}
|
||||
|
||||
// if this is the first time the player joins this raffle, and the player wasn't the starter of the raffle:
|
||||
// extend the countdown, but only if doing so will not make the raffle take longer than the maximum
|
||||
// duration
|
||||
if (raffle.AllMembers.Add(player) && raffle.AllMembers.Count > 1
|
||||
&& raffle.CumulativeTime.Add(raffle.JoinExtendsDurationBy) <= raffle.MaxDuration)
|
||||
{
|
||||
raffle.Countdown += raffle.JoinExtendsDurationBy;
|
||||
raffle.CumulativeTime += raffle.JoinExtendsDurationBy;
|
||||
}
|
||||
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
public void Takeover(ICommonSession player, uint identifier)
|
||||
/// <summary>
|
||||
/// Makes the given player leave the raffle corresponding to the given ID.
|
||||
/// </summary>
|
||||
public void LeaveRaffle(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoleRaffles.TryGetValue(identifier, out var raffleEnt))
|
||||
return;
|
||||
|
||||
if (raffleEnt.Comp.CurrentMembers.Remove(player))
|
||||
{
|
||||
UpdateAllEui();
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"{player.Name} tried to leave raffle for ghost role {identifier} but they are not in the raffle");
|
||||
}
|
||||
|
||||
// (raffle ending because all players left is handled in update())
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the given player leave all ghost role raffles.
|
||||
/// </summary>
|
||||
public void LeaveAllRaffles(ICommonSession player)
|
||||
{
|
||||
var shouldUpdateEui = false;
|
||||
|
||||
foreach (var raffleEnt in _ghostRoleRaffles.Values)
|
||||
{
|
||||
shouldUpdateEui |= raffleEnt.Comp.CurrentMembers.Remove(player);
|
||||
}
|
||||
|
||||
if (shouldUpdateEui)
|
||||
UpdateAllEui();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request a ghost role. If it's a raffled role starts or joins a raffle, otherwise the player immediately
|
||||
/// takes over the ghost role if possible.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="identifier">ID of the ghost role.</param>
|
||||
public void Request(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var roleEnt))
|
||||
return;
|
||||
|
||||
if (roleEnt.Comp.RaffleConfig is not null)
|
||||
{
|
||||
JoinRaffle(player, identifier);
|
||||
}
|
||||
else
|
||||
{
|
||||
Takeover(player, identifier);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts having the player take over the ghost role with the corresponding ID. Does not start a raffle.
|
||||
/// </summary>
|
||||
/// <returns>True if takeover was successful, otherwise false.</returns>
|
||||
public bool Takeover(ICommonSession player, uint identifier)
|
||||
{
|
||||
if (!_ghostRoles.TryGetValue(identifier, out var role))
|
||||
return;
|
||||
return false;
|
||||
|
||||
var ev = new TakeGhostRoleEvent(player);
|
||||
RaiseLocalEvent(role, ref ev);
|
||||
|
||||
if (!ev.TookRole)
|
||||
return;
|
||||
return false;
|
||||
|
||||
if (player.AttachedEntity != null)
|
||||
_adminLogger.Add(LogType.GhostRoleTaken, LogImpact.Low, $"{player:player} took the {role.Comp.RoleName:roleName} ghost role {ToPrettyString(player.AttachedEntity.Value):entity}");
|
||||
|
||||
CloseEui(player);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Follow(ICommonSession player, uint identifier)
|
||||
@@ -247,7 +510,22 @@ namespace Content.Server.Ghost.Roles
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
}
|
||||
|
||||
public GhostRoleInfo[] GetGhostRolesInfo()
|
||||
/// <summary>
|
||||
/// Returns the number of available ghost roles.
|
||||
/// </summary>
|
||||
public int GetGhostRoleCount()
|
||||
{
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
return _ghostRoles.Count(pair => metaQuery.GetComponent(pair.Value.Owner).EntityPaused == false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns information about all available ghost roles.
|
||||
/// </summary>
|
||||
/// <param name="player">
|
||||
/// If not null, the <see cref="GhostRoleInfo"/>s will show if the given player is in a raffle.
|
||||
/// </param>
|
||||
public GhostRoleInfo[] GetGhostRolesInfo(ICommonSession? player)
|
||||
{
|
||||
var roles = new List<GhostRoleInfo>();
|
||||
var metaQuery = GetEntityQuery<MetaDataComponent>();
|
||||
@@ -257,7 +535,40 @@ namespace Content.Server.Ghost.Roles
|
||||
if (metaQuery.GetComponent(uid).EntityPaused)
|
||||
continue;
|
||||
|
||||
roles.Add(new GhostRoleInfo { Identifier = id, Name = role.RoleName, Description = role.RoleDescription, Rules = role.RoleRules, Requirements = role.Requirements });
|
||||
|
||||
var kind = GhostRoleKind.FirstComeFirstServe;
|
||||
GhostRoleRaffleComponent? raffle = null;
|
||||
|
||||
if (role.RaffleConfig is not null)
|
||||
{
|
||||
kind = GhostRoleKind.RaffleReady;
|
||||
|
||||
if (_ghostRoleRaffles.TryGetValue(id, out var raffleEnt))
|
||||
{
|
||||
kind = GhostRoleKind.RaffleInProgress;
|
||||
raffle = raffleEnt.Comp;
|
||||
|
||||
if (player is not null && raffle.CurrentMembers.Contains(player))
|
||||
kind = GhostRoleKind.RaffleJoined;
|
||||
}
|
||||
}
|
||||
|
||||
var rafflePlayerCount = (uint?) raffle?.CurrentMembers.Count ?? 0;
|
||||
var raffleEndTime = raffle is not null
|
||||
? _timing.CurTime.Add(raffle.Countdown)
|
||||
: TimeSpan.MinValue;
|
||||
|
||||
roles.Add(new GhostRoleInfo
|
||||
{
|
||||
Identifier = id,
|
||||
Name = role.RoleName,
|
||||
Description = role.RoleDescription,
|
||||
Rules = role.RoleRules,
|
||||
Requirements = role.Requirements,
|
||||
Kind = kind,
|
||||
RafflePlayerCount = rafflePlayerCount,
|
||||
RaffleEndTime = raffleEndTime
|
||||
});
|
||||
}
|
||||
|
||||
return roles.ToArray();
|
||||
@@ -272,6 +583,10 @@ namespace Content.Server.Ghost.Roles
|
||||
if (HasComp<GhostComponent>(message.Entity))
|
||||
return;
|
||||
|
||||
// The player is not a ghost (anymore), so they should not be in any raffles. Remove them.
|
||||
// This ensures player doesn't win a raffle after returning to their (revived) body and ends up being
|
||||
// forced into a ghost role.
|
||||
LeaveAllRaffles(message.Player);
|
||||
CloseEui(message.Player);
|
||||
}
|
||||
|
||||
@@ -306,6 +621,7 @@ namespace Content.Server.Ghost.Roles
|
||||
|
||||
_openUis.Clear();
|
||||
_ghostRoles.Clear();
|
||||
_ghostRoleRaffles.Clear();
|
||||
_nextRoleIdentifier = 0;
|
||||
}
|
||||
|
||||
@@ -331,12 +647,12 @@ namespace Content.Server.Ghost.Roles
|
||||
RemCompDeferred<GhostRoleComponent>(ent);
|
||||
}
|
||||
|
||||
private void OnStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
|
||||
private void OnRoleStartup(Entity<GhostRoleComponent> ent, ref ComponentStartup args)
|
||||
{
|
||||
RegisterGhostRole(ent);
|
||||
}
|
||||
|
||||
private void OnShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
|
||||
private void OnRoleShutdown(Entity<GhostRoleComponent> role, ref ComponentShutdown args)
|
||||
{
|
||||
UnregisterGhostRole(role);
|
||||
}
|
||||
|
||||
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal file
127
Content.Server/Ghost/Roles/MakeRaffledGhostRoleCommand.cs
Normal file
@@ -0,0 +1,127 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles
|
||||
{
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class MakeRaffledGhostRoleCommand : IConsoleCommand
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoManager = default!;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
public string Command => "makeghostroleraffled";
|
||||
public string Description => "Turns an entity into a raffled ghost role.";
|
||||
public string Help => $"Usage: {Command} <entity uid> <name> <description> (<settings prototype> | <initial duration> <extend by> <max duration>) [<rules>]\n" +
|
||||
$"Durations are in seconds.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is < 4 or > 7)
|
||||
{
|
||||
shell.WriteLine($"Invalid amount of arguments.\n{Help}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!NetEntity.TryParse(args[0], out var uidNet) || !_entManager.TryGetEntity(uidNet, out var uid))
|
||||
{
|
||||
shell.WriteLine($"{args[0]} is not a valid entity uid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_entManager.TryGetComponent(uid, out MetaDataComponent? metaData))
|
||||
{
|
||||
shell.WriteLine($"No entity found with uid {uid}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent(uid, out MindContainerComponent? mind) &&
|
||||
mind.HasMind)
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a mind.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.TryGetComponent(uid, out GhostRoleComponent? ghostRole))
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostRoleComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (_entManager.HasComponent<GhostTakeoverAvailableComponent>(uid))
|
||||
{
|
||||
shell.WriteLine($"Entity {metaData.EntityName} with id {uid} already has a {nameof(GhostTakeoverAvailableComponent)}");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[1];
|
||||
var description = args[2];
|
||||
|
||||
// if the rules are specified then use those, otherwise use the default
|
||||
var rules = args.Length switch
|
||||
{
|
||||
5 => args[4],
|
||||
7 => args[6],
|
||||
_ => Loc.GetString("ghost-role-component-default-rules"),
|
||||
};
|
||||
|
||||
// is it an invocation with a prototype ID and optional rules?
|
||||
var isProto = args.Length is 4 or 5;
|
||||
GhostRoleRaffleSettings settings;
|
||||
|
||||
if (isProto)
|
||||
{
|
||||
if (!_protoManager.TryIndex<GhostRoleRaffleSettingsPrototype>(args[4], out var proto))
|
||||
{
|
||||
var validProtos = string.Join(", ",
|
||||
_protoManager.EnumeratePrototypes<GhostRoleRaffleSettingsPrototype>().Select(p => p.ID)
|
||||
);
|
||||
|
||||
shell.WriteLine($"{args[4]} is not a valid raffle settings prototype. Valid options: {validProtos}");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = proto.Settings;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!uint.TryParse(args[3], out var initial)
|
||||
|| !uint.TryParse(args[4], out var extends)
|
||||
|| !uint.TryParse(args[5], out var max)
|
||||
|| initial == 0 || max == 0)
|
||||
{
|
||||
shell.WriteLine($"The raffle initial/extends/max settings must be positive numbers.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (initial > max)
|
||||
{
|
||||
shell.WriteLine("The initial duration must be smaller than or equal to the maximum duration.");
|
||||
return;
|
||||
}
|
||||
|
||||
settings = new GhostRoleRaffleSettings()
|
||||
{
|
||||
InitialDuration = initial,
|
||||
JoinExtendsDurationBy = extends,
|
||||
MaxDuration = max
|
||||
};
|
||||
}
|
||||
|
||||
ghostRole = _entManager.AddComponent<GhostRoleComponent>(uid.Value);
|
||||
_entManager.AddComponent<GhostTakeoverAvailableComponent>(uid.Value);
|
||||
ghostRole.RoleName = name;
|
||||
ghostRole.RoleDescription = description;
|
||||
ghostRole.RoleRules = rules;
|
||||
ghostRole.RaffleConfig = new GhostRoleRaffleConfig(settings);
|
||||
|
||||
shell.WriteLine($"Made entity {metaData.EntityName} a raffled ghost role.");
|
||||
}
|
||||
}
|
||||
}
|
||||
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal file
35
Content.Server/Ghost/Roles/Raffles/GhostRoleRaffleConfig.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Content.Shared.Ghost.Roles.Raffles;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Raffle configuration.
|
||||
/// </summary>
|
||||
[DataDefinition]
|
||||
public sealed partial class GhostRoleRaffleConfig
|
||||
{
|
||||
public GhostRoleRaffleConfig(GhostRoleRaffleSettings settings)
|
||||
{
|
||||
SettingsOverride = settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the raffle settings to use.
|
||||
/// </summary>
|
||||
[DataField("settings", required: true)]
|
||||
public ProtoId<GhostRoleRaffleSettingsPrototype> Settings { get; set; } = "default";
|
||||
|
||||
/// <summary>
|
||||
/// If not null, the settings from <see cref="Settings"/> are ignored and these settings are used instead.
|
||||
/// Intended for allowing admins to set custom raffle settings for admeme ghost roles.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public GhostRoleRaffleSettings? SettingsOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets which <see cref="IGhostRoleRaffleDecider"/> is used.
|
||||
/// </summary>
|
||||
[DataField("decider")]
|
||||
public ProtoId<GhostRoleRaffleDeciderPrototype> Decider { get; set; } = "default";
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Allows getting a <see cref="IGhostRoleRaffleDecider"/> as prototype.
|
||||
/// </summary>
|
||||
[Prototype("ghostRoleRaffleDecider")]
|
||||
public sealed class GhostRoleRaffleDeciderPrototype : IPrototype
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[IdDataField]
|
||||
public string ID { get; private set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IGhostRoleRaffleDecider"/> instance that chooses the winner of a raffle.
|
||||
/// </summary>
|
||||
[DataField("decider", required: true)]
|
||||
public IGhostRoleRaffleDecider Decider { get; private set; } = new RngGhostRoleRaffleDecider();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses a winner of a ghost role raffle.
|
||||
/// </summary>
|
||||
[ImplicitDataDefinitionForInheritors]
|
||||
public partial interface IGhostRoleRaffleDecider
|
||||
{
|
||||
/// <summary>
|
||||
/// Chooses a winner of a ghost role raffle draw from the given pool of candidates.
|
||||
/// </summary>
|
||||
/// <param name="candidates">The players in the session at the time of drawing.</param>
|
||||
/// <param name="tryTakeover">
|
||||
/// Call this with the chosen winner as argument.
|
||||
/// <ul><li>If <c>true</c> is returned, your winner was able to take over the ghost role, and the drawing is complete.
|
||||
/// <b>Do not call <see cref="tryTakeover"/> again after true is returned.</b></li>
|
||||
/// <li>If <c>false</c> is returned, your winner was not able to take over the ghost role,
|
||||
/// and you must choose another winner, and call <see cref="tryTakeover"/> with the new winner as argument.</li>
|
||||
/// </ul>
|
||||
///
|
||||
/// If <see cref="tryTakeover"/> is not called, or only returns false, the raffle will end without a winner.
|
||||
/// Do not call <see cref="tryTakeover"/> with the same player several times.
|
||||
/// </param>
|
||||
void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Ghost.Roles.Raffles;
|
||||
|
||||
/// <summary>
|
||||
/// Chooses the winner of a ghost role raffle entirely randomly, without any weighting.
|
||||
/// </summary>
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
|
||||
public sealed partial class RngGhostRoleRaffleDecider : IGhostRoleRaffleDecider
|
||||
{
|
||||
public void PickWinner(IEnumerable<ICommonSession> candidates, Func<ICommonSession, bool> tryTakeover)
|
||||
{
|
||||
var random = IoCManager.Resolve<IRobustRandom>();
|
||||
|
||||
var choices = candidates.ToList();
|
||||
random.Shuffle(choices); // shuffle the list so we can pick a lucky winner!
|
||||
|
||||
foreach (var candidate in choices)
|
||||
{
|
||||
if (tryTakeover(candidate))
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,16 @@ namespace Content.Server.Ghost.Roles.UI
|
||||
{
|
||||
public sealed class GhostRolesEui : BaseEui
|
||||
{
|
||||
[Dependency] private readonly GhostRoleSystem _ghostRoleSystem;
|
||||
|
||||
public GhostRolesEui()
|
||||
{
|
||||
_ghostRoleSystem = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GhostRoleSystem>();
|
||||
}
|
||||
|
||||
public override GhostRolesEuiState GetNewState()
|
||||
{
|
||||
return new(EntitySystem.Get<GhostRoleSystem>().GetGhostRolesInfo());
|
||||
return new(_ghostRoleSystem.GetGhostRolesInfo(Player));
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
@@ -17,11 +24,14 @@ namespace Content.Server.Ghost.Roles.UI
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case GhostRoleTakeoverRequestMessage req:
|
||||
EntitySystem.Get<GhostRoleSystem>().Takeover(Player, req.Identifier);
|
||||
case RequestGhostRoleMessage req:
|
||||
_ghostRoleSystem.Request(Player, req.Identifier);
|
||||
break;
|
||||
case GhostRoleFollowRequestMessage req:
|
||||
EntitySystem.Get<GhostRoleSystem>().Follow(Player, req.Identifier);
|
||||
case FollowGhostRoleMessage req:
|
||||
_ghostRoleSystem.Follow(Player, req.Identifier);
|
||||
break;
|
||||
case LeaveGhostRoleRaffleMessage req:
|
||||
_ghostRoleSystem.LeaveRaffle(Player, req.Identifier);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ using Content.Shared.Database;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Lathe;
|
||||
using Content.Shared.Materials;
|
||||
using Content.Shared.ReagentSpeed;
|
||||
using Content.Shared.Research.Components;
|
||||
using Content.Shared.Research.Prototypes;
|
||||
using JetBrains.Annotations;
|
||||
@@ -35,6 +36,7 @@ namespace Content.Server.Lathe
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
|
||||
[Dependency] private readonly ReagentSpeedSystem _reagentSpeed = default!;
|
||||
[Dependency] private readonly StackSystem _stack = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
|
||||
@@ -186,9 +188,11 @@ namespace Content.Server.Lathe
|
||||
var recipe = component.Queue.First();
|
||||
component.Queue.RemoveAt(0);
|
||||
|
||||
var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime);
|
||||
|
||||
var lathe = EnsureComp<LatheProducingComponent>(uid);
|
||||
lathe.StartTime = _timing.CurTime;
|
||||
lathe.ProductionLength = recipe.CompleteTime * component.TimeMultiplier;
|
||||
lathe.ProductionLength = time * component.TimeMultiplier;
|
||||
component.CurrentRecipe = recipe;
|
||||
|
||||
var ev = new LatheStartPrintingEvent(recipe);
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed partial class GunAmmoPrecondition : HTNPrecondition
|
||||
else
|
||||
percent = ammoEv.Count / (float) ammoEv.Capacity;
|
||||
|
||||
percent = Math.Clamp(percent, 0f, 1f);
|
||||
percent = System.Math.Clamp(percent, 0f, 1f);
|
||||
|
||||
if (MaxPercent < percent)
|
||||
return false;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace Content.Server.NPC.HTN.Preconditions;
|
||||
|
||||
public sealed partial class KeyNotExistsPrecondition : HTNPrecondition
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return !blackboard.ContainsKey(Key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace Content.Server.NPC.HTN.Preconditions.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Checks for the presence of data in the blackboard and makes a comparison with the specified boolean
|
||||
/// </summary>
|
||||
public sealed partial class KeyBoolEqualsPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public bool Value;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
if (!blackboard.TryGetValue<bool>(Key, out var value, _entManager))
|
||||
return false;
|
||||
|
||||
return Value == value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Content.Server.NPC.HTN.Preconditions.Math;
|
||||
|
||||
public sealed partial class KeyFloatEqualsPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public float Value;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return blackboard.TryGetValue<float>(Key, out var value, _entManager) &&
|
||||
MathHelper.CloseTo(value, value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Content.Server.NPC.HTN.Preconditions.Math;
|
||||
|
||||
public sealed partial class KeyFloatGreaterPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public float Value;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return blackboard.TryGetValue<float>(Key, out var value, _entManager) && value > Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Content.Server.NPC.HTN.Preconditions.Math;
|
||||
|
||||
public sealed partial class KeyFloatLessPrecondition : HTNPrecondition
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
[DataField(required: true)]
|
||||
public float Value;
|
||||
|
||||
public override bool IsMet(NPCBlackboard blackboard)
|
||||
{
|
||||
return blackboard.TryGetValue<float>(Key, out var value, _entManager) && value < Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the key, and adds the value to that float
|
||||
/// </summary>
|
||||
public sealed partial class AddFloatOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Amount;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
if (!blackboard.TryGetValue<float>(TargetKey, out var value, _entManager))
|
||||
return (false, null);
|
||||
|
||||
return (
|
||||
true,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ TargetKey, value + Amount }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Just sets a blackboard key to a bool
|
||||
/// </summary>
|
||||
public sealed partial class SetBoolOperator : HTNOperator
|
||||
{
|
||||
[DataField(required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Value;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
return (
|
||||
true,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ TargetKey, Value }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,28 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Just sets a blackboard key to a float
|
||||
/// </summary>
|
||||
public sealed partial class SetFloatOperator : HTNOperator
|
||||
{
|
||||
[DataField("targetKey", required: true)] public string TargetKey = string.Empty;
|
||||
[DataField(required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("amount")]
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Amount;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
return (true, new Dictionary<string, object>()
|
||||
{
|
||||
{TargetKey, Amount},
|
||||
});
|
||||
return (
|
||||
true,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ TargetKey, Amount }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Math;
|
||||
|
||||
/// <summary>
|
||||
/// Sets a random float from MinAmount to MaxAmount to blackboard
|
||||
/// </summary>
|
||||
public sealed partial class SetRandomFloatOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string TargetKey = string.Empty;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MaxAmount = 1f;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float MinAmount = 0f;
|
||||
|
||||
public override async Task<(bool Valid, Dictionary<string, object>? Effects)> Plan(NPCBlackboard blackboard,
|
||||
CancellationToken cancelToken)
|
||||
{
|
||||
return (
|
||||
true,
|
||||
new Dictionary<string, object>
|
||||
{
|
||||
{ TargetKey, _random.NextFloat(MinAmount, MaxAmount) }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Robust.Server.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
|
||||
public sealed partial class PlaySoundOperator : HTNOperator
|
||||
{
|
||||
private AudioSystem _audio = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public SoundSpecifier? Sound;
|
||||
|
||||
public override void Initialize(IEntitySystemManager sysManager)
|
||||
{
|
||||
base.Initialize(sysManager);
|
||||
|
||||
_audio = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<AudioSystem>();
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
var uid = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
|
||||
_audio.PlayPvs(Sound, uid);
|
||||
|
||||
return base.Update(blackboard, frameTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
|
||||
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators;
|
||||
|
||||
public sealed partial class SayKeyOperator : HTNOperator
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
|
||||
private ChatSystem _chat = default!;
|
||||
|
||||
[DataField(required: true)]
|
||||
public string Key = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to hide message from chat window and logs.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool Hidden;
|
||||
|
||||
public override void Initialize(IEntitySystemManager sysManager)
|
||||
{
|
||||
base.Initialize(sysManager);
|
||||
_chat = IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<ChatSystem>();
|
||||
}
|
||||
|
||||
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
|
||||
{
|
||||
if (!blackboard.TryGetValue<object>(Key, out var value, _entManager))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
var speaker = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
|
||||
_chat.TrySendInGameICMessage(speaker, value.ToString() ?? "Oh no...", InGameICChatType.Speak, hideChat: Hidden, hideLog: Hidden);
|
||||
|
||||
return base.Update(blackboard, frameTime);
|
||||
}
|
||||
}
|
||||
@@ -973,7 +973,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
/// <summary>
|
||||
/// Creates a simple planet setup for a map.
|
||||
/// </summary>
|
||||
public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, int? seed = null, MetaDataComponent? metadata = null)
|
||||
public void EnsurePlanet(EntityUid mapUid, BiomeTemplatePrototype biomeTemplate, int? seed = null, MetaDataComponent? metadata = null, Color? mapLight = null)
|
||||
{
|
||||
if (!Resolve(mapUid, ref metadata))
|
||||
return;
|
||||
@@ -998,7 +998,7 @@ public sealed partial class BiomeSystem : SharedBiomeSystem
|
||||
// Lava: #A34931
|
||||
|
||||
var light = EnsureComp<MapLightComponent>(mapUid);
|
||||
light.AmbientLightColor = Color.FromHex("#D8B059");
|
||||
light.AmbientLightColor = mapLight ?? Color.FromHex("#D8B059");
|
||||
Dirty(mapUid, light, metadata);
|
||||
|
||||
var moles = new float[Atmospherics.AdjustedNumberOfGases];
|
||||
|
||||
@@ -309,7 +309,7 @@ public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
|
||||
var data = new PlayTimeData();
|
||||
_playTimeData.Add(session, data);
|
||||
|
||||
var playTimes = await _db.GetPlayTimes(session.UserId);
|
||||
var playTimes = await _db.GetPlayTimes(session.UserId, cancel);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
foreach (var timer in playTimes)
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace Content.Server.Preferences.Managers
|
||||
void Init();
|
||||
|
||||
Task LoadData(ICommonSession session, CancellationToken cancel);
|
||||
void FinishLoad(ICommonSession session);
|
||||
void OnClientDisconnected(ICommonSession session);
|
||||
|
||||
bool TryGetCachedPreferences(NetUserId userId, [NotNullWhen(true)] out PlayerPreferences? playerPreferences);
|
||||
|
||||
@@ -13,6 +13,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
|
||||
namespace Content.Server.Preferences.Managers
|
||||
@@ -27,6 +28,7 @@ namespace Content.Server.Preferences.Managers
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IDependencyCollection _dependencies = default!;
|
||||
[Dependency] private readonly IPrototypeManager _protos = default!;
|
||||
|
||||
// Cache player prefs on the server so we don't need as much async hell related to them.
|
||||
@@ -101,9 +103,8 @@ namespace Content.Server.Preferences.Managers
|
||||
|
||||
var curPrefs = prefsData.Prefs!;
|
||||
var session = _playerManager.GetSessionById(userId);
|
||||
var collection = IoCManager.Instance!;
|
||||
|
||||
profile.EnsureValid(session, collection);
|
||||
profile.EnsureValid(session, _dependencies);
|
||||
|
||||
var profiles = new Dictionary<int, ICharacterProfile>(curPrefs.Characters)
|
||||
{
|
||||
@@ -196,21 +197,32 @@ namespace Content.Server.Preferences.Managers
|
||||
|
||||
async Task LoadPrefs()
|
||||
{
|
||||
var prefs = await GetOrCreatePreferencesAsync(session.UserId);
|
||||
var prefs = await GetOrCreatePreferencesAsync(session.UserId, cancel);
|
||||
prefsData.Prefs = prefs;
|
||||
prefsData.PrefsLoaded = true;
|
||||
|
||||
var msg = new MsgPreferencesAndSettings();
|
||||
msg.Preferences = prefs;
|
||||
msg.Settings = new GameSettings
|
||||
{
|
||||
MaxCharacterSlots = MaxCharacterSlots
|
||||
};
|
||||
_netManager.ServerSendMessage(msg, session.Channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void FinishLoad(ICommonSession session)
|
||||
{
|
||||
// This is a separate step from the actual database load.
|
||||
// Sanitizing preferences requires play time info due to loadouts.
|
||||
// And play time info is loaded concurrently from the DB with preferences.
|
||||
var prefsData = _cachedPlayerPrefs[session.UserId];
|
||||
DebugTools.Assert(prefsData.Prefs != null);
|
||||
prefsData.Prefs = SanitizePreferences(session, prefsData.Prefs, _dependencies);
|
||||
|
||||
prefsData.PrefsLoaded = true;
|
||||
|
||||
var msg = new MsgPreferencesAndSettings();
|
||||
msg.Preferences = prefsData.Prefs;
|
||||
msg.Settings = new GameSettings
|
||||
{
|
||||
MaxCharacterSlots = MaxCharacterSlots
|
||||
};
|
||||
_netManager.ServerSendMessage(msg, session.Channel);
|
||||
}
|
||||
|
||||
public void OnClientDisconnected(ICommonSession session)
|
||||
{
|
||||
_cachedPlayerPrefs.Remove(session.UserId);
|
||||
@@ -270,18 +282,15 @@ namespace Content.Server.Preferences.Managers
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId)
|
||||
private async Task<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel)
|
||||
{
|
||||
var prefs = await _db.GetPlayerPreferencesAsync(userId);
|
||||
var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel);
|
||||
if (prefs is null)
|
||||
{
|
||||
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random());
|
||||
return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random(), cancel);
|
||||
}
|
||||
|
||||
var session = _playerManager.GetSessionById(userId);
|
||||
var collection = IoCManager.Instance!;
|
||||
|
||||
return SanitizePreferences(session, prefs, collection);
|
||||
return prefs;
|
||||
}
|
||||
|
||||
private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPreferences prefs, IDependencyCollection collection)
|
||||
|
||||
@@ -40,14 +40,21 @@ public sealed partial class SalvageExpeditionComponent : SharedSalvageExpedition
|
||||
/// <summary>
|
||||
/// Countdown audio stream.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? Stream = null;
|
||||
|
||||
/// <summary>
|
||||
/// Sound that plays when the mission end is imminent.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField("sound")]
|
||||
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Misc/tension_session.ogg")
|
||||
[ViewVariables(VVAccess.ReadWrite), DataField]
|
||||
public SoundSpecifier Sound = new SoundCollectionSpecifier("ExpeditionEnd")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-5),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Song selected on MapInit so we can predict the audio countdown properly.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public SoundPathSpecifier SelectedSong;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ using Content.Server.Salvage.Expeditions;
|
||||
using Content.Server.Salvage.Expeditions.Structure;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Content.Shared.Salvage.Expeditions;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.CPUJob.JobQueues;
|
||||
using Robust.Shared.CPUJob.JobQueues.Queues;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -32,6 +34,7 @@ public sealed partial class SalvageSystem
|
||||
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, EntParentChangedMessage>(OnSalvageConsoleParent);
|
||||
SubscribeLocalEvent<SalvageExpeditionConsoleComponent, ClaimSalvageMessage>(OnSalvageClaimMessage);
|
||||
|
||||
SubscribeLocalEvent<SalvageExpeditionComponent, MapInitEvent>(OnExpeditionMapInit);
|
||||
SubscribeLocalEvent<SalvageExpeditionComponent, ComponentShutdown>(OnExpeditionShutdown);
|
||||
SubscribeLocalEvent<SalvageExpeditionComponent, ComponentGetState>(OnExpeditionGetState);
|
||||
|
||||
@@ -64,6 +67,12 @@ public sealed partial class SalvageSystem
|
||||
_cooldown = obj;
|
||||
}
|
||||
|
||||
private void OnExpeditionMapInit(EntityUid uid, SalvageExpeditionComponent component, MapInitEvent args)
|
||||
{
|
||||
var selectedFile = _audio.GetSound(component.Sound);
|
||||
component.SelectedSong = new SoundPathSpecifier(selectedFile, component.Sound.Params);
|
||||
}
|
||||
|
||||
private void OnExpeditionShutdown(EntityUid uid, SalvageExpeditionComponent component, ComponentShutdown args)
|
||||
{
|
||||
component.Stream = _audio.Stop(component.Stream);
|
||||
|
||||
@@ -144,6 +144,7 @@ public sealed partial class SalvageSystem
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
var remaining = comp.EndTime - _timing.CurTime;
|
||||
var audioLength = _audio.GetAudioLength(comp.SelectedSong.Path.ToString());
|
||||
|
||||
if (comp.Stage < ExpeditionStage.FinalCountdown && remaining < TimeSpan.FromSeconds(45))
|
||||
{
|
||||
@@ -151,13 +152,14 @@ public sealed partial class SalvageSystem
|
||||
Dirty(uid, comp);
|
||||
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-seconds", ("duration", TimeSpan.FromSeconds(45).Seconds)));
|
||||
}
|
||||
else if (comp.Stage < ExpeditionStage.MusicCountdown && remaining < TimeSpan.FromMinutes(2))
|
||||
else if (comp.Stream == null && remaining < audioLength)
|
||||
{
|
||||
// TODO: Some way to play audio attached to a map for players.
|
||||
comp.Stream = _audio.PlayGlobal(comp.Sound, Filter.BroadcastMap(Comp<MapComponent>(uid).MapId), true).Value.Entity;
|
||||
var audio = _audio.PlayPvs(comp.Sound, uid).Value;
|
||||
comp.Stream = audio.Entity;
|
||||
_audio.SetMapAudio(audio);
|
||||
comp.Stage = ExpeditionStage.MusicCountdown;
|
||||
Dirty(uid, comp);
|
||||
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", TimeSpan.FromMinutes(2).Minutes)));
|
||||
Announce(uid, Loc.GetString("salvage-expedition-announcement-countdown-minutes", ("duration", audioLength.Minutes)));
|
||||
}
|
||||
else if (comp.Stage < ExpeditionStage.Countdown && remaining < TimeSpan.FromMinutes(4))
|
||||
{
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Shuttles.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class DockingSignalControlComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Output port that is high while docked.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ProtoId<SourcePortPrototype> DockStatusSignalPort = "DockStatus";
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using Content.Server.DeviceLinking.Systems;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Shuttles.Events;
|
||||
|
||||
namespace Content.Server.Shuttles.Systems;
|
||||
|
||||
public sealed class DockingSignalControlSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly DeviceLinkSystem _deviceLinkSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<DockingSignalControlComponent, DockEvent>(OnDocked);
|
||||
SubscribeLocalEvent<DockingSignalControlComponent, UndockEvent>(OnUndocked);
|
||||
}
|
||||
|
||||
private void OnDocked(Entity<DockingSignalControlComponent> ent, ref DockEvent args)
|
||||
{
|
||||
_deviceLinkSystem.SendSignal(ent, ent.Comp.DockStatusSignalPort, signal: true);
|
||||
}
|
||||
|
||||
private void OnUndocked(Entity<DockingSignalControlComponent> ent, ref UndockEvent args)
|
||||
{
|
||||
_deviceLinkSystem.SendSignal(ent, ent.Comp.DockStatusSignalPort, signal: false);
|
||||
}
|
||||
}
|
||||
@@ -264,11 +264,6 @@ public sealed class ThrusterSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower))
|
||||
{
|
||||
apcPower.NeedsPower = true;
|
||||
}
|
||||
|
||||
component.IsOn = true;
|
||||
|
||||
if (!EntityManager.TryGetComponent(xform.GridUid, out ShuttleComponent? shuttleComponent))
|
||||
@@ -371,11 +366,6 @@ public sealed class ThrusterSystem : EntitySystem
|
||||
if (!EntityManager.TryGetComponent(gridId, out ShuttleComponent? shuttleComponent))
|
||||
return;
|
||||
|
||||
if (TryComp<ApcPowerReceiverComponent>(uid, out var apcPower))
|
||||
{
|
||||
apcPower.NeedsPower = false;
|
||||
}
|
||||
|
||||
// Logger.DebugS("thruster", $"Disabled thruster {uid}");
|
||||
|
||||
switch (component.Type)
|
||||
|
||||
@@ -10,6 +10,10 @@ public sealed class FrenchAccentSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
private static readonly Regex RegexTh = new(@"th", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex RegexStartH = new(@"(?<!\w)h", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex RegexSpacePunctuation = new(@"(?<=\w\w)[!?;:](?!\w)", RegexOptions.IgnoreCase);
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -23,17 +27,14 @@ public sealed class FrenchAccentSystem : EntitySystem
|
||||
|
||||
msg = _replacement.ApplyReplacements(msg, "french");
|
||||
|
||||
// replaces th with dz
|
||||
msg = Regex.Replace(msg, @"th", "'z", RegexOptions.IgnoreCase);
|
||||
// replaces th with dz
|
||||
msg = RegexTh.Replace(msg, "'z");
|
||||
|
||||
// removes the letter h from the start of words.
|
||||
msg = Regex.Replace(msg, @"(?<!\w)[h]", "'", RegexOptions.IgnoreCase);
|
||||
msg = RegexStartH.Replace(msg, "'");
|
||||
|
||||
// spaces out ! ? : and ;.
|
||||
msg = Regex.Replace(msg, @"(?<=\w\w)!(?!\w)", " !", RegexOptions.IgnoreCase);
|
||||
msg = Regex.Replace(msg, @"(?<=\w\w)[?](?!\w)", " ?", RegexOptions.IgnoreCase);
|
||||
msg = Regex.Replace(msg, @"(?<=\w\w)[;](?!\w)", " ;", RegexOptions.IgnoreCase);
|
||||
msg = Regex.Replace(msg, @"(?<=\w\w)[:](?!\w)", " :", RegexOptions.IgnoreCase);
|
||||
msg = RegexSpacePunctuation.Replace(msg, " $&");
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class FrontalLispSystem : EntitySystem
|
||||
{
|
||||
// @formatter:off
|
||||
private static readonly Regex RegexUpperTh = new(@"[T]+[Ss]+|[S]+[Cc]+(?=[IiEeYy]+)|[C]+(?=[IiEeYy]+)|[P][Ss]+|([S]+[Tt]+|[T]+)(?=[Ii]+[Oo]+[Uu]*[Nn]*)|[C]+[Hh]+(?=[Ii]*[Ee]*)|[Z]+|[S]+|[X]+(?=[Ee]+)");
|
||||
private static readonly Regex RegexLowerTh = new(@"[t]+[s]+|[s]+[c]+(?=[iey]+)|[c]+(?=[iey]+)|[p][s]+|([s]+[t]+|[t]+)(?=[i]+[o]+[u]*[n]*)|[c]+[h]+(?=[i]*[e]*)|[z]+|[s]+|[x]+(?=[e]+)");
|
||||
private static readonly Regex RegexUpperEcks = new(@"[E]+[Xx]+[Cc]*|[X]+");
|
||||
private static readonly Regex RegexLowerEcks = new(@"[e]+[x]+[c]*|[x]+");
|
||||
// @formatter:on
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -16,11 +23,11 @@ public sealed class FrontalLispSystem : EntitySystem
|
||||
var message = args.Message;
|
||||
|
||||
// handles ts, sc(i|e|y), c(i|e|y), ps, st(io(u|n)), ch(i|e), z, s
|
||||
message = Regex.Replace(message, @"[T]+[Ss]+|[S]+[Cc]+(?=[IiEeYy]+)|[C]+(?=[IiEeYy]+)|[P][Ss]+|([S]+[Tt]+|[T]+)(?=[Ii]+[Oo]+[Uu]*[Nn]*)|[C]+[Hh]+(?=[Ii]*[Ee]*)|[Z]+|[S]+|[X]+(?=[Ee]+)", "TH");
|
||||
message = Regex.Replace(message, @"[t]+[s]+|[s]+[c]+(?=[iey]+)|[c]+(?=[iey]+)|[p][s]+|([s]+[t]+|[t]+)(?=[i]+[o]+[u]*[n]*)|[c]+[h]+(?=[i]*[e]*)|[z]+|[s]+|[x]+(?=[e]+)", "th");
|
||||
message = RegexUpperTh.Replace(message, "TH");
|
||||
message = RegexLowerTh.Replace(message, "th");
|
||||
// handles ex(c), x
|
||||
message = Regex.Replace(message, @"[E]+[Xx]+[Cc]*|[X]+", "EKTH");
|
||||
message = Regex.Replace(message, @"[e]+[x]+[c]*|[x]+", "ekth");
|
||||
message = RegexUpperEcks.Replace(message, "EKTH");
|
||||
message = RegexLowerEcks.Replace(message, "ekth");
|
||||
|
||||
args.Message = message;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,12 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class LizardAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex RegexLowerS = new("s+");
|
||||
private static readonly Regex RegexUpperS = new("S+");
|
||||
private static readonly Regex RegexInternalX = new(@"(\w)x");
|
||||
private static readonly Regex RegexLowerEndX = new(@"\bx([\-|r|R]|\b)");
|
||||
private static readonly Regex RegexUpperEndX = new(@"\bX([\-|r|R]|\b)");
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -16,15 +22,15 @@ public sealed class LizardAccentSystem : EntitySystem
|
||||
var message = args.Message;
|
||||
|
||||
// hissss
|
||||
message = Regex.Replace(message, "s+", "sss");
|
||||
message = RegexLowerS.Replace(message, "sss");
|
||||
// hiSSS
|
||||
message = Regex.Replace(message, "S+", "SSS");
|
||||
message = RegexUpperS.Replace(message, "SSS");
|
||||
// ekssit
|
||||
message = Regex.Replace(message, @"(\w)x", "$1kss");
|
||||
message = RegexInternalX.Replace(message, "$1kss");
|
||||
// ecks
|
||||
message = Regex.Replace(message, @"\bx([\-|r|R]|\b)", "ecks$1");
|
||||
message = RegexLowerEndX.Replace(message, "ecks$1");
|
||||
// eckS
|
||||
message = Regex.Replace(message, @"\bX([\-|r|R]|\b)", "ECKS$1");
|
||||
message = RegexUpperEndX.Replace(message, "ECKS$1");
|
||||
|
||||
args.Message = message;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Server.Speech.Components;
|
||||
@@ -8,30 +7,17 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class MobsterAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex RegexIng = new(@"(?<=\w\w)(in)g(?!\w)", RegexOptions.IgnoreCase);
|
||||
private static readonly Regex RegexLowerOr = new(@"(?<=\w)o[Rr](?=\w)");
|
||||
private static readonly Regex RegexUpperOr = new(@"(?<=\w)O[Rr](?=\w)");
|
||||
private static readonly Regex RegexLowerAr = new(@"(?<=\w)a[Rr](?=\w)");
|
||||
private static readonly Regex RegexUpperAr = new(@"(?<=\w)A[Rr](?=\w)");
|
||||
private static readonly Regex RegexFirstWord = new(@"^(\S+)");
|
||||
private static readonly Regex RegexLastWord = new(@"(\S+)$");
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
private static readonly Dictionary<string, string> DirectReplacements = new()
|
||||
{
|
||||
{ "let me", "lemme" },
|
||||
{ "should", "oughta" },
|
||||
{ "the", "da" },
|
||||
{ "them", "dem" },
|
||||
{ "attack", "whack" },
|
||||
{ "kill", "whack" },
|
||||
{ "murder", "whack" },
|
||||
{ "dead", "sleepin' with da fishies"},
|
||||
{ "hey", "ey'o" },
|
||||
{ "hi", "ey'o"},
|
||||
{ "hello", "ey'o"},
|
||||
{ "rules", "roolz" },
|
||||
{ "you", "yous" },
|
||||
{ "have to", "gotta" },
|
||||
{ "going to", "boutta" },
|
||||
{ "about to", "boutta" },
|
||||
{ "here", "'ere" }
|
||||
};
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -51,20 +37,20 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
// thinking -> thinkin'
|
||||
// king -> king
|
||||
//Uses captures groups to make sure the captialization of IN is kept
|
||||
msg = Regex.Replace(msg, @"(?<=\w\w)(in)g(?!\w)", "$1'", RegexOptions.IgnoreCase);
|
||||
msg = RegexIng.Replace(msg, "$1'");
|
||||
|
||||
// or -> uh and ar -> ah in the middle of words (fuhget, tahget)
|
||||
msg = Regex.Replace(msg, @"(?<=\w)o[Rr](?=\w)", "uh");
|
||||
msg = Regex.Replace(msg, @"(?<=\w)O[Rr](?=\w)", "UH");
|
||||
msg = Regex.Replace(msg, @"(?<=\w)a[Rr](?=\w)", "ah");
|
||||
msg = Regex.Replace(msg, @"(?<=\w)A[Rr](?=\w)", "AH");
|
||||
msg = RegexLowerOr.Replace(msg, "uh");
|
||||
msg = RegexUpperOr.Replace(msg, "UH");
|
||||
msg = RegexLowerAr.Replace(msg, "ah");
|
||||
msg = RegexUpperAr.Replace(msg, "AH");
|
||||
|
||||
// Prefix
|
||||
if (_random.Prob(0.15f))
|
||||
{
|
||||
//Checks if the first word of the sentence is all caps
|
||||
//So the prefix can be allcapped and to not resanitize the captial
|
||||
var firstWordAllCaps = !Regex.Match(msg, @"^(\S+)").Value.Any(char.IsLower);
|
||||
var firstWordAllCaps = !RegexFirstWord.Match(msg).Value.Any(char.IsLower);
|
||||
var pick = _random.Next(1, 2);
|
||||
|
||||
// Reverse sanitize capital
|
||||
@@ -84,7 +70,7 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
{
|
||||
//Checks if the last word of the sentence is all caps
|
||||
//So the suffix can be allcapped
|
||||
var lastWordAllCaps = !Regex.Match(msg, @"(\S+)$").Value.Any(char.IsLower);
|
||||
var lastWordAllCaps = !RegexLastWord.Match(msg).Value.Any(char.IsLower);
|
||||
var suffix = "";
|
||||
if (component.IsBoss)
|
||||
{
|
||||
@@ -94,7 +80,7 @@ public sealed class MobsterAccentSystem : EntitySystem
|
||||
else
|
||||
{
|
||||
var pick = _random.Next(1, 3);
|
||||
suffix = Loc.GetString($"accent-mobster-suffix-minion-{pick}");
|
||||
suffix = Loc.GetString($"accent-mobster-suffix-minion-{pick}");
|
||||
}
|
||||
if (lastWordAllCaps)
|
||||
suffix = suffix.ToUpper();
|
||||
|
||||
@@ -5,6 +5,9 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class MothAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex RegexLowerBuzz = new Regex("z{1,3}");
|
||||
private static readonly Regex RegexUpperBuzz = new Regex("Z{1,3}");
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -16,10 +19,10 @@ public sealed class MothAccentSystem : EntitySystem
|
||||
var message = args.Message;
|
||||
|
||||
// buzzz
|
||||
message = Regex.Replace(message, "z{1,3}", "zzz");
|
||||
message = RegexLowerBuzz.Replace(message, "zzz");
|
||||
// buZZZ
|
||||
message = Regex.Replace(message, "Z{1,3}", "ZZZ");
|
||||
|
||||
message = RegexUpperBuzz.Replace(message, "ZZZ");
|
||||
|
||||
args.Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed partial class ParrotAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex WordCleanupRegex = new Regex("[^A-Za-z0-9 -]");
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -27,7 +29,7 @@ public sealed partial class ParrotAccentSystem : EntitySystem
|
||||
if (_random.Prob(entity.Comp.LongestWordRepeatChance))
|
||||
{
|
||||
// Don't count non-alphanumeric characters as parts of words
|
||||
var cleaned = Regex.Replace(message, "[^A-Za-z0-9 -]", string.Empty);
|
||||
var cleaned = WordCleanupRegex.Replace(message, string.Empty);
|
||||
// Split on whitespace and favor words towards the end of the message
|
||||
var words = cleaned.Split(null).Reverse();
|
||||
// Find longest word
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class PirateAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex FirstWordAllCapsRegex = new(@"^(\S+)");
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
@@ -26,7 +28,7 @@ public sealed class PirateAccentSystem : EntitySystem
|
||||
return msg;
|
||||
//Checks if the first word of the sentence is all caps
|
||||
//So the prefix can be allcapped and to not resanitize the captial
|
||||
var firstWordAllCaps = !Regex.Match(msg, @"^(\S+)").Value.Any(char.IsLower);
|
||||
var firstWordAllCaps = !FirstWordAllCapsRegex.Match(msg).Value.Any(char.IsLower);
|
||||
|
||||
var pick = _random.Pick(component.PirateWords);
|
||||
var pirateWord = Loc.GetString(pick);
|
||||
|
||||
@@ -7,6 +7,8 @@ namespace Content.Server.Speech.EntitySystems
|
||||
{
|
||||
public sealed class ScrambledAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex RegexLoneI = new(@"(?<=\ )i(?=[\ \.\?]|$)");
|
||||
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -34,7 +36,7 @@ namespace Content.Server.Speech.EntitySystems
|
||||
msg = msg[0].ToString().ToUpper() + msg.Remove(0, 1);
|
||||
|
||||
// Capitalize lone i's
|
||||
msg = Regex.Replace(msg, @"(?<=\ )i(?=[\ \.\?]|$)", "I");
|
||||
msg = RegexLoneI.Replace(msg, "I");
|
||||
return msg;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,12 @@ namespace Content.Server.Speech.EntitySystems;
|
||||
|
||||
public sealed class SouthernAccentSystem : EntitySystem
|
||||
{
|
||||
private static readonly Regex RegexIng = new(@"ing\b");
|
||||
private static readonly Regex RegexAnd = new(@"\band\b");
|
||||
private static readonly Regex RegexDve = new("d've");
|
||||
|
||||
[Dependency] private readonly ReplacementAccentSystem _replacement = default!;
|
||||
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
@@ -20,9 +24,9 @@ public sealed class SouthernAccentSystem : EntitySystem
|
||||
message = _replacement.ApplyReplacements(message, "southern");
|
||||
|
||||
//They shoulda started runnin' an' hidin' from me!
|
||||
message = Regex.Replace(message, @"ing\b", "in'");
|
||||
message = Regex.Replace(message, @"\band\b", "an'");
|
||||
message = Regex.Replace(message, "d've", "da");
|
||||
message = RegexIng.Replace(message, "in'");
|
||||
message = RegexAnd.Replace(message, "an'");
|
||||
message = RegexDve.Replace(message, "da");
|
||||
args.Message = message;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,4 +16,7 @@ public sealed partial class StationBiomeComponent : Component
|
||||
// If null, its random
|
||||
[DataField]
|
||||
public int? Seed = null;
|
||||
|
||||
[DataField]
|
||||
public Color MapLightColor = Color.Black;
|
||||
}
|
||||
|
||||
@@ -30,6 +30,6 @@ public sealed partial class StationBiomeSystem : EntitySystem
|
||||
var mapId = Transform(station.Value).MapID;
|
||||
var mapUid = _mapManager.GetMapEntityId(mapId);
|
||||
|
||||
_biome.EnsurePlanet(mapUid, _proto.Index(map.Comp.Biome), map.Comp.Seed);
|
||||
_biome.EnsurePlanet(mapUid, _proto.Index(map.Comp.Biome), map.Comp.Seed, mapLight: map.Comp.MapLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Components;
|
||||
using Content.Server.GameTicking.Rules;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
@@ -22,6 +23,9 @@ namespace Content.Server.StationEvents
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly EventManagerSystem _event = default!;
|
||||
|
||||
public const float MinEventTime = 60 * 3;
|
||||
public const float MaxEventTime = 60 * 10;
|
||||
|
||||
protected override void Ended(EntityUid uid, BasicStationEventSchedulerComponent component, GameRuleComponent gameRule,
|
||||
GameRuleEndedEvent args)
|
||||
{
|
||||
@@ -58,7 +62,7 @@ namespace Content.Server.StationEvents
|
||||
/// </summary>
|
||||
private void ResetTimer(BasicStationEventSchedulerComponent component)
|
||||
{
|
||||
component.TimeUntilNextEvent = _random.Next(3 * 60, 10 * 60);
|
||||
component.TimeUntilNextEvent = _random.NextFloat(MinEventTime, MaxEventTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +70,59 @@ namespace Content.Server.StationEvents
|
||||
public sealed class StationEventCommand : ToolshedCommand
|
||||
{
|
||||
private EventManagerSystem? _stationEvent;
|
||||
private BasicStationEventSchedulerSystem? _basicScheduler;
|
||||
private IRobustRandom? _random;
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the expected number of times an event will run over the course of X rounds, taking into account weights and
|
||||
/// how many events are expected to run over a given timeframe for a given playercount by repeatedly simulating rounds.
|
||||
/// Effectively /100 (if you put 100 rounds) = probability an event will run per round.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This isn't perfect. Code path eventually goes into <see cref="EventManagerSystem.CanRun"/>, which requires
|
||||
/// state from <see cref="GameTicker"/>. As a result, you should probably just run this locally and not doing
|
||||
/// a real round (it won't pollute the state, but it will get contaminated by previously ran events in the actual round)
|
||||
/// and things like `MaxOccurrences` and `ReoccurrenceDelay` won't be respected.
|
||||
///
|
||||
/// I consider these to not be that relevant to the analysis here though (and I don't want most uses of them
|
||||
/// to even exist) so I think it's fine.
|
||||
/// </remarks>
|
||||
[CommandImplementation("simulate")]
|
||||
public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
|
||||
{
|
||||
_stationEvent ??= GetSys<EventManagerSystem>();
|
||||
_basicScheduler ??= GetSys<BasicStationEventSchedulerSystem>();
|
||||
_random ??= IoCManager.Resolve<IRobustRandom>();
|
||||
|
||||
var occurrences = new Dictionary<string, int>();
|
||||
|
||||
foreach (var ev in _stationEvent.AllEvents())
|
||||
{
|
||||
occurrences.Add(ev.Key.ID, 0);
|
||||
}
|
||||
|
||||
for (var i = 0; i < rounds; i++)
|
||||
{
|
||||
var curTime = TimeSpan.Zero;
|
||||
var randomEndTime = _random.NextGaussian(roundEndMean, roundEndStdDev) * 60; // *60 = minutes to seconds
|
||||
if (randomEndTime <= 0)
|
||||
continue;
|
||||
|
||||
while (curTime.TotalSeconds < randomEndTime)
|
||||
{
|
||||
// sim an event
|
||||
curTime += TimeSpan.FromSeconds(_random.NextFloat(BasicStationEventSchedulerSystem.MinEventTime, BasicStationEventSchedulerSystem.MaxEventTime));
|
||||
var available = _stationEvent.AvailableEvents(false, playerCount, curTime);
|
||||
var ev = _stationEvent.FindEvent(available);
|
||||
if (ev == null)
|
||||
continue;
|
||||
|
||||
occurrences[ev] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return occurrences.Select(p => (p.Key, (float) p.Value)).OrderByDescending(p => p.Item2);
|
||||
}
|
||||
|
||||
[CommandImplementation("lsprob")]
|
||||
public IEnumerable<(string, float)> LsProb()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.StationEvents.Components;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -15,6 +16,7 @@ public sealed class EventManagerSystem : EntitySystem
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] public readonly GameTicker GameTicker = default!;
|
||||
|
||||
public bool EventsEnabled { get; private set; }
|
||||
@@ -43,6 +45,7 @@ public sealed class EventManagerSystem : EntitySystem
|
||||
|
||||
var ent = GameTicker.AddGameRule(randomEvent);
|
||||
var str = Loc.GetString("station-event-system-run-event",("eventName", ToPrettyString(ent)));
|
||||
_chat.SendAdminAlert(str);
|
||||
Log.Info(str);
|
||||
return str;
|
||||
}
|
||||
@@ -61,7 +64,7 @@ public sealed class EventManagerSystem : EntitySystem
|
||||
/// Pick a random event from the available events at this time, also considering their weightings.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
|
||||
public string? FindEvent(Dictionary<EntityPrototype, StationEventComponent> availableEvents)
|
||||
{
|
||||
if (availableEvents.Count == 0)
|
||||
{
|
||||
@@ -95,16 +98,20 @@ public sealed class EventManagerSystem : EntitySystem
|
||||
/// <summary>
|
||||
/// Gets the events that have met their player count, time-until start, etc.
|
||||
/// </summary>
|
||||
/// <param name="ignoreEarliestStart"></param>
|
||||
/// <param name="playerCountOverride">Override for player count, if using this to simulate events rather than in an actual round.</param>
|
||||
/// <param name="currentTimeOverride">Override for round time, if using this to simulate events rather than in an actual round.</param>
|
||||
/// <returns></returns>
|
||||
private Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(bool ignoreEarliestStart = false)
|
||||
public Dictionary<EntityPrototype, StationEventComponent> AvailableEvents(
|
||||
bool ignoreEarliestStart = false,
|
||||
int? playerCountOverride = null,
|
||||
TimeSpan? currentTimeOverride = null)
|
||||
{
|
||||
var playerCount = _playerManager.PlayerCount;
|
||||
var playerCount = playerCountOverride ?? _playerManager.PlayerCount;
|
||||
|
||||
// playerCount does a lock so we'll just keep the variable here
|
||||
var currentTime = !ignoreEarliestStart
|
||||
var currentTime = currentTimeOverride ?? (!ignoreEarliestStart
|
||||
? GameTicker.RoundDuration()
|
||||
: TimeSpan.Zero;
|
||||
: TimeSpan.Zero);
|
||||
|
||||
var result = new Dictionary<EntityPrototype, StationEventComponent>();
|
||||
|
||||
@@ -112,7 +119,6 @@ public sealed class EventManagerSystem : EntitySystem
|
||||
{
|
||||
if (CanRun(proto, stationEvent, playerCount, currentTime))
|
||||
{
|
||||
Log.Debug($"Adding event {proto.ID} to possibilities");
|
||||
result.Add(proto, stationEvent);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user