From 297853929b7b3859760dcdda95e21888672ce8e1 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:37:16 +0200 Subject: [PATCH 01/49] Game server api (#24015) * Revert "Revert "Game server api (#23129)"" * Review pt.1 * Reviews pt.2 * Reviews pt. 3 * Reviews pt. 4 --- Content.Server/Administration/ServerAPI.cs | 1033 +++++++++++++++++ .../Administration/Systems/AdminSystem.cs | 24 +- Content.Server/Entry/EntryPoint.cs | 2 + Content.Server/IoC/ServerContentIoC.cs | 1 + Content.Shared/CCVar/CCVars.cs | 7 + 5 files changed, 1055 insertions(+), 12 deletions(-) create mode 100644 Content.Server/Administration/ServerAPI.cs diff --git a/Content.Server/Administration/ServerAPI.cs b/Content.Server/Administration/ServerAPI.cs new file mode 100644 index 0000000000..d7591fb80c --- /dev/null +++ b/Content.Server/Administration/ServerAPI.cs @@ -0,0 +1,1033 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Content.Server.Administration.Systems; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Presets; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Maps; +using Content.Server.RoundEnd; +using Content.Shared.Administration.Events; +using Content.Shared.Administration.Managers; +using Content.Shared.CCVar; +using Content.Shared.Prototypes; +using Robust.Server.ServerStatus; +using Robust.Shared.Asynchronous; +using Robust.Shared.Configuration; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + +namespace Content.Server.Administration; + +public sealed class ServerApi : IPostInjectInit +{ + [Dependency] private readonly IStatusHost _statusHost = default!; + [Dependency] private readonly IConfigurationManager _config = default!; + [Dependency] private readonly ISharedPlayerManager _playerManager = default!; // Players + [Dependency] private readonly ISharedAdminManager _adminManager = default!; // Admins + [Dependency] private readonly IGameMapManager _gameMapManager = default!; // Map name + [Dependency] private readonly IServerNetManager _netManager = default!; // Kick + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Game rules + [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly ITaskManager _taskManager = default!; // game explodes when calling stuff from the non-game thread + [Dependency] private readonly EntityManager _entityManager = default!; + + [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + + private string _token = string.Empty; + private ISawmill _sawmill = default!; + + public static Dictionary PanicPunkerCvarNames = new() + { + { "Enabled", "game.panic_bunker.enabled" }, + { "DisableWithAdmins", "game.panic_bunker.disable_with_admins" }, + { "EnableWithoutAdmins", "game.panic_bunker.enable_without_admins" }, + { "CountDeadminnedAdmins", "game.panic_bunker.count_deadminned_admins" }, + { "ShowReason", "game.panic_bunker.show_reason" }, + { "MinAccountAgeHours", "game.panic_bunker.min_account_age" }, + { "MinOverallHours", "game.panic_bunker.min_overall_hours" }, + { "CustomReason", "game.panic_bunker.custom_reason" } + }; + + void IPostInjectInit.PostInject() + { + _sawmill = Logger.GetSawmill("serverApi"); + + // Get + _statusHost.AddHandler(InfoHandler); + _statusHost.AddHandler(GetGameRules); + _statusHost.AddHandler(GetForcePresets); + + // Post + _statusHost.AddHandler(ActionRoundStatus); + _statusHost.AddHandler(ActionKick); + _statusHost.AddHandler(ActionAddGameRule); + _statusHost.AddHandler(ActionEndGameRule); + _statusHost.AddHandler(ActionForcePreset); + _statusHost.AddHandler(ActionForceMotd); + _statusHost.AddHandler(ActionPanicPunker); + } + + public void Initialize() + { + _config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true); + } + + public void Shutdown() + { + _config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken); + } + + private void UpdateToken(string token) + { + _token = token; + } + + +#region Actions + + /// + /// Changes the panic bunker settings. + /// + private async Task ActionPanicPunker(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Patch || context.Url.AbsolutePath != "/admin/actions/panic_bunker") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var body = await ReadJson>(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + foreach (var panicPunkerActions in body!.Select(x => new { Action = x.Key, Value = x.Value.ToString() })) + { + if (panicPunkerActions.Action == null || panicPunkerActions.Value == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Action and value are required to perform this action.", + Exception = new ExceptionData() + { + Message = "Action and value are required to perform this action.", + ErrorType = ErrorTypes.ActionNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + + if (!PanicPunkerCvarNames.TryGetValue(panicPunkerActions.Action, out var cvarName)) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.", + Exception = new ExceptionData() + { + Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.", + ErrorType = ErrorTypes.ActionNotSupported + } + }, HttpStatusCode.BadRequest); + return true; + } + + // Since the CVar can be of different types, we need to parse it to the correct type + // First, I try to parse it as a bool, if it fails, I try to parse it as an int + // And as a last resort, I do nothing and put it as a string + if (bool.TryParse(panicPunkerActions.Value, out var boolValue)) + { + await RunOnMainThread(() => _config.SetCVar(cvarName, boolValue)); + } + else if (int.TryParse(panicPunkerActions.Value, out var intValue)) + { + await RunOnMainThread(() => _config.SetCVar(cvarName, intValue)); + } + else + { + await RunOnMainThread(() => _config.SetCVar(cvarName, panicPunkerActions.Value)); + } + _sawmill.Info($"Panic bunker property {panicPunkerActions} changed to {panicPunkerActions.Value} by {actor!.Name} ({actor.Guid})."); + } + + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } + + /// + /// Sets the current MOTD. + /// + private async Task ActionForceMotd(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/set_motd") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var motd = await ReadJson(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + + if (motd!.Motd == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A motd is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A motd is required to perform this action.", + ErrorType = ErrorTypes.MotdNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + + _sawmill.Info($"MOTD changed to \"{motd.Motd}\" by {actor!.Name} ({actor.Guid})."); + + await RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.Motd)); + // A hook in the MOTD system sends the changes to each client + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } + + /// + /// Forces the next preset- + /// + private async Task ActionForcePreset(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/force_preset") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var body = await ReadJson(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem()); + + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Round already started", + Exception = new ExceptionData() + { + Message = "Round already started", + ErrorType = ErrorTypes.RoundAlreadyStarted + } + }, HttpStatusCode.Conflict); + return true; + } + + if (body!.PresetId == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A preset is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A preset is required to perform this action.", + ErrorType = ErrorTypes.PresetNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + + var result = await RunOnMainThread(() => ticker.FindGamePreset(body.PresetId)); + if (result == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Preset not found", + Exception = new ExceptionData() + { + Message = "Preset not found", + ErrorType = ErrorTypes.PresetNotSpecified + } + }, HttpStatusCode.UnprocessableContent); + return true; + } + + await RunOnMainThread(() => + { + ticker.SetGamePreset(result); + }); + _sawmill.Info($"Forced the game to start with preset {body.PresetId} by {actor!.Name}({actor.Guid})."); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } + + /// + /// Ends an active game rule. + /// + private async Task ActionEndGameRule(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/end_game_rule") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var body = await ReadJson(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + if (body!.GameRuleId == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A game rule is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A game rule is required to perform this action.", + ErrorType = ErrorTypes.GuidNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem()); + + var gameRuleEntity = await RunOnMainThread(() => ticker + .GetActiveGameRules() + .FirstOrNull(rule => _entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == body.GameRuleId)); + + if (gameRuleEntity == null) // Game rule not found + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Game rule not found or not active", + Exception = new ExceptionData() + { + Message = "Game rule not found or not active", + ErrorType = ErrorTypes.GameRuleNotFound + } + }, HttpStatusCode.Conflict); + return true; + } + + _sawmill.Info($"Ended game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid})."); + await RunOnMainThread(() => ticker.EndGameRule((EntityUid) gameRuleEntity)); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } + + /// + /// Adds a game rule to the current round. + /// + private async Task ActionAddGameRule(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/add_game_rule") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var body = await ReadJson(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + if (body!.GameRuleId == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A game rule is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A game rule is required to perform this action.", + ErrorType = ErrorTypes.GuidNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + + var ruleEntity = await RunOnMainThread(() => + { + var ticker = _entitySystemManager.GetEntitySystem(); + // See if prototype exists + try + { + _prototypeManager.Index(body.GameRuleId); + } + catch (KeyNotFoundException e) + { + return null; + } + + var ruleEntity = ticker.AddGameRule(body.GameRuleId); + _sawmill.Info($"Added game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid})."); + if (ticker.RunLevel == GameRunLevel.InRound) + { + ticker.StartGameRule(ruleEntity); + _sawmill.Info($"Started game rule {body.GameRuleId} by {actor.Name} ({actor.Guid})."); + } + return ruleEntity; + }); + if (ruleEntity == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Game rule not found", + Exception = new ExceptionData() + { + Message = "Game rule not found", + ErrorType = ErrorTypes.GameRuleNotFound + } + }, HttpStatusCode.UnprocessableContent); + return true; + } + + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } + + /// + /// Kicks a player. + /// + private async Task ActionKick(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/kick") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var body = await ReadJson(context); + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + if (body == null) + { + _sawmill.Info($"Attempted to kick player without supplying a body by {actor!.Name}({actor.Guid})."); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A body is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A body is required to perform this action.", + ErrorType = ErrorTypes.BodyUnableToParse + } + }, HttpStatusCode.BadRequest); + return true; + } + + if (body.Guid == null) + { + _sawmill.Info($"Attempted to kick player without supplying a username by {actor!.Name}({actor.Guid})."); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "A player is required to perform this action.", + Exception = new ExceptionData() + { + Message = "A player is required to perform this action.", + ErrorType = ErrorTypes.GuidNotSpecified + } + }, HttpStatusCode.BadRequest); + return true; + } + + var session = await RunOnMainThread(() => + { + _playerManager.TryGetSessionById(new NetUserId(new Guid(body.Guid)), out var player); + return player; + }); + + if (session == null) + { + _sawmill.Info($"Attempted to kick player {body.Guid} by {actor!.Name} ({actor.Guid}), but they were not found."); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Player not found", + Exception = new ExceptionData() + { + Message = "Player not found", + ErrorType = ErrorTypes.PlayerNotFound + } + }, HttpStatusCode.UnprocessableContent); + return true; + } + + var reason = body.Reason ?? "No reason supplied"; + reason += " (kicked by admin)"; + + await RunOnMainThread(() => + { + _netManager.DisconnectChannel(session.Channel, reason); + }); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + _sawmill.Info("Kicked player {0} ({1}) for {2} by {3}({4})", session.Name, session.UserId.UserId.ToString(), reason, actor!.Name, actor.Guid); + return true; + } + + /// + /// Round restart/end + /// + private async Task ActionRoundStatus(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Post || !context.Url.AbsolutePath.StartsWith("/admin/actions/round/")) + { + return false; + } + + // Make sure paths like /admin/actions/round/lol/start don't work + if (context.Url.AbsolutePath.Split('/').Length != 5) + { + return false; + } + + if (!CheckAccess(context)) + return true; + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + + var (ticker, roundEndSystem) = await RunOnMainThread(() => + { + var ticker = _entitySystemManager.GetEntitySystem(); + var roundEndSystem = _entitySystemManager.GetEntitySystem(); + return (ticker, roundEndSystem); + }); + + // Action is the last part of the URL path (e.g. /admin/actions/round/start -> start) + var action = context.Url.AbsolutePath.Split('/').Last(); + + switch (action) + { + case "start": + if (ticker.RunLevel != GameRunLevel.PreRoundLobby) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Round already started", + Exception = new ExceptionData() + { + Message = "Round already started", + ErrorType = ErrorTypes.RoundAlreadyStarted + } + }, HttpStatusCode.Conflict); + _sawmill.Debug("Forced round start failed: round already started"); + return true; + } + + await RunOnMainThread(() => + { + ticker.StartRound(); + }); + _sawmill.Info("Forced round start"); + break; + case "end": + if (ticker.RunLevel != GameRunLevel.InRound) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Round already ended", + Exception = new ExceptionData() + { + Message = "Round already ended", + ErrorType = ErrorTypes.RoundAlreadyEnded + } + }, HttpStatusCode.Conflict); + _sawmill.Debug("Forced round end failed: round is not in progress"); + return true; + } + await RunOnMainThread(() => + { + roundEndSystem.EndRound(); + }); + _sawmill.Info("Forced round end"); + break; + case "restart": + if (ticker.RunLevel != GameRunLevel.InRound) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Round not in progress", + Exception = new ExceptionData() + { + Message = "Round not in progress", + ErrorType = ErrorTypes.RoundNotInProgress + } + }, HttpStatusCode.Conflict); + _sawmill.Debug("Forced round restart failed: round is not in progress"); + return true; + } + await RunOnMainThread(() => + { + roundEndSystem.EndRound(); + }); + _sawmill.Info("Forced round restart"); + break; + case "restartnow": // You should restart yourself NOW!!! + await RunOnMainThread(() => + { + ticker.RestartRound(); + }); + _sawmill.Info("Forced instant round restart"); + break; + default: + return false; + } + + _sawmill.Info($"Round {action} by {actor!.Name} ({actor.Guid})."); + await context.RespondJsonAsync(new BaseResponse() + { + Message = "OK" + }); + return true; + } +#endregion + +#region Fetching + + /// + /// Returns an array containing all available presets. + /// + private async Task GetForcePresets(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/force_presets") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var presets = new List<(string id, string desc)>(); + foreach (var preset in _prototypeManager.EnumeratePrototypes()) + { + presets.Add((preset.ID, preset.Description)); + } + + await context.RespondJsonAsync(new PresetResponse() + { + Presets = presets + }); + return true; + } + + /// + /// Returns an array containing all game rules. + /// + private async Task GetGameRules(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/game_rules") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var gameRules = new List(); + foreach (var gameRule in _prototypeManager.EnumeratePrototypes()) + { + if (gameRule.Abstract) + continue; + + if (gameRule.HasComponent(_componentFactory)) + gameRules.Add(gameRule.ID); + } + + await context.RespondJsonAsync(new GameruleResponse() + { + GameRules = gameRules + }); + return true; + } + + + /// + /// Handles fetching information. + /// + private async Task InfoHandler(IStatusHandlerContext context) + { + if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/info") + { + return false; + } + + if (!CheckAccess(context)) + return true; + + var (success, actor) = await CheckActor(context); + if (!success) + return true; + + /* Information to display + Round number + Connected players + Active admins + Active game rules + Active game preset + Active map + MOTD + Panic bunker status + */ + + var (ticker, adminSystem) = await RunOnMainThread(() => + { + var ticker = _entitySystemManager.GetEntitySystem(); + var adminSystem = _entitySystemManager.GetEntitySystem(); + return (ticker, adminSystem); + }); + + var players = new List(); + await RunOnMainThread(async () => + { + foreach (var player in _playerManager.Sessions) + { + var isAdmin = _adminManager.IsAdmin(player); + var isDeadmined = _adminManager.IsAdmin(player, true) && !isAdmin; + + players.Add(new Actor() + { + Guid = player.UserId.UserId.ToString(), + Name = player.Name, + IsAdmin = isAdmin, + IsDeadmined = isDeadmined + }); + } + }); + var gameRules = await RunOnMainThread(() => + { + var gameRules = new List(); + foreach (var addedGameRule in ticker.GetActiveGameRules()) + { + var meta = _entityManager.MetaQuery.GetComponent(addedGameRule); + gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown"); + } + + return gameRules; + }); + + _sawmill.Info($"Info requested by {actor!.Name} ({actor.Guid})."); + await context.RespondJsonAsync(new InfoResponse() + { + Players = players, + RoundId = ticker.RoundId, + Map = await RunOnMainThread(() => _gameMapManager.GetSelectedMap()?.MapName ?? "Unknown"), + PanicBunker = adminSystem.PanicBunker, + GamePreset = ticker.CurrentPreset?.ID, + GameRules = gameRules, + MOTD = _config.GetCVar(CCVars.MOTD) + }); + return true; + } + +#endregion + + private bool CheckAccess(IStatusHandlerContext context) + { + var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken); + if (!auth) + { + context.RespondJsonAsync(new BaseResponse() + { + Message = "An authorization header is required to perform this action.", + Exception = new ExceptionData() + { + Message = "An authorization header is required to perform this action.", + ErrorType = ErrorTypes.MissingAuthentication + } + }); + return false; + } + + + if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(authToken.ToString()), Encoding.UTF8.GetBytes(_token))) + return true; + + context.RespondJsonAsync(new BaseResponse() + { + Message = "Invalid authorization header.", + Exception = new ExceptionData() + { + Message = "Invalid authorization header.", + ErrorType = ErrorTypes.InvalidAuthentication + } + }); + // Invalid auth header, no access + _sawmill.Info("Unauthorized access attempt to admin API."); + return false; + } + + /// + /// Async helper function which runs a task on the main thread and returns the result. + /// + private async Task RunOnMainThread(Func func) + { + var taskCompletionSource = new TaskCompletionSource(); + _taskManager.RunOnMainThread(() => + { + try + { + taskCompletionSource.TrySetResult(func()); + } + catch (Exception e) + { + taskCompletionSource.TrySetException(e); + } + }); + + var result = await taskCompletionSource.Task; + return result; + } + + /// + /// Runs an action on the main thread. This does not return any value and is meant to be used for void functions. Use for functions that return a value. + /// + private async Task RunOnMainThread(Action action) + { + var taskCompletionSource = new TaskCompletionSource(); + _taskManager.RunOnMainThread(() => + { + try + { + action(); + taskCompletionSource.TrySetResult(true); + } + catch (Exception e) + { + taskCompletionSource.TrySetException(e); + } + }); + + await taskCompletionSource.Task; + } + + private async Task<(bool, Actor? actor)> CheckActor(IStatusHandlerContext context) + { + // The actor is JSON encoded in the header + var actor = context.RequestHeaders.TryGetValue("Actor", out var actorHeader) ? actorHeader.ToString() : null; + if (actor != null) + { + var actionData = JsonSerializer.Deserialize(actor); + if (actionData == null) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Unable to parse actor.", + Exception = new ExceptionData() + { + Message = "Unable to parse actor.", + ErrorType = ErrorTypes.BodyUnableToParse + } + }, HttpStatusCode.BadRequest); + return (false, null); + } + // Check if the actor is valid, like if all the required fields are present + if (string.IsNullOrWhiteSpace(actionData.Guid) || string.IsNullOrWhiteSpace(actionData.Name)) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Invalid actor supplied.", + Exception = new ExceptionData() + { + Message = "Invalid actor supplied.", + ErrorType = ErrorTypes.InvalidActor + } + }, HttpStatusCode.BadRequest); + return (false, null); + } + + // See if the parsed GUID is a valid GUID + if (!Guid.TryParse(actionData.Guid, out _)) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Invalid GUID supplied.", + Exception = new ExceptionData() + { + Message = "Invalid GUID supplied.", + ErrorType = ErrorTypes.InvalidActor + } + }, HttpStatusCode.BadRequest); + return (false, null); + } + + return (true, actionData); + } + + await context.RespondJsonAsync(new BaseResponse() + { + Message = "An actor is required to perform this action.", + Exception = new ExceptionData() + { + Message = "An actor is required to perform this action.", + ErrorType = ErrorTypes.MissingActor + } + }, HttpStatusCode.BadRequest); + return (false, null); + } + + /// + /// Helper function to read JSON encoded data from the request body. + /// + private async Task ReadJson(IStatusHandlerContext context) + { + try + { + var json = await context.RequestBodyJsonAsync(); + return json; + } + catch (Exception e) + { + await context.RespondJsonAsync(new BaseResponse() + { + Message = "Unable to parse request body.", + Exception = new ExceptionData() + { + Message = e.Message, + ErrorType = ErrorTypes.BodyUnableToParse, + StackTrace = e.StackTrace + } + }, HttpStatusCode.BadRequest); + return default; + } + } + +#region From Client + + private record Actor + { + public string? Guid { get; init; } + public string? Name { get; init; } + public bool IsAdmin { get; init; } = false; + public bool IsDeadmined { get; init; } = false; + } + + private record KickActionBody + { + public string? Guid { get; init; } + public string? Reason { get; init; } + } + + private record GameRuleActionBody + { + public string? GameRuleId { get; init; } + } + + private record PresetActionBody + { + public string? PresetId { get; init; } + } + + private record MotdActionBody + { + public string? Motd { get; init; } + } + +#endregion + +#region Responses + + private record BaseResponse + { + public string? Message { get; init; } = "OK"; + public ExceptionData? Exception { get; init; } = null; + } + + private record ExceptionData + { + public string Message { get; init; } = string.Empty; + public ErrorTypes ErrorType { get; init; } = ErrorTypes.None; + public string? StackTrace { get; init; } = null; + } + + private enum ErrorTypes + { + BodyUnableToParse = -2, + None = -1, + MissingAuthentication = 0, + InvalidAuthentication = 1, + MissingActor = 2, + InvalidActor = 3, + RoundNotInProgress = 4, + RoundAlreadyStarted = 5, + RoundAlreadyEnded = 6, + ActionNotSpecified = 7, + ActionNotSupported = 8, + GuidNotSpecified = 9, + PlayerNotFound = 10, + GameRuleNotFound = 11, + PresetNotSpecified = 12, + MotdNotSpecified = 13 + } + +#endregion + +#region Misc + + /// + /// Record used to send the response for the info endpoint. + /// + private record InfoResponse + { + public int RoundId { get; init; } = 0; + public List Players { get; init; } = new(); + public List GameRules { get; init; } = new(); + public string? GamePreset { get; init; } = null; + public string? Map { get; init; } = null; + public string? MOTD { get; init; } = null; + public PanicBunkerStatus PanicBunker { get; init; } = new(); + } + + private record PresetResponse : BaseResponse + { + public List<(string id, string desc)> Presets { get; init; } = new(); + } + + private record GameruleResponse : BaseResponse + { + public List GameRules { get; init; } = new(); + } + +#endregion + +} diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index c3c024174a..b7ca4e915b 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -61,7 +61,7 @@ namespace Content.Server.Administration.Systems public IReadOnlySet RoundActivePlayers => _roundActivePlayers; private readonly HashSet _roundActivePlayers = new(); - private readonly PanicBunkerStatus _panicBunker = new(); + public readonly PanicBunkerStatus PanicBunker = new(); public override void Initialize() { @@ -240,7 +240,7 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerChanged(bool enabled) { - _panicBunker.Enabled = enabled; + PanicBunker.Enabled = enabled; _chat.SendAdminAlert(Loc.GetString(enabled ? "admin-ui-panic-bunker-enabled-admin-alert" : "admin-ui-panic-bunker-disabled-admin-alert" @@ -251,52 +251,52 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerDisableWithAdminsChanged(bool enabled) { - _panicBunker.DisableWithAdmins = enabled; + PanicBunker.DisableWithAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled) { - _panicBunker.EnableWithoutAdmins = enabled; + PanicBunker.EnableWithoutAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled) { - _panicBunker.CountDeadminnedAdmins = enabled; + PanicBunker.CountDeadminnedAdmins = enabled; UpdatePanicBunker(); } private void OnShowReasonChanged(bool enabled) { - _panicBunker.ShowReason = enabled; + PanicBunker.ShowReason = enabled; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinAccountAgeChanged(int minutes) { - _panicBunker.MinAccountAgeHours = minutes / 60; + PanicBunker.MinAccountAgeHours = minutes / 60; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinOverallHoursChanged(int hours) { - _panicBunker.MinOverallHours = hours; + PanicBunker.MinOverallHours = hours; SendPanicBunkerStatusAll(); } private void UpdatePanicBunker() { - var admins = _panicBunker.CountDeadminnedAdmins + var admins = PanicBunker.CountDeadminnedAdmins ? _adminManager.AllAdmins : _adminManager.ActiveAdmins; var hasAdmins = admins.Any(); - if (hasAdmins && _panicBunker.DisableWithAdmins) + if (hasAdmins && PanicBunker.DisableWithAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, false); } - else if (!hasAdmins && _panicBunker.EnableWithoutAdmins) + else if (!hasAdmins && PanicBunker.EnableWithoutAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, true); } @@ -306,7 +306,7 @@ namespace Content.Server.Administration.Systems private void SendPanicBunkerStatusAll() { - var ev = new PanicBunkerChangedEvent(_panicBunker); + var ev = new PanicBunkerChangedEvent(PanicBunker); foreach (var admin in _adminManager.AllAdmins) { RaiseNetworkEvent(ev, admin); diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index bf7f3ea84a..3cdf3bfe8e 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -102,6 +102,7 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); _voteManager.Initialize(); _updateManager.Initialize(); @@ -167,6 +168,7 @@ namespace Content.Server.Entry { _playTimeTracking?.Shutdown(); _dbManager?.Shutdown(); + IoCManager.Resolve().Shutdown(); } private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill) diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 2a63ace8e3..25bb1072a5 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -58,6 +58,7 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index c9271331f3..aa14c565a0 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -773,6 +773,13 @@ namespace Content.Shared.CCVar public static readonly CVarDef AdminAnnounceLogout = CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY); + /// + /// The token used to authenticate with the admin API. Leave empty to disable the admin API. This is a secret! Do not share! + /// + public static readonly CVarDef AdminApiToken = + CVarDef.Create("admin.api_token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); + + /// /// Should users be able to see their own notes? Admins will be able to see and set notes regardless /// From 3aee19792391cbfb06edb65d6f16f77da0f36f13 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 10 Apr 2024 14:19:32 +0200 Subject: [PATCH 02/49] Revert "Game server api" (#26871) Revert "Game server api (#24015)" This reverts commit 297853929b7b3859760dcdda95e21888672ce8e1. --- Content.Server/Administration/ServerAPI.cs | 1033 ----------------- .../Administration/Systems/AdminSystem.cs | 24 +- Content.Server/Entry/EntryPoint.cs | 2 - Content.Server/IoC/ServerContentIoC.cs | 1 - Content.Shared/CCVar/CCVars.cs | 7 - 5 files changed, 12 insertions(+), 1055 deletions(-) delete mode 100644 Content.Server/Administration/ServerAPI.cs diff --git a/Content.Server/Administration/ServerAPI.cs b/Content.Server/Administration/ServerAPI.cs deleted file mode 100644 index d7591fb80c..0000000000 --- a/Content.Server/Administration/ServerAPI.cs +++ /dev/null @@ -1,1033 +0,0 @@ -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading.Tasks; -using Content.Server.Administration.Systems; -using Content.Server.GameTicking; -using Content.Server.GameTicking.Presets; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Maps; -using Content.Server.RoundEnd; -using Content.Shared.Administration.Events; -using Content.Shared.Administration.Managers; -using Content.Shared.CCVar; -using Content.Shared.Prototypes; -using Robust.Server.ServerStatus; -using Robust.Shared.Asynchronous; -using Robust.Shared.Configuration; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; -using Robust.Shared.Utility; - -namespace Content.Server.Administration; - -public sealed class ServerApi : IPostInjectInit -{ - [Dependency] private readonly IStatusHost _statusHost = default!; - [Dependency] private readonly IConfigurationManager _config = default!; - [Dependency] private readonly ISharedPlayerManager _playerManager = default!; // Players - [Dependency] private readonly ISharedAdminManager _adminManager = default!; // Admins - [Dependency] private readonly IGameMapManager _gameMapManager = default!; // Map name - [Dependency] private readonly IServerNetManager _netManager = default!; // Kick - [Dependency] private readonly IPrototypeManager _prototypeManager = default!; // Game rules - [Dependency] private readonly IComponentFactory _componentFactory = default!; - [Dependency] private readonly ITaskManager _taskManager = default!; // game explodes when calling stuff from the non-game thread - [Dependency] private readonly EntityManager _entityManager = default!; - - [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - - private string _token = string.Empty; - private ISawmill _sawmill = default!; - - public static Dictionary PanicPunkerCvarNames = new() - { - { "Enabled", "game.panic_bunker.enabled" }, - { "DisableWithAdmins", "game.panic_bunker.disable_with_admins" }, - { "EnableWithoutAdmins", "game.panic_bunker.enable_without_admins" }, - { "CountDeadminnedAdmins", "game.panic_bunker.count_deadminned_admins" }, - { "ShowReason", "game.panic_bunker.show_reason" }, - { "MinAccountAgeHours", "game.panic_bunker.min_account_age" }, - { "MinOverallHours", "game.panic_bunker.min_overall_hours" }, - { "CustomReason", "game.panic_bunker.custom_reason" } - }; - - void IPostInjectInit.PostInject() - { - _sawmill = Logger.GetSawmill("serverApi"); - - // Get - _statusHost.AddHandler(InfoHandler); - _statusHost.AddHandler(GetGameRules); - _statusHost.AddHandler(GetForcePresets); - - // Post - _statusHost.AddHandler(ActionRoundStatus); - _statusHost.AddHandler(ActionKick); - _statusHost.AddHandler(ActionAddGameRule); - _statusHost.AddHandler(ActionEndGameRule); - _statusHost.AddHandler(ActionForcePreset); - _statusHost.AddHandler(ActionForceMotd); - _statusHost.AddHandler(ActionPanicPunker); - } - - public void Initialize() - { - _config.OnValueChanged(CCVars.AdminApiToken, UpdateToken, true); - } - - public void Shutdown() - { - _config.UnsubValueChanged(CCVars.AdminApiToken, UpdateToken); - } - - private void UpdateToken(string token) - { - _token = token; - } - - -#region Actions - - /// - /// Changes the panic bunker settings. - /// - private async Task ActionPanicPunker(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Patch || context.Url.AbsolutePath != "/admin/actions/panic_bunker") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var body = await ReadJson>(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - foreach (var panicPunkerActions in body!.Select(x => new { Action = x.Key, Value = x.Value.ToString() })) - { - if (panicPunkerActions.Action == null || panicPunkerActions.Value == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Action and value are required to perform this action.", - Exception = new ExceptionData() - { - Message = "Action and value are required to perform this action.", - ErrorType = ErrorTypes.ActionNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - - if (!PanicPunkerCvarNames.TryGetValue(panicPunkerActions.Action, out var cvarName)) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.", - Exception = new ExceptionData() - { - Message = $"Cannot set: Action {panicPunkerActions.Action} does not exist.", - ErrorType = ErrorTypes.ActionNotSupported - } - }, HttpStatusCode.BadRequest); - return true; - } - - // Since the CVar can be of different types, we need to parse it to the correct type - // First, I try to parse it as a bool, if it fails, I try to parse it as an int - // And as a last resort, I do nothing and put it as a string - if (bool.TryParse(panicPunkerActions.Value, out var boolValue)) - { - await RunOnMainThread(() => _config.SetCVar(cvarName, boolValue)); - } - else if (int.TryParse(panicPunkerActions.Value, out var intValue)) - { - await RunOnMainThread(() => _config.SetCVar(cvarName, intValue)); - } - else - { - await RunOnMainThread(() => _config.SetCVar(cvarName, panicPunkerActions.Value)); - } - _sawmill.Info($"Panic bunker property {panicPunkerActions} changed to {panicPunkerActions.Value} by {actor!.Name} ({actor.Guid})."); - } - - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } - - /// - /// Sets the current MOTD. - /// - private async Task ActionForceMotd(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/set_motd") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var motd = await ReadJson(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - - if (motd!.Motd == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A motd is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A motd is required to perform this action.", - ErrorType = ErrorTypes.MotdNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - - _sawmill.Info($"MOTD changed to \"{motd.Motd}\" by {actor!.Name} ({actor.Guid})."); - - await RunOnMainThread(() => _config.SetCVar(CCVars.MOTD, motd.Motd)); - // A hook in the MOTD system sends the changes to each client - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } - - /// - /// Forces the next preset- - /// - private async Task ActionForcePreset(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/force_preset") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var body = await ReadJson(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem()); - - if (ticker.RunLevel != GameRunLevel.PreRoundLobby) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Round already started", - Exception = new ExceptionData() - { - Message = "Round already started", - ErrorType = ErrorTypes.RoundAlreadyStarted - } - }, HttpStatusCode.Conflict); - return true; - } - - if (body!.PresetId == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A preset is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A preset is required to perform this action.", - ErrorType = ErrorTypes.PresetNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - - var result = await RunOnMainThread(() => ticker.FindGamePreset(body.PresetId)); - if (result == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Preset not found", - Exception = new ExceptionData() - { - Message = "Preset not found", - ErrorType = ErrorTypes.PresetNotSpecified - } - }, HttpStatusCode.UnprocessableContent); - return true; - } - - await RunOnMainThread(() => - { - ticker.SetGamePreset(result); - }); - _sawmill.Info($"Forced the game to start with preset {body.PresetId} by {actor!.Name}({actor.Guid})."); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } - - /// - /// Ends an active game rule. - /// - private async Task ActionEndGameRule(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/end_game_rule") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var body = await ReadJson(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - if (body!.GameRuleId == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A game rule is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A game rule is required to perform this action.", - ErrorType = ErrorTypes.GuidNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - var ticker = await RunOnMainThread(() => _entitySystemManager.GetEntitySystem()); - - var gameRuleEntity = await RunOnMainThread(() => ticker - .GetActiveGameRules() - .FirstOrNull(rule => _entityManager.MetaQuery.GetComponent(rule).EntityPrototype?.ID == body.GameRuleId)); - - if (gameRuleEntity == null) // Game rule not found - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Game rule not found or not active", - Exception = new ExceptionData() - { - Message = "Game rule not found or not active", - ErrorType = ErrorTypes.GameRuleNotFound - } - }, HttpStatusCode.Conflict); - return true; - } - - _sawmill.Info($"Ended game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid})."); - await RunOnMainThread(() => ticker.EndGameRule((EntityUid) gameRuleEntity)); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } - - /// - /// Adds a game rule to the current round. - /// - private async Task ActionAddGameRule(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/add_game_rule") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var body = await ReadJson(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - if (body!.GameRuleId == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A game rule is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A game rule is required to perform this action.", - ErrorType = ErrorTypes.GuidNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - - var ruleEntity = await RunOnMainThread(() => - { - var ticker = _entitySystemManager.GetEntitySystem(); - // See if prototype exists - try - { - _prototypeManager.Index(body.GameRuleId); - } - catch (KeyNotFoundException e) - { - return null; - } - - var ruleEntity = ticker.AddGameRule(body.GameRuleId); - _sawmill.Info($"Added game rule {body.GameRuleId} by {actor!.Name} ({actor.Guid})."); - if (ticker.RunLevel == GameRunLevel.InRound) - { - ticker.StartGameRule(ruleEntity); - _sawmill.Info($"Started game rule {body.GameRuleId} by {actor.Name} ({actor.Guid})."); - } - return ruleEntity; - }); - if (ruleEntity == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Game rule not found", - Exception = new ExceptionData() - { - Message = "Game rule not found", - ErrorType = ErrorTypes.GameRuleNotFound - } - }, HttpStatusCode.UnprocessableContent); - return true; - } - - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } - - /// - /// Kicks a player. - /// - private async Task ActionKick(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/admin/actions/kick") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var body = await ReadJson(context); - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - if (body == null) - { - _sawmill.Info($"Attempted to kick player without supplying a body by {actor!.Name}({actor.Guid})."); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A body is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A body is required to perform this action.", - ErrorType = ErrorTypes.BodyUnableToParse - } - }, HttpStatusCode.BadRequest); - return true; - } - - if (body.Guid == null) - { - _sawmill.Info($"Attempted to kick player without supplying a username by {actor!.Name}({actor.Guid})."); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "A player is required to perform this action.", - Exception = new ExceptionData() - { - Message = "A player is required to perform this action.", - ErrorType = ErrorTypes.GuidNotSpecified - } - }, HttpStatusCode.BadRequest); - return true; - } - - var session = await RunOnMainThread(() => - { - _playerManager.TryGetSessionById(new NetUserId(new Guid(body.Guid)), out var player); - return player; - }); - - if (session == null) - { - _sawmill.Info($"Attempted to kick player {body.Guid} by {actor!.Name} ({actor.Guid}), but they were not found."); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Player not found", - Exception = new ExceptionData() - { - Message = "Player not found", - ErrorType = ErrorTypes.PlayerNotFound - } - }, HttpStatusCode.UnprocessableContent); - return true; - } - - var reason = body.Reason ?? "No reason supplied"; - reason += " (kicked by admin)"; - - await RunOnMainThread(() => - { - _netManager.DisconnectChannel(session.Channel, reason); - }); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - _sawmill.Info("Kicked player {0} ({1}) for {2} by {3}({4})", session.Name, session.UserId.UserId.ToString(), reason, actor!.Name, actor.Guid); - return true; - } - - /// - /// Round restart/end - /// - private async Task ActionRoundStatus(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Post || !context.Url.AbsolutePath.StartsWith("/admin/actions/round/")) - { - return false; - } - - // Make sure paths like /admin/actions/round/lol/start don't work - if (context.Url.AbsolutePath.Split('/').Length != 5) - { - return false; - } - - if (!CheckAccess(context)) - return true; - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - - var (ticker, roundEndSystem) = await RunOnMainThread(() => - { - var ticker = _entitySystemManager.GetEntitySystem(); - var roundEndSystem = _entitySystemManager.GetEntitySystem(); - return (ticker, roundEndSystem); - }); - - // Action is the last part of the URL path (e.g. /admin/actions/round/start -> start) - var action = context.Url.AbsolutePath.Split('/').Last(); - - switch (action) - { - case "start": - if (ticker.RunLevel != GameRunLevel.PreRoundLobby) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Round already started", - Exception = new ExceptionData() - { - Message = "Round already started", - ErrorType = ErrorTypes.RoundAlreadyStarted - } - }, HttpStatusCode.Conflict); - _sawmill.Debug("Forced round start failed: round already started"); - return true; - } - - await RunOnMainThread(() => - { - ticker.StartRound(); - }); - _sawmill.Info("Forced round start"); - break; - case "end": - if (ticker.RunLevel != GameRunLevel.InRound) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Round already ended", - Exception = new ExceptionData() - { - Message = "Round already ended", - ErrorType = ErrorTypes.RoundAlreadyEnded - } - }, HttpStatusCode.Conflict); - _sawmill.Debug("Forced round end failed: round is not in progress"); - return true; - } - await RunOnMainThread(() => - { - roundEndSystem.EndRound(); - }); - _sawmill.Info("Forced round end"); - break; - case "restart": - if (ticker.RunLevel != GameRunLevel.InRound) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Round not in progress", - Exception = new ExceptionData() - { - Message = "Round not in progress", - ErrorType = ErrorTypes.RoundNotInProgress - } - }, HttpStatusCode.Conflict); - _sawmill.Debug("Forced round restart failed: round is not in progress"); - return true; - } - await RunOnMainThread(() => - { - roundEndSystem.EndRound(); - }); - _sawmill.Info("Forced round restart"); - break; - case "restartnow": // You should restart yourself NOW!!! - await RunOnMainThread(() => - { - ticker.RestartRound(); - }); - _sawmill.Info("Forced instant round restart"); - break; - default: - return false; - } - - _sawmill.Info($"Round {action} by {actor!.Name} ({actor.Guid})."); - await context.RespondJsonAsync(new BaseResponse() - { - Message = "OK" - }); - return true; - } -#endregion - -#region Fetching - - /// - /// Returns an array containing all available presets. - /// - private async Task GetForcePresets(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/force_presets") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var presets = new List<(string id, string desc)>(); - foreach (var preset in _prototypeManager.EnumeratePrototypes()) - { - presets.Add((preset.ID, preset.Description)); - } - - await context.RespondJsonAsync(new PresetResponse() - { - Presets = presets - }); - return true; - } - - /// - /// Returns an array containing all game rules. - /// - private async Task GetGameRules(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/game_rules") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var gameRules = new List(); - foreach (var gameRule in _prototypeManager.EnumeratePrototypes()) - { - if (gameRule.Abstract) - continue; - - if (gameRule.HasComponent(_componentFactory)) - gameRules.Add(gameRule.ID); - } - - await context.RespondJsonAsync(new GameruleResponse() - { - GameRules = gameRules - }); - return true; - } - - - /// - /// Handles fetching information. - /// - private async Task InfoHandler(IStatusHandlerContext context) - { - if (context.RequestMethod != HttpMethod.Get || context.Url.AbsolutePath != "/admin/info") - { - return false; - } - - if (!CheckAccess(context)) - return true; - - var (success, actor) = await CheckActor(context); - if (!success) - return true; - - /* Information to display - Round number - Connected players - Active admins - Active game rules - Active game preset - Active map - MOTD - Panic bunker status - */ - - var (ticker, adminSystem) = await RunOnMainThread(() => - { - var ticker = _entitySystemManager.GetEntitySystem(); - var adminSystem = _entitySystemManager.GetEntitySystem(); - return (ticker, adminSystem); - }); - - var players = new List(); - await RunOnMainThread(async () => - { - foreach (var player in _playerManager.Sessions) - { - var isAdmin = _adminManager.IsAdmin(player); - var isDeadmined = _adminManager.IsAdmin(player, true) && !isAdmin; - - players.Add(new Actor() - { - Guid = player.UserId.UserId.ToString(), - Name = player.Name, - IsAdmin = isAdmin, - IsDeadmined = isDeadmined - }); - } - }); - var gameRules = await RunOnMainThread(() => - { - var gameRules = new List(); - foreach (var addedGameRule in ticker.GetActiveGameRules()) - { - var meta = _entityManager.MetaQuery.GetComponent(addedGameRule); - gameRules.Add(meta.EntityPrototype?.ID ?? meta.EntityPrototype?.Name ?? "Unknown"); - } - - return gameRules; - }); - - _sawmill.Info($"Info requested by {actor!.Name} ({actor.Guid})."); - await context.RespondJsonAsync(new InfoResponse() - { - Players = players, - RoundId = ticker.RoundId, - Map = await RunOnMainThread(() => _gameMapManager.GetSelectedMap()?.MapName ?? "Unknown"), - PanicBunker = adminSystem.PanicBunker, - GamePreset = ticker.CurrentPreset?.ID, - GameRules = gameRules, - MOTD = _config.GetCVar(CCVars.MOTD) - }); - return true; - } - -#endregion - - private bool CheckAccess(IStatusHandlerContext context) - { - var auth = context.RequestHeaders.TryGetValue("Authorization", out var authToken); - if (!auth) - { - context.RespondJsonAsync(new BaseResponse() - { - Message = "An authorization header is required to perform this action.", - Exception = new ExceptionData() - { - Message = "An authorization header is required to perform this action.", - ErrorType = ErrorTypes.MissingAuthentication - } - }); - return false; - } - - - if (CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(authToken.ToString()), Encoding.UTF8.GetBytes(_token))) - return true; - - context.RespondJsonAsync(new BaseResponse() - { - Message = "Invalid authorization header.", - Exception = new ExceptionData() - { - Message = "Invalid authorization header.", - ErrorType = ErrorTypes.InvalidAuthentication - } - }); - // Invalid auth header, no access - _sawmill.Info("Unauthorized access attempt to admin API."); - return false; - } - - /// - /// Async helper function which runs a task on the main thread and returns the result. - /// - private async Task RunOnMainThread(Func func) - { - var taskCompletionSource = new TaskCompletionSource(); - _taskManager.RunOnMainThread(() => - { - try - { - taskCompletionSource.TrySetResult(func()); - } - catch (Exception e) - { - taskCompletionSource.TrySetException(e); - } - }); - - var result = await taskCompletionSource.Task; - return result; - } - - /// - /// Runs an action on the main thread. This does not return any value and is meant to be used for void functions. Use for functions that return a value. - /// - private async Task RunOnMainThread(Action action) - { - var taskCompletionSource = new TaskCompletionSource(); - _taskManager.RunOnMainThread(() => - { - try - { - action(); - taskCompletionSource.TrySetResult(true); - } - catch (Exception e) - { - taskCompletionSource.TrySetException(e); - } - }); - - await taskCompletionSource.Task; - } - - private async Task<(bool, Actor? actor)> CheckActor(IStatusHandlerContext context) - { - // The actor is JSON encoded in the header - var actor = context.RequestHeaders.TryGetValue("Actor", out var actorHeader) ? actorHeader.ToString() : null; - if (actor != null) - { - var actionData = JsonSerializer.Deserialize(actor); - if (actionData == null) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Unable to parse actor.", - Exception = new ExceptionData() - { - Message = "Unable to parse actor.", - ErrorType = ErrorTypes.BodyUnableToParse - } - }, HttpStatusCode.BadRequest); - return (false, null); - } - // Check if the actor is valid, like if all the required fields are present - if (string.IsNullOrWhiteSpace(actionData.Guid) || string.IsNullOrWhiteSpace(actionData.Name)) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Invalid actor supplied.", - Exception = new ExceptionData() - { - Message = "Invalid actor supplied.", - ErrorType = ErrorTypes.InvalidActor - } - }, HttpStatusCode.BadRequest); - return (false, null); - } - - // See if the parsed GUID is a valid GUID - if (!Guid.TryParse(actionData.Guid, out _)) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Invalid GUID supplied.", - Exception = new ExceptionData() - { - Message = "Invalid GUID supplied.", - ErrorType = ErrorTypes.InvalidActor - } - }, HttpStatusCode.BadRequest); - return (false, null); - } - - return (true, actionData); - } - - await context.RespondJsonAsync(new BaseResponse() - { - Message = "An actor is required to perform this action.", - Exception = new ExceptionData() - { - Message = "An actor is required to perform this action.", - ErrorType = ErrorTypes.MissingActor - } - }, HttpStatusCode.BadRequest); - return (false, null); - } - - /// - /// Helper function to read JSON encoded data from the request body. - /// - private async Task ReadJson(IStatusHandlerContext context) - { - try - { - var json = await context.RequestBodyJsonAsync(); - return json; - } - catch (Exception e) - { - await context.RespondJsonAsync(new BaseResponse() - { - Message = "Unable to parse request body.", - Exception = new ExceptionData() - { - Message = e.Message, - ErrorType = ErrorTypes.BodyUnableToParse, - StackTrace = e.StackTrace - } - }, HttpStatusCode.BadRequest); - return default; - } - } - -#region From Client - - private record Actor - { - public string? Guid { get; init; } - public string? Name { get; init; } - public bool IsAdmin { get; init; } = false; - public bool IsDeadmined { get; init; } = false; - } - - private record KickActionBody - { - public string? Guid { get; init; } - public string? Reason { get; init; } - } - - private record GameRuleActionBody - { - public string? GameRuleId { get; init; } - } - - private record PresetActionBody - { - public string? PresetId { get; init; } - } - - private record MotdActionBody - { - public string? Motd { get; init; } - } - -#endregion - -#region Responses - - private record BaseResponse - { - public string? Message { get; init; } = "OK"; - public ExceptionData? Exception { get; init; } = null; - } - - private record ExceptionData - { - public string Message { get; init; } = string.Empty; - public ErrorTypes ErrorType { get; init; } = ErrorTypes.None; - public string? StackTrace { get; init; } = null; - } - - private enum ErrorTypes - { - BodyUnableToParse = -2, - None = -1, - MissingAuthentication = 0, - InvalidAuthentication = 1, - MissingActor = 2, - InvalidActor = 3, - RoundNotInProgress = 4, - RoundAlreadyStarted = 5, - RoundAlreadyEnded = 6, - ActionNotSpecified = 7, - ActionNotSupported = 8, - GuidNotSpecified = 9, - PlayerNotFound = 10, - GameRuleNotFound = 11, - PresetNotSpecified = 12, - MotdNotSpecified = 13 - } - -#endregion - -#region Misc - - /// - /// Record used to send the response for the info endpoint. - /// - private record InfoResponse - { - public int RoundId { get; init; } = 0; - public List Players { get; init; } = new(); - public List GameRules { get; init; } = new(); - public string? GamePreset { get; init; } = null; - public string? Map { get; init; } = null; - public string? MOTD { get; init; } = null; - public PanicBunkerStatus PanicBunker { get; init; } = new(); - } - - private record PresetResponse : BaseResponse - { - public List<(string id, string desc)> Presets { get; init; } = new(); - } - - private record GameruleResponse : BaseResponse - { - public List GameRules { get; init; } = new(); - } - -#endregion - -} diff --git a/Content.Server/Administration/Systems/AdminSystem.cs b/Content.Server/Administration/Systems/AdminSystem.cs index b7ca4e915b..c3c024174a 100644 --- a/Content.Server/Administration/Systems/AdminSystem.cs +++ b/Content.Server/Administration/Systems/AdminSystem.cs @@ -61,7 +61,7 @@ namespace Content.Server.Administration.Systems public IReadOnlySet RoundActivePlayers => _roundActivePlayers; private readonly HashSet _roundActivePlayers = new(); - public readonly PanicBunkerStatus PanicBunker = new(); + private readonly PanicBunkerStatus _panicBunker = new(); public override void Initialize() { @@ -240,7 +240,7 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerChanged(bool enabled) { - PanicBunker.Enabled = enabled; + _panicBunker.Enabled = enabled; _chat.SendAdminAlert(Loc.GetString(enabled ? "admin-ui-panic-bunker-enabled-admin-alert" : "admin-ui-panic-bunker-disabled-admin-alert" @@ -251,52 +251,52 @@ namespace Content.Server.Administration.Systems private void OnPanicBunkerDisableWithAdminsChanged(bool enabled) { - PanicBunker.DisableWithAdmins = enabled; + _panicBunker.DisableWithAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled) { - PanicBunker.EnableWithoutAdmins = enabled; + _panicBunker.EnableWithoutAdmins = enabled; UpdatePanicBunker(); } private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled) { - PanicBunker.CountDeadminnedAdmins = enabled; + _panicBunker.CountDeadminnedAdmins = enabled; UpdatePanicBunker(); } private void OnShowReasonChanged(bool enabled) { - PanicBunker.ShowReason = enabled; + _panicBunker.ShowReason = enabled; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinAccountAgeChanged(int minutes) { - PanicBunker.MinAccountAgeHours = minutes / 60; + _panicBunker.MinAccountAgeHours = minutes / 60; SendPanicBunkerStatusAll(); } private void OnPanicBunkerMinOverallHoursChanged(int hours) { - PanicBunker.MinOverallHours = hours; + _panicBunker.MinOverallHours = hours; SendPanicBunkerStatusAll(); } private void UpdatePanicBunker() { - var admins = PanicBunker.CountDeadminnedAdmins + var admins = _panicBunker.CountDeadminnedAdmins ? _adminManager.AllAdmins : _adminManager.ActiveAdmins; var hasAdmins = admins.Any(); - if (hasAdmins && PanicBunker.DisableWithAdmins) + if (hasAdmins && _panicBunker.DisableWithAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, false); } - else if (!hasAdmins && PanicBunker.EnableWithoutAdmins) + else if (!hasAdmins && _panicBunker.EnableWithoutAdmins) { _config.SetCVar(CCVars.PanicBunkerEnabled, true); } @@ -306,7 +306,7 @@ namespace Content.Server.Administration.Systems private void SendPanicBunkerStatusAll() { - var ev = new PanicBunkerChangedEvent(PanicBunker); + var ev = new PanicBunkerChangedEvent(_panicBunker); foreach (var admin in _adminManager.AllAdmins) { RaiseNetworkEvent(ev, admin); diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 3cdf3bfe8e..bf7f3ea84a 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -102,7 +102,6 @@ namespace Content.Server.Entry IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().Initialize(); _voteManager.Initialize(); _updateManager.Initialize(); @@ -168,7 +167,6 @@ namespace Content.Server.Entry { _playTimeTracking?.Shutdown(); _dbManager?.Shutdown(); - IoCManager.Resolve().Shutdown(); } private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill) diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 25bb1072a5..2a63ace8e3 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -58,7 +58,6 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); } } } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index aa14c565a0..c9271331f3 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -773,13 +773,6 @@ namespace Content.Shared.CCVar public static readonly CVarDef AdminAnnounceLogout = CVarDef.Create("admin.announce_logout", true, CVar.SERVERONLY); - /// - /// The token used to authenticate with the admin API. Leave empty to disable the admin API. This is a secret! Do not share! - /// - public static readonly CVarDef AdminApiToken = - CVarDef.Create("admin.api_token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); - - /// /// Should users be able to see their own notes? Admins will be able to see and set notes regardless /// From 935127f25fef5cce6e0d5c4b73db5c6077badf56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B6=D0=B5=D0=BA=D1=81=D0=BE=D0=BD=20=D0=9C=D0=B8?= =?UTF-8?q?=D1=81=D1=81=D0=B8=D1=81=D1=81=D0=B8=D0=BF=D0=BF=D0=B8?= Date: Wed, 10 Apr 2024 12:28:03 -0500 Subject: [PATCH 03/49] Give botanists droppers (#26839) Start botanists with droppers so that they can better dose robust harvest or mutagen. --- Resources/Prototypes/Catalog/Fills/Lockers/service.yml | 1 + .../Prototypes/Catalog/VendingMachines/Inventories/nutri.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml index e5dbe2d524..945ae0dd7b 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/service.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/service.yml @@ -104,6 +104,7 @@ - id: ClothingBeltPlant - id: PlantBag ##Some maps don't have nutrivend - id: BoxMouthSwab + - id: Dropper - id: HandLabeler - id: ClothingUniformOveralls - id: ClothingHeadHatTrucker diff --git a/Resources/Prototypes/Catalog/VendingMachines/Inventories/nutri.yml b/Resources/Prototypes/Catalog/VendingMachines/Inventories/nutri.yml index c3933dfa3d..29b3cb8aee 100644 --- a/Resources/Prototypes/Catalog/VendingMachines/Inventories/nutri.yml +++ b/Resources/Prototypes/Catalog/VendingMachines/Inventories/nutri.yml @@ -6,6 +6,7 @@ HydroponicsToolClippers: 4 HydroponicsToolScythe: 4 HydroponicsToolHatchet: 4 + Dropper: 4 PlantBag: 3 PlantBGoneSpray: 20 WeedSpray: 20 From 7d599a7199f21d71f3befa26e7ffec003a887dd3 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 10 Apr 2024 17:29:11 +0000 Subject: [PATCH 04/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index b7e07e53bc..ef8cf9a297 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: mirrorcult - changes: - - message: Throwing items now scale a bit when thrown to simulate rising/falling - type: Add - id: 5827 - time: '2024-01-30T10:50:41.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24724 - author: Emisse changes: - message: Gemini @@ -3825,3 +3818,10 @@ id: 6326 time: '2024-04-09T22:20:57.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26846 +- author: notquitehadouken + changes: + - message: Gave botanists droppers for easier chemical moving. + type: Add + id: 6327 + time: '2024-04-10T17:28:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26839 From 58e4b5fe4ceab7f9bd60eb3d87daab9d01954a3b Mon Sep 17 00:00:00 2001 From: botanySupremist <160211017+botanySupremist@users.noreply.github.com> Date: Wed, 10 Apr 2024 10:51:25 -0700 Subject: [PATCH 05/49] Clipping a harvestable plant yields undamaged seeds (#25541) Clipping a plant in any condition currently causes it and its clippings to be damaged. Make clipping harvestable (already eligible for seed extractor) plants yield seeds at full health. --- .../Botany/Systems/PlantHolderSystem.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/Content.Server/Botany/Systems/PlantHolderSystem.cs b/Content.Server/Botany/Systems/PlantHolderSystem.cs index 0f54fd0da5..721536a7c0 100644 --- a/Content.Server/Botany/Systems/PlantHolderSystem.cs +++ b/Content.Server/Botany/Systems/PlantHolderSystem.cs @@ -159,7 +159,6 @@ public sealed class PlantHolderSystem : EntitySystem if (!_botany.TryGetSeed(seeds, out var seed)) return; - float? seedHealth = seeds.HealthOverride; var name = Loc.GetString(seed.Name); var noun = Loc.GetString(seed.Noun); _popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message", @@ -169,9 +168,9 @@ public sealed class PlantHolderSystem : EntitySystem component.Seed = seed; component.Dead = false; component.Age = 1; - if (seedHealth is float realSeedHealth) + if (seeds.HealthOverride != null) { - component.Health = realSeedHealth; + component.Health = seeds.HealthOverride.Value; } else { @@ -288,8 +287,18 @@ public sealed class PlantHolderSystem : EntitySystem } component.Health -= (_random.Next(3, 5) * 10); + + float? healthOverride; + if (component.Harvest) + { + healthOverride = null; + } + else + { + healthOverride = component.Health; + } component.Seed.Unique = false; - var seed = _botany.SpawnSeedPacket(component.Seed, Transform(args.User).Coordinates, args.User, component.Health); + var seed = _botany.SpawnSeedPacket(component.Seed, Transform(args.User).Coordinates, args.User, healthOverride); _randomHelper.RandomOffset(seed, 0.25f); var displayName = Loc.GetString(component.Seed.DisplayName); _popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message", From 57911975c70dafcc3af9dfa08a86c9acda472497 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 10 Apr 2024 17:52:33 +0000 Subject: [PATCH 06/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ef8cf9a297..5be7a78a6d 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Emisse - changes: - - message: Gemini - type: Remove - id: 5828 - time: '2024-01-30T10:54:55.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24728 - author: CrigCrag changes: - message: Added a new large salvage wreck. @@ -3825,3 +3818,10 @@ id: 6327 time: '2024-04-10T17:28:03.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26839 +- author: botanySupremist + changes: + - message: Clipping harvestable plants now yields undamaged seeds. + type: Add + id: 6328 + time: '2024-04-10T17:51:25.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/25541 From 1cdf05a7a7169ed655a294b6bbe24a75f58f62fa Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Wed, 10 Apr 2024 20:06:31 +0000 Subject: [PATCH 07/49] fix lots of door access (#26858) * dirty after calling SetAccesses * fix door access * D * pro ops * nukeop --------- Co-authored-by: deltanedas <@deltanedas:kde.org> --- .../Devices/Electronics/door_access.yml | 32 ++ .../Structures/Doors/Airlocks/access.yml | 296 +++++++++--------- .../Structures/Doors/Airlocks/airlocks.yml | 30 +- 3 files changed, 208 insertions(+), 150 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/door_access.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/door_access.yml index fc6d8e2697..0c876ebd0c 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Electronics/door_access.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Electronics/door_access.yml @@ -55,6 +55,14 @@ - type: AccessReader access: [["Hydroponics"]] +- type: entity + parent: DoorElectronics + id: DoorElectronicsLawyer + suffix: Lawyer, Locked + components: + - type: AccessReader + access: [["Lawyer"]] + - type: entity parent: DoorElectronics id: DoorElectronicsCaptain @@ -151,6 +159,14 @@ - type: AccessReader access: [["Command"]] +- type: entity + parent: DoorElectronics + id: DoorElectronicsCentralCommand + suffix: CentralCommand, Locked + components: + - type: AccessReader + access: [["CentralCommand"]] + - type: entity parent: DoorElectronics id: DoorElectronicsChiefMedicalOfficer @@ -207,6 +223,14 @@ - type: AccessReader access: [["Security"]] +- type: entity + parent: DoorElectronics + id: DoorElectronicsSecurityLawyer + suffix: Security/Lawyer, Locked + components: + - type: AccessReader + access: [["Security", "Lawyer"]] + - type: entity parent: DoorElectronics id: DoorElectronicsDetective @@ -255,6 +279,14 @@ - type: AccessReader access: [["SyndicateAgent"]] +- type: entity + parent: DoorElectronics + id: DoorElectronicsNukeop + suffix: Nukeop, Locked + components: + - type: AccessReader + access: [["NuclearOperative"]] + - type: entity parent: DoorElectronics id: DoorElectronicsRnDMed diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml index 8ffd18e7e2..5648d68d78 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/access.yml @@ -7,19 +7,20 @@ - type: ContainerFill containers: board: [ DoorElectronicsService ] - -- type: entity - parent: Airlock - id: AirlockLawyerLocked - suffix: Lawyer, Locked - components: - - type: AccessReader - access: [["Lawyer"]] - type: Wires layoutId: AirlockService - type: entity - parent: Airlock + parent: AirlockServiceLocked + id: AirlockLawyerLocked + suffix: Lawyer, Locked + components: + - type: ContainerFill + containers: + board: [ DoorElectronicsLawyer ] + +- type: entity + parent: AirlockServiceLocked id: AirlockTheatreLocked suffix: Theatre, Locked components: @@ -28,7 +29,7 @@ board: [ DoorElectronicsTheatre ] - type: entity - parent: Airlock + parent: AirlockServiceLocked id: AirlockChapelLocked suffix: Chapel, Locked components: @@ -37,7 +38,7 @@ board: [ DoorElectronicsChapel ] - type: entity - parent: Airlock + parent: AirlockServiceLocked id: AirlockJanitorLocked suffix: Janitor, Locked components: @@ -46,7 +47,7 @@ board: [ DoorElectronicsJanitor ] - type: entity - parent: Airlock + parent: AirlockServiceLocked id: AirlockKitchenLocked suffix: Kitchen, Locked components: @@ -55,7 +56,7 @@ board: [ DoorElectronicsKitchen ] - type: entity - parent: Airlock + parent: AirlockServiceLocked id: AirlockBarLocked suffix: Bar, Locked components: @@ -64,7 +65,7 @@ board: [ DoorElectronicsBar ] - type: entity - parent: Airlock + parent: AirlockServiceLocked id: AirlockHydroponicsLocked suffix: Hydroponics, Locked components: @@ -73,7 +74,7 @@ board: [ DoorElectronicsHydroponics ] - type: entity - parent: Airlock + parent: AirlockCommandLocked id: AirlockServiceCaptainLocked suffix: Captain, Locked components: @@ -122,16 +123,18 @@ id: AirlockExternalSyndicateLocked suffix: External, Syndicate, Locked components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockExternal id: AirlockExternalNukeopLocked suffix: External, Nukeop, Locked components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] - type: entity parent: AirlockFreezer @@ -156,10 +159,9 @@ id: AirlockFreezerHydroponicsLocked suffix: Hydroponics, Locked components: - - type: AccessReader - access: [["Hydroponics"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsHydroponics ] - type: entity parent: AirlockEngineering @@ -202,10 +204,9 @@ id: AirlockMiningLocked suffix: Mining(Salvage), Locked components: - - type: AccessReader - access: [["Salvage"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsSalvage ] - type: entity parent: AirlockMedical @@ -257,10 +258,9 @@ id: AirlockCentralCommandLocked suffix: Central Command, Locked components: - - type: AccessReader - access: [["CentralCommand"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsCentralCommand ] - type: entity parent: AirlockCommand @@ -270,8 +270,6 @@ - type: ContainerFill containers: board: [ DoorElectronicsCommand ] - - type: Wires - layoutId: AirlockCommand - type: entity parent: AirlockCommand @@ -344,8 +342,6 @@ - type: ContainerFill containers: board: [ DoorElectronicsSecurity ] - - type: Wires - layoutId: AirlockSecurity - type: entity parent: AirlockSecurity @@ -355,8 +351,6 @@ - type: ContainerFill containers: board: [ DoorElectronicsDetective ] - - type: Wires - layoutId: AirlockSecurity - type: entity parent: AirlockSecurity @@ -366,18 +360,15 @@ - type: ContainerFill containers: board: [ DoorElectronicsBrig ] - - type: Wires - layoutId: AirlockSecurity - type: entity parent: AirlockSecurity id: AirlockSecurityLawyerLocked suffix: Security/Lawyer, Locked components: - - type: AccessReader - access: [["Security"], ["Lawyer"]] - - type: Wires - layoutId: AirlockSecurity + - type: ContainerFill + containers: + board: [ DoorElectronicsSecurityLawyer ] - type: entity parent: AirlockSecurity @@ -417,26 +408,26 @@ - type: ContainerFill containers: board: [ DoorElectronicsService ] + - type: Wires + layoutId: AirlockService - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockLawyerGlassLocked suffix: Lawyer, Locked components: - - type: AccessReader - access: [["Lawyer"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsLawyer ] - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockTheatreGlassLocked suffix: Theatre, Locked components: - - type: AccessReader - access: [["Theatre"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsTheatre ] - type: entity parent: AirlockGlass @@ -470,16 +461,18 @@ id: AirlockExternalGlassSyndicateLocked suffix: External, Glass, Syndicate, Locked components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockExternalGlass id: AirlockExternalGlassNukeopLocked suffix: External, Glass, Nukeop, Locked components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] - type: entity parent: AirlockExternalGlass @@ -500,7 +493,7 @@ board: [ DoorElectronicsAtmospherics ] - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockKitchenGlassLocked suffix: Kitchen, Locked components: @@ -509,17 +502,16 @@ board: [ DoorElectronicsKitchen ] - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockJanitorGlassLocked suffix: Janitor, Locked components: - - type: AccessReader - access: [["Janitor"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsJanitor ] - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockHydroGlassLocked suffix: Hydroponics, Locked components: @@ -528,7 +520,7 @@ board: [ DoorElectronicsHydroponics ] - type: entity - parent: AirlockGlass + parent: AirlockServiceGlassLocked id: AirlockChapelGlassLocked suffix: Chapel, Locked components: @@ -577,20 +569,18 @@ id: AirlockMiningGlassLocked suffix: Mining(Salvage), Locked components: - - type: AccessReader - access: [["Salvage"]] - - type: Wires - layoutId: AirlockCargo + - type: ContainerFill + containers: + board: [ DoorElectronicsSalvage ] - type: entity parent: AirlockChemistryGlass id: AirlockChemistryGlassLocked suffix: Chemistry, Locked components: - - type: AccessReader - access: [["Chemistry"]] - - type: Wires - layoutId: AirlockMedical + - type: ContainerFill + containers: + board: [ DoorElectronicsChemistry ] - type: entity parent: AirlockMedicalGlass @@ -633,10 +623,9 @@ id: AirlockCentralCommandGlassLocked suffix: Central Command, Locked components: - - type: AccessReader - access: [["CentralCommand"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsCentralCommand ] - type: entity parent: AirlockCommandGlass @@ -742,10 +731,9 @@ id: AirlockSecurityLawyerGlassLocked suffix: Security/Lawyer, Locked components: - - type: AccessReader - access: [["Security"], ["Lawyer"]] - - type: Wires - layoutId: AirlockSecurity + - type: ContainerFill + containers: + board: [ DoorElectronicsSecurityLawyer ] - type: entity parent: AirlockSecurityGlass @@ -770,16 +758,18 @@ id: AirlockSyndicateGlassLocked suffix: Syndicate, Locked components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockSyndicateGlass id: AirlockSyndicateNukeopGlassLocked suffix: Nukeop, Locked components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] # Maintenance Hatches - type: entity @@ -855,7 +845,7 @@ board: [ DoorElectronicsAtmospherics ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintBarLocked suffix: Bar, Locked components: @@ -864,7 +854,7 @@ board: [ DoorElectronicsBar ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintChapelLocked suffix: Chapel, Locked components: @@ -873,7 +863,7 @@ board: [ DoorElectronicsChapel ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintHydroLocked suffix: Hydroponics, Locked components: @@ -882,7 +872,7 @@ board: [ DoorElectronicsHydroponics ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintJanitorLocked suffix: Janitor, Locked components: @@ -891,27 +881,27 @@ board: [ DoorElectronicsJanitor ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintLawyerLocked suffix: Lawyer, Locked components: - - type: AccessReader - access: [["Lawyer"]] - - type: Wires - layoutId: AirlockService + - type: ContainerFill + containers: + board: [ DoorElectronicsLawyer ] - type: entity parent: AirlockMaint id: AirlockMaintServiceLocked suffix: Service, Locked components: - - type: AccessReader - access: [["Service"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsService ] - type: Wires layoutId: AirlockService - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintTheatreLocked suffix: Theatre, Locked components: @@ -920,7 +910,7 @@ board: [ DoorElectronicsTheatre ] - type: entity - parent: AirlockMaint + parent: AirlockMaintServiceLocked id: AirlockMaintKitchenLocked suffix: Kitchen, Locked components: @@ -945,9 +935,11 @@ - type: ContainerFill containers: board: [ DoorElectronicsMedical ] + - type: Wires + layoutId: AirlockMedical - type: entity - parent: AirlockMaint + parent: AirlockMaintMedLocked id: AirlockMaintChemLocked suffix: Chemistry, Locked components: @@ -963,9 +955,11 @@ - type: ContainerFill containers: board: [ DoorElectronicsResearch ] + - type: Wires + layoutId: AirlockScience - type: entity - parent: AirlockMaint + parent: AirlockMaintRnDLocked id: AirlockMaintRnDMedLocked suffix: Medical/Science, Locked components: @@ -981,9 +975,11 @@ - type: ContainerFill containers: board: [ DoorElectronicsSecurity ] + - type: Wires + layoutId: AirlockSecurity - type: entity - parent: AirlockMaint + parent: AirlockMaintSecLocked id: AirlockMaintDetectiveLocked suffix: Detective, Locked components: @@ -992,7 +988,7 @@ board: [ DoorElectronicsDetective ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintHOPLocked suffix: HeadOfPersonnel, Locked components: @@ -1001,7 +997,7 @@ board: [ DoorElectronicsHeadOfPersonnel ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintCaptainLocked suffix: Captain, Locked components: @@ -1010,70 +1006,69 @@ board: [ DoorElectronicsCaptain ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintChiefEngineerLocked suffix: ChiefEngineer, Locked components: - - type: AccessReader - access: [["ChiefEngineer"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsChiefEngineer ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintChiefMedicalOfficerLocked suffix: ChiefMedicalOfficer, Locked components: - - type: AccessReader - access: [["ChiefMedicalOfficer"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsChiefMedicalOfficer ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintHeadOfSecurityLocked suffix: HeadOfSecurity, Locked components: - - type: AccessReader - access: [["HeadOfSecurity"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsHeadOfSecurity ] - type: entity - parent: AirlockMaint + parent: AirlockMaintCommandLocked id: AirlockMaintResearchDirectorLocked suffix: ResearchDirector, Locked components: - - type: AccessReader - access: [["ResearchDirector"]] - - type: Wires - layoutId: AirlockCommand + - type: ContainerFill + containers: + board: [ DoorElectronicsResearchDirector ] - type: entity parent: AirlockMaint id: AirlockMaintArmoryLocked suffix: Armory, Locked components: - - type: AccessReader - access: [["Armory"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsArmory ] - type: Wires - layoutId: AirlockSecurity + layoutId: AirlockArmory - type: entity parent: AirlockSyndicate id: AirlockSyndicateLocked suffix: Syndicate, Locked components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockSyndicate id: AirlockSyndicateNukeopLocked suffix: Nukeop, Locked components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] # Shuttle airlocks - type: entity @@ -1090,16 +1085,18 @@ id: AirlockExternalShuttleSyndicateLocked suffix: External, Docking, Syndicate, Locked components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockShuttleSyndicate id: AirlockExternalShuttleNukeopLocked suffix: External, Docking, Nukeop, Locked components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] - type: entity parent: AirlockGlassShuttle @@ -1115,42 +1112,44 @@ id: AirlockExternalGlassShuttleSyndicateLocked suffix: Syndicate, Locked, Glass components: - - type: AccessReader - access: [["SyndicateAgent"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsSyndicateAgent ] - type: entity parent: AirlockGlassShuttleSyndicate id: AirlockExternalGlassShuttleNukeopLocked suffix: Nukeop, Locked, Glass components: - - type: AccessReader - access: [["NuclearOperative"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsNukeop ] - type: entity parent: AirlockGlassShuttle id: AirlockExternalGlassShuttleEmergencyLocked suffix: External, Emergency, Glass, Docking, Locked components: - - type: PriorityDock - tag: DockEmergency - - type: ContainerFill - containers: - board: [ DoorElectronicsExternal ] + - type: PriorityDock + tag: DockEmergency + - type: ContainerFill + containers: + board: [ DoorElectronicsExternal ] - type: entity parent: AirlockGlassShuttle id: AirlockExternalGlassShuttleArrivals suffix: External, Arrivals, Glass, Docking components: - - type: PriorityDock - tag: DockArrivals + - type: PriorityDock + tag: DockArrivals - type: entity parent: AirlockGlassShuttle id: AirlockExternalGlassShuttleEscape suffix: External, Escape 3x4, Glass, Docking components: - - type: GridFill + - type: GridFill #HighSecDoors - type: entity @@ -1158,8 +1157,9 @@ id: HighSecCentralCommandLocked suffix: Central Command, Locked components: - - type: AccessReader - access: [["CentralCommand"]] + - type: ContainerFill + containers: + board: [ DoorElectronicsCentralCommand ] - type: entity parent: HighSecDoor diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml index ff02e315cb..aead307d75 100644 --- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml +++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml @@ -5,6 +5,8 @@ components: - type: Sprite sprite: Structures/Doors/Airlocks/Standard/freezer.rsi + - type: Wires + layoutId: AirlockService - type: entity parent: Airlock @@ -15,6 +17,8 @@ sprite: Structures/Doors/Airlocks/Standard/engineering.rsi - type: PaintableAirlock department: Engineering + - type: Wires + layoutId: AirlockEngineering - type: entity parent: AirlockEngineering @@ -33,6 +37,8 @@ sprite: Structures/Doors/Airlocks/Standard/cargo.rsi - type: PaintableAirlock department: Cargo + - type: Wires + layoutId: AirlockCargo - type: entity parent: Airlock @@ -43,6 +49,8 @@ sprite: Structures/Doors/Airlocks/Standard/medical.rsi - type: PaintableAirlock department: Medical + - type: Wires + layoutId: AirlockMedical - type: entity parent: AirlockMedical @@ -66,6 +74,8 @@ sprite: Structures/Doors/Airlocks/Standard/science.rsi - type: PaintableAirlock department: Science + - type: Wires + layoutId: AirlockScience - type: entity parent: Airlock @@ -78,6 +88,8 @@ securityLevel: medSecurity - type: PaintableAirlock department: Command + - type: Wires + layoutId: AirlockCommand - type: entity parent: Airlock @@ -88,6 +100,8 @@ sprite: Structures/Doors/Airlocks/Standard/security.rsi - type: PaintableAirlock department: Security + - type: Wires + layoutId: AirlockSecurity - type: entity parent: Airlock @@ -112,6 +126,8 @@ components: - type: Sprite sprite: Structures/Doors/Airlocks/Standard/mining.rsi + - type: Wires + layoutId: AirlockCargo - type: entity parent: AirlockCommand # if you get centcom door somehow it counts as command, also inherit panel @@ -147,6 +163,8 @@ sprite: Structures/Doors/Airlocks/Glass/engineering.rsi - type: PaintableAirlock department: Engineering + - type: Wires + layoutId: AirlockEngineering - type: entity parent: AirlockGlass @@ -173,6 +191,8 @@ sprite: Structures/Doors/Airlocks/Glass/cargo.rsi - type: PaintableAirlock department: Cargo + - type: Wires + layoutId: AirlockCargo - type: entity parent: AirlockGlass @@ -183,6 +203,8 @@ sprite: Structures/Doors/Airlocks/Glass/medical.rsi - type: PaintableAirlock department: Medical + - type: Wires + layoutId: AirlockMedical - type: entity parent: AirlockMedicalGlass @@ -206,6 +228,8 @@ sprite: Structures/Doors/Airlocks/Glass/science.rsi - type: PaintableAirlock department: Science + - type: Wires + layoutId: AirlockScience - type: entity parent: AirlockGlass @@ -218,6 +242,8 @@ department: Command - type: WiresPanelSecurity securityLevel: medSecurity + - type: Wires + layoutId: AirlockCommand - type: entity parent: AirlockGlass @@ -228,6 +254,8 @@ sprite: Structures/Doors/Airlocks/Glass/security.rsi - type: PaintableAirlock department: Security + - type: Wires + layoutId: AirlockSecurity - type: entity parent: AirlockSecurityGlass # see standard @@ -252,5 +280,3 @@ components: - type: Sprite sprite: Structures/Doors/Airlocks/Glass/centcomm.rsi - - type: WiresPanelSecurity - securityLevel: medSecurity \ No newline at end of file From f1cbf934b0142518e71189359950c85f16c13a0d Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 10 Apr 2024 20:07:37 +0000 Subject: [PATCH 08/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 5be7a78a6d..8e3e89e986 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: CrigCrag - changes: - - message: Added a new large salvage wreck. - type: Add - id: 5829 - time: '2024-01-30T10:56:38.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24656 - author: themias changes: - message: Fixed Centcom cargo gifts not being fulfilled @@ -3825,3 +3818,10 @@ id: 6328 time: '2024-04-10T17:51:25.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/25541 +- author: deltanedas + changes: + - message: Fixed some doors having no access when they should. + type: Fix + id: 6329 + time: '2024-04-10T20:06:31.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26858 From 4017f9bd28251a3cbf3c0fb34499c6b821051498 Mon Sep 17 00:00:00 2001 From: Flareguy <78941145+Flareguy@users.noreply.github.com> Date: Wed, 10 Apr 2024 16:20:05 -0500 Subject: [PATCH 09/49] Add emergency nitrogen lockers (#26752) --- .../Prototypes/Catalog/Fills/Lockers/misc.yml | 15 +++++++++++++++ .../Structures/Storage/Closets/closets.yml | 13 +++++++++++++ .../Structures/Storage/closet.rsi/meta.json | 5 ++++- .../Structures/Storage/closet.rsi/n2_door.png | Bin 0 -> 357 bytes 4 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 Resources/Textures/Structures/Storage/closet.rsi/n2_door.png diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml index d8b6004ec3..fac36ba710 100644 --- a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml +++ b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml @@ -61,6 +61,21 @@ - id: BoxMRE prob: 0.1 +- type: entity + id: ClosetEmergencyN2FilledRandom + parent: ClosetEmergencyN2 + suffix: Filled, Random + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyNitrogenTankFilled + prob: 0.80 + orGroup: EmergencyTankOrRegularTank + - id: NitrogenTankFilled + prob: 0.20 + orGroup: EmergencyTankOrRegularTank + - type: entity id: ClosetFireFilled parent: ClosetFire diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml index 5fda0ddbe2..fb00db7aa6 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/closets.yml @@ -37,6 +37,19 @@ stateDoorOpen: emergency_open stateDoorClosed: emergency_door +# Emergency N2 closet +- type: entity + id: ClosetEmergencyN2 + name: emergency nitrogen closet + parent: ClosetSteelBase + description: It's full of life-saving equipment. Assuming, that is, that you breathe nitrogen. + components: + - type: Appearance + - type: EntityStorageVisuals + stateBaseClosed: fire + stateDoorOpen: fire_open + stateDoorClosed: n2_door + # Fire safety closet - type: entity id: ClosetFire diff --git a/Resources/Textures/Structures/Storage/closet.rsi/meta.json b/Resources/Textures/Structures/Storage/closet.rsi/meta.json index cf0c204447..c52bc13540 100644 --- a/Resources/Textures/Structures/Storage/closet.rsi/meta.json +++ b/Resources/Textures/Structures/Storage/closet.rsi/meta.json @@ -4,7 +4,7 @@ "x": 32, "y": 32 }, - "copyright": "Taken from tgstation, brigmedic locker is a resprited CMO locker by PuroSlavKing (Github)", + "copyright": "Taken from tgstation, brigmedic locker is a resprited CMO locker by PuroSlavKing (Github), n2_door state modified by Flareguy from fire_door, using sprites from /vg/station at https://github.com/vgstation-coders/vgstation13/commit/02b9f6894af4419c9f7e699a22c402b086d8067e", "license": "CC-BY-SA-3.0", "states": [ { @@ -398,6 +398,9 @@ { "name": "mixed_door" }, + { + "name": "n2_door" + }, { "name": "oldcloset" }, diff --git a/Resources/Textures/Structures/Storage/closet.rsi/n2_door.png b/Resources/Textures/Structures/Storage/closet.rsi/n2_door.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d9499f10ab3a034df4055af67a163859fd68d3 GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCikV0(?STA8y#PMMdT0$&=5YKi8FGm?I^1LRa_N(xoR>t~_!0@ZrUan^IFd zLqaM9MfB)x#KzYsrkH}&M2EM}} z%y>M1MG8=GlBbJfh=u>%zK48G20U!;(P|w$Ti^d*S2MHpTFTUTwPW0pPs(0bKgyLAq>xb#0h^knT`$K6g%+Q+W@_bqGS4?6eLB+>J4 zf}MF<@B!A-OE=D7(u*+M6Xa#cy6a$>DeFFwDOW`*`17XDIs72*r@)^>E$WUB3|M0> dJXZ*~%V_e{;ZwMjj404`44$rjF6*2UngH?&jmZE2 literal 0 HcmV?d00001 From 8c16b466132d946578e7ab4c8c92dd3ddfe94f89 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 10 Apr 2024 21:21:11 +0000 Subject: [PATCH 10/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 8e3e89e986..dcf08cc9a7 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: themias - changes: - - message: Fixed Centcom cargo gifts not being fulfilled - type: Fix - id: 5830 - time: '2024-01-30T11:05:20.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24701 - author: Scribbles0 changes: - message: You can now make anomaly cores orbit you. @@ -3825,3 +3818,11 @@ id: 6329 time: '2024-04-10T20:06:31.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26858 +- author: Flareguy + changes: + - message: Added emergency nitrogen lockers. You can scarcely find them in public + areas, or scattered around in maintenance tunnels. + type: Add + id: 6330 + time: '2024-04-10T21:20:05.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26752 From 9eb1e12022c22d0a8b431e4def3a8178aa1924d8 Mon Sep 17 00:00:00 2001 From: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com> Date: Wed, 10 Apr 2024 20:22:29 -0400 Subject: [PATCH 11/49] Update ashtray to allow all cigarettes / cigars (#26864) * Update ashtray to allow all cigarettes / cigars This also includes joints (as they are technically cigarettes) * ? --- Resources/Prototypes/Entities/Objects/Decoration/ashtray.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Decoration/ashtray.yml b/Resources/Prototypes/Entities/Objects/Decoration/ashtray.yml index 613b6cc67c..61554d0621 100644 --- a/Resources/Prototypes/Entities/Objects/Decoration/ashtray.yml +++ b/Resources/Prototypes/Entities/Objects/Decoration/ashtray.yml @@ -17,6 +17,8 @@ whitelist: tags: - Burnt + - Cigarette + - Cigar maxItemSize: Tiny grid: - 0,0,9,0 @@ -27,4 +29,4 @@ fillBaseName: icon maxFillLevels: 10 - type: Appearance - - type: Dumpable \ No newline at end of file + - type: Dumpable From 2bcdb608a3ddd9c91a1169d3f0d2d5b31aaebc88 Mon Sep 17 00:00:00 2001 From: Jark255 Date: Thu, 11 Apr 2024 15:21:15 +0300 Subject: [PATCH 12/49] Fix door electronics configurator usage (#26888) * allow usage of network configurator for door electronics * add checks for "allowed" items --- Content.Server/UserInterface/ActivatableUISystem.cs | 6 ++++++ .../Entities/Objects/Devices/Electronics/door.yml | 2 +- Resources/Prototypes/Entities/Objects/Tools/tools.yml | 10 +++++++--- Resources/Prototypes/tags.yml | 5 ++++- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Content.Server/UserInterface/ActivatableUISystem.cs b/Content.Server/UserInterface/ActivatableUISystem.cs index e3a11af429..5f2314df90 100644 --- a/Content.Server/UserInterface/ActivatableUISystem.cs +++ b/Content.Server/UserInterface/ActivatableUISystem.cs @@ -93,6 +93,9 @@ public sealed partial class ActivatableUISystem : EntitySystem if (component.InHandsOnly) return; + if (component.AllowedItems != null) + return; + args.Handled = InteractUI(args.User, uid, component); } @@ -104,6 +107,9 @@ public sealed partial class ActivatableUISystem : EntitySystem if (component.RightClickOnly) return; + if (component.AllowedItems != null) + return; + args.Handled = InteractUI(args.User, uid, component); } diff --git a/Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml b/Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml index 16713a6692..670b556e35 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml @@ -18,7 +18,7 @@ key: enum.DoorElectronicsConfigurationUiKey.Key allowedItems: tags: - - Multitool + - DoorElectronicsConfigurator - type: UserInterface interfaces: - key: enum.DoorElectronicsConfigurationUiKey.Key diff --git a/Resources/Prototypes/Entities/Objects/Tools/tools.yml b/Resources/Prototypes/Entities/Objects/Tools/tools.yml index 2b11c211e8..b3103816eb 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/tools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/tools.yml @@ -225,6 +225,7 @@ - type: Tag tags: - Multitool + - DoorElectronicsConfigurator - type: PhysicalComposition materialComposition: Steel: 100 @@ -266,6 +267,9 @@ - type: ActivatableUI key: enum.NetworkConfiguratorUiKey.List inHandsOnly: true + - type: Tag + tags: + - DoorElectronicsConfigurator - type: UserInterface interfaces: - key: enum.NetworkConfiguratorUiKey.List @@ -343,13 +347,13 @@ description: The rapid construction device can be used to quickly place and remove various station structures and fixtures. Requires compressed matter to function. components: - type: RCD - availablePrototypes: + availablePrototypes: - WallSolid - FloorSteel - Plating - Catwalk - Grille - - Window + - Window - WindowDirectional - WindowReinforcedDirectional - ReinforcedWindow @@ -398,7 +402,7 @@ - type: LimitedCharges charges: 0 - type: RCD - availablePrototypes: + availablePrototypes: - WallSolid - FloorSteel - Plating diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 961912d609..021427014c 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -523,6 +523,9 @@ - type: Tag id: DoorElectronics +- type: Tag + id: DoorElectronicsConfigurator + - type: Tag id: DrinkBottle @@ -1177,7 +1180,7 @@ - type: Tag id: SuitEVA - + - type: Tag id: Sunglasses From 210ed3ece4230a7fa31c12a43f4fdee0f0614915 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 11 Apr 2024 14:22:21 +0200 Subject: [PATCH 13/49] Fix TEG assert (#26881) It's possible to trigger this by stacking it weirdly with the spawn panel. Just make it bail instead. --- Content.Server/Power/Generation/Teg/TegNodeGroup.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs index ed6b46e304..3c937f8f71 100644 --- a/Content.Server/Power/Generation/Teg/TegNodeGroup.cs +++ b/Content.Server/Power/Generation/Teg/TegNodeGroup.cs @@ -66,11 +66,16 @@ public sealed class TegNodeGroup : BaseNodeGroup public override void LoadNodes(List groupNodes) { - DebugTools.Assert(groupNodes.Count <= 3, "The TEG has at most 3 parts"); DebugTools.Assert(_entityManager != null); base.LoadNodes(groupNodes); + if (groupNodes.Count > 3) + { + // Somehow got more TEG parts. Probably shenanigans. Bail. + return; + } + Generator = groupNodes.OfType().SingleOrDefault(); if (Generator != null) { From 00dc99769c2442410204c1ea57bb24cc8353c16a Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 11 Apr 2024 12:22:21 +0000 Subject: [PATCH 14/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index dcf08cc9a7..741325d4b5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Scribbles0 - changes: - - message: You can now make anomaly cores orbit you. - type: Add - id: 5831 - time: '2024-01-30T11:12:24.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24716 - author: MIXnikita changes: - message: New sound effects for some shuttle guns @@ -3826,3 +3819,11 @@ id: 6330 time: '2024-04-10T21:20:05.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26752 +- author: Jark255 + changes: + - message: NT removed developers' configuration device from all doors electronics + and patched network configurators to work with them + type: Fix + id: 6331 + time: '2024-04-11T12:21:15.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26888 From fc5a90be0da4801aa7ff1fbc996c2f55f8cb7ae7 Mon Sep 17 00:00:00 2001 From: chromiumboy <50505512+chromiumboy@users.noreply.github.com> Date: Thu, 11 Apr 2024 07:26:34 -0500 Subject: [PATCH 15/49] Bug fix for deconstructing tiles and lattice with RCDs (#26863) * Fixed mixed deconstruction times for tiles and lattice * Lattice and power cables can be deconstructed instantly --- Content.Shared/RCD/Systems/RCDSystem.cs | 2 +- .../Prototypes/Entities/Structures/Power/cable_terminal.yml | 4 ++-- Resources/Prototypes/Entities/Structures/Power/cables.yml | 4 ++-- Resources/Prototypes/RCD/rcd.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Content.Shared/RCD/Systems/RCDSystem.cs b/Content.Shared/RCD/Systems/RCDSystem.cs index bf312ee6fc..f172060ded 100644 --- a/Content.Shared/RCD/Systems/RCDSystem.cs +++ b/Content.Shared/RCD/Systems/RCDSystem.cs @@ -175,7 +175,7 @@ public class RCDSystem : EntitySystem else { var deconstructedTile = _mapSystem.GetTileRef(mapGridData.Value.GridUid, mapGridData.Value.Component, mapGridData.Value.Location); - var protoName = deconstructedTile.IsSpace() ? _deconstructTileProto : _deconstructLatticeProto; + var protoName = !deconstructedTile.IsSpace() ? _deconstructTileProto : _deconstructLatticeProto; if (_protoManager.TryIndex(protoName, out var deconProto)) { diff --git a/Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml b/Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml index 2e8f047c21..cc33791174 100644 --- a/Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml +++ b/Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml @@ -19,8 +19,8 @@ damageModifierSet: Metallic - type: RCDDeconstructable cost: 2 - delay: 2 - fx: EffectRCDDeconstruct2 + delay: 0 + fx: EffectRCDConstruct0 - type: Destructible thresholds: - trigger: diff --git a/Resources/Prototypes/Entities/Structures/Power/cables.yml b/Resources/Prototypes/Entities/Structures/Power/cables.yml index a81c89de0f..d064cc187c 100644 --- a/Resources/Prototypes/Entities/Structures/Power/cables.yml +++ b/Resources/Prototypes/Entities/Structures/Power/cables.yml @@ -45,8 +45,8 @@ node: power - type: RCDDeconstructable cost: 2 - delay: 2 - fx: EffectRCDDeconstruct2 + delay: 0 + fx: EffectRCDConstruct0 - type: entity parent: CableBase diff --git a/Resources/Prototypes/RCD/rcd.yml b/Resources/Prototypes/RCD/rcd.yml index bc1aa91d28..500b5f59bf 100644 --- a/Resources/Prototypes/RCD/rcd.yml +++ b/Resources/Prototypes/RCD/rcd.yml @@ -17,9 +17,9 @@ name: rcd-component-deconstruct mode: Deconstruct cost: 2 - delay: 1 + delay: 0 rotation: Camera - fx: EffectRCDDeconstruct2 + fx: EffectRCDConstruct0 - type: rcd id: DeconstructTile # Hidden prototype - do not add to RCDs From 036abacbb731c0d1128a4c6cd1658f64dd488985 Mon Sep 17 00:00:00 2001 From: keronshb <54602815+keronshb@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:40:02 -0400 Subject: [PATCH 16/49] Immovable Rod changes (#26757) --- .../ImmovableRod/ImmovableRodComponent.cs | 13 +++++++ .../ImmovableRod/ImmovableRodSystem.cs | 29 +++++++++++--- .../Entities/Objects/Fun/immovable_rod.yml | 38 +++++++++++++++++-- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Content.Server/ImmovableRod/ImmovableRodComponent.cs b/Content.Server/ImmovableRod/ImmovableRodComponent.cs index f360591479..05fa3d9d9b 100644 --- a/Content.Server/ImmovableRod/ImmovableRodComponent.cs +++ b/Content.Server/ImmovableRod/ImmovableRodComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.Damage; using Robust.Shared.Audio; namespace Content.Server.ImmovableRod; @@ -36,4 +37,16 @@ public sealed partial class ImmovableRodComponent : Component /// [DataField("destroyTiles")] public bool DestroyTiles = true; + + /// + /// If true, this will gib & delete bodies + /// + [DataField] + public bool ShouldGib = true; + + /// + /// Damage done, if not gibbing + /// + [DataField] + public DamageSpecifier? Damage; } diff --git a/Content.Server/ImmovableRod/ImmovableRodSystem.cs b/Content.Server/ImmovableRod/ImmovableRodSystem.cs index 429361cd8c..c8f36e864c 100644 --- a/Content.Server/ImmovableRod/ImmovableRodSystem.cs +++ b/Content.Server/ImmovableRod/ImmovableRodSystem.cs @@ -1,9 +1,10 @@ using Content.Server.Body.Systems; +using Content.Server.Polymorph.Components; using Content.Server.Popups; using Content.Shared.Body.Components; +using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Popups; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Map; using Robust.Shared.Map.Components; @@ -22,6 +23,8 @@ public sealed class ImmovableRodSystem : EntitySystem [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Update(float frameTime) { @@ -64,11 +67,11 @@ public sealed class ImmovableRodSystem : EntitySystem var vel = component.DirectionOverride.Degrees switch { 0f => _random.NextVector2(component.MinSpeed, component.MaxSpeed), - _ => xform.WorldRotation.RotateVec(component.DirectionOverride.ToVec()) * _random.NextFloat(component.MinSpeed, component.MaxSpeed) + _ => _transform.GetWorldRotation(uid).RotateVec(component.DirectionOverride.ToVec()) * _random.NextFloat(component.MinSpeed, component.MaxSpeed) }; _physics.ApplyLinearImpulse(uid, vel, body: phys); - xform.LocalRotation = (vel - xform.WorldPosition).ToWorldAngle() + MathHelper.PiOver2; + xform.LocalRotation = (vel - _transform.GetWorldPosition(uid)).ToWorldAngle() + MathHelper.PiOver2; } } @@ -94,12 +97,28 @@ public sealed class ImmovableRodSystem : EntitySystem return; } - // gib em + // dont delete/hurt self if polymoprhed into a rod + if (TryComp(uid, out var polymorphed)) + { + if (polymorphed.Parent == ent) + return; + } + + // gib or damage em if (TryComp(ent, out var body)) { component.MobCount++; - _popup.PopupEntity(Loc.GetString("immovable-rod-penetrated-mob", ("rod", uid), ("mob", ent)), uid, PopupType.LargeCaution); + + if (!component.ShouldGib) + { + if (component.Damage == null || !TryComp(ent, out var damageable)) + return; + + _damageable.SetDamage(ent, damageable, component.Damage); + return; + } + _bodySystem.GibBody(ent, body: body); return; } diff --git a/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml index f46abe8a5a..aad12a5025 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/immovable_rod.yml @@ -11,8 +11,6 @@ state: icon noRot: false - type: ImmovableRod - - type: TimedDespawn - lifetime: 30.0 - type: Physics bodyType: Dynamic linearDamping: 0 @@ -36,8 +34,15 @@ location: immovable rod - type: entity + id: ImmovableRodDespawn parent: ImmovableRod + components: + - type: TimedDespawn + lifetime: 30.0 + +- type: entity id: ImmovableRodSlow + parent: ImmovableRodDespawn suffix: Slow components: - type: ImmovableRod @@ -45,7 +50,7 @@ maxSpeed: 5 - type: entity - parent: ImmovableRod + parent: ImmovableRodDespawn id: ImmovableRodKeepTiles suffix: Keep Tiles components: @@ -53,6 +58,33 @@ destroyTiles: false hitSoundProbability: 1.0 +# For Wizard Polymorph +- type: entity + parent: ImmovableRod + id: ImmovableRodWizard + suffix: Wizard + components: + - type: ImmovableRod + minSpeed: 35 + destroyTiles: false + randomizeVelocity: false + shouldGib: false + damage: + types: + Blunt: 200 + - type: MovementIgnoreGravity + gravityState: true + - type: InputMover + - type: MovementSpeedModifier + weightlessAcceleration: 5 + weightlessModifier: 2 + weightlessFriction: 0 + friction: 0 + frictionNoInput: 0 + - type: CanMoveInAir + - type: MovementAlwaysTouching + - type: NoSlip + - type: entity parent: ImmovableRodKeepTiles id: ImmovableRodKeepTilesStill From 75d3502d267d2050b4f9db1a4c0260c9fb6205e9 Mon Sep 17 00:00:00 2001 From: "Mr. 27" <45323883+Dutch-VanDerLinde@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:11:13 -0400 Subject: [PATCH 17/49] fix evil roleplay changelog (#26893) agh --- Resources/Changelog/Changelog.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 741325d4b5..ebf8310e7b 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -3821,8 +3821,7 @@ url: https://github.com/space-wizards/space-station-14/pull/26752 - author: Jark255 changes: - - message: NT removed developers' configuration device from all doors electronics - and patched network configurators to work with them + - message: Door electronics now require network configurators to change the access of them, rather by hand. type: Fix id: 6331 time: '2024-04-11T12:21:15.0000000+00:00' From 9d62b3c3e690cdda48143774a5e5db853894e1b8 Mon Sep 17 00:00:00 2001 From: lunarcomets <140772713+lunarcomets@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:48:46 -0700 Subject: [PATCH 18/49] Cryogenic storage tweaks (#26813) * make cryo remove crewmember's station record when going to cryo * Revert "make cryo remove crewmember's station record when going to cryo" This reverts commit 9ac9707289b5e553e3015c8c3ef88a78439977c6. * make cryo remove crewmember from station records when the mind is removed from the body * add stationwide announcement for people cryoing (remember to change pr title and desc) * minor changes * announcement actually shows job now * requested changes * get outta here derivative --- .../Bed/Cryostorage/CryostorageSystem.cs | 42 ++++++++++++++++--- .../bed/cryostorage/cryogenic-storage.ftl | 5 +++ 2 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl diff --git a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs index a3b7fb8d67..bb2ab4099d 100644 --- a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs +++ b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs @@ -1,11 +1,16 @@ +using System.Globalization; using System.Linq; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Hands.Systems; using Content.Server.Inventory; using Content.Server.Popups; +using Content.Server.Chat.Systems; using Content.Server.Station.Components; using Content.Server.Station.Systems; +using Content.Server.StationRecords; +using Content.Server.StationRecords.Systems; +using Content.Shared.StationRecords; using Content.Shared.UserInterface; using Content.Shared.Access.Systems; using Content.Shared.Bed.Cryostorage; @@ -32,6 +37,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly AudioSystem _audio = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; + [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly ClimbSystem _climb = default!; [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly GameTicker _gameTicker = default!; @@ -40,6 +46,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem [Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly StationSystem _station = default!; [Dependency] private readonly StationJobsSystem _stationJobs = default!; + [Dependency] private readonly StationRecordsSystem _stationRecords = default!; [Dependency] private readonly TransformSystem _transform = default!; [Dependency] private readonly UserInterfaceSystem _ui = default!; @@ -163,26 +170,30 @@ public sealed class CryostorageSystem : SharedCryostorageSystem { var comp = ent.Comp; var cryostorageEnt = ent.Comp.Cryostorage; + + var station = _station.GetOwningStation(ent); + var name = Name(ent.Owner); + if (!TryComp(cryostorageEnt, out var cryostorageComponent)) return; // if we have a session, we use that to add back in all the job slots the player had. if (userId != null) { - foreach (var station in _station.GetStationsSet()) + foreach (var uniqueStation in _station.GetStationsSet()) { - if (!TryComp(station, out var stationJobs)) + if (!TryComp(uniqueStation, out var stationJobs)) continue; - if (!_stationJobs.TryGetPlayerJobs(station, userId.Value, out var jobs, stationJobs)) + if (!_stationJobs.TryGetPlayerJobs(uniqueStation, userId.Value, out var jobs, stationJobs)) continue; foreach (var job in jobs) { - _stationJobs.TryAdjustJobSlot(station, job, 1, clamp: true); + _stationJobs.TryAdjustJobSlot(uniqueStation, job, 1, clamp: true); } - _stationJobs.TryRemovePlayerJobs(station, userId.Value, stationJobs); + _stationJobs.TryRemovePlayerJobs(uniqueStation, userId.Value, stationJobs); } } @@ -203,12 +214,33 @@ public sealed class CryostorageSystem : SharedCryostorageSystem _gameTicker.OnGhostAttempt(mind.Value, false); } } + comp.AllowReEnteringBody = false; _transform.SetParent(ent, PausedMap.Value); cryostorageComponent.StoredPlayers.Add(ent); Dirty(ent, comp); UpdateCryostorageUIState((cryostorageEnt.Value, cryostorageComponent)); AdminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent):player} was entered into cryostorage inside of {ToPrettyString(cryostorageEnt.Value)}"); + + if (!TryComp(station, out var stationRecords)) + return; + + var key = new StationRecordKey(_stationRecords.GetRecordByName(station.Value, name) ?? default(uint), station.Value); + var jobName = "Unknown"; + + if (_stationRecords.TryGetRecord(key, out var entry, stationRecords)) + jobName = entry.JobTitle; + + _stationRecords.RemoveRecord(key, stationRecords); + + _chatSystem.DispatchStationAnnouncement(station.Value, + Loc.GetString( + "earlyleave-cryo-announcement", + ("character", name), + ("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName)) + ), Loc.GetString("earlyleave-cryo-sender"), + playDefaultSound: false + ); } private void HandleCryostorageReconnection(Entity entity) diff --git a/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl b/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl new file mode 100644 index 0000000000..8de5f9019b --- /dev/null +++ b/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl @@ -0,0 +1,5 @@ + +### Announcement + +earlyleave-cryo-announcement = {$character} ({$job}) has entered cryogenic storage! +earlyleave-cryo-sender = Station From 6fa90e06c737d449f354b727550a5d1e13aeae44 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 11 Apr 2024 20:49:54 +0000 Subject: [PATCH 19/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ebf8310e7b..6c741b4017 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: MIXnikita - changes: - - message: New sound effects for some shuttle guns - type: Tweak - id: 5832 - time: '2024-01-30T13:28:17.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24714 - author: themias changes: - message: Stun batons now toggle off after being drained by an EMP. @@ -3821,8 +3814,17 @@ url: https://github.com/space-wizards/space-station-14/pull/26752 - author: Jark255 changes: - - message: Door electronics now require network configurators to change the access of them, rather by hand. + - message: Door electronics now require network configurators to change the access + of them, rather by hand. type: Fix id: 6331 time: '2024-04-11T12:21:15.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26888 +- author: lunarcomets + changes: + - message: Cryogenic sleep units now remove crew members from the manifest, and + announce when crew members go into cryogenic storage. + type: Tweak + id: 6332 + time: '2024-04-11T20:48:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26813 From 8e9d2744f3d196fc11e88a4755f98cac8ad8dbee Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 12 Apr 2024 02:30:34 -0400 Subject: [PATCH 20/49] Fix potted plant popup/sfx spam (#26901) Fixed potted plant hide popup/sfx spam. --- Content.Shared/Plants/PottedPlantHideSystem.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Content.Shared/Plants/PottedPlantHideSystem.cs b/Content.Shared/Plants/PottedPlantHideSystem.cs index fd256fd926..cbe052f8d5 100644 --- a/Content.Shared/Plants/PottedPlantHideSystem.cs +++ b/Content.Shared/Plants/PottedPlantHideSystem.cs @@ -31,7 +31,7 @@ namespace Content.Shared.Plants if (args.Handled) return; - Rustle(uid, component); + Rustle(uid, component, args.User); args.Handled = _stashSystem.TryHideItem(uid, args.User, args.Used); } @@ -40,24 +40,24 @@ namespace Content.Shared.Plants if (args.Handled) return; - Rustle(uid, component); + Rustle(uid, component, args.User); var gotItem = _stashSystem.TryGetItem(uid, args.User); if (!gotItem) { var msg = Loc.GetString("potted-plant-hide-component-interact-hand-got-no-item-message"); - _popupSystem.PopupEntity(msg, uid, args.User); + _popupSystem.PopupClient(msg, uid, args.User); } args.Handled = gotItem; } - private void Rustle(EntityUid uid, PottedPlantHideComponent? component = null) + private void Rustle(EntityUid uid, PottedPlantHideComponent? component = null, EntityUid? user = null) { if (!Resolve(uid, ref component)) return; - _audio.PlayPvs(component.RustleSound, uid, AudioParams.Default.WithVariation(0.25f)); + _audio.PlayPredicted(component.RustleSound, uid, user, AudioParams.Default.WithVariation(0.25f)); } } } From 264bf7199d805bd07dbdccc4345c672b19df9333 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Fri, 12 Apr 2024 02:42:20 -0400 Subject: [PATCH 21/49] Allow advertisement timers to prewarm (#26900) Allow advertisement timers to prewarm. --- Content.Server/Advertise/Components/AdvertiseComponent.cs | 8 ++++++++ Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Content.Server/Advertise/Components/AdvertiseComponent.cs b/Content.Server/Advertise/Components/AdvertiseComponent.cs index 531b31031d..3d617e3a34 100644 --- a/Content.Server/Advertise/Components/AdvertiseComponent.cs +++ b/Content.Server/Advertise/Components/AdvertiseComponent.cs @@ -24,6 +24,14 @@ public sealed partial class AdvertiseComponent : Component [DataField] public int MaximumWait { get; private set; } = 10 * 60; + /// + /// If true, the delay before the first advertisement (at MapInit) will ignore + /// and instead be rolled between 0 and . This only applies to the initial delay; + /// will be respected after that. + /// + [DataField] + public bool Prewarm = true; + /// /// The identifier for the advertisements pack prototype. /// diff --git a/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs b/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs index 12eac72cfe..28fa01628f 100644 --- a/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs +++ b/Content.Server/Advertise/EntitySystems/AdvertiseSystem.cs @@ -37,13 +37,14 @@ public sealed class AdvertiseSystem : EntitySystem private void OnMapInit(EntityUid uid, AdvertiseComponent advert, MapInitEvent args) { - RandomizeNextAdvertTime(advert); + var prewarm = advert.Prewarm; + RandomizeNextAdvertTime(advert, prewarm); _nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime); } - private void RandomizeNextAdvertTime(AdvertiseComponent advert) + private void RandomizeNextAdvertTime(AdvertiseComponent advert, bool prewarm = false) { - var minDuration = Math.Max(1, advert.MinimumWait); + var minDuration = prewarm ? 0 : Math.Max(1, advert.MinimumWait); var maxDuration = Math.Max(minDuration, advert.MaximumWait); var waitDuration = TimeSpan.FromSeconds(_random.Next(minDuration, maxDuration)); From b895e557d4503074622ec1ca60e1f7749783a29e Mon Sep 17 00:00:00 2001 From: Verm <32827189+Vermidia@users.noreply.github.com> Date: Fri, 12 Apr 2024 01:44:47 -0500 Subject: [PATCH 22/49] Fix shaker sprites (#26899) * Change basefoodshaker to parent from basefoodcondiment instead * Make them still refillable --- .../Objects/Consumable/Food/Containers/condiments.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml index babc2533f1..bbe5cf244b 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/condiments.yml @@ -522,7 +522,7 @@ # Shakers - type: entity - parent: BaseFoodCondimentBottle + parent: BaseFoodCondiment id: BaseFoodShaker abstract: true name: empty shaker @@ -560,6 +560,8 @@ acts: [ "Destruction" ] - type: Sprite state: shaker-empty + - type: RefillableSolution + solution: food - type: entity parent: BaseFoodShaker From 4627c7c859f12da60b75880c50b761b4646ea3a0 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 12 Apr 2024 06:45:53 +0000 Subject: [PATCH 23/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6c741b4017..69800e1656 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: themias - changes: - - message: Stun batons now toggle off after being drained by an EMP. - type: Fix - id: 5833 - time: '2024-01-30T23:04:26.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24706 - author: Tayrtahn changes: - message: Fixed weird rotation while strapped to a bed. @@ -3828,3 +3821,10 @@ id: 6332 time: '2024-04-11T20:48:46.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26813 +- author: Vermidia + changes: + - message: Salt and Pepper shakers look like shakers. + type: Fix + id: 6333 + time: '2024-04-12T06:44:47.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26899 From 882aeb03143d07a4cef91412008c81c9902075d8 Mon Sep 17 00:00:00 2001 From: Token Date: Fri, 12 Apr 2024 11:50:10 +0500 Subject: [PATCH 24/49] Update .editorconfig to correspond Code Conventions (#26824) Update editorconfig to Code Style End of line is: CRLF (suggestion) Namespace declarations are: file scoped (suggestion). Instead of block scoped --- .editorconfig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 872a068c7c..58d0d332bb 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ indent_style = space tab_width = 4 # New line preferences -#end_of_line = crlf +end_of_line = crlf:suggestion insert_final_newline = true trim_trailing_whitespace = true @@ -104,6 +104,7 @@ csharp_preferred_modifier_order = public, private, protected, internal, new, abs # 'using' directive preferences csharp_using_directive_placement = outside_namespace:silent +csharp_style_namespace_declarations = file_scoped:suggestion #### C# Formatting Rules #### From e12223c355b3b452d6d6043ec126124189b64f84 Mon Sep 17 00:00:00 2001 From: liltenhead <104418166+liltenhead@users.noreply.github.com> Date: Thu, 11 Apr 2024 23:50:45 -0700 Subject: [PATCH 25/49] Remove reagent slimes from ghost role pool (#26840) reagentslimeghostrole --- Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index 01fce382e3..c2380c4027 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -229,6 +229,7 @@ description: It consists of a liquid, and it wants to dissolve you in itself. components: - type: GhostRole + prob: 0 description: ghost-role-information-angry-slimes-description - type: NpcFactionMember factions: From 8f17bf1a3d96c8392c227ecc27b0e7d32c971126 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 12 Apr 2024 06:51:51 +0000 Subject: [PATCH 26/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 69800e1656..6cf32a6c0c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Tayrtahn - changes: - - message: Fixed weird rotation while strapped to a bed. - type: Fix - id: 5834 - time: '2024-01-30T23:23:31.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24746 - author: themias changes: - message: Some glasses and masks can be worn together to hide your identity. @@ -3828,3 +3821,10 @@ id: 6333 time: '2024-04-12T06:44:47.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26899 +- author: liltenhead + changes: + - message: Reagent slimes are no longer ghost roles. + type: Remove + id: 6334 + time: '2024-04-12T06:50:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26840 From 261e5354d3601b5ed1a8ea6c6a161a6f79c3f791 Mon Sep 17 00:00:00 2001 From: Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com> Date: Fri, 12 Apr 2024 06:56:35 +0000 Subject: [PATCH 27/49] Fix grammar in changelog (#26894) Grammar Co-authored-by: metalgearsloth --- Resources/Changelog/Changelog.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 6cf32a6c0c..817ec04451 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -3800,8 +3800,7 @@ url: https://github.com/space-wizards/space-station-14/pull/26752 - author: Jark255 changes: - - message: Door electronics now require network configurators to change the access - of them, rather by hand. + - message: Door electronics now require network configurators to change their access settings, rather than doing so by hand. type: Fix id: 6331 time: '2024-04-11T12:21:15.0000000+00:00' From e88b2467ca36c25c15095b7e7e0357c55ed581e2 Mon Sep 17 00:00:00 2001 From: Token Date: Fri, 12 Apr 2024 11:58:02 +0500 Subject: [PATCH 28/49] NoticeBoard is craftable now (#26847) * NoticeBoard is craftable now * Fix notice board to proper name capitalization * Fix notice board proper name in description * Update Resources/Prototypes/Recipes/Construction/furniture.yml --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- .../Structures/Wallmounts/noticeboard.yml | 8 ++++++ .../Graphs/furniture/noticeboard.yml | 26 +++++++++++++++++++ .../Recipes/Construction/furniture.yml | 18 +++++++++++++ 3 files changed, 52 insertions(+) create mode 100644 Resources/Prototypes/Recipes/Construction/Graphs/furniture/noticeboard.yml diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml index 4a442d0542..421ab93be9 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml @@ -2,6 +2,8 @@ id: NoticeBoard name: notice board description: Is there a job for a witcher? + placement: + mode: SnapgridCenter components: - type: WallMount - type: Sprite @@ -53,3 +55,9 @@ - type: ContainerContainer containers: storagebase: !type:Container + - type: Tag + tags: + - Wooden + - type: Construction + graph: NoticeBoard + node: noticeBoard diff --git a/Resources/Prototypes/Recipes/Construction/Graphs/furniture/noticeboard.yml b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/noticeboard.yml new file mode 100644 index 0000000000..324745cbb9 --- /dev/null +++ b/Resources/Prototypes/Recipes/Construction/Graphs/furniture/noticeboard.yml @@ -0,0 +1,26 @@ +- type: constructionGraph + id: NoticeBoard + start: start + graph: + - node: start + actions: + - !type:DestroyEntity {} + edges: + - to: noticeBoard + completed: + - !type:SnapToGrid { } + steps: + - material: WoodPlank + amount: 3 + doAfter: 2 + - node: noticeBoard + entity: NoticeBoard + edges: + - to: start + completed: + - !type:SpawnPrototype + prototype: MaterialWoodPlank + amount: 3 + steps: + - tool: Prying + doAfter: 3 diff --git a/Resources/Prototypes/Recipes/Construction/furniture.yml b/Resources/Prototypes/Recipes/Construction/furniture.yml index a5cf53107d..5f7ec9c92d 100644 --- a/Resources/Prototypes/Recipes/Construction/furniture.yml +++ b/Resources/Prototypes/Recipes/Construction/furniture.yml @@ -884,3 +884,21 @@ canBuildInImpassable: false conditions: - !type:TileNotBlocked + +- type: construction + id: NoticeBoard + name: notice board + description: Wooden notice board, can store paper inside itself. + graph: NoticeBoard + startNode: start + targetNode: noticeBoard + category: construction-category-furniture + icon: + sprite: Structures/Wallmounts/noticeboard.rsi + state: noticeboard + objectType: Structure + placementMode: SnapgridCenter + canRotate: true + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked From 85aef16954725a72a2c590e9cf7445b15b93d23e Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 12 Apr 2024 06:59:08 +0000 Subject: [PATCH 29/49] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 817ec04451..bcf612a1a1 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: themias - changes: - - message: Some glasses and masks can be worn together to hide your identity. - type: Tweak - id: 5835 - time: '2024-01-31T03:49:19.0000000+00:00' - url: https://api.github.com/repos/space-wizards/space-station-14/pulls/24741 - author: mirrorcult changes: - message: Throwing recoil reduced further @@ -3800,7 +3793,8 @@ url: https://github.com/space-wizards/space-station-14/pull/26752 - author: Jark255 changes: - - message: Door electronics now require network configurators to change their access settings, rather than doing so by hand. + - message: Door electronics now require network configurators to change their access + settings, rather than doing so by hand. type: Fix id: 6331 time: '2024-04-11T12:21:15.0000000+00:00' @@ -3827,3 +3821,10 @@ id: 6334 time: '2024-04-12T06:50:46.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/26840 +- author: TokenStyle + changes: + - message: Notice board can be crafted + type: Add + id: 6335 + time: '2024-04-12T06:58:02.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/26847 From 7d480acb0c859e0f186df41b7f2940b2ef5789bb Mon Sep 17 00:00:00 2001 From: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com> Date: Fri, 12 Apr 2024 02:04:35 -0500 Subject: [PATCH 30/49] Add drink container suffixes (#26835) Co-authored-by: Velcroboy --- .../Entities/Objects/Consumable/Drinks/drinks-cartons.yml | 1 + .../Entities/Objects/Consumable/Drinks/drinks_bottles.yml | 3 ++- .../Entities/Objects/Consumable/Drinks/trash_drinks.yml | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml index 84639c9af0..d60134fce0 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks-cartons.yml @@ -2,6 +2,7 @@ parent: DrinkBase id: DrinkCartonBaseFull abstract: true + suffix: Full components: - type: Openable sound: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml index b6455735cc..0c7707c5f2 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_bottles.yml @@ -3,6 +3,7 @@ parent: DrinkBase id: DrinkBottlePlasticBaseFull abstract: true + suffix: Full components: - type: Tag tags: @@ -744,7 +745,7 @@ parent: DrinkBottlePlasticBaseFull id: DrinkSugarJug name: sugar jug - suffix: for drinks + suffix: For Drinks, Full description: some people put this in their coffee... components: - type: SolutionContainerManager diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml index 86bc34f3c8..a3f5bf1903 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/trash_drinks.yml @@ -5,6 +5,7 @@ parent: BaseItem abstract: true description: An empty bottle. + suffix: Empty components: - type: Sprite state: icon @@ -93,6 +94,7 @@ parent: BaseItem abstract: true description: An empty carton. + suffix: Empty components: - type: Sprite state: icon From 9d5a3992fa635194cfe1d9fbfa60a5ea72288f4e Mon Sep 17 00:00:00 2001 From: Nemanja <98561806+EmoGarbage404@users.noreply.github.com> Date: Fri, 12 Apr 2024 03:07:25 -0400 Subject: [PATCH 31/49] uplink and store freshening (#26444) * uplink and store freshening * more * im gonna POOOOOOGGGGGGG * we love it --- .../Store/Ui/StoreBoundUserInterface.cs | 9 +-- .../Store/Ui/StoreListingControl.xaml | 1 + .../Store/Ui/StoreListingControl.xaml.cs | 80 +++++++++++++++++-- Content.Client/Store/Ui/StoreMenu.xaml | 5 -- Content.Client/Store/Ui/StoreMenu.xaml.cs | 76 ++++++------------ .../Store/Ui/StoreWithdrawWindow.xaml.cs | 4 +- .../Store/Systems/StoreSystem.Ui.cs | 7 +- .../Store/ListingLocalisationHelpers.cs | 26 +++--- Content.Shared/Store/ListingPrototype.cs | 53 ++++++------ Content.Shared/Store/StoreUi.cs | 9 +-- Resources/Locale/en-US/store/store.ftl | 2 +- .../Prototypes/Catalog/uplink_catalog.yml | 26 +++--- 12 files changed, 158 insertions(+), 140 deletions(-) diff --git a/Content.Client/Store/Ui/StoreBoundUserInterface.cs b/Content.Client/Store/Ui/StoreBoundUserInterface.cs index f87b92bc61..88ad0e3de8 100644 --- a/Content.Client/Store/Ui/StoreBoundUserInterface.cs +++ b/Content.Client/Store/Ui/StoreBoundUserInterface.cs @@ -17,7 +17,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface private string _windowName = Loc.GetString("store-ui-default-title"); [ViewVariables] - private string _search = ""; + private string _search = string.Empty; [ViewVariables] private HashSet _listings = new(); @@ -41,7 +41,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface _menu.OnCategoryButtonPressed += (_, category) => { _menu.CurrentCategory = category; - SendMessage(new StoreRequestUpdateInterfaceMessage()); + _menu?.UpdateListing(); }; _menu.OnWithdrawAttempt += (_, type, amount) => @@ -49,11 +49,6 @@ public sealed class StoreBoundUserInterface : BoundUserInterface SendMessage(new StoreRequestWithdrawMessage(type, amount)); }; - _menu.OnRefreshButtonPressed += (_) => - { - SendMessage(new StoreRequestUpdateInterfaceMessage()); - }; - _menu.SearchTextUpdated += (_, search) => { _search = search.Trim().ToLowerInvariant(); diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml b/Content.Client/Store/Ui/StoreListingControl.xaml index aefeec17cc..12b4d7b5b3 100644 --- a/Content.Client/Store/Ui/StoreListingControl.xaml +++ b/Content.Client/Store/Ui/StoreListingControl.xaml @@ -15,6 +15,7 @@ Margin="0,0,4,0" MinSize="48 48" Stretch="KeepAspectCentered" /> + diff --git a/Content.Client/Store/Ui/StoreListingControl.xaml.cs b/Content.Client/Store/Ui/StoreListingControl.xaml.cs index bb600588e0..030f07dc7c 100644 --- a/Content.Client/Store/Ui/StoreListingControl.xaml.cs +++ b/Content.Client/Store/Ui/StoreListingControl.xaml.cs @@ -1,25 +1,91 @@ +using Content.Client.GameTicking.Managers; +using Content.Shared.Store; using Robust.Client.AutoGenerated; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.XAML; -using Robust.Shared.Graphics; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; namespace Content.Client.Store.Ui; [GenerateTypedNameReferences] public sealed partial class StoreListingControl : Control { - public StoreListingControl(string itemName, string itemDescription, - string price, bool canBuy, Texture? texture = null) + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly IEntityManager _entity = default!; + [Dependency] private readonly IGameTiming _timing = default!; + private readonly ClientGameTicker _ticker; + + private readonly ListingData _data; + + private readonly bool _hasBalance; + private readonly string _price; + public StoreListingControl(ListingData data, string price, bool hasBalance, Texture? texture = null) { + IoCManager.InjectDependencies(this); RobustXamlLoader.Load(this); - StoreItemName.Text = itemName; - StoreItemDescription.SetMessage(itemDescription); + _ticker = _entity.System(); - StoreItemBuyButton.Text = price; - StoreItemBuyButton.Disabled = !canBuy; + _data = data; + _hasBalance = hasBalance; + _price = price; + + StoreItemName.Text = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype); + StoreItemDescription.SetMessage(ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(_data, _prototype)); + + UpdateBuyButtonText(); + StoreItemBuyButton.Disabled = !CanBuy(); StoreItemTexture.Texture = texture; } + + private bool CanBuy() + { + if (!_hasBalance) + return false; + + var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan); + if (_data.RestockTime > stationTime) + return false; + + return true; + } + + private void UpdateBuyButtonText() + { + var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan); + if (_data.RestockTime > stationTime) + { + var timeLeftToBuy = stationTime - _data.RestockTime; + StoreItemBuyButton.Text = timeLeftToBuy.Duration().ToString(@"mm\:ss"); + } + else + { + StoreItemBuyButton.Text = _price; + } + } + + private void UpdateName() + { + var name = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype); + + var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan); + if (_data.RestockTime > stationTime) + { + name += Loc.GetString("store-ui-button-out-of-stock"); + } + + StoreItemName.Text = name; + } + + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + UpdateBuyButtonText(); + UpdateName(); + StoreItemBuyButton.Disabled = !CanBuy(); + } } diff --git a/Content.Client/Store/Ui/StoreMenu.xaml b/Content.Client/Store/Ui/StoreMenu.xaml index fc4cbe444f..843c9dc029 100644 --- a/Content.Client/Store/Ui/StoreMenu.xaml +++ b/Content.Client/Store/Ui/StoreMenu.xaml @@ -12,11 +12,6 @@ HorizontalAlignment="Left" Access="Public" HorizontalExpand="True" /> -