Merge remote-tracking branch 'upstream/master' into ed-13-06-2024-upstream2

# Conflicts:
#	Content.Client/Guidebook/GuidebookSystem.cs
#	Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
#	Content.Shared/Guidebook/GuideEntry.cs
#	Content.Shared/Preferences/HumanoidCharacterProfile.cs
#	Resources/Prototypes/Accents/word_replacements.yml
#	Resources/Prototypes/Maps/arenas.yml
#	Resources/Prototypes/Maps/atlas.yml
#	Resources/Prototypes/Maps/bagel.yml
#	Resources/Prototypes/Maps/box.yml
#	Resources/Prototypes/Maps/cluster.yml
#	Resources/Prototypes/Maps/core.yml
#	Resources/Prototypes/Maps/debug.yml
#	Resources/Prototypes/Maps/europa.yml
#	Resources/Prototypes/Maps/fland.yml
#	Resources/Prototypes/Maps/marathon.yml
#	Resources/Prototypes/Maps/meta.yml
#	Resources/Prototypes/Maps/oasis.yml
#	Resources/Prototypes/Maps/omega.yml
#	Resources/Prototypes/Maps/origin.yml
#	Resources/Prototypes/Maps/packed.yml
#	Resources/Prototypes/Maps/reach.yml
#	Resources/Prototypes/Maps/saltern.yml
#	Resources/Prototypes/Maps/train.yml
#	Resources/Prototypes/lobbyscreens.yml
This commit is contained in:
Ed
2024-06-13 12:35:00 +03:00
475 changed files with 23765 additions and 12166 deletions

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Serialization;
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
namespace Content.Shared.Administration.Events;
[Serializable, NetSerializable]
public sealed class BabyJailStatus
{
public bool Enabled;
public bool ShowReason;
public int MaxAccountAgeMinutes;
public int MaxOverallMinutes;
}
[Serializable, NetSerializable]
public sealed class BabyJailChangedEvent(BabyJailStatus status) : EntityEventArgs
{
public BabyJailStatus Status = status;
}

View File

@@ -10,8 +10,8 @@ public sealed class PanicBunkerStatus
public bool EnableWithoutAdmins;
public bool CountDeadminnedAdmins;
public bool ShowReason;
public int MinAccountAgeHours;
public int MinOverallHours;
public int MinAccountAgeMinutes;
public int MinOverallMinutes;
}
[Serializable, NetSerializable]

View File

@@ -1,4 +1,5 @@
using Content.Shared.Maps;
using Content.Shared.Roles;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Physics.Components;
@@ -20,16 +21,16 @@ namespace Content.Shared.CCVar
CVarDef.Create("server.id", "unknown_server_id", CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Name of the rules txt file in the "Resources/Server Info" dir. Include the extension.
/// Guide Entry Prototype ID to be displayed as the server rules.
/// </summary>
public static readonly CVarDef<string> RulesFile =
CVarDef.Create("server.rules_file", "Rules.txt", CVar.REPLICATED | CVar.SERVER);
CVarDef.Create("server.rules_file", "DefaultRuleset", CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// A loc string for what should be displayed as the title on the Rules window.
/// Guide entry that is displayed by default when a guide is opened.
/// </summary>
public static readonly CVarDef<string> RulesHeader =
CVarDef.Create("server.rules_header", "ui-rules-header", CVar.REPLICATED | CVar.SERVER);
public static readonly CVarDef<string> DefaultGuide =
CVarDef.Create("server.default_guide", "NewPlayer", CVar.REPLICATED | CVar.SERVER);
/*
* Ambience
@@ -225,6 +226,12 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool>
GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Override default role requirements using a <see cref="JobRequirementOverridePrototype"/>
/// </summary>
public static readonly CVarDef<string>
GameRoleTimerOverride = CVarDef.Create("game.role_timer_override", "", CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// If roles should be restricted based on whether or not they are whitelisted.
/// </summary>
@@ -297,8 +304,8 @@ namespace Content.Shared.CCVar
/// <summary>
/// Minimal overall played time.
/// </summary>
public static readonly CVarDef<int> PanicBunkerMinOverallHours =
CVarDef.Create("game.panic_bunker.min_overall_hours", 10, CVar.SERVERONLY);
public static readonly CVarDef<int> PanicBunkerMinOverallMinutes =
CVarDef.Create("game.panic_bunker.min_overall_minutes", 600, CVar.SERVERONLY);
/// <summary>
/// A custom message that will be used for connections denied to the panic bunker
@@ -313,6 +320,48 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> BypassBunkerWhitelist =
CVarDef.Create("game.panic_bunker.whitelisted_can_bypass", true, CVar.SERVERONLY);
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
/// <summary>
/// Whether the baby jail is currently enabled.
/// </summary>
public static readonly CVarDef<bool> BabyJailEnabled =
CVarDef.Create("game.baby_jail.enabled", false, CVar.NOTIFY | CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Show reason of disconnect for user or not.
/// </summary>
public static readonly CVarDef<bool> BabyJailShowReason =
CVarDef.Create("game.baby_jail.show_reason", false, CVar.SERVERONLY);
/// <summary>
/// Maximum age of the account (from server's PoV, so from first-seen date) in minutes that can access baby
/// jailed servers.
/// </summary>
public static readonly CVarDef<int> BabyJailMaxAccountAge =
CVarDef.Create("game.baby_jail.max_account_age", 1440, CVar.SERVERONLY);
/// <summary>
/// Maximum overall played time allowed to access baby jailed servers.
/// </summary>
public static readonly CVarDef<int> BabyJailMaxOverallMinutes =
CVarDef.Create("game.baby_jail.max_overall_minutes", 120, CVar.SERVERONLY);
/// <summary>
/// A custom message that will be used for connections denied due to the baby jail.
/// If not empty, then will overwrite <see cref="BabyJailShowReason"/>
/// </summary>
public static readonly CVarDef<string> BabyJailCustomReason =
CVarDef.Create("game.baby_jail.custom_reason", string.Empty, CVar.SERVERONLY);
/// <summary>
/// Allow bypassing the baby jail if the user is whitelisted.
/// </summary>
public static readonly CVarDef<bool> BypassBabyJailWhitelist =
CVarDef.Create("game.baby_jail.whitelisted_can_bypass", true, CVar.SERVERONLY);
/// <summary>
/// Make people bonk when trying to climb certain objects like tables.
/// </summary>
@@ -776,7 +825,7 @@ namespace Content.Shared.CCVar
/// Minimum particle accelerator strength to create an admin alert message.
/// </summary>
public static readonly CVarDef<int> AdminAlertParticleAcceleratorMinPowerState =
CVarDef.Create("admin.alert.particle_accelerator_min_power_state", 3, CVar.SERVERONLY);
CVarDef.Create("admin.alert.particle_accelerator_min_power_state", 5, CVar.SERVERONLY); // strength 4
/// <summary>
/// Should the ban details in admin channel include PII? (IP, HWID, etc)
@@ -1804,7 +1853,7 @@ namespace Content.Shared.CCVar
/// Don't show rules to localhost/loopback interface.
/// </summary>
public static readonly CVarDef<bool> RulesExemptLocal =
CVarDef.Create("rules.exempt_local", true, CVar.SERVERONLY);
CVarDef.Create("rules.exempt_local", false, CVar.SERVERONLY);
/*

View File

@@ -1,54 +1,47 @@
using Content.Shared.Construction.Prototypes;
using Content.Shared.Stacks;
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
namespace Content.Shared.Construction.Components
namespace Content.Shared.Construction.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class MachineBoardComponent : Component
{
[RegisterComponent, NetworkedComponent]
public sealed partial class MachineBoardComponent : Component
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
/// <summary>
/// The stacks needed to construct this machine
/// </summary>
[DataField]
public Dictionary<ProtoId<StackPrototype>, int> StackRequirements = new();
[DataField("requirements", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<int, MachinePartPrototype>))]
public Dictionary<string, int> Requirements = new();
/// <summary>
/// Entities needed to construct this machine, discriminated by tag.
/// </summary>
[DataField]
public Dictionary<ProtoId<TagPrototype>, GenericPartInfo> TagRequirements = new();
[DataField("materialRequirements")]
public Dictionary<string, int> MaterialIdRequirements = new();
/// <summary>
/// Entities needed to construct this machine, discriminated by component.
/// </summary>
[DataField]
public Dictionary<string, GenericPartInfo> ComponentRequirements = new();
[DataField("tagRequirements")]
public Dictionary<string, GenericPartInfo> TagRequirements = new();
[DataField("componentRequirements")]
public Dictionary<string, GenericPartInfo> ComponentRequirements = new();
[ViewVariables(VVAccess.ReadWrite)]
[DataField("prototype")]
public string? Prototype { get; private set; }
public IEnumerable<KeyValuePair<StackPrototype, int>> MaterialRequirements
{
get
{
foreach (var (materialId, amount) in MaterialIdRequirements)
{
var material = _prototypeManager.Index<StackPrototype>(materialId);
yield return new KeyValuePair<StackPrototype, int>(material, amount);
}
}
}
}
[Serializable]
[DataDefinition]
public partial struct GenericPartInfo
{
[DataField("Amount")]
public int Amount;
[DataField("ExamineName")]
public string ExamineName;
[DataField("DefaultPrototype")]
public string DefaultPrototype;
}
/// <summary>
/// The machine that's constructed when this machine board is completed.
/// </summary>
[DataField(required: true)]
public EntProtoId Prototype;
}
[DataDefinition, Serializable]
public partial struct GenericPartInfo
{
[DataField(required: true)]
public int Amount;
[DataField(required: true)]
public EntProtoId DefaultPrototype;
[DataField]
public LocId? ExamineName;
}

View File

@@ -1,24 +0,0 @@
using Content.Shared.Construction.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Construction.Components
{
[RegisterComponent, NetworkedComponent]
public sealed partial class MachinePartComponent : Component
{
[DataField("part", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
public string PartType { get; private set; } = default!;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("rating")]
public int Rating { get; private set; } = 1;
/// <summary>
/// This number is used in tests to ensure that you can't use high quality machines for arbitrage. In
/// principle there is nothing wrong with using higher quality parts, but you have to be careful to not
/// allow them to be put into a lathe or something like that.
/// </summary>
public const int MaxRating = 4;
}
}

View File

@@ -1,10 +1,8 @@
using System.Linq;
using Content.Shared.Construction.Components;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Examine;
using Content.Shared.Lathe;
using Content.Shared.Materials;
using Content.Shared.Stacks;
using Robust.Shared.Prototypes;
namespace Content.Shared.Construction
@@ -16,6 +14,7 @@ namespace Content.Shared.Construction
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly SharedLatheSystem _lathe = default!;
[Dependency] private readonly SharedConstructionSystem _construction = default!;
public override void Initialize()
{
@@ -31,32 +30,30 @@ namespace Content.Shared.Construction
using (args.PushGroup(nameof(MachineBoardComponent)))
{
args.PushMarkup(Loc.GetString("machine-board-component-on-examine-label"));
foreach (var (part, amount) in component.Requirements)
foreach (var (material, amount) in component.StackRequirements)
{
args.PushMarkup(Loc.GetString("machine-board-component-required-element-entry-text",
("amount", amount),
("requiredElement", Loc.GetString(_prototype.Index<MachinePartPrototype>(part).Name))));
}
var stack = _prototype.Index(material);
var name = _prototype.Index(stack.Spawn).Name;
foreach (var (material, amount) in component.MaterialRequirements)
{
args.PushMarkup(Loc.GetString("machine-board-component-required-element-entry-text",
("amount", amount),
("requiredElement", Loc.GetString(material.Name))));
("requiredElement", Loc.GetString(name))));
}
foreach (var (_, info) in component.ComponentRequirements)
{
var examineName = _construction.GetExamineName(info);
args.PushMarkup(Loc.GetString("machine-board-component-required-element-entry-text",
("amount", info.Amount),
("requiredElement", Loc.GetString(info.ExamineName))));
("requiredElement", examineName)));
}
foreach (var (_, info) in component.TagRequirements)
{
var examineName = _construction.GetExamineName(info);
args.PushMarkup(Loc.GetString("machine-board-component-required-element-entry-text",
("amount", info.Amount),
("requiredElement", Loc.GetString(info.ExamineName))));
("requiredElement", examineName)));
}
}
}
@@ -66,30 +63,13 @@ namespace Content.Shared.Construction
var (_, comp) = entity;
var materials = new Dictionary<string, int>();
foreach (var (partId, amount) in comp.Requirements)
foreach (var (stackId, amount) in comp.StackRequirements)
{
var partProto = _prototype.Index<MachinePartPrototype>(partId);
if (!_lathe.TryGetRecipesFromEntity(partProto.StockPartPrototype, out var recipes))
continue;
var partRecipe = recipes[0];
if (recipes.Count > 1)
partRecipe = recipes.MinBy(p => p.RequiredMaterials.Values.Sum());
foreach (var (mat, matAmount) in partRecipe!.RequiredMaterials)
{
materials.TryAdd(mat, 0);
materials[mat] += matAmount * amount * coefficient;
}
}
foreach (var (stackId, amount) in comp.MaterialIdRequirements)
{
var stackProto = _prototype.Index<StackPrototype>(stackId);
var stackProto = _prototype.Index(stackId);
var defaultProto = _prototype.Index(stackProto.Spawn);
if (defaultProto.TryGetComponent<PhysicalCompositionComponent>(out var physComp))
if (defaultProto.TryGetComponent<PhysicalCompositionComponent>(out var physComp, EntityManager.ComponentFactory))
{
foreach (var (mat, matAmount) in physComp.MaterialComposition)
{
@@ -130,7 +110,7 @@ namespace Content.Shared.Construction
}
}
else if (_prototype.TryIndex(defaultProtoId, out var defaultProto) &&
defaultProto.TryGetComponent<PhysicalCompositionComponent>(out var physComp))
defaultProto.TryGetComponent<PhysicalCompositionComponent>(out var physComp, EntityManager.ComponentFactory))
{
foreach (var (mat, matAmount) in physComp.MaterialComposition)
{

View File

@@ -1,28 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Construction.Prototypes;
/// <summary>
/// This is a prototype for categorizing
/// different types of machine parts.
/// </summary>
[Prototype("machinePart")]
public sealed partial class MachinePartPrototype : IPrototype
{
/// <inheritdoc/>
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// A human-readable name for the machine part type.
/// </summary>
[DataField("name")]
public string Name = string.Empty;
/// <summary>
/// A stock part entity based on the machine part.
/// </summary>
[DataField("stockPartPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>), required: true)]
public string StockPartPrototype = string.Empty;
}

View File

@@ -1,5 +1,7 @@
using System.Linq;
using Content.Shared.Construction.Components;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Content.Shared.Interaction.SharedInteractionSystem;
namespace Content.Shared.Construction
@@ -7,6 +9,7 @@ namespace Content.Shared.Construction
public abstract class SharedConstructionSystem : EntitySystem
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
/// <summary>
/// Get predicate for construction obstruction checks.
@@ -22,5 +25,13 @@ namespace Content.Shared.Construction
var ignored = grid.GetAnchoredEntities(coords).ToHashSet();
return e => ignored.Contains(e);
}
public string GetExamineName(GenericPartInfo info)
{
if (info.ExamineName is not null)
return Loc.GetString(info.ExamineName.Value);
return PrototypeManager.Index(info.DefaultPrototype).Name;
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Numerics;
using Content.Shared.Construction.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Interaction;
@@ -9,10 +9,8 @@ using Content.Shared.Popups;
using Content.Shared.Tools.Systems;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
namespace Content.Shared.Construction;
@@ -39,7 +37,21 @@ public abstract class SharedFlatpackSystem : EntitySystem
SubscribeLocalEvent<FlatpackComponent, InteractUsingEvent>(OnFlatpackInteractUsing);
SubscribeLocalEvent<FlatpackComponent, ExaminedEvent>(OnFlatpackExamined);
SubscribeLocalEvent<FlatpackCreatorComponent, ContainerIsRemovingAttemptEvent>(OnCreatorRemovingAttempt);
SubscribeLocalEvent<FlatpackCreatorComponent, ItemSlotInsertAttemptEvent>(OnInsertAttempt);
}
private void OnInsertAttempt(Entity<FlatpackCreatorComponent> ent, ref ItemSlotInsertAttemptEvent args)
{
if (args.Slot.ID != ent.Comp.SlotId || args.Cancelled)
return;
if (HasComp<MachineBoardComponent>(args.Item))
return;
if (TryComp<ComputerBoardComponent>(args.Item, out var computer) && computer.Prototype != null)
return;
args.Cancelled = true;
}
private void OnFlatpackInteractUsing(Entity<FlatpackComponent> ent, ref InteractUsingEvent args)
@@ -67,19 +79,12 @@ public abstract class SharedFlatpackSystem : EntitySystem
var buildPos = _map.TileIndicesFor(grid, gridComp, xform.Coordinates);
var coords = _map.ToCenterCoordinates(grid, buildPos);
var intersecting = _entityLookup.GetEntitiesIntersecting(coords, LookupFlags.Dynamic | LookupFlags.Static);
// todo make this logic smarter.
// This should eventually allow for shit like building microwaves on tables and such.
foreach (var intersect in intersecting)
// TODO FLATPAK
// Make this logic smarter. This should eventually allow for shit like building microwaves on tables and such.
// Also: make it ignore ghosts
if (_entityLookup.AnyEntitiesIntersecting(coords, LookupFlags.Dynamic | LookupFlags.Static))
{
if (!TryComp<PhysicsComponent>(intersect, out var intersectBody))
continue;
if (!intersectBody.Hard || !intersectBody.CanCollide)
continue;
// this popup is on the server because the mispredicts on the intersection is crazy
// this popup is on the server because the predicts on the intersection is crazy
if (_net.IsServer)
_popup.PopupEntity(Loc.GetString("flatpack-unpack-no-room"), uid, args.User);
return;
@@ -88,7 +93,8 @@ public abstract class SharedFlatpackSystem : EntitySystem
if (_net.IsServer)
{
var spawn = Spawn(comp.Entity, _map.GridTileToLocal(grid, gridComp, buildPos));
_adminLogger.Add(LogType.Construction, LogImpact.Low,
_adminLogger.Add(LogType.Construction,
LogImpact.Low,
$"{ToPrettyString(args.User):player} unpacked {ToPrettyString(spawn):entity} at {xform.Coordinates} from {ToPrettyString(uid):entity}");
QueueDel(uid);
}
@@ -103,40 +109,24 @@ public abstract class SharedFlatpackSystem : EntitySystem
args.PushMarkup(Loc.GetString("flatpack-examine"));
}
private void OnCreatorRemovingAttempt(Entity<FlatpackCreatorComponent> ent, ref ContainerIsRemovingAttemptEvent args)
{
if (args.Container.ID == ent.Comp.SlotId && ent.Comp.Packing)
args.Cancel();
}
public void SetupFlatpack(Entity<FlatpackComponent?> ent, EntityUid? board)
protected void SetupFlatpack(Entity<FlatpackComponent?> ent, EntProtoId proto, EntityUid board)
{
if (!Resolve(ent, ref ent.Comp))
return;
var machinePrototypeId = new EntProtoId();
if (TryComp<MachineBoardComponent>(board, out var machineBoard) && machineBoard.Prototype is not null)
machinePrototypeId = machineBoard.Prototype;
else if (TryComp<ComputerBoardComponent>(board, out var computerBoard) && computerBoard.Prototype is not null)
machinePrototypeId = computerBoard.Prototype;
var comp = ent.Comp!;
var machinePrototype = PrototypeManager.Index(machinePrototypeId);
ent.Comp.Entity = proto;
var machinePrototype = PrototypeManager.Index<EntityPrototype>(proto);
var meta = MetaData(ent);
_metaData.SetEntityName(ent, Loc.GetString("flatpack-entity-name", ("name", machinePrototype.Name)), meta);
_metaData.SetEntityDescription(ent, Loc.GetString("flatpack-entity-description", ("name", machinePrototype.Name)), meta);
comp.Entity = machinePrototypeId;
Dirty(ent, comp);
if (board is null)
return;
Appearance.SetData(ent, FlatpackVisuals.Machine, MetaData(board.Value).EntityPrototype?.ID ?? string.Empty);
Dirty(ent, meta);
Appearance.SetData(ent, FlatpackVisuals.Machine, MetaData(board).EntityPrototype?.ID ?? string.Empty);
}
public Dictionary<string, int> GetFlatpackCreationCost(Entity<FlatpackCreatorComponent> entity, Entity<MachineBoardComponent>? machineBoard = null)
/// <param name="machineBoard">The machine board to pack. If null, this implies we are packing a computer board</param>
public Dictionary<string, int> GetFlatpackCreationCost(Entity<FlatpackCreatorComponent> entity, Entity<MachineBoardComponent>? machineBoard)
{
Dictionary<string, int> cost = new();
Dictionary<ProtoId<MaterialPrototype>, int> baseCost;

View File

@@ -484,7 +484,7 @@ namespace Content.Shared.Cuffs
BreakOnWeightlessMove = false,
BreakOnDamage = true,
NeedHand = true,
DistanceThreshold = 0.3f
DistanceThreshold = 1f // shorter than default but still feels good
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))
@@ -581,7 +581,7 @@ namespace Content.Shared.Cuffs
BreakOnDamage = true,
NeedHand = true,
RequireCanInteract = false, // Trust in UncuffAttemptEvent
DistanceThreshold = 0.3f
DistanceThreshold = 1f // shorter than default but still feels good
};
if (!_doAfter.TryStartDoAfter(doAfterEventArgs))

View File

@@ -45,5 +45,10 @@ namespace Content.Shared.Explosion.Components
/// Whether you can examine the item to see its timer or not.
/// </summary>
[DataField] public bool Examinable = true;
/// <summary>
/// Whether or not to show the user a popup when starting the timer.
/// </summary>
[DataField] public bool DoPopup = true;
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Roles;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Markdown.Mapping;
@@ -128,19 +129,17 @@ namespace Content.Shared.GameTicking
}
[Serializable, NetSerializable]
public sealed class TickerJobsAvailableEvent : EntityEventArgs
public sealed class TickerJobsAvailableEvent(
Dictionary<NetEntity, string> stationNames,
Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> jobsAvailableByStation)
: EntityEventArgs
{
/// <summary>
/// The Status of the Player in the lobby (ready, observer, ...)
/// </summary>
public Dictionary<NetEntity, Dictionary<string, uint?>> JobsAvailableByStation { get; }
public Dictionary<NetEntity, string> StationNames { get; }
public Dictionary<NetEntity, Dictionary<ProtoId<JobPrototype>, int?>> JobsAvailableByStation { get; } = jobsAvailableByStation;
public TickerJobsAvailableEvent(Dictionary<NetEntity, string> stationNames, Dictionary<NetEntity, Dictionary<string, uint?>> jobsAvailableByStation)
{
StationNames = stationNames;
JobsAvailableByStation = jobsAvailableByStation;
}
public Dictionary<NetEntity, string> StationNames { get; } = stationNames;
}
[Serializable, NetSerializable, DataDefinition]

View File

@@ -15,24 +15,24 @@ public sealed partial class GhostRolePrototype : IPrototype
/// <summary>
/// The name of the ghostrole.
/// </summary>
[DataField]
[DataField(required: true)]
public string Name { get; set; } = default!;
/// <summary>
/// The description of the ghostrole.
/// </summary>
[DataField]
[DataField(required: true)]
public string Description { get; set; } = default!;
/// <summary>
/// The entity prototype of the ghostrole
/// </summary>
[DataField]
public string EntityPrototype = default!;
[DataField(required: true)]
public EntProtoId EntityPrototype;
/// <summary>
/// Rules of the ghostrole
/// </summary>
[DataField]
[DataField(required: true)]
public string Rules = default!;
}
}

View File

@@ -11,6 +11,11 @@ namespace Content.Shared.Ghost.Roles
public string Name { get; set; }
public string Description { get; set; }
public string Rules { get; set; }
// TODO ROLE TIMERS
// Actually make use of / enforce this requirement?
// Why is this even here.
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
public HashSet<JobRequirement>? Requirements { get; set; }
/// <inheritdoc cref="GhostRoleKind"/>

View File

@@ -0,0 +1,52 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Guidebook;
[Prototype("guideEntry")]
public sealed partial class GuideEntryPrototype : GuideEntry, IPrototype
{
public string ID => Id;
}
[Virtual]
public class GuideEntry
{
/// <summary>
/// The file containing the contents of this guide.
/// </summary>
[DataField(required: true)] public ResPath Text = default!;
/// <summary>
/// The unique id for this guide.
/// </summary>
[IdDataField]
public string Id = default!;
/// <summary>
/// The name of this guide. This gets localized.
/// </summary>
[DataField(required: true)] public string Name = default!;
/// <summary>
/// The "children" of this guide for when guides are shown in a tree / table of contents.
/// </summary>
[DataField]
public List<ProtoId<GuideEntryPrototype>> Children = new();
/// <summary>
/// Enable filtering of items.
/// </summary>
[DataField] public bool FilterEnabled = default!;
[DataField] public bool RuleEntry;
/// <summary>
/// Priority for sorting top-level guides when shown in a tree / table of contents.
/// If the guide is the child of some other guide, the order simply determined by the order of children in <see cref="Children"/>.
/// </summary>
[DataField] public int Priority = 0;
[DataField]
public bool CrystallPunkAllowed = false;
}

View File

@@ -0,0 +1,41 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.Info;
/// <summary>
/// Sent by the server to show the rules to the client instantly.
/// </summary>
public sealed class ShowRulesPopupMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public float PopupTime { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
PopupTime = buffer.ReadFloat();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(PopupTime);
}
}
/// <summary>
/// Sent by the client when it has accepted the rules.
/// </summary>
public sealed class RulesAcceptedMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
}
}

View File

@@ -1,28 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Info
{
/// <summary>
/// A client request for server rules.
/// </summary>
[Serializable, NetSerializable]
public sealed class RequestRulesMessage : EntityEventArgs
{
}
/// <summary>
/// A server response with server rules.
/// </summary>
[Serializable, NetSerializable]
public sealed class RulesMessage : EntityEventArgs
{
public string Title;
public string Text;
public RulesMessage(string title, string rules)
{
Title = title;
Text = rules;
}
}
}

View File

@@ -1,60 +0,0 @@
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
namespace Content.Shared.Info;
public abstract class SharedRulesManager
{
/// <summary>
/// Sent by the server to show the rules to the client instantly.
/// </summary>
public sealed class ShowRulesPopupMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public float PopupTime { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
PopupTime = buffer.ReadFloat();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(PopupTime);
}
}
/// <summary>
/// Sent by the server when the client needs to display the rules on join.
/// </summary>
public sealed class ShouldShowRulesPopupMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
}
}
/// <summary>
/// Sent by the client when it has accepted the rules.
/// </summary>
public sealed class RulesAcceptedMessage : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
}
}
}

View File

@@ -59,8 +59,6 @@ namespace Content.Shared.Lathe
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float MaterialUseMultiplier = 1;
public const float DefaultPartRatingMaterialUseMultiplier = 0.85f;
#endregion
}

View File

@@ -0,0 +1,51 @@
using System.Numerics;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.MapText;
/// <summary>
/// This is used for displaying text in world space
/// </summary>
[NetworkedComponent, Access(typeof(SharedMapTextSystem))]
public abstract partial class SharedMapTextComponent : Component
{
public const string DefaultFont = "Default";
/// <summary>
/// The text to display. This will override <see cref="LocText"/>.
/// </summary>
[DataField]
public string? Text;
/// <summary>
/// The localized-id of the text that should be displayed.
/// </summary>
[DataField]
public LocId LocText = "map-text-default";
// TODO VV: LocId editing
[DataField]
public Color Color = Color.White;
[DataField]
public string FontId = DefaultFont;
[DataField]
public int FontSize = 12;
[DataField]
public Vector2 Offset = Vector2.Zero;
}
[Serializable, NetSerializable]
public sealed class MapTextComponentState : ComponentState
{
public string? Text { get; init;}
public LocId LocText { get; init;}
public Color Color { get; init;}
public string FontId { get; init; } = default!;
public int FontSize { get; init;}
public Vector2 Offset { get; init;}
}

View File

@@ -0,0 +1,6 @@
namespace Content.Shared.MapText;
/// <summary>
/// This handles registering the map text overlay, caching the text font and handling component state
/// </summary>
public abstract class SharedMapTextSystem : EntitySystem;

View File

@@ -26,7 +26,7 @@ public sealed partial class PrayableComponent : Component
/// <summary>
/// Prefix used in the notification to admins
/// </summary>
[DataField("notifiactionPrefix")]
[DataField("notificationPrefix")]
[ViewVariables(VVAccess.ReadWrite)]
public string NotificationPrefix = "prayer-chat-notify-pray";

View File

@@ -25,6 +25,9 @@ namespace Content.Shared.Preferences
[Serializable, NetSerializable]
public sealed partial class HumanoidCharacterProfile : ICharacterProfile
{
private static readonly Regex RestrictedNameRegex = new("[^А-Я,а-я,A-Z,a-z,0-9, ,\\-,']"); //CP14 Cyrillic add
private static readonly Regex ICNameCaseRegex = new(@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)");
public const int MaxNameLength = 32;
public const int MaxDescLength = 512;
@@ -32,7 +35,7 @@ namespace Content.Shared.Preferences
/// Job preferences for initial spawn.
/// </summary>
[DataField]
private Dictionary<string, JobPriority> _jobPriorities = new()
private Dictionary<ProtoId<JobPrototype>, JobPriority> _jobPriorities = new()
{
{
SharedGameTicker.FallbackOverflowJob, JobPriority.High
@@ -43,13 +46,13 @@ namespace Content.Shared.Preferences
/// Antags we have opted in to.
/// </summary>
[DataField]
private HashSet<string> _antagPreferences = new();
private HashSet<ProtoId<AntagPrototype>> _antagPreferences = new();
/// <summary>
/// Enabled traits.
/// </summary>
[DataField]
private HashSet<string> _traitPreferences = new();
private HashSet<ProtoId<TraitPrototype>> _traitPreferences = new();
/// <summary>
/// <see cref="_loadouts"/>
@@ -72,7 +75,7 @@ namespace Content.Shared.Preferences
/// Associated <see cref="SpeciesPrototype"/> for this profile.
/// </summary>
[DataField]
public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
public ProtoId<SpeciesPrototype> Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
[DataField]
public int Age { get; set; } = 18;
@@ -103,17 +106,17 @@ namespace Content.Shared.Preferences
/// <summary>
/// <see cref="_jobPriorities"/>
/// </summary>
public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
public IReadOnlyDictionary<ProtoId<JobPrototype>, JobPriority> JobPriorities => _jobPriorities;
/// <summary>
/// <see cref="_antagPreferences"/>
/// </summary>
public IReadOnlySet<string> AntagPreferences => _antagPreferences;
public IReadOnlySet<ProtoId<AntagPrototype>> AntagPreferences => _antagPreferences;
/// <summary>
/// <see cref="_traitPreferences"/>
/// </summary>
public IReadOnlySet<string> TraitPreferences => _traitPreferences;
public IReadOnlySet<ProtoId<TraitPrototype>> TraitPreferences => _traitPreferences;
/// <summary>
/// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
@@ -131,10 +134,10 @@ namespace Content.Shared.Preferences
Gender gender,
HumanoidCharacterAppearance appearance,
SpawnPriorityPreference spawnPriority,
Dictionary<string, JobPriority> jobPriorities,
Dictionary<ProtoId<JobPrototype>, JobPriority> jobPriorities,
PreferenceUnavailableMode preferenceUnavailable,
HashSet<string> antagPreferences,
HashSet<string> traitPreferences,
HashSet<ProtoId<AntagPrototype>> antagPreferences,
HashSet<ProtoId<TraitPrototype>> traitPreferences,
Dictionary<string, RoleLoadout> loadouts)
{
Name = name;
@@ -150,6 +153,20 @@ namespace Content.Shared.Preferences
_antagPreferences = antagPreferences;
_traitPreferences = traitPreferences;
_loadouts = loadouts;
var hasHighPrority = false;
foreach (var (key, value) in _jobPriorities)
{
if (value == JobPriority.Never)
_jobPriorities.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
_jobPriorities[key] = JobPriority.Medium;
hasHighPrority = true;
}
}
/// <summary>Copy constructor</summary>
@@ -162,10 +179,10 @@ namespace Content.Shared.Preferences
other.Gender,
other.Appearance.Clone(),
other.SpawnPriority,
new Dictionary<string, JobPriority>(other.JobPriorities),
new Dictionary<ProtoId<JobPrototype>, JobPriority>(other.JobPriorities),
other.PreferenceUnavailable,
new HashSet<string>(other.AntagPreferences),
new HashSet<string>(other.TraitPreferences),
new HashSet<ProtoId<AntagPrototype>>(other.AntagPreferences),
new HashSet<ProtoId<TraitPrototype>>(other.TraitPreferences),
new Dictionary<string, RoleLoadout>(other.Loadouts))
{
}
@@ -286,21 +303,48 @@ namespace Content.Shared.Preferences
return new(this) { SpawnPriority = spawnPriority };
}
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<ProtoId<JobPrototype>, JobPriority>> jobPriorities)
{
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(jobPriorities);
var hasHighPrority = false;
foreach (var (key, value) in dictionary)
{
if (value == JobPriority.Never)
dictionary.Remove(key);
else if (value != JobPriority.High)
continue;
if (hasHighPrority)
dictionary[key] = JobPriority.Medium;
hasHighPrority = true;
}
return new(this)
{
_jobPriorities = new Dictionary<string, JobPriority>(jobPriorities),
_jobPriorities = dictionary
};
}
public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
public HumanoidCharacterProfile WithJobPriority(ProtoId<JobPrototype> jobId, JobPriority priority)
{
var dictionary = new Dictionary<string, JobPriority>(_jobPriorities);
var dictionary = new Dictionary<ProtoId<JobPrototype>, JobPriority>(_jobPriorities);
if (priority == JobPriority.Never)
{
dictionary.Remove(jobId);
}
else if (priority == JobPriority.High)
{
// There can only ever be one high priority job.
foreach (var (job, value) in dictionary)
{
if (value == JobPriority.High)
dictionary[job] = JobPriority.Medium;
}
dictionary[jobId] = priority;
}
else
{
dictionary[jobId] = priority;
@@ -317,17 +361,17 @@ namespace Content.Shared.Preferences
return new(this) { PreferenceUnavailable = mode };
}
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<ProtoId<AntagPrototype>> antagPreferences)
{
return new(this)
{
_antagPreferences = new HashSet<string>(antagPreferences),
_antagPreferences = new (antagPreferences),
};
}
public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
public HumanoidCharacterProfile WithAntagPreference(ProtoId<AntagPrototype> antagId, bool pref)
{
var list = new HashSet<string>(_antagPreferences);
var list = new HashSet<ProtoId<AntagPrototype>>(_antagPreferences);
if (pref)
{
list.Add(antagId);
@@ -343,16 +387,16 @@ namespace Content.Shared.Preferences
};
}
public HumanoidCharacterProfile WithTraitPreference(string traitId, string? categoryId, bool pref)
public HumanoidCharacterProfile WithTraitPreference(ProtoId<TraitPrototype> traitId, string? categoryId, bool pref)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
var traitProto = prototypeManager.Index<TraitPrototype>(traitId);
var traitProto = prototypeManager.Index(traitId);
TraitCategoryPrototype? categoryProto = null;
if (categoryId != null && categoryId != "default")
categoryProto = prototypeManager.Index<TraitCategoryPrototype>(categoryId);
var list = new HashSet<string>(_traitPreferences);
var list = new HashSet<ProtoId<TraitPrototype>>(_traitPreferences);
if (pref)
{
@@ -369,7 +413,7 @@ namespace Content.Shared.Preferences
var count = 0;
foreach (var trait in list)
{
var traitProtoTemp = prototypeManager.Index<TraitPrototype>(trait);
var traitProtoTemp = prototypeManager.Index(trait);
count += traitProtoTemp.Cost;
}
@@ -423,10 +467,10 @@ namespace Content.Shared.Preferences
var configManager = collection.Resolve<IConfigurationManager>();
var prototypeManager = collection.Resolve<IPrototypeManager>();
if (!prototypeManager.TryIndex<SpeciesPrototype>(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
if (!prototypeManager.TryIndex(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
{
Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
speciesPrototype = prototypeManager.Index<SpeciesPrototype>(Species);
speciesPrototype = prototypeManager.Index(Species);
}
var sex = Sex switch
@@ -470,15 +514,13 @@ namespace Content.Shared.Preferences
if (configManager.GetCVar(CCVars.RestrictedNames))
{
name = Regex.Replace(name, configManager.GetCVar(CCVars.RestrictedNameRegex), string.Empty);
name = RestrictedNameRegex.Replace(name, string.Empty);
}
if (configManager.GetCVar(CCVars.ICNameCase))
{
// This regex replaces the first character of the first and last words of the name with their uppercase version
name = Regex.Replace(name,
@"^(?<word>\w)|\b(?<word>\w)(?=\w*$)",
m => m.Groups["word"].Value.ToUpper());
name = ICNameCaseRegex.Replace(name, m => m.Groups["word"].Value.ToUpper());
}
if (string.IsNullOrEmpty(name))
@@ -513,7 +555,7 @@ namespace Content.Shared.Preferences
_ => SpawnPriorityPreference.None // Invalid enum values.
};
var priorities = new Dictionary<string, JobPriority>(JobPriorities
var priorities = new Dictionary<ProtoId<JobPrototype>, JobPriority>(JobPriorities
.Where(p => prototypeManager.TryIndex<JobPrototype>(p.Key, out var job) && job.SetPreference && p.Value switch
{
JobPriority.Never => false, // Drop never since that's assumed default.
@@ -523,6 +565,17 @@ namespace Content.Shared.Preferences
_ => false
}));
var hasHighPrio = false;
foreach (var (key, value) in priorities)
{
if (value != JobPriority.High)
continue;
if (hasHighPrio)
priorities[key] = JobPriority.Medium;
hasHighPrio = true;
}
var antags = AntagPreferences
.Where(id => prototypeManager.TryIndex<AntagPrototype>(id, out var antag) && antag.SetPreference)
.ToList();

View File

@@ -59,11 +59,28 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
return;
}
// In some instances we might not have picked up a new group for existing data.
foreach (var groupProto in roleProto.Groups)
{
if (SelectedLoadouts.ContainsKey(groupProto))
continue;
// Data will get set below.
SelectedLoadouts[groupProto] = new List<Loadout>();
}
// Reset points to recalculate.
Points = roleProto.Points;
foreach (var (group, groupLoadouts) in SelectedLoadouts)
{
// Check the group is even valid for this role.
if (!roleProto.Groups.Contains(group))
{
groupRemove.Add(group);
continue;
}
// Dump if Group doesn't exist
if (!protoManager.TryIndex(group, out var groupProto))
{
@@ -120,7 +137,10 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
if (loadouts.Contains(defaultLoadout))
continue;
// Still need to apply the effects even if validation is ignored.
// Not valid so don't default to it anyway.
if (!IsValid(profile, session, defaultLoadout.Prototype, collection, out _))
continue;
loadouts.Add(defaultLoadout);
Apply(loadoutProto);
}
@@ -146,6 +166,7 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
/// <summary>
/// Resets the selected loadouts to default if no data is present.
/// </summary>
/// <param name="force">Clear existing data first</param>
public void SetDefault(IPrototypeManager protoManager, bool force = false)
{
if (force)

View File

@@ -12,9 +12,13 @@ namespace Content.Shared.Random.Helpers
return random.Pick(prototype.Values);
}
/// <summary>
/// Randomly selects an entry from <paramref name="prototype"/>, attempts to localize it, and returns the result.
/// </summary>
public static string Pick(this IRobustRandom random, LocalizedDatasetPrototype prototype)
{
return random.Pick(prototype.Values);
var index = random.Next(prototype.Values.Count);
return Loc.GetString(prototype.Values[index]);
}
public static string Pick(this IWeightedRandomPrototype prototype, System.Random random)

View File

@@ -1,3 +1,4 @@
using Content.Shared.Guidebook;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
@@ -41,6 +42,15 @@ public sealed partial class AntagPrototype : IPrototype
/// <summary>
/// Requirements that must be met to opt in to this antag role.
/// </summary>
[DataField("requirements")]
// TODO ROLE TIMERS
// Actually check if the requirements are met. Because apparently this is actually unused.
[DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
public HashSet<JobRequirement>? Requirements;
/// <summary>
/// Optional list of guides associated with this antag. If the guides are opened, the first entry in this list
/// will be used to select the currently selected guidebook.
/// </summary>
[DataField]
public List<ProtoId<GuideEntryPrototype>>? Guides;
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Access;
using Content.Shared.Guidebook;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes;
@@ -40,7 +41,7 @@ namespace Content.Shared.Roles
[ViewVariables(VVAccess.ReadOnly)]
public string? LocalizedDescription => Description is null ? null : Loc.GetString(Description);
[DataField("requirements")]
[DataField, Access(typeof(SharedRoleSystem), Other = AccessPermissions.None)]
public HashSet<JobRequirement>? Requirements;
[DataField("joinNotifyCrew")]
@@ -63,8 +64,8 @@ namespace Content.Shared.Roles
public bool CanBeAntag { get; private set; } = true;
/// <summary>
/// Whether this job is a head.
/// The job system will try to pick heads before other jobs on the same priority level.
/// The "weight" or importance of this job. If this number is large, the job system will assign this job
/// before assigning other jobs.
/// </summary>
[DataField("weight")]
public int Weight { get; private set; }
@@ -117,6 +118,13 @@ namespace Content.Shared.Roles
[DataField]
public bool Whitelisted;
/// <summary>
/// Optional list of guides associated with this role. If the guides are opened, the first entry in this list
/// will be used to select the currently selected guidebook.
/// </summary>
[DataField]
public List<ProtoId<GuideEntryPrototype>>? Guides;
}
/// <summary>

View File

@@ -0,0 +1,20 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Roles;
/// <summary>
/// Collection of job, antag, and ghost-role job requirements for per-server requirement overrides.
/// </summary>
[Prototype]
public sealed partial class JobRequirementOverridePrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
[DataField]
public Dictionary<ProtoId<JobPrototype>, HashSet<JobRequirement>> Jobs = new ();
[DataField]
public Dictionary<ProtoId<AntagPrototype>, HashSet<JobRequirement>> Antags = new ();
}

View File

@@ -73,16 +73,18 @@ namespace Content.Shared.Roles
{
public static bool TryRequirementsMet(
JobPrototype job,
Dictionary<string, TimeSpan> playTimes,
IReadOnlyDictionary<string, TimeSpan> playTimes,
[NotNullWhen(false)] out FormattedMessage? reason,
IEntityManager entManager,
IPrototypeManager prototypes)
{
var sys = entManager.System<SharedRoleSystem>();
var requirements = sys.GetJobRequirement(job);
reason = null;
if (job.Requirements == null)
if (requirements == null)
return true;
foreach (var requirement in job.Requirements)
foreach (var requirement in requirements)
{
if (!TryRequirementMet(requirement, playTimes, out reason, entManager, prototypes))
return false;
@@ -130,7 +132,7 @@ namespace Content.Shared.Roles
if (deptDiff <= 0)
return true;
reason = FormattedMessage.FromMarkup(Loc.GetString(
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-insufficient",
("time", Math.Ceiling(deptDiff)),
("department", Loc.GetString(deptRequirement.Department)),
@@ -141,7 +143,7 @@ namespace Content.Shared.Roles
{
if (deptDiff <= 0)
{
reason = FormattedMessage.FromMarkup(Loc.GetString(
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-department-too-high",
("time", -deptDiff),
("department", Loc.GetString(deptRequirement.Department)),
@@ -161,7 +163,7 @@ namespace Content.Shared.Roles
if (overallDiff <= 0 || overallTime >= overallRequirement.Time)
return true;
reason = FormattedMessage.FromMarkup(Loc.GetString(
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-overall-insufficient",
("time", Math.Ceiling(overallDiff))));
return false;
@@ -170,7 +172,7 @@ namespace Content.Shared.Roles
{
if (overallDiff <= 0 || overallTime >= overallRequirement.Time)
{
reason = FormattedMessage.FromMarkup(Loc.GetString("role-timer-overall-too-high", ("time", -overallDiff)));
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString("role-timer-overall-too-high", ("time", -overallDiff)));
return false;
}
@@ -197,7 +199,7 @@ namespace Content.Shared.Roles
if (roleDiff <= 0)
return true;
reason = FormattedMessage.FromMarkup(Loc.GetString(
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-insufficient",
("time", Math.Ceiling(roleDiff)),
("job", Loc.GetString(proto)),
@@ -208,7 +210,7 @@ namespace Content.Shared.Roles
{
if (roleDiff <= 0)
{
reason = FormattedMessage.FromMarkup(Loc.GetString(
reason = FormattedMessage.FromMarkupPermissive(Loc.GetString(
"role-timer-role-too-high",
("time", -roleDiff),
("job", Loc.GetString(proto)),

View File

@@ -118,6 +118,18 @@ public abstract class SharedJobSystem : EntitySystem
_prototypes.TryIndex(comp.Prototype, out prototype);
}
public bool MindTryGetJobId([NotNullWhen(true)] EntityUid? mindId, out ProtoId<JobPrototype>? job)
{
if (!TryComp(mindId, out JobComponent? comp))
{
job = null;
return false;
}
job = comp.Prototype;
return true;
}
/// <summary>
/// Tries to get the job name for this mind.
/// Returns unknown if not found.

View File

@@ -1,10 +1,12 @@
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Content.Shared.Ghost.Roles;
using Content.Shared.Mind;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -16,14 +18,30 @@ public abstract class SharedRoleSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
// TODO please lord make role entities
private readonly HashSet<Type> _antagTypes = new();
private JobRequirementOverridePrototype? _requirementOverride;
public override void Initialize()
{
// TODO make roles entities
SubscribeLocalEvent<JobComponent, MindGetAllRolesEvent>(OnJobGetAllRoles);
Subs.CVar(_cfg, CCVars.GameRoleTimerOverride, SetRequirementOverride, true);
}
private void SetRequirementOverride(string value)
{
if (string.IsNullOrEmpty(value))
{
_requirementOverride = null;
return;
}
if (!_prototypes.TryIndex(value, out _requirementOverride ))
Log.Error($"Unknown JobRequirementOverridePrototype: {value}");
}
private void OnJobGetAllRoles(EntityUid uid, JobComponent component, ref MindGetAllRolesEvent args)
@@ -253,4 +271,36 @@ public abstract class SharedRoleSystem : EntitySystem
if (Resolve(mindId, ref mind) && mind.Session != null)
_audio.PlayGlobal(sound, mind.Session);
}
public HashSet<JobRequirement>? GetJobRequirement(JobPrototype job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job.ID, out var req))
return req;
return job.Requirements;
}
public HashSet<JobRequirement>? GetJobRequirement(ProtoId<JobPrototype> job)
{
if (_requirementOverride != null && _requirementOverride.Jobs.TryGetValue(job, out var req))
return req;
return _prototypes.Index(job).Requirements;
}
public HashSet<JobRequirement>? GetAntagRequirement(ProtoId<AntagPrototype> antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag, out var req))
return req;
return _prototypes.Index(antag).Requirements;
}
public HashSet<JobRequirement>? GetAntagRequirement(AntagPrototype antag)
{
if (_requirementOverride != null && _requirementOverride.Antags.TryGetValue(antag.ID, out var req))
return req;
return antag.Requirements;
}
}

View File

@@ -81,26 +81,4 @@ public sealed partial class SingularityComponent : Component
);
#endregion Audio
#region Update Timing
/// <summary>
/// The amount of time that should elapse between automated updates to this singularity.
/// </summary>
[DataField("updatePeriod")]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan TargetUpdatePeriod = TimeSpan.FromSeconds(1.0);
/// <summary>
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan NextUpdateTime = default!;
/// <summary>
/// The last time this singularity was updated.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public TimeSpan LastUpdateTime = default!;
#endregion Update Timing
}

View File

@@ -302,7 +302,7 @@ public abstract class SharedStorageSystem : EntitySystem
return;
// prevent spamming bag open / honkerton honk sound
silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay));
silent |= TryComp<UseDelayComponent>(uid, out var useDelay) && UseDelay.IsDelayed((uid, useDelay), id: OpenUiUseDelayID);
if (!CanInteract(entity, (uid, storageComp), silent: silent))
return;
@@ -312,7 +312,7 @@ public abstract class SharedStorageSystem : EntitySystem
Audio.PlayPredicted(storageComp.StorageOpenSound, uid, entity);
if (useDelay != null)
UseDelay.TryResetDelay((uid, useDelay));
UseDelay.TryResetDelay((uid, useDelay), id: OpenUiUseDelayID);
}
_ui.OpenUi(uid, StorageComponent.StorageUiKey.Key, entity);

View File

@@ -1,4 +1,4 @@
using Content.Shared.DoAfter;
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -20,6 +20,12 @@ public sealed partial class HandTeleporterComponent : Component
[ViewVariables, DataField("secondPortal")]
public EntityUid? SecondPortal = null;
/// <summary>
/// Portals can't be placed on different grids?
/// </summary>
[DataField]
public bool AllowPortalsOnDifferentGrids;
[DataField("firstPortalPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string FirstPortalPrototype = "PortalRed";

View File

@@ -146,13 +146,14 @@ public abstract partial class SharedGunSystem
private void FinaliseMagazineTakeAmmo(EntityUid uid, MagazineAmmoProviderComponent component, int count, int capacity, EntityUid? user, AppearanceComponent? appearance)
{
// If no ammo then check for autoeject
if (component.AutoEject && count == 0)
var ejectMag = component.AutoEject && count == 0;
if (ejectMag)
{
EjectMagazine(uid, component);
Audio.PlayPredicted(component.SoundAutoEject, uid, user);
}
UpdateMagazineAppearance(uid, appearance, true, count, capacity);
UpdateMagazineAppearance(uid, appearance, !ejectMag, count, capacity);
}
private void UpdateMagazineAppearance(EntityUid uid, MagazineAmmoProviderComponent component, EntityUid magEnt)

View File

@@ -17,6 +17,7 @@ using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Content.Shared.Wieldable.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Shared.Wieldable;
@@ -32,6 +33,7 @@ public sealed class WieldableSystem : EntitySystem
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
public override void Initialize()
{
@@ -213,21 +215,27 @@ public sealed class WieldableSystem : EntitySystem
if (component.WieldSound != null)
_audioSystem.PlayPredicted(component.WieldSound, used, user);
var virtuals = new List<EntityUid>();
for (var i = 0; i < component.FreeHandsRequired; i++)
//This section handles spawning the virtual item(s) to occupy the required additional hand(s).
//Since the client can't currently predict entity spawning, only do this if this is running serverside.
//Remove this check if TrySpawnVirtualItem in SharedVirtualItemSystem is allowed to complete clientside.
if (_netManager.IsServer)
{
if (_virtualItemSystem.TrySpawnVirtualItemInHand(used, user, out var virtualItem, true))
var virtuals = new List<EntityUid>();
for (var i = 0; i < component.FreeHandsRequired; i++)
{
virtuals.Add(virtualItem.Value);
continue;
}
if (_virtualItemSystem.TrySpawnVirtualItemInHand(used, user, out var virtualItem, true))
{
virtuals.Add(virtualItem.Value);
continue;
}
foreach (var existingVirtual in virtuals)
{
QueueDel(existingVirtual);
}
foreach (var existingVirtual in virtuals)
{
QueueDel(existingVirtual);
}
return false;
return false;
}
}
if (TryComp(used, out UseDelayComponent? useDelay)