diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index 8b68487547f..c5452731fc9 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -16,6 +16,7 @@
+
diff --git a/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml
new file mode 100644
index 00000000000..d6bbb49c196
--- /dev/null
+++ b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml.cs b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml.cs
new file mode 100644
index 00000000000..0c3d1da604a
--- /dev/null
+++ b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferEntry.xaml.cs
@@ -0,0 +1,46 @@
+using Content.Shared.Roles;
+using Content.Shared.StatusIcon;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._Goobstation.Administration.UI.TimeTransferPanel;
+
+[GenerateTypedNameReferences]
+public sealed partial class TimeTransferEntry : BoxContainer
+{
+ public string PlaytimeTracker;
+ public string JobName;
+
+ public TimeTransferEntry(JobPrototype jobProto, SpriteSystem spriteSystem, IPrototypeManager prototypeManager)
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ PlaytimeTracker = jobProto.PlayTimeTracker;
+ JobLabel.Text = jobProto.LocalizedName;
+ JobName = jobProto.LocalizedName;
+
+ if (prototypeManager.TryIndex(jobProto.Icon, out var jobIcon))
+ JobIcon.Texture = spriteSystem.Frame0(jobIcon.Icon);
+ }
+
+ public void UpdateGroupVisibility(bool inGrouped)
+ {
+ TimeLabel.Visible = !inGrouped;
+ TimeEdit.Visible = !inGrouped;
+ GroupCheckbox.Visible = inGrouped;
+ }
+
+ public string GetJobTimeString()
+ {
+ return TimeEdit.Text != null ? TimeEdit.Text : "";
+ }
+
+ public bool InGroup()
+ {
+ return GroupCheckbox.Pressed;
+ }
+}
diff --git a/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml
new file mode 100644
index 00000000000..2c3d99e2f88
--- /dev/null
+++ b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml.cs b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml.cs
new file mode 100644
index 00000000000..b967ba399ff
--- /dev/null
+++ b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanel.xaml.cs
@@ -0,0 +1,210 @@
+using Content.Shared._Goobstation.Administration;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using System.Linq;
+
+namespace Content.Client._Goobstation.Administration.UI.TimeTransferPanel;
+
+[GenerateTypedNameReferences]
+public sealed partial class TimeTransferPanel : DefaultWindow
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+
+ public Action<(string playerId, List transferList, bool overwrite)>? OnTransferMessageSend;
+ private TimeSpan? SetButtonResetOn { get; set; }
+
+ public TimeTransferPanel()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _spriteSystem = _entityManager.System();
+
+ AddTimeButton.OnButtonUp += OnAddTimeButtonPressed;
+ SetTimeButton.OnButtonUp += OnSetTimeButtonPressed;
+ GroupCheckbox.OnPressed += OnGroupCheckboxPressed;
+
+ JobSearch.OnTextChanged += OnJobSearchTextChanged;
+
+ PopulateJobs();
+ UpdateGroup();
+ UpdateWarning(" ", Color.LightGreen);
+ }
+
+ public void PopulateJobs()
+ {
+ var jobs = _prototypeManager.EnumeratePrototypes()
+ .OrderBy(job => job.LocalizedName)
+ .ToList();
+
+ foreach(var job in jobs)
+ {
+ var jobEntry = new TimeTransferEntry(job, _spriteSystem, _prototypeManager);
+ JobContainer.AddChild(jobEntry);
+ }
+ }
+
+ public void UpdateFlag(bool hasFlag)
+ {
+ AddTimeButton.Visible = hasFlag;
+ SetTimeButton.Visible = hasFlag;
+
+ if (!hasFlag)
+ UpdateWarning(Loc.GetString("time-transfer-panel-warning-no-perms"), Color.DarkRed);
+ else
+ UpdateWarning(" ", Color.LightGreen);
+ }
+
+ public void TimeTransfer(bool overwrite = false)
+ {
+ var player = PlayerLine.Text;
+
+ if (string.IsNullOrEmpty(player))
+ {
+ UpdateWarning(Loc.GetString("time-transfer-panel-warning-no-player"), Color.DarkRed);
+ return;
+ }
+
+ var dataList = new List();
+
+ if (GroupCheckbox.Pressed)
+ {
+ if (string.IsNullOrEmpty(GroupTimeLine.Text))
+ {
+ UpdateWarning(Loc.GetString("time-transfer-panel-warning-group-no-time"), Color.DarkRed);
+ return;
+ }
+
+ var entryList = GetGroupEntries();
+ foreach (var entry in entryList)
+ {
+ var data = new TimeTransferData(entry.PlaytimeTracker, GroupTimeLine.Text);
+ dataList.Add(data);
+ }
+ }
+ else
+ {
+ foreach (var entry in JobContainer.Children)
+ {
+ if (entry is not TimeTransferEntry jobEntry)
+ continue;
+
+ var tracker = jobEntry.PlaytimeTracker;
+ var timeString = jobEntry.GetJobTimeString();
+
+ if (string.IsNullOrEmpty(timeString))
+ continue;
+
+ var data = new TimeTransferData(tracker, timeString);
+ dataList.Add(data);
+ }
+ }
+
+ OnTransferMessageSend?.Invoke((player, dataList, overwrite));
+ UpdateWarning(Loc.GetString("time-transfer-panel-warning-transfer-process"), Color.Gold);
+ }
+
+ public List GetGroupEntries()
+ {
+ var list = new List();
+
+ foreach (var entry in JobContainer.Children)
+ {
+ if (entry is not TimeTransferEntry jobEntry)
+ continue;
+
+ if (jobEntry.InGroup())
+ list.Add(jobEntry);
+ }
+
+ return list;
+ }
+
+ public void UpdateGroup()
+ {
+ GroupTimeLine.Visible = GroupCheckbox.Pressed;
+
+ foreach (var entry in JobContainer.Children)
+ {
+ if (entry is not TimeTransferEntry jobEntry)
+ continue;
+
+ jobEntry.UpdateGroupVisibility(GroupCheckbox.Pressed);
+ }
+ }
+
+ public void OnJobSearchTextChanged(LineEdit.LineEditEventArgs args)
+ {
+ UpdateSearch();
+ }
+
+ public void UpdateSearch()
+ {
+ foreach (var entry in JobContainer.Children)
+ {
+ if (entry is not TimeTransferEntry jobEntry)
+ continue;
+
+ jobEntry.Visible = ShouldShowJob(jobEntry);
+ }
+ }
+
+ public void UpdateWarning(string text, Color color)
+ {
+ WarningLabel.FontColorOverride = color;
+ WarningLabel.Text = text;
+ }
+
+ public bool ShouldShowJob(TimeTransferEntry jobEntry)
+ {
+ return jobEntry.JobName != null && JobSearch.Text != null && jobEntry.JobName.Contains(JobSearch.Text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public void OnGroupCheckboxPressed(BaseButton.ButtonEventArgs obj)
+ {
+ UpdateGroup();
+ }
+
+ public void OnAddTimeButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ TimeTransfer(false);
+ }
+
+ public void OnSetTimeButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ if (SetButtonResetOn is null)
+ {
+ SetButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
+ SetTimeButton.ModulateSelfOverride = Color.DarkRed;
+ SetTimeButton.Text = Loc.GetString("time-transfer-panel-set-time-confirm");
+ return;
+ }
+
+ TimeTransfer(true);
+ ResetSetButton();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (SetButtonResetOn != null && _gameTiming.CurTime > SetButtonResetOn)
+ ResetSetButton();
+ }
+
+ private void ResetSetButton()
+ {
+ SetButtonResetOn = null;
+ SetTimeButton.ModulateSelfOverride = null;
+ SetTimeButton.Text = Loc.GetString("time-transfer-panel-set-time");
+ }
+}
diff --git a/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanelEui.cs b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanelEui.cs
new file mode 100644
index 00000000000..9ba8c100cb6
--- /dev/null
+++ b/Content.Client/_Goobstation/Administration/UI/TimeTransferPanel/TimeTransferPanelEui.cs
@@ -0,0 +1,44 @@
+using Content.Client.Eui;
+using Content.Shared._Goobstation.Administration;
+using Content.Shared.Eui;
+
+namespace Content.Client._Goobstation.Administration.UI.TimeTransferPanel;
+
+public sealed class TimeTransferPanelEui : BaseEui
+{
+ public TimeTransferPanel TimeTransferPanel { get; }
+
+ public TimeTransferPanelEui()
+ {
+ TimeTransferPanel = new TimeTransferPanel();
+ TimeTransferPanel.OnTransferMessageSend += args => SendMessage(new TimeTransferEuiMessage(args.playerId, args.transferList, args.overwrite));
+ }
+
+ public override void Opened()
+ {
+ TimeTransferPanel.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ TimeTransferPanel.Close();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not TimeTransferPanelEuiState cast)
+ return;
+
+ TimeTransferPanel.UpdateFlag(cast.HasFlag);
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not TimeTransferWarningEuiMessage warning)
+ return;
+
+ TimeTransferPanel.UpdateWarning(warning.Message, warning.WarningColor);
+ }
+}
diff --git a/Content.Server/Administration/Commands/PlayTimeCommands.cs b/Content.Server/Administration/Commands/PlayTimeCommands.cs
index 97d3f12e38d..9ad1f283eb0 100644
--- a/Content.Server/Administration/Commands/PlayTimeCommands.cs
+++ b/Content.Server/Administration/Commands/PlayTimeCommands.cs
@@ -1,11 +1,95 @@
-using Content.Server.Players.PlayTimeTracking;
+using Content.Server.Players.PlayTimeTracking;
using Content.Shared.Administration;
using Content.Shared.Players.PlayTimeTracking;
using Robust.Server.Player;
using Robust.Shared.Console;
+using System.Text.RegularExpressions;
namespace Content.Server.Administration.Commands;
+public sealed class PlayTimeCommandUtilities
+{
+ private readonly static Dictionary Units = new() {
+ { "y", 525960 },
+ { "mo", 43800 },
+ { "w", 10080 },
+ { "d", 1440 },
+ { "h", 60 },
+ { "m", 1 },
+ };
+
+ public struct TimeUnit
+ {
+ public int TimeValue { get; }
+ public string Unit { get; }
+
+ public TimeUnit(int timeValue)
+ {
+ TimeValue = timeValue;
+ Unit = "m";
+ }
+
+ public TimeUnit(int timeValue, string unit)
+ {
+ TimeValue = timeValue;
+ Unit = unit;
+ }
+ public int ToMinutes()
+ {
+ var unitExists = Units.TryGetValue(Unit, out int minutes);
+
+ if (!unitExists)
+ return TimeValue;
+
+ return TimeValue * minutes;
+ }
+ }
+
+ public static List ConvertToTimeUnits(string timeString)
+ {
+ // Searching for something similar to 365d24h, etc.
+ List result = new();
+
+ // We want to support plain numbers as a translation to just minutes, just in case people don't know things like 30d or 1d are an option.
+ if (int.TryParse(timeString, out int timeValue))
+ {
+ result.Add(new TimeUnit(timeValue, "m"));
+ return result;
+ }
+
+ MatchCollection timeRegex = Regex.Matches(timeString, "(\\d+)([A-Za-z]+)");
+
+ foreach (Match match in timeRegex)
+ {
+ bool isTimeAmountNumber = int.TryParse(match.Groups[1].Value, out int amountOfTime);
+ string timeUnit = match.Groups[2].Value;
+
+ if (!isTimeAmountNumber)
+ continue;
+
+ if (!Units.ContainsKey(timeUnit))
+ continue;
+
+ result.Add(new TimeUnit(amountOfTime, timeUnit));
+ }
+
+ return result;
+ }
+
+ public static int CountMinutes(string timeString)
+ {
+ List timeUnits = ConvertToTimeUnits(timeString);
+ int total = 0;
+
+ foreach (var timeUnit in timeUnits)
+ {
+ total += timeUnit.ToMinutes();
+ }
+
+ return total;
+ }
+}
+
[AdminCommand(AdminFlags.Admin)]
public sealed class PlayTimeAddOverallCommand : IConsoleCommand
{
@@ -24,11 +108,7 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args)
return;
}
- if (!int.TryParse(args[1], out var minutes))
- {
- shell.WriteError(Loc.GetString("parse-minutes-fail", ("minutes", args[1])));
- return;
- }
+ var minutes = PlayTimeCommandUtilities.CountMinutes(args[1]);
if (!_playerManager.TryGetSessionByUsername(args[0], out var player))
{
@@ -85,15 +165,10 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args)
var role = args[1];
- var m = args[2];
- if (!int.TryParse(m, out var minutes))
- {
- shell.WriteError(Loc.GetString("parse-minutes-fail", ("minutes", minutes)));
- return;
- }
+ var m = PlayTimeCommandUtilities.CountMinutes(args[2]);
- _playTimeTracking.AddTimeToTracker(player, role, TimeSpan.FromMinutes(minutes));
- var time = _playTimeTracking.GetOverallPlaytime(player);
+ _playTimeTracking.AddTimeToTracker(player, role, TimeSpan.FromMinutes(m));
+ var time = _playTimeTracking.GetPlayTimeForTracker(player, role);
shell.WriteLine(Loc.GetString("cmd-playtime_addrole-succeed",
("username", userName),
("role", role),
diff --git a/Content.Server/Language/Commands/AdminLanguageCommand.cs b/Content.Server/Language/Commands/AdminLanguageCommand.cs
index 2e7a0b193a1..12899747f70 100644
--- a/Content.Server/Language/Commands/AdminLanguageCommand.cs
+++ b/Content.Server/Language/Commands/AdminLanguageCommand.cs
@@ -20,13 +20,11 @@ public sealed class AdminLanguageCommand : ToolshedCommand
public EntityUid AddLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref,
+ [CommandArgument] ProtoId language,
[CommandArgument] bool canSpeak = true,
[CommandArgument] bool canUnderstand = true
)
{
- var language = @ref.Evaluate(ctx)!;
-
if (language == SharedLanguageSystem.UniversalPrototype)
{
EnsureComp(input);
@@ -45,12 +43,11 @@ public EntityUid AddLanguage(
public EntityUid RemoveLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref,
+ [CommandArgument] ProtoId language,
[CommandArgument] bool removeSpeak = true,
[CommandArgument] bool removeUnderstand = true
)
{
- var language = @ref.Evaluate(ctx)!;
if (language == SharedLanguageSystem.UniversalPrototype && HasComp(input))
{
RemComp(input);
diff --git a/Content.Server/Language/Commands/AdminTranslatorCommand.cs b/Content.Server/Language/Commands/AdminTranslatorCommand.cs
index fc8ee02e386..d7cdef0a0c2 100644
--- a/Content.Server/Language/Commands/AdminTranslatorCommand.cs
+++ b/Content.Server/Language/Commands/AdminTranslatorCommand.cs
@@ -26,12 +26,11 @@ public sealed class AdminTranslatorCommand : ToolshedCommand
public EntityUid AddLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref,
+ [CommandArgument] ProtoId language,
[CommandArgument] bool addSpeak = true,
[CommandArgument] bool addUnderstand = true
)
{
- var language = @ref.Evaluate(ctx)!;
// noob trap - needs a universallanguagespeakercomponent
if (language == SharedLanguageSystem.UniversalPrototype)
throw new ArgumentException(Loc.GetString("command-language-error-this-will-not-work"));
@@ -53,12 +52,11 @@ public EntityUid AddLanguage(
public EntityUid RemoveLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref,
+ [CommandArgument] ProtoId language,
[CommandArgument] bool removeSpeak = true,
[CommandArgument] bool removeUnderstand = true
)
{
- var language = @ref.Evaluate(ctx)!;
if (!TryGetTranslatorComp(input, out var translator))
throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input)));
@@ -76,9 +74,8 @@ public EntityUid RemoveLanguage(
public EntityUid AddRequiredLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref)
+ [CommandArgument] ProtoId language)
{
- var language = @ref.Evaluate(ctx)!;
if (!TryGetTranslatorComp(input, out var translator))
throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input)));
@@ -95,9 +92,8 @@ public EntityUid AddRequiredLanguage(
public EntityUid RemoveRequiredLanguage(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid input,
- [CommandArgument] ValueRef> @ref)
+ [CommandArgument] ProtoId language)
{
- var language = @ref.Evaluate(ctx)!;
if (!TryGetTranslatorComp(input, out var translator))
throw new ArgumentException(Loc.GetString("command-language-error-not-a-translator", ("entity", input)));
diff --git a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs
index 295d11f8986..957fa77cdd6 100644
--- a/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs
+++ b/Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs
@@ -380,6 +380,19 @@ public bool TryGetTrackerTimes(ICommonSession id, [NotNullWhen(true)] out Dictio
return true;
}
+ public bool TryGetTrackerTime(ICommonSession id, string tracker, [NotNullWhen(true)] out TimeSpan? time)
+ {
+ time = null;
+ if (!TryGetTrackerTimes(id, out var times))
+ return false;
+
+ if (!times.TryGetValue(tracker, out var t))
+ return false;
+
+ time = t;
+ return true;
+ }
+
public Dictionary GetTrackerTimes(ICommonSession id)
{
if (!_playTimeData.TryGetValue(id, out var data) || !data.Initialized)
diff --git a/Content.Server/Speech/SpeechNoiseSystem.cs b/Content.Server/Speech/SpeechNoiseSystem.cs
index 5530f3fe57a..071b1569071 100644
--- a/Content.Server/Speech/SpeechNoiseSystem.cs
+++ b/Content.Server/Speech/SpeechNoiseSystem.cs
@@ -60,7 +60,7 @@ public override void Initialize()
private void OnEntitySpoke(EntityUid uid, SpeechComponent component, EntitySpokeEvent args)
{
- if (component.SpeechSounds == null)
+ if (component.SpeechSounds == null || !args.Language.SpeechOverride.RequireSpeech)
return;
var currentTime = _gameTiming.CurTime;
diff --git a/Content.Server/Station/Commands/JobsCommand.cs b/Content.Server/Station/Commands/JobsCommand.cs
index 1e39140e840..3f26090828b 100644
--- a/Content.Server/Station/Commands/JobsCommand.cs
+++ b/Content.Server/Station/Commands/JobsCommand.cs
@@ -3,6 +3,7 @@
using Content.Server.Station.Systems;
using Content.Shared.Administration;
using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers;
@@ -26,28 +27,24 @@ public IEnumerable Jobs([PipedArgument] EntityUid station)
}
[CommandImplementation("jobs")]
- public IEnumerable Jobs([PipedArgument] IEnumerable stations)
- => stations.SelectMany(Jobs);
+ public IEnumerable Jobs([PipedArgument] IEnumerable stations) => stations.SelectMany(Jobs);
[CommandImplementation("job")]
- public JobSlotRef Job([PipedArgument] EntityUid station, Prototype job)
+ public JobSlotRef Job([PipedArgument] EntityUid station, ProtoId job)
{
_jobs ??= GetSys();
- return new JobSlotRef(job.Value.ID, station, _jobs, EntityManager);
+ return new JobSlotRef(job, station, _jobs, EntityManager);
}
[CommandImplementation("job")]
- public IEnumerable Job([PipedArgument] IEnumerable stations, Prototype job)
- => stations.Select(x => Job(x, job));
+ public IEnumerable Job([PipedArgument] IEnumerable stations, ProtoId job) => stations.Select(x => Job(x, job));
[CommandImplementation("isinfinite")]
- public bool IsInfinite([PipedArgument] JobSlotRef job, [CommandInverted] bool inverted)
- => job.Infinite() ^ inverted;
+ public bool IsInfinite([PipedArgument] JobSlotRef job, [CommandInverted] bool inverted) => job.Infinite() ^ inverted;
[CommandImplementation("isinfinite")]
- public IEnumerable IsInfinite([PipedArgument] IEnumerable jobs, [CommandInverted] bool inverted)
- => jobs.Select(x => IsInfinite(x, inverted));
+ public IEnumerable IsInfinite([PipedArgument] IEnumerable jobs, [CommandInverted] bool inverted) => jobs.Select(x => IsInfinite(x, inverted));
[CommandImplementation("adjust")]
public JobSlotRef Adjust([PipedArgument] JobSlotRef @ref, int by)
@@ -58,8 +55,7 @@ public JobSlotRef Adjust([PipedArgument] JobSlotRef @ref, int by)
}
[CommandImplementation("adjust")]
- public IEnumerable Adjust([PipedArgument] IEnumerable @ref, int by)
- => @ref.Select(x => Adjust(x, by));
+ public IEnumerable Adjust([PipedArgument] IEnumerable @ref, int by) => @ref.Select(x => Adjust(x, by));
[CommandImplementation("set")]
@@ -71,20 +67,18 @@ public JobSlotRef Set([PipedArgument] JobSlotRef @ref, int by)
}
[CommandImplementation("set")]
- public IEnumerable Set([PipedArgument] IEnumerable @ref, int by)
- => @ref.Select(x => Set(x, by));
+ public IEnumerable Set([PipedArgument] IEnumerable @ref, int by) => @ref.Select(x => Set(x, by));
[CommandImplementation("amount")]
public uint Amount([PipedArgument] JobSlotRef @ref)
{
_jobs ??= GetSys();
_jobs.TryGetJobSlot(@ref.Station, @ref.Job, out var slots);
- return slots.HasValue ? slots.Value : 0;
+ return slots ?? 0;
}
[CommandImplementation("amount")]
- public IEnumerable Amount([PipedArgument] IEnumerable @ref)
- => @ref.Select(Amount);
+ public IEnumerable Amount([PipedArgument] IEnumerable @ref) => @ref.Select(Amount);
}
// Used for Toolshed queries.
diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
index 95c2b0085bd..7dcb5ac30df 100644
--- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
+++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
@@ -92,7 +92,7 @@ public sealed class StationEventCommand : ToolshedCommand
/// to even exist) so I think it's fine.
///
[CommandImplementation("simulate")]
- public IEnumerable<(string, float)> Simulate([CommandArgument] int rounds, [CommandArgument] int playerCount, [CommandArgument] float roundEndMean, [CommandArgument] float roundEndStdDev)
+ public IEnumerable<(string, float)> Simulate(EntityPrototype eventScheduler, int rounds, int playerCount, float roundEndMean, float roundEndStdDev)
{
_stationEvent ??= GetSys();
_basicScheduler ??= GetSys();
diff --git a/Content.Server/_Goobstation/Administration/Commands/TimeTransferPanelCommand.cs b/Content.Server/_Goobstation/Administration/Commands/TimeTransferPanelCommand.cs
new file mode 100644
index 00000000000..422e7c711c8
--- /dev/null
+++ b/Content.Server/_Goobstation/Administration/Commands/TimeTransferPanelCommand.cs
@@ -0,0 +1,25 @@
+using Content.Server.Administration;
+using Content.Server.EUI;
+using Content.Shared.Administration;
+using Robust.Shared.Console;
+
+namespace Content.Server._Goobstation.Administration.Commands;
+
+[AdminCommand(AdminFlags.Admin)]
+public sealed class TimeTransferPanelCommand : LocalizedCommands
+{
+ [Dependency] private readonly EuiManager _euis = default!;
+
+ public override string Command => "timetransferpanel";
+
+ public override async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (shell.Player is not { } player)
+ {
+ shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
+ return;
+ }
+
+ _euis.OpenEui(new TimeTransferPanelEui(), player);
+ }
+}
diff --git a/Content.Server/_Goobstation/Administration/TimeTransferPanelEui.cs b/Content.Server/_Goobstation/Administration/TimeTransferPanelEui.cs
new file mode 100644
index 00000000000..3c11a31bd06
--- /dev/null
+++ b/Content.Server/_Goobstation/Administration/TimeTransferPanelEui.cs
@@ -0,0 +1,141 @@
+using Content.Server.Administration;
+using Content.Server.Administration.Commands;
+using Content.Server.Administration.Managers;
+using Content.Server.Database;
+using Content.Server.EUI;
+using Content.Server.Players.PlayTimeTracking;
+using Content.Shared._Goobstation.Administration;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using Robust.Server.Player;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server._Goobstation.Administration;
+
+public sealed class TimeTransferPanelEui : BaseEui
+{
+ [Dependency] private readonly IAdminManager _adminMan = default!;
+ [Dependency] private readonly ILogManager _log = default!;
+ [Dependency] private readonly IPlayerLocator _playerLocator = default!;
+ [Dependency] private readonly IPlayerManager _playerMan = default!;
+ [Dependency] private readonly IPrototypeManager _protoMan = default!;
+ [Dependency] private readonly IServerDbManager _databaseMan = default!;
+ [Dependency] private readonly PlayTimeTrackingManager _playTimeMan = default!;
+
+ private readonly ISawmill _sawmill;
+
+ public TimeTransferPanelEui()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _sawmill = _log.GetSawmill("admin.time_eui");
+ }
+
+ public override TimeTransferPanelEuiState GetNewState()
+ {
+ var hasFlag = _adminMan.HasAdminFlag(Player, AdminFlags.Admin);
+
+ return new TimeTransferPanelEuiState(hasFlag);
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ if (msg is not TimeTransferEuiMessage message)
+ return;
+
+ TransferTime(message.PlayerId, message.TimeData, message.Overwrite);
+ }
+
+ public async void TransferTime(string playerId, List timeData, bool overwrite)
+ {
+ if (!_adminMan.HasAdminFlag(Player, AdminFlags.Admin))
+ {
+ _sawmill.Warning($"{Player.Name} ({Player.UserId} tried to add roles time without moderator flag)");
+ return;
+ }
+
+ var playerData = await _playerLocator.LookupIdByNameAsync(playerId);
+ if (playerData == null)
+ {
+ _sawmill.Warning($"{Player.Name} ({Player.UserId} tried to add roles time to not existing player {playerId})");
+ SendMessage(new TimeTransferWarningEuiMessage(Loc.GetString("time-transfer-panel-no-player-database-message"), Color.Red));
+ return;
+ }
+
+ if (overwrite)
+ SetTime(playerData.UserId, timeData);
+ else
+ AddTime(playerData.UserId, timeData);
+ }
+
+ public async void SetTime(NetUserId userId, List timeData)
+ {
+ var updateList = new List();
+
+ foreach (var data in timeData)
+ {
+ var time = TimeSpan.FromMinutes(PlayTimeCommandUtilities.CountMinutes(data.TimeString));
+ updateList.Add(new PlayTimeUpdate(userId, data.PlaytimeTracker, time));
+ }
+
+ await _databaseMan.UpdatePlayTimes(updateList);
+
+ _sawmill.Info($"{Player.Name} ({Player.UserId} saved {updateList.Count} trackers for {userId})");
+
+ SendMessage(new TimeTransferWarningEuiMessage(Loc.GetString("time-transfer-panel-warning-set-success"), Color.LightGreen));
+ }
+
+ public async void AddTime(NetUserId userId, List timeData)
+ {
+ var playTimeList = await _databaseMan.GetPlayTimes(userId);
+
+ Dictionary playTimeDict = new();
+
+ foreach (var playTime in playTimeList)
+ {
+ playTimeDict.Add(playTime.Tracker, playTime.TimeSpent);
+ }
+
+ var updateList = new List();
+
+ foreach (var data in timeData)
+ {
+ var time = TimeSpan.FromMinutes(PlayTimeCommandUtilities.CountMinutes(data.TimeString));
+ if (playTimeDict.TryGetValue(data.PlaytimeTracker, out var addTime))
+ time += addTime;
+
+ updateList.Add(new PlayTimeUpdate(userId, data.PlaytimeTracker, time));
+ }
+
+ await _databaseMan.UpdatePlayTimes(updateList);
+
+ _sawmill.Info($"{Player.Name} ({Player.UserId} saved {updateList.Count} trackers for {userId})");
+
+ SendMessage(new TimeTransferWarningEuiMessage(Loc.GetString("time-transfer-panel-warning-add-success"), Color.LightGreen));
+ }
+
+ public override async void Opened()
+ {
+ base.Opened();
+ _adminMan.OnPermsChanged += OnPermsChanged;
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+ _adminMan.OnPermsChanged -= OnPermsChanged;
+ }
+
+ private void OnPermsChanged(AdminPermsChangedEventArgs args)
+ {
+ if (args.Player != Player)
+ {
+ return;
+ }
+
+ StateDirty();
+ }
+}
diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
index fc44ed5e3b9..8c1ecf833cd 100644
--- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
+++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
@@ -27,7 +27,7 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance,
public Color EyeColor { get; private set; }
[DataField]
- public Color SkinColor { get; private set; }
+ public Color SkinColor { get; set; }
[DataField]
public List Markings { get; private set; } = new();
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 499da11babc..de009eb20cd 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -191,9 +191,19 @@ public HumanoidCharacterProfile()
/// Humanoid character profile with default settings.
public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
{
+ var prototypeManager = IoCManager.Resolve();
+ var skinColor = SkinColor.ValidHumanSkinTone;
+
+ if (prototypeManager.TryIndex(species, out var speciesPrototype))
+ skinColor = speciesPrototype.DefaultSkinTone;
+
return new()
{
Species = species,
+ Appearance = new()
+ {
+ SkinColor = skinColor,
+ },
};
}
diff --git a/Content.Shared/_Goobstation/Administration/TimeTransferPanelEui.cs b/Content.Shared/_Goobstation/Administration/TimeTransferPanelEui.cs
new file mode 100644
index 00000000000..057c31a4950
--- /dev/null
+++ b/Content.Shared/_Goobstation/Administration/TimeTransferPanelEui.cs
@@ -0,0 +1,61 @@
+using Content.Shared.Eui;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared._Goobstation.Administration;
+
+[Serializable, NetSerializable]
+public sealed class TimeTransferPanelEuiState : EuiStateBase
+{
+ public bool HasFlag { get; }
+
+ public TimeTransferPanelEuiState(bool hasFlag)
+ {
+ HasFlag = hasFlag;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class TimeTransferEuiMessage : EuiMessageBase
+{
+ public string PlayerId { get; }
+ public List TimeData { get; }
+
+ public bool Overwrite { get; }
+
+ public TimeTransferEuiMessage(string playerId, List timeData, bool overwrite)
+ {
+ PlayerId = playerId;
+ TimeData = timeData;
+ Overwrite = overwrite;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class TimeTransferWarningEuiMessage : EuiMessageBase
+{
+ public string Message { get; }
+ public Color WarningColor { get; }
+
+ public TimeTransferWarningEuiMessage(string message, Color color)
+ {
+ Message = message;
+ WarningColor = color;
+ }
+}
+
+[DataDefinition]
+[Serializable, NetSerializable]
+public partial record struct TimeTransferData
+{
+ [DataField]
+ public string TimeString { get; init; }
+
+ [DataField]
+ public string PlaytimeTracker { get; init; }
+
+ public TimeTransferData(string tracker, string timeString)
+ {
+ PlaytimeTracker = tracker;
+ TimeString = timeString;
+ }
+}
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index da6e4e20581..3f882d185b4 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -9367,3 +9367,28 @@ Entries:
id: 6644
time: '2025-01-07T04:08:32.0000000+00:00'
url: https://github.com/Simple-Station/Einstein-Engines/pull/1446
+- author: Spatison
+ changes:
+ - type: Fix
+ message: Humanoid color in the spawn panel is now correct.
+ - type: Fix
+ message: IPCs can no longer spawn invisible.
+ id: 6645
+ time: '2025-01-07T17:02:45.0000000+00:00'
+ url: https://github.com/Simple-Station/Einstein-Engines/pull/1454
+- author: RadsammyT
+ changes:
+ - type: Tweak
+ message: >-
+ Languages that don't require speech (I.E. Sign Language) will no longer
+ play speech sounds.
+ id: 6646
+ time: '2025-01-07T17:15:10.0000000+00:00'
+ url: https://github.com/Simple-Station/Einstein-Engines/pull/1448
+- author: CerberusWolfie
+ changes:
+ - type: Add
+ message: Add Time Transfer Panel
+ id: 6647
+ time: '2025-01-07T17:23:36.0000000+00:00'
+ url: https://github.com/Simple-Station/Einstein-Engines/pull/1429
diff --git a/Resources/Locale/en-US/_Goobstation/administration/time_transfer.ftl b/Resources/Locale/en-US/_Goobstation/administration/time_transfer.ftl
new file mode 100644
index 00000000000..eebe76e95a4
--- /dev/null
+++ b/Resources/Locale/en-US/_Goobstation/administration/time_transfer.ftl
@@ -0,0 +1,23 @@
+time-transfer-panel-title = Time Transfer
+time-transfer-panel-player-label = Player
+time-transfer-panel-time = Time
+time-transfer-panel-add-time = Add time
+time-transfer-panel-set-time = Set time
+time-transfer-panel-set-time-confirm = Confirm
+time-transfer-panel-warning-no-player = Player not selected
+time-transfer-panel-warning-no-job = Role not selected
+time-transfer-panel-warning-group-no-time = Group time is empty
+time-transfer-panel-warning-add-success = Time successfully added
+time-transfer-panel-warning-set-success = Time successfully overwritten
+time-transfer-panel-warning-transfer-process = Time transfer in progress...
+time-transfer-panel-checkbox-group = Group
+time-transfer-overall-checkbox = Overall
+time-transfer-panel-search-placeholder = Search jobs
+
+
+cmd-timetransferpanel-desc = Opens time transfer menu
+admin-player-actions-window-time-transfer = Time Transfer
+time-transfer-panel-no-player-database-message = Player not found in database
+
+# Please change it if you really-really need to do it
+time-transfer-panel-author = Made by BombasterDS for Goobstation
diff --git a/Resources/Prototypes/DeltaV/Species/lamia.yml b/Resources/Prototypes/DeltaV/Species/lamia.yml
index 2e58a1681c2..0c2f48666a4 100644
--- a/Resources/Prototypes/DeltaV/Species/lamia.yml
+++ b/Resources/Prototypes/DeltaV/Species/lamia.yml
@@ -5,6 +5,7 @@
prototype: MobLamia
dollPrototype: MobLamiaDummy
sprites: MobLamiaSprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobLamiaMarkingLimits
skinColoration: HumanToned
maleFirstNames: names_cyno_male
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
index 9cf6cff45e4..5247ed351b6 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
@@ -9,7 +9,6 @@
scale: 0.8, 0.8
- type: HumanoidAppearance
species: Felinid
- initial: Felinid
- type: Fixtures
fixtures: # TODO: This needs a second fixture just for mob collisions.
fix1:
diff --git a/Resources/Prototypes/Nyanotrasen/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Species/Oni.yml
index 79d7e2b86f8..f4312009154 100644
--- a/Resources/Prototypes/Nyanotrasen/Species/Oni.yml
+++ b/Resources/Prototypes/Nyanotrasen/Species/Oni.yml
@@ -6,6 +6,7 @@
dollPrototype: MobOniDummy
markingLimits: MobOniMarkingLimits
sprites: MobHumanSprites
+ defaultSkinTone: "#ab5150"
skinColoration: Hues
maleFirstNames: names_oni_male
femaleFirstNames: names_oni_female
diff --git a/Resources/Prototypes/Nyanotrasen/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Species/felinid.yml
index ad92ea8046f..2bb8d02c471 100644
--- a/Resources/Prototypes/Nyanotrasen/Species/felinid.yml
+++ b/Resources/Prototypes/Nyanotrasen/Species/felinid.yml
@@ -4,6 +4,7 @@
roundStart: true
prototype: MobFelinid
sprites: MobHumanSprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobFelinidMarkingLimits
dollPrototype: MobFelinidDummy
skinColoration: HumanToned
diff --git a/Resources/Prototypes/Species/arachne.yml b/Resources/Prototypes/Species/arachne.yml
index fff75454fcb..1a80cb43590 100644
--- a/Resources/Prototypes/Species/arachne.yml
+++ b/Resources/Prototypes/Species/arachne.yml
@@ -4,6 +4,7 @@
roundStart: true # I'll kill these issues somehow.
prototype: MobArachne
sprites: MobArachneSprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobArachneMarkingLimits
dollPrototype: MobArachneDummy
skinColoration: HumanToned
diff --git a/Resources/Prototypes/Species/dwarf.yml b/Resources/Prototypes/Species/dwarf.yml
index d32f1e6476a..f501884e99b 100644
--- a/Resources/Prototypes/Species/dwarf.yml
+++ b/Resources/Prototypes/Species/dwarf.yml
@@ -4,6 +4,7 @@
roundStart: false # DeltaV - Disable dwarf
prototype: MobDwarf
sprites: MobHumanSprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobHumanMarkingLimits
dollPrototype: MobDwarfDummy
skinColoration: HumanToned
diff --git a/Resources/Prototypes/Species/harpy.yml b/Resources/Prototypes/Species/harpy.yml
index 1b61ebdd76f..da23fb2a01d 100644
--- a/Resources/Prototypes/Species/harpy.yml
+++ b/Resources/Prototypes/Species/harpy.yml
@@ -4,6 +4,7 @@
roundStart: true
prototype: MobHarpy
sprites: MobHarpySprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobHarpyMarkingLimits
dollPrototype: MobHarpyDummy
skinColoration: HumanToned
diff --git a/Resources/Prototypes/Species/human.yml b/Resources/Prototypes/Species/human.yml
index cc304a6eeb1..9c5083de23b 100644
--- a/Resources/Prototypes/Species/human.yml
+++ b/Resources/Prototypes/Species/human.yml
@@ -4,6 +4,7 @@
roundStart: true
prototype: MobHuman
sprites: MobHumanSprites
+ defaultSkinTone: "#c0967f"
markingLimits: MobHumanMarkingLimits
dollPrototype: MobHumanDummy
skinColoration: HumanToned
diff --git a/Resources/Prototypes/Species/ipc.yml b/Resources/Prototypes/Species/ipc.yml
index 6c3a8266940..c4361d65f36 100644
--- a/Resources/Prototypes/Species/ipc.yml
+++ b/Resources/Prototypes/Species/ipc.yml
@@ -4,6 +4,7 @@
roundStart: true
prototype: MobIPC
sprites: MobIPCSprites
+ defaultSkinTone: "#aaa9ad"
markingLimits: MobIPCMarkingLimits
dollPrototype: MobIPCDummy
skinColoration: Hues