From dabf3d1521fc077df00256cbb04414632ff59010 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Wed, 19 Jun 2024 07:47:19 +1000 Subject: [PATCH 001/109] Fix material storage going BRRT (#29167) If the volume hits 0 we just remove it. --- Content.Client/Lathe/UI/LatheMenu.xaml.cs | 3 --- .../Materials/SharedMaterialStorageSystem.cs | 12 ++++++++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Content.Client/Lathe/UI/LatheMenu.xaml.cs b/Content.Client/Lathe/UI/LatheMenu.xaml.cs index 9e15f8239e5..40f553fcd66 100644 --- a/Content.Client/Lathe/UI/LatheMenu.xaml.cs +++ b/Content.Client/Lathe/UI/LatheMenu.xaml.cs @@ -78,9 +78,6 @@ public LatheMenu(LatheBoundUserInterface owner) /// public void PopulateRecipes() { - if (!_entityManager.TryGetComponent(_owner, out var component)) - return; - var recipesToShow = new List(); foreach (var recipe in Recipes) { diff --git a/Content.Shared/Materials/SharedMaterialStorageSystem.cs b/Content.Shared/Materials/SharedMaterialStorageSystem.cs index a27e0fb9cf0..dc4858fd745 100644 --- a/Content.Shared/Materials/SharedMaterialStorageSystem.cs +++ b/Content.Shared/Materials/SharedMaterialStorageSystem.cs @@ -1,11 +1,13 @@ using System.Linq; using Content.Shared.Interaction; using Content.Shared.Interaction.Components; +using Content.Shared.Mobs; using Content.Shared.Stacks; using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Shared.Materials; @@ -166,8 +168,14 @@ public bool TryChangeMaterialAmount(EntityUid uid, string materialId, int volume return false; if (!CanChangeMaterialAmount(uid, materialId, volume, component)) return false; - component.Storage.TryAdd(materialId, 0); - component.Storage[materialId] += volume; + + var existing = component.Storage.GetOrNew(materialId); + existing += volume; + + if (existing == 0) + component.Storage.Remove(materialId); + else + component.Storage[materialId] = existing; var ev = new MaterialAmountChangedEvent(); RaiseLocalEvent(uid, ref ev); From 0b98ce83f0218841fc9b0afce97cccd00013b601 Mon Sep 17 00:00:00 2001 From: PJBot Date: Tue, 18 Jun 2024 21:48:25 +0000 Subject: [PATCH 002/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 4790f603a07..41c3e8c8835 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: lzk228 - changes: - - message: Flower crown and wreath were combined. Now you can wear wreath both on - head and on neck. - type: Tweak - id: 6273 - time: '2024-03-31T08:52:52.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26605 - author: Ubaser changes: - message: Throwing knives now additionally do armour piercing damage. @@ -3851,3 +3843,10 @@ id: 6772 time: '2024-06-18T15:04:15.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29103 +- author: metalgearsloth + changes: + - message: Fix lathe materials sometimes flickering. + type: Fix + id: 6773 + time: '2024-06-18T21:47:19.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29167 From 707f1f4981ce1e0bdc6c8356351f25d370c84a70 Mon Sep 17 00:00:00 2001 From: osjarw <62134478+osjarw@users.noreply.github.com> Date: Wed, 19 Jun 2024 03:20:57 +0300 Subject: [PATCH 003/109] Fix air alarms (#29172) Broken by #28272 --- Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs index 2875d4a3d5d..1b3b11068c5 100644 --- a/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs +++ b/Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs @@ -11,6 +11,7 @@ using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Audio; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Atmos.Monitor.Systems; @@ -86,7 +87,7 @@ private void OnPacketRecv(EntityUid uid, AtmosAlarmableComponent component, Devi return; if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd) - || !args.Data.TryGetValue(AlertSource, out HashSet? sourceTags)) + || !args.Data.TryGetValue(AlertSource, out HashSet>? sourceTags)) { return; } From baa683648908dccb4004fb6a0bb250f27e101957 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 19 Jun 2024 00:22:03 +0000 Subject: [PATCH 004/109] 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 41c3e8c8835..5349a890801 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Ubaser - changes: - - message: Throwing knives now additionally do armour piercing damage. - type: Tweak - id: 6274 - time: '2024-03-31T11:48:36.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26380 - author: graevy changes: - message: clicking the brig timer button will now cancel its countdown @@ -3850,3 +3843,10 @@ id: 6773 time: '2024-06-18T21:47:19.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29167 +- author: osjarw + changes: + - message: Fix air alarms not alarming air. + type: Fix + id: 6774 + time: '2024-06-19T00:20:57.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29172 From 9b2f9ce05f7efc5ad57c45b8dd4295902ff08c72 Mon Sep 17 00:00:00 2001 From: Errant <35878406+Errant-4@users.noreply.github.com> Date: Wed, 19 Jun 2024 02:26:46 +0200 Subject: [PATCH 005/109] Hidden loadout groups (#29170) * loadout hiding * department of redundancy department --- Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs | 3 +++ .../Preferences/Loadouts/LoadoutGroupPrototype.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs index 2737eef1f1a..d029eb1223d 100644 --- a/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs +++ b/Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs @@ -29,6 +29,9 @@ public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, Role if (!protoManager.TryIndex(group, out var groupProto)) continue; + if (groupProto.Hidden) + continue; + var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection); LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name)); _groups.Add(container); diff --git a/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs b/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs index 1d41f8dd7f9..0c787e965f1 100644 --- a/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs +++ b/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs @@ -29,6 +29,12 @@ public sealed partial class LoadoutGroupPrototype : IPrototype [DataField] public int MaxLimit = 1; + /// + /// Hides the loadout group from the player. + /// + [DataField] + public bool Hidden; + [DataField(required: true)] public List> Loadouts = new(); } From 81136648da5561eb8d04eb5e74f1103b6f6b8ebb Mon Sep 17 00:00:00 2001 From: Vasilis Date: Wed, 19 Jun 2024 03:27:12 +0300 Subject: [PATCH 006/109] Upgrade rsi-diff's changed files action | Make it only return rsi and png changes (#29185) It was brought up to me in https://github.com/space-wizards/space-station-14/pull/29179#issuecomment-2177140740 (and from a dm from them) that space bars can cause issues with the rsi bot. Upon investigation its case we use "space-delimited" on the "get changes files" check. Which returns ALL changed files. Even if the change has nothing to do with png's or rsi's (example a downstream merging upstream) --- .github/workflows/rsi-diff.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rsi-diff.yml b/.github/workflows/rsi-diff.yml index 1f122526d73..98cc97e9221 100644 --- a/.github/workflows/rsi-diff.yml +++ b/.github/workflows/rsi-diff.yml @@ -15,9 +15,12 @@ jobs: - name: Get changed files id: files - uses: Ana06/get-changed-files@v1.2 + uses: Ana06/get-changed-files@v2.3.0 with: format: 'space-delimited' + filter: | + **.rsi + **.png - name: Diff changed RSIs id: diff From 0910983c4b538caf1445b56ca74bb4082de484f3 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Wed, 19 Jun 2024 00:29:06 +0000 Subject: [PATCH 007/109] unhardcode thief MaxSelectedSets (#29175) * unhardcode thief MaxSelectedSets * we do a little copy paste :trollface: * :trollface: --------- Co-authored-by: deltanedas <@deltanedas:kde.org> --- Content.Client/Thief/ThiefBackpackMenu.xaml | 2 +- Content.Client/Thief/ThiefBackpackMenu.xaml.cs | 1 + .../Thief/Components/ThiefUndeterminedBackpackComponent.cs | 6 ++++++ .../Thief/Systems/ThiefUndeterminedBackpackSystem.cs | 5 ++--- Resources/Locale/en-US/thief/backpack.ftl | 5 ++++- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Content.Client/Thief/ThiefBackpackMenu.xaml b/Content.Client/Thief/ThiefBackpackMenu.xaml index c1739eb321d..e46f18d4ed5 100644 --- a/Content.Client/Thief/ThiefBackpackMenu.xaml +++ b/Content.Client/Thief/ThiefBackpackMenu.xaml @@ -5,7 +5,7 @@ MinSize="700 700"> - - - + + + + textures; + EntityPrototype? recipeProto = null; if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto) && entityProto != null) - { - textures = SpriteComponent.GetPrototypeTextures(entityProto, _resources).Select(o => o.Default).ToList(); - } - else - { - textures = prototype.Icon == null - ? new List { _spriteSystem.GetPrototypeIcon(prototype.Result).Default } - : new List { _spriteSystem.Frame0(prototype.Icon) }; - } + recipeProto = entityProto; var canProduce = _lathe.CanProduce(_owner, prototype, quantity); - var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, textures); + var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, recipeProto); control.OnButtonPressed += s => { if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0) @@ -216,14 +208,23 @@ public void UpdateCategories() /// public void PopulateQueueList(List queue) { - QueueList.Clear(); + QueueList.DisposeAllChildren(); + var idx = 1; foreach (var recipe in queue) { - var icon = recipe.Icon == null - ? _spriteSystem.GetPrototypeIcon(recipe.Result).Default - : _spriteSystem.Frame0(recipe.Icon); - QueueList.AddItem($"{idx}. {recipe.Name}", icon); + var queuedRecipeBox = new BoxContainer(); + queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal; + + var queuedRecipeProto = new EntityPrototypeView(); + if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null) + queuedRecipeProto.SetPrototype(entityProto); + + var queuedRecipeLabel = new Label(); + queuedRecipeLabel.Text = $"{idx}. {recipe.Name}"; + queuedRecipeBox.AddChild(queuedRecipeProto); + queuedRecipeBox.AddChild(queuedRecipeLabel); + QueueList.AddChild(queuedRecipeBox); idx++; } } @@ -233,9 +234,10 @@ public void SetQueueInfo(LatheRecipePrototype? recipe) FabricatingContainer.Visible = recipe != null; if (recipe == null) return; - Icon.Texture = recipe.Icon == null - ? _spriteSystem.GetPrototypeIcon(recipe.Result).Default - : _spriteSystem.Frame0(recipe.Icon); + + if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null) + FabricatingEntityProto.SetPrototype(entityProto); + NameLabel.Text = $"{recipe.Name}"; } diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml b/Content.Client/Lathe/UI/RecipeControl.xaml index d1371a026a2..19e20c7c06d 100644 --- a/Content.Client/Lathe/UI/RecipeControl.xaml +++ b/Content.Client/Lathe/UI/RecipeControl.xaml @@ -5,14 +5,12 @@ Margin="0" StyleClasses="ButtonSquare"> - diff --git a/Content.Client/Lathe/UI/RecipeControl.xaml.cs b/Content.Client/Lathe/UI/RecipeControl.xaml.cs index 47b6b5932c4..db428d3cf0e 100644 --- a/Content.Client/Lathe/UI/RecipeControl.xaml.cs +++ b/Content.Client/Lathe/UI/RecipeControl.xaml.cs @@ -4,6 +4,7 @@ using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; namespace Content.Client.Lathe.UI; @@ -13,12 +14,13 @@ public sealed partial class RecipeControl : Control public Action? OnButtonPressed; public Func TooltipTextSupplier; - public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, List textures) + public RecipeControl(LatheRecipePrototype recipe, Func tooltipTextSupplier, bool canProduce, EntityPrototype? entityPrototype = null) { RobustXamlLoader.Load(this); RecipeName.Text = recipe.Name; - RecipeTextures.Textures = textures; + if (entityPrototype != null) + RecipePrototype.SetPrototype(entityPrototype); Button.Disabled = !canProduce; TooltipTextSupplier = tooltipTextSupplier; Button.TooltipSupplier = SupplyTooltip; From 6c810003db4ff63a2d309739dc5f7f8dd66855e9 Mon Sep 17 00:00:00 2001 From: beck-thompson <107373427+beck-thompson@users.noreply.github.com> Date: Tue, 18 Jun 2024 20:42:45 -0700 Subject: [PATCH 020/109] Grammar fix to medibot! (#29197) fixed --- Resources/Locale/en-US/advertisements/other/medibot.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Locale/en-US/advertisements/other/medibot.ftl b/Resources/Locale/en-US/advertisements/other/medibot.ftl index 2f58912228d..08cd5dbf633 100644 --- a/Resources/Locale/en-US/advertisements/other/medibot.ftl +++ b/Resources/Locale/en-US/advertisements/other/medibot.ftl @@ -8,7 +8,7 @@ advertisement-medibot-7 = Why are we still here? Just to suffer? advertisement-medibot-8 = I... I've never lost a patient before. Not today, I mean. advertisement-medibot-9 = Lexorin in. advertisement-medibot-10 = Have a great day! -advertisement-medibot-11 = Hopefully you wont need to come back! +advertisement-medibot-11 = Hopefully you won't need to come back! advertisement-medibot-12 = Remember to brush your teeth. advertisement-medibot-13 = I wish I had hands. advertisement-medibot-14 = I'm here to help! From c966db408d1fe41f6f9d74e1e122c0ddd30e4bbf Mon Sep 17 00:00:00 2001 From: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> Date: Tue, 18 Jun 2024 23:36:05 -0500 Subject: [PATCH 021/109] Restore default panic bunker and tweak baby jail (#29198) restore default panic bunker --- Resources/ConfigPresets/WizardsDen/leviathan.toml | 4 ++++ Resources/ConfigPresets/WizardsDen/lizard.toml | 4 ++++ Resources/ConfigPresets/WizardsDen/wizardsDen.toml | 4 +++- Resources/ConfigPresets/WizardsDen/wizardsDenCoreOnly.toml | 2 -- Resources/ConfigPresets/WizardsDen/wizardsDenGateway.toml | 3 ++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Resources/ConfigPresets/WizardsDen/leviathan.toml b/Resources/ConfigPresets/WizardsDen/leviathan.toml index a1a0e5b704d..7560833f13a 100644 --- a/Resources/ConfigPresets/WizardsDen/leviathan.toml +++ b/Resources/ConfigPresets/WizardsDen/leviathan.toml @@ -4,6 +4,10 @@ [game] hostname = "[EN] Wizard's Den Leviathan [US East 1]" +panic_bunker.enabled = false +panic_bunker.disable_with_admins = false +panic_bunker.enable_without_admins = false +panic_bunker.custom_reason = "" [hub] tags = "lang:en,region:am_n_e,rp:low" diff --git a/Resources/ConfigPresets/WizardsDen/lizard.toml b/Resources/ConfigPresets/WizardsDen/lizard.toml index 2e3809a87dc..72688efbf9c 100644 --- a/Resources/ConfigPresets/WizardsDen/lizard.toml +++ b/Resources/ConfigPresets/WizardsDen/lizard.toml @@ -4,6 +4,10 @@ [game] hostname = "[EN] Wizard's Den Lizard [US West]" +panic_bunker.enabled = false +panic_bunker.disable_with_admins = false +panic_bunker.enable_without_admins = false +panic_bunker.custom_reason = "" [hub] tags = "lang:en,region:am_n_w,rp:low" diff --git a/Resources/ConfigPresets/WizardsDen/wizardsDen.toml b/Resources/ConfigPresets/WizardsDen/wizardsDen.toml index ecf2aa81943..46e53c0c885 100644 --- a/Resources/ConfigPresets/WizardsDen/wizardsDen.toml +++ b/Resources/ConfigPresets/WizardsDen/wizardsDen.toml @@ -5,8 +5,10 @@ desc = "Official English Space Station 14 servers. Vanilla, roleplay ruleset." lobbyenabled = true soft_max_players = 80 +panic_bunker.disable_with_admins = true +panic_bunker.enable_without_admins = true panic_bunker.show_reason = true -panic_bunker.custom_reason = "You have not played on a Wizard's Den server long enough to connect to this server. Please play on another Wizard's Den server for now." +panic_bunker.custom_reason = "You have not played on a Wizard's Den server long enough to connect to this server. Please play on Wizard's Den Lizard, Leviathan, or Farm Grass Hopper until you have more playtime." [infolinks] bug_report = "https://github.com/space-wizards/space-station-14/issues/new/choose" diff --git a/Resources/ConfigPresets/WizardsDen/wizardsDenCoreOnly.toml b/Resources/ConfigPresets/WizardsDen/wizardsDenCoreOnly.toml index 630025fc5db..5cc9a72061c 100644 --- a/Resources/ConfigPresets/WizardsDen/wizardsDenCoreOnly.toml +++ b/Resources/ConfigPresets/WizardsDen/wizardsDenCoreOnly.toml @@ -8,8 +8,6 @@ soft_max_players = 50 hostname = "[EN] Wizard's Den Red Firefly [US East]" desc = "Official English Space Station 14 servers. Vanilla, core ruleset." -panic_bunker.custom_reason = "" -panic_bunker.show_reason = true panic_bunker.min_overall_minutes = 15 [server] diff --git a/Resources/ConfigPresets/WizardsDen/wizardsDenGateway.toml b/Resources/ConfigPresets/WizardsDen/wizardsDenGateway.toml index 2d085c816c3..cdf5dfbcdd7 100644 --- a/Resources/ConfigPresets/WizardsDen/wizardsDenGateway.toml +++ b/Resources/ConfigPresets/WizardsDen/wizardsDenGateway.toml @@ -13,8 +13,9 @@ panic_bunker.enable_without_admins = false panic_bunker.custom_reason = "" baby_jail.enabled = true baby_jail.show_reason = true -baby_jail.max_overall_minutes = 15 +baby_jail.max_overall_minutes = 3000 # 50 hours baby_jail.custom_reason = "Sorry! Only new players can join the servers, try joining another one instead!" baby_jail.whitelisted_can_bypass = true + [hub] tags = "lang:en,region:am_n_e,rp:low" From fc40cd79fb1e4a9890b6b7eba3ab707ddbd35bf6 Mon Sep 17 00:00:00 2001 From: Truoizys <153248924+Truoizys@users.noreply.github.com> Date: Wed, 19 Jun 2024 14:21:05 +0100 Subject: [PATCH 022/109] Fixed cartridges installing more than once (#29187) * fixed cartridges installing more than once * replaced prototypes with CartridgeComponent * relocated checks from InstallProgram to InstallCartridge --- .../CartridgeLoader/CartridgeLoaderSystem.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs index 7896a7822e2..cd422328c3e 100644 --- a/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs +++ b/Content.Server/CartridgeLoader/CartridgeLoaderSystem.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.DeviceNetwork.Systems; using Content.Server.PDA; @@ -164,6 +164,15 @@ public bool InstallCartridge(EntityUid loaderUid, EntityUid cartridgeUid, Cartri if (!Resolve(loaderUid, ref loader)) return false; + if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge)) + return false; + + foreach (var program in GetInstalled(loaderUid)) + { + if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName) + return false; + } + //This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs //the data on the cartridge to carry over when installing @@ -191,7 +200,6 @@ public bool InstallProgram(EntityUid loaderUid, string prototype, bool deinstall if (container.Count >= loader.DiskSpace) return false; - // TODO cancel duplicate program installations var ev = new ProgramInstallationAttempt(loaderUid, prototype); RaiseLocalEvent(ref ev); From 37120c1f7f639c9fa08865bdb5fb4a6884f86ecc Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 19 Jun 2024 13:22:13 +0000 Subject: [PATCH 023/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c1fc9781eea..3c9dd357b62 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,14 +1,4 @@ Entries: -- author: Flareguy - changes: - - message: All wheeled objects now have lower friction. Portable items, like fuel - canisters & portable scrubbers, should now be easier to carry around. - type: Tweak - - message: High-capacity reagent tanks are now much heavier. - type: Tweak - id: 6279 - time: '2024-04-01T03:23:59.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26601 - author: Tayrtahn changes: - message: spears, darts, and hypodarts can inject targets again. @@ -3850,3 +3840,10 @@ id: 6778 time: '2024-06-19T02:41:42.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/28685 +- author: Truoizys + changes: + - message: PDA cartridges can no longer be installed more than once. + type: Fix + id: 6779 + time: '2024-06-19T13:21:06.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29187 From 2b885272c864bc256ff7cdf88b7ecdd0b48ed6de Mon Sep 17 00:00:00 2001 From: lzk <124214523+lzk228@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:06:03 +0200 Subject: [PATCH 024/109] Musician's skirt (#29203) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sprites&Meta * Changing prototypes * Adding to Theater vend * Sprite_Change * Sprite_Change * Prototype_Changes Is this exactly how it should be?... * FUCKING FIX * weh --------- Co-authored-by: Арт <123451459+JustArt1m@users.noreply.github.com> --- .../en-US/preferences/loadout-groups.ftl | 1 + .../Entities/Clothing/Uniforms/jumpskirts.yml | 11 ++++++++ .../Loadouts/Jobs/Civilian/musician.yml | 19 +++++++++++++ .../Prototypes/Loadouts/loadout_groups.yml | 7 +++++ .../Prototypes/Loadouts/role_loadouts.yml | 1 + .../Roles/Jobs/Civilian/musician.yml | 1 - .../musician.rsi/equipped-INNERCLOTHING.png | Bin 0 -> 1134 bytes .../Uniforms/Jumpskirt/musician.rsi/icon.png | Bin 0 -> 451 bytes .../Jumpskirt/musician.rsi/inhand-left.png | Bin 0 -> 658 bytes .../Jumpskirt/musician.rsi/inhand-right.png | Bin 0 -> 646 bytes .../Uniforms/Jumpskirt/musician.rsi/meta.json | 26 ++++++++++++++++++ 11 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/equipped-INNERCLOTHING.png create mode 100644 Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/icon.png create mode 100644 Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-left.png create mode 100644 Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-right.png create mode 100644 Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/meta.json diff --git a/Resources/Locale/en-US/preferences/loadout-groups.ftl b/Resources/Locale/en-US/preferences/loadout-groups.ftl index a107ee24f84..ef0937c9109 100644 --- a/Resources/Locale/en-US/preferences/loadout-groups.ftl +++ b/Resources/Locale/en-US/preferences/loadout-groups.ftl @@ -66,6 +66,7 @@ loadout-group-mime-jumpsuit = Mime jumpsuit loadout-group-mime-backpack = Mime backpack loadout-group-mime-outerclothing = Mime outer clothing +loadout-group-musician-jumpsuit = Musician jumpsuit loadout-group-musician-outerclothing = Musician outer clothing # Cargo diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml index 5ef711556b5..d105d4cd776 100644 --- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml +++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpskirts.yml @@ -832,3 +832,14 @@ sprite: Clothing/Uniforms/Jumpskirt/olddress.rsi - type: Clothing sprite: Clothing/Uniforms/Jumpskirt/olddress.rsi + +- type: entity + parent: ClothingUniformSkirtBase + id: ClothingUniformJumpskirtMusician + name: musician's skirt + description: A fancy skirt for the musically inclined. Perfect for any lounge act! + components: + - type: Sprite + sprite: Clothing/Uniforms/Jumpskirt/musician.rsi + - type: Clothing + sprite: Clothing/Uniforms/Jumpskirt/musician.rsi diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml index 486ff25d47c..eae05d2005d 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml @@ -1,3 +1,22 @@ +# Jumpsuit +- type: loadout + id: MusicianJumpsuit + equipment: MusicianJumpsuit + +- type: startingGear + id: MusicianJumpsuit + equipment: + jumpsuit: ClothingUniformJumpsuitMusician + +- type: loadout + id: MusicianJumpskirt + equipment: MusicianJumpskirt + +- type: startingGear + id: MusicianJumpskirt + equipment: + jumpsuit: ClothingUniformJumpskirtMusician + # Outerclothing - type: loadout id: MusicianWintercoat diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml index cc0aefb69c1..3d4583a063e 100644 --- a/Resources/Prototypes/Loadouts/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/loadout_groups.yml @@ -429,6 +429,13 @@ loadouts: - MimeWintercoat +- type: loadoutGroup + id: MusicianJumpsuit + name: loadout-group-musician-jumpsuit + loadouts: + - MusicianJumpsuit + - MusicianJumpskirt + - type: loadoutGroup id: MusicianOuterClothing name: loadout-group-musician-outerclothing diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml index 0e80113ce3f..5f0ec2b274f 100644 --- a/Resources/Prototypes/Loadouts/role_loadouts.yml +++ b/Resources/Prototypes/Loadouts/role_loadouts.yml @@ -137,6 +137,7 @@ - type: roleLoadout id: JobMusician groups: + - MusicianJumpsuit - CommonBackpack - MusicianOuterClothing - Glasses diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml b/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml index 58335ba52dd..5d01cd4938c 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml @@ -17,7 +17,6 @@ - type: startingGear id: MusicianGear equipment: - jumpsuit: ClothingUniformJumpsuitMusician eyes: ClothingEyesGlassesSunglasses shoes: ClothingShoesBootsLaceup id: MusicianPDA diff --git a/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/equipped-INNERCLOTHING.png new file mode 100644 index 0000000000000000000000000000000000000000..78b2c2ea5dbee201f4e29a1eb776bc1f8a414c0f GIT binary patch literal 1134 zcmV-!1d;oRP)3~KjEN7?ph41{_if&slfAvWop*PMff>l#o!OoFX21R6d3(mFC_NwcIfpT)~{^u5YhB z`u=lmz-=q518;QrViIO;<9$-+_Sh)G4uk0#-f3tBm!T-2AXGz%E+2-Rn&@p(lLx2yz5d2&MI0_wXS*w8T)Q|gC zQKz_DzrUHm!GGHgv7y5bp?+2g_$efSX4Mz#!M#U5v$f^#$|{q0NdEj3-*NSI&BgP$0m5VDK(d+|1cm)6)C+nG!N%L!a(;Oda zrpnLj3y2x~5Q5I3LioUi_)6p#iwM8)5JW^0u%0JH?RmZ-ZAI~m+I<*7D}dO?(#sO8 z3bgk#3t2JAUMGGK0lj_geYB;4BUw6_sy+wm#1|cc7$jx`rBA9JxDVr?@qIa|O&L%I zlmTTx8Bhk40cAiLPzIC%Wk4BF29$wA#XzYyLUZ%4>|n>o#|QAHW5hgBXN$^@8?%9ezI4>GEdpoLUp~1x@Q0umZ`#)H%B7ize!{hQg<-lziI0%7Aa|j}Um_MlR19k_t z`xstz4nYL;mbCYT-H-1BKtNqN1fKy&0z&^8+12*`*0uNdqWm8@xH+k=3@8K2fHH6x z8Tbo#yHZoU@@}gD000hUSV?A0O#mtY000O800000007cclK=n!07*qoM6N<$f*QC6 AJOBUy literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/icon.png b/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..48c415163267176464dfe9fba198fd9330b5fa59 GIT binary patch literal 451 zcmV;!0X+VRP)T?c-|Z#F#1k2a3`7PZ0|5*)8=V55EXxX62AgBjuhF+ONyx!~ zEU=&RWUWj00^qI6n%rEE%iQF1)ybl5Y1q1No5}PMG}~pyS0@X+49H|# zAN0>`SFr75psbsumKnhFm;SQ4DxaDG+~{O?pKd%4I~|UwFKoOCh-+@X1QWzSv(cj3 z!`_M6sVC<1E2qJbIEd#TGt#wRDgeCwYQ3!N3lNBdg`ZPsFQN*-Z?nyxjzqzO;p6SK t!%;&&E>N_`^^8LXYv;wh$iV++;2RkXndURVXlnof002ovPDHLkV1fbl$%p^| literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-left.png b/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..5919ab9cff200bf1d1af06740d97bbf8b67f8d2a GIT binary patch literal 658 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEV2bo~aSW+od^^k5o7qvI&3~h~ zi&B9`id9R(T->MqG%yv3v9Fr^kN1GrDrL#zI`${ASn7$ZO%cUJLzVA4REc%bR+& z>RO!{gG%|O{W@{W7BjuQ{d>`^kN*z-40;xBn2vX62r zd7X20#jNGm&diGM+WfP^^~=|HeD3@=awC5!hWoP34T)}y`eyK(_k!RH*&{U;*EVge zTOr+-{A=ZFJ%$ax_n!UxNucHN<;SZp+4H%cdA(g(EMoTB8S7H-vNl-6)%a+0c*rHc zew?{lw{4BF15bVZ{;TKZpG`cFW2d=n{dv!Hhc4ZTA9GeapE;ayclA2{?Rs`j(P7rH zH9^x@JQ{GL15O8mgfCq659W0YzrRjg*{buy{aaPK+vPXQMqmGTm$76m=Y@kyzD%4G zZp-v%CzC@#Y!*|H&^4nC??d|)%I-2GJWmXH_x0lcUg_A};F_yf1u`s;T>7%(4)38I zOVt)=u2@!CYrAa!XPNtX`3)>{z2cq}-?|WP;kWTEo5FH61=RrE=DXilxfd`uFcfDq zl}ysC?U!QOZp^?Koy`<9>8jDZuN?b>y}qW~e%t@K2h6 S-oOD&YYd*QelF{r5}E+C79%GB literal 0 HcmV?d00001 diff --git a/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-right.png b/Resources/Textures/Clothing/Uniforms/Jumpskirt/musician.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..988fda91fabdaf886430b2c45229ac0112d46b09 GIT binary patch literal 646 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7T#lEVDj~JaSW+od^^j~+u2ay_~ISh zN}_KSEJ9>ScJuv%{7>KAJG!>{!ojs3-6CCAw|=Vsa>(>-wf@TYKZAax zXB18Me)n#=Nvqni4HFnRaiN9-XRkiKqq=eX?Rmeytc&$3iac|BUxML@<>IlC`)@1_ zd46;c6GNN$-RRGTax?Ot-)j2tYhI-7ug_{ZRxfYNn9iWE^4r0gm2d55KYVdkW}8*y zqP=nXbu}fkp0e5gN}R6L0#@U!qQSbG;pxMFnyX#g7QEeb&c#mU*o7J8C3hyjEbva* zyX)^k?y%@xJ=->)IwG~uaQz#Gx$V18ty_~DoVnX@*Cy%Z;tXN?x7H^W+*lYh?|!Oj z%8Re>PFM;3>gc`6?eJ;l-yU5SA0ErELeifvNh+Mw-S_yLx zYkptQdtLrE`H9tYKBfkS0^=)3W;fd0H)QC}@n%$LSay%+$xl5mjcw~i7}Tzqui5zh z{YJ)}-|X)H679&A*RFog_EqIq6sNXQ?ZWTxllJ??KB=DjvnIKztU+!?I7?OdwQZr> zUw;tgVOV|t3dr1020k{0b4l;-r^bkG{T6$)X2#+#JOb|B?-$!LFW}!Wfz^j()HIxmub1+)Sy)c*QvmFXA~@ Date: Wed, 19 Jun 2024 14:07:18 +0000 Subject: [PATCH 025/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 3c9dd357b62..44c7f829a1d 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: Tayrtahn - changes: - - message: spears, darts, and hypodarts can inject targets again. - type: Fix - - message: arrows can no longer inject targets they don't actually hit. - type: Fix - id: 6280 - time: '2024-04-01T03:39:35.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26268 - author: SoulFN changes: - message: Changed textures of the assault borg modules @@ -3847,3 +3838,10 @@ id: 6779 time: '2024-06-19T13:21:06.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29187 +- author: Just_Art + changes: + - message: Musician's jumpskirt added to loadouts. + type: Add + id: 6780 + time: '2024-06-19T14:06:03.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29203 From 7c01ac931f1ffb21e36f289b0f5ef04b0750d70a Mon Sep 17 00:00:00 2001 From: Flareguy <78941145+Flareguy@users.noreply.github.com> Date: Wed, 19 Jun 2024 09:49:19 -0500 Subject: [PATCH 026/109] hos cap resprite (#29208) * hos cap resprite * 1. 2. 3 4 Oh --- .../Head/Hats/hoshat.rsi/equipped-HELMET.png | Bin 505 -> 316 bytes .../Head/Hats/hoshat.rsi/inhand-left.png | Bin 315 -> 310 bytes .../Head/Hats/hoshat.rsi/inhand-right.png | Bin 310 -> 307 bytes .../Clothing/Head/Hats/hoshat.rsi/meta.json | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/equipped-HELMET.png index d897247c6ae512c1c7b64791719d2bf70745942b..b49cf3fc4bd9745616722f52456e181ee2069365 100644 GIT binary patch delta 280 zcmey#yoYImWFj*I1H;_yjcNS%G}%0G|+7 zAg!mTw^&ZTAdzLm3K2!mGFb?~e%d>1t4``naL^_Jy8zoGa>|C@JhR|F!dE&4QD{XWbc# zR|e~@Y^=;%d$uf(Wedj?u0=c77SuRSe!lXMwnBqL0|OHy6GzK^&TW6|%N1FYg-a`! zv0Y-9DnE97*MIM;Z&nZwWbH?a0X9TKbLh*2~7aJ_G;V! literal 505 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F5M?jcysy3fA0|Vn~ zPZ!6KiaBp*8Ri{w5OFyk2_Fof%C` zt+zgwZ<*Lyuu=M&W?EeHmTBMq*JdYsZ}wL>9(^EdYu52~p`lX~tlDZ>u74GJAT~c$ z@{-A4+5Itk8MBt{my14hg6&JP;?}+I8`gE0Uey0_ufiuOr(rt7Y@73+ztz|!Z@>L; z$IS<+`;V9Hb~U#&q$D1y!c)7u_XN9boum4=pT^9eg@!#*T`5`DVkH6ro&H3j)cEv3|wPWq) z>&Be-nAYq#JZr;drfJuwr{0_|UST_T(Xl7ncE3NJ`udz|L~Zx6_S&BhbRT>!*s=S- zx|w2|XP(S>%fqOY_<8?dk(SlT?aEG-H=-4uElR$2u^ioP*c{Ebw;@0I#zuzr3)iH8 PamnE6>gTe~DWM4fU3k|u diff --git a/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-left.png index 78a51140c60e47d0273ef2716682b931ccad237f..4900beca1d76219dfafaf9997cb0ff01e60f556c 100644 GIT binary patch delta 253 zcmdnZw2f(kW4&~MPlzi61B0HP-eNiVf<%@LGZw`-Si}fkSC}Z=~4P$vQA`Hn0dW0JY1Deh^`Ce|6hZct%RT!v*tVHG`A) sE~wgz#;{txX#O|TU&gWNJFDTlFV%JYZ;r@b0=kXC)78&qol`;+01QrEB>(^b delta 258 zcmdnSw3}&yW4%OxPl&6Yo}R0#YjAKd0|SGMjLhM~hi8k40ojbH{(eA;qa?^L_&)?N z+-u$&2^8Zj@Q5sCVBk9p!i>lBSEK+1XL!0ehD5l(owkwhumOh)r`ps1|1DJ08lKp= z$VP>J7wdMe^HQ7tqdmaE(SecmU_IB9>NB^J+$Q{XO9}qI_TEX2<$MA>oNl6LfDDhb z->&CPyL_^z=!L@C6|Yp;1Dh_hLo_n7im=#g9XOv+@Se{@;cuqif_D?rj~)wR*yTKr wHMWl76`zB`>)-F6Zq-;2U%|q9UwnuB$>7{y-63^zfsSPGboFyt=akR{0FlXMZ~y=R diff --git a/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-right.png index aa51e2962bd1f56c2d4aeb59683217fca8de07a0..79e7e68ec6d06e82fce86d63a1232925f4694f7b 100644 GIT binary patch delta 250 zcmdnSw3%swW4&~MPlzi61B0HP-eNiVf<%@LGZcg zot5=2^U@8k8Ebd;RXT)5Qg-;DVenjDXR-t)%ftSFo8 n`OlkzZGLqGJAByJkiq_9M;(8TruPq^!x%hW{an^LB{Ts5QY~RG delta 253 zcmdnYw2f(kW4%OxPl&6Yo}R0#YjAKd0|SGMjLhM~hi8k40ojbH{(eA;qa?^L_&)?N z+-u$&2^8Zj@Q5sCVBk9p!i>lBSEK+1Cwsa$hD5l(o#x1QSb>K{cJr10{|ldRL`~o* zyXw3;>-i*wYsnUg8~GPDFtsyr&9N7XDT}XkzkX<~%M*jk%GnZ!Vgrt7dL9&bC+bie zvS6)J+lS3dS7dGaFXDIojw2&0AFGIkNWgZmw)L{bii{C&e3}wG93o{B wx!j$Z7QB6b^p38SkXZSRqrm|mY8!8I?rh2ZwL&u766ih#Pgg&ebxsLQ01ZZDrvLx| diff --git a/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/meta.json b/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/meta.json index 214d5b0e9ef..648760ce176 100644 --- a/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/meta.json +++ b/Resources/Textures/Clothing/Head/Hats/hoshat.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d90c7e5de6f6d94596c164da78dbc8d3cd35bb99. Inhands made by Flareguy using equipped-HELMET state", + "copyright": "Made by Flareguy for SS14. Based on the HOS cap sprite taken from tgstation at commit https://github.com/tgstation/tgstation/commit/d90c7e5de6f6d94596c164da78dbc8d3cd35bb99.", "size": { "x": 32, "y": 32 From 8a4ce13d1c72c99c16c354b7c7f29a2b3edd4479 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 19 Jun 2024 14:50:25 +0000 Subject: [PATCH 027/109] 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 44c7f829a1d..1fea991f55e 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: SoulFN - changes: - - message: Changed textures of the assault borg modules - type: Tweak - id: 6281 - time: '2024-04-01T04:21:12.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26502 - author: Keer-Sar changes: - message: Cyborgs now have audio for some emotes. @@ -3845,3 +3838,10 @@ id: 6780 time: '2024-06-19T14:06:03.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29203 +- author: Flareguy + changes: + - message: Made the HOS's cap look significantly better. + type: Tweak + id: 6781 + time: '2024-06-19T14:49:19.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29208 From 20544b0c7656dd1deec583e9d8ad867c26c0dd4c Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 20 Jun 2024 03:03:32 +1200 Subject: [PATCH 028/109] Rejig device link sink & source startup & shutdown (#29035) * Fix DeviceLinkSinkComponent not updating sources on shutdown * Log error * Misc link changes & fixes * Fix core --- .../DeviceLinking/DeviceLinkSinkComponent.cs | 15 +- .../DeviceLinkSourceComponent.cs | 8 +- .../DeviceLinking/SharedDeviceLinkSystem.cs | 143 ++++++------------ Resources/Maps/core.yml | 3 - 4 files changed, 60 insertions(+), 109 deletions(-) diff --git a/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs b/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs index a66431e68ae..5d901b3fa68 100644 --- a/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs +++ b/Content.Shared/DeviceLinking/DeviceLinkSinkComponent.cs @@ -1,4 +1,5 @@ using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; namespace Content.Shared.DeviceLinking; @@ -11,13 +12,14 @@ public sealed partial class DeviceLinkSinkComponent : Component /// /// The ports this sink has /// - [DataField("ports", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] - public HashSet? Ports; + [DataField] + public HashSet> Ports = new(); /// - /// Used for removing a sink from all linked sources when it gets removed + /// Used for removing a sink from all linked sources when this component gets removed. + /// This is not serialized to yaml as it can be inferred from source components. /// - [DataField("links")] + [ViewVariables] public HashSet LinkedSources = new(); /// @@ -25,14 +27,13 @@ public sealed partial class DeviceLinkSinkComponent : Component /// The counter is counted down by one every tick if it's higher than 0 /// This is for preventing infinite loops /// - [DataField("invokeCounter")] + [DataField] public int InvokeCounter; /// /// How high the invoke counter is allowed to get before the links to the sink are removed and the DeviceLinkOverloadedEvent gets raised /// If the invoke limit is smaller than 1 the sink can't overload /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("invokeLimit")] + [DataField] public int InvokeLimit = 10; } diff --git a/Content.Shared/DeviceLinking/DeviceLinkSourceComponent.cs b/Content.Shared/DeviceLinking/DeviceLinkSourceComponent.cs index 332eff23cb2..856dc91acdc 100644 --- a/Content.Shared/DeviceLinking/DeviceLinkSourceComponent.cs +++ b/Content.Shared/DeviceLinking/DeviceLinkSourceComponent.cs @@ -12,12 +12,12 @@ public sealed partial class DeviceLinkSourceComponent : Component /// The ports the device link source sends signals from /// [DataField] - public HashSet>? Ports; + public HashSet> Ports = new(); /// - /// A list of sink uids that got linked for each port + /// Dictionary mapping each port to a set of linked sink entities. /// - [ViewVariables] + [ViewVariables] // This is not serialized as it can be constructed from LinkedPorts public Dictionary, HashSet> Outputs = new(); /// @@ -32,7 +32,7 @@ public sealed partial class DeviceLinkSourceComponent : Component /// The list of source to sink ports for each linked sink entity for easier managing of links /// [DataField] - public Dictionary source, ProtoId sink)>> LinkedPorts = new(); + public Dictionary Source, ProtoId Sink)>> LinkedPorts = new(); /// /// Limits the range devices can be linked across. diff --git a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs index 79a32268e80..3f969684b64 100644 --- a/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs +++ b/Content.Shared/DeviceLinking/SharedDeviceLinkSystem.cs @@ -20,98 +20,56 @@ public abstract class SharedDeviceLinkSystem : EntitySystem /// public override void Initialize() { - SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnSourceStartup); - SubscribeLocalEvent(OnSinkStartup); SubscribeLocalEvent(OnSourceRemoved); SubscribeLocalEvent(OnSinkRemoved); } #region Link Validation - private void OnInit(EntityUid uid, DeviceLinkSourceComponent component, ComponentInit args) - { - // Populate the output dictionary. - foreach (var (sinkUid, links) in component.LinkedPorts) - { - foreach (var link in links) - { - component.Outputs.GetOrNew(link.source).Add(sinkUid); - } - } - } - /// /// Removes invalid links where the saved sink doesn't exist/have a sink component for example /// - private void OnSourceStartup(EntityUid sourceUid, DeviceLinkSourceComponent sourceComponent, ComponentStartup args) + private void OnSourceStartup(Entity source, ref ComponentStartup args) { List invalidSinks = new(); - foreach (var sinkUid in sourceComponent.LinkedPorts.Keys) + List<(string, string)> invalidLinks = new(); + foreach (var (sink, links) in source.Comp.LinkedPorts) { - if (!TryComp(sinkUid, out var sinkComponent)) + if (!TryComp(sink, out DeviceLinkSinkComponent? sinkComponent)) { - invalidSinks.Add(sinkUid); - foreach (var savedSinks in sourceComponent.Outputs.Values) - { - savedSinks.Remove(sinkUid); - } - + invalidSinks.Add(sink); continue; } - sinkComponent.LinkedSources.Add(sourceUid); - } - - foreach (var invalidSink in invalidSinks) - { - sourceComponent.LinkedPorts.Remove(invalidSink); - } - } - - /// - /// Same with but also checks that the saved ports are present on the sink - /// - private void OnSinkStartup(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentStartup args) - { - List invalidSources = new(); - foreach (var sourceUid in sinkComponent.LinkedSources) - { - if (!TryComp(sourceUid, out var sourceComponent)) + foreach (var link in links) { - invalidSources.Add(sourceUid); - continue; + if (sinkComponent.Ports.Contains(link.Sink) && source.Comp.Ports.Contains(link.Source)) + source.Comp.Outputs.GetOrNew(link.Source).Add(sink); + else + invalidLinks.Add(link); } - if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var linkedPorts)) + foreach (var link in invalidLinks) { - foreach (var savedSinks in sourceComponent.Outputs.Values) - { - savedSinks.Remove(sinkUid); - } - continue; + Log.Warning($"Device source {ToPrettyString(source)} contains invalid links to entity {ToPrettyString(sink)}: {link.Item1}->{link.Item2}"); + links.Remove(link); } - if (sinkComponent.Ports == null) - continue; - - List<(string, string)> invalidLinks = new(); - foreach (var link in linkedPorts) + if (links.Count == 0) { - if (!sinkComponent.Ports.Contains(link.sink)) - invalidLinks.Add(link); + invalidSinks.Add(sink); + continue; } - foreach (var invalidLink in invalidLinks) - { - linkedPorts.Remove(invalidLink); - sourceComponent.Outputs.GetValueOrDefault(invalidLink.Item1)?.Remove(sinkUid); - } + invalidLinks.Clear(); + sinkComponent.LinkedSources.Add(source.Owner); } - foreach (var invalidSource in invalidSources) + foreach (var sink in invalidSinks) { - sinkComponent.LinkedSources.Remove(invalidSource); + source.Comp.LinkedPorts.Remove(sink); + Log.Warning($"Device source {ToPrettyString(source)} contains invalid sink: {ToPrettyString(sink)}"); } } #endregion @@ -119,26 +77,29 @@ private void OnSinkStartup(EntityUid sinkUid, DeviceLinkSinkComponent sinkCompon /// /// Ensures that its links get deleted when a source gets removed /// - private void OnSourceRemoved(EntityUid uid, DeviceLinkSourceComponent component, ComponentRemove args) + private void OnSourceRemoved(Entity source, ref ComponentRemove args) { var query = GetEntityQuery(); - foreach (var sinkUid in component.LinkedPorts.Keys) + foreach (var sinkUid in source.Comp.LinkedPorts.Keys) { if (query.TryGetComponent(sinkUid, out var sink)) - RemoveSinkFromSourceInternal(uid, sinkUid, component, sink); + RemoveSinkFromSourceInternal(source, sinkUid, source, sink); + else + Log.Error($"Device source {ToPrettyString(source)} links to invalid entity: {ToPrettyString(sinkUid)}"); } } /// /// Ensures that its links get deleted when a sink gets removed /// - private void OnSinkRemoved(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentRemove args) + private void OnSinkRemoved(Entity sink, ref ComponentRemove args) { - var query = GetEntityQuery(); - foreach (var linkedSource in sinkComponent.LinkedSources) + foreach (var sourceUid in sink.Comp.LinkedSources) { - if (query.TryGetComponent(sinkUid, out var source)) - RemoveSinkFromSourceInternal(linkedSource, sinkUid, source, sinkComponent); + if (TryComp(sourceUid, out DeviceLinkSourceComponent? source)) + RemoveSinkFromSourceInternal(sourceUid, sink, source, sink); + else + Log.Error($"Device sink {ToPrettyString(sink)} source list contains invalid entity: {ToPrettyString(sourceUid)}"); } } @@ -146,36 +107,36 @@ private void OnSinkRemoved(EntityUid sinkUid, DeviceLinkSinkComponent sinkCompon /// /// Convenience function to add several ports to an entity /// - public void EnsureSourcePorts(EntityUid uid, params string[] ports) + public void EnsureSourcePorts(EntityUid uid, params ProtoId[] ports) { if (ports.Length == 0) return; var comp = EnsureComp(uid); - comp.Ports ??= new HashSet>(); - foreach (var port in ports) { - DebugTools.Assert(_prototypeManager.HasIndex(port)); - comp.Ports?.Add(port); + if (!_prototypeManager.HasIndex(port)) + Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}"); + else + comp.Ports.Add(port); } } /// /// Convenience function to add several ports to an entity. /// - public void EnsureSinkPorts(EntityUid uid, params string[] ports) + public void EnsureSinkPorts(EntityUid uid, params ProtoId[] ports) { if (ports.Length == 0) return; var comp = EnsureComp(uid); - comp.Ports ??= new HashSet(); - foreach (var port in ports) { - DebugTools.Assert(_prototypeManager.HasIndex(port)); - comp.Ports?.Add(port); + if (!_prototypeManager.HasIndex(port)) + Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}"); + else + comp.Ports.Add(port); } } @@ -185,13 +146,13 @@ public void EnsureSinkPorts(EntityUid uid, params string[] ports) /// A list of source port prototypes public List GetSourcePorts(EntityUid sourceUid, DeviceLinkSourceComponent? sourceComponent = null) { - if (!Resolve(sourceUid, ref sourceComponent) || sourceComponent.Ports == null) + if (!Resolve(sourceUid, ref sourceComponent)) return new List(); var sourcePorts = new List(); foreach (var port in sourceComponent.Ports) { - sourcePorts.Add(_prototypeManager.Index(port)); + sourcePorts.Add(_prototypeManager.Index(port)); } return sourcePorts; @@ -203,13 +164,13 @@ public List GetSourcePorts(EntityUid sourceUid, DeviceLinkS /// A list of sink port prototypes public List GetSinkPorts(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null) { - if (!Resolve(sinkUid, ref sinkComponent) || sinkComponent.Ports == null) + if (!Resolve(sinkUid, ref sinkComponent)) return new List(); var sinkPorts = new List(); foreach (var port in sinkComponent.Ports) { - sinkPorts.Add(_prototypeManager.Index(port)); + sinkPorts.Add(_prototypeManager.Index(port)); } return sinkPorts; @@ -315,9 +276,6 @@ public void SaveLinks( if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent)) return; - if (sourceComponent.Ports == null || sinkComponent.Ports == null) - return; - if (!InRange(sourceUid, sinkUid, sourceComponent.Range)) { if (userId != null) @@ -391,7 +349,7 @@ public void RemoveSinkFromSource( else { Log.Error($"Attempted to remove link between {ToPrettyString(sourceUid)} and {ToPrettyString(sinkUid)}, but the sink component was missing."); - sourceComponent.LinkedPorts.Remove(sourceUid); + sourceComponent.LinkedPorts.Remove(sinkUid); } } @@ -414,12 +372,10 @@ private void RemoveSinkFromSourceInternal( sinkComponent.LinkedSources.Remove(sourceUid); sourceComponent.LinkedPorts.Remove(sinkUid); - var outputLists = sourceComponent.Outputs.Values; - foreach (var outputList in outputLists) + foreach (var outputList in sourceComponent.Outputs.Values) { outputList.Remove(sinkUid); } - } /// @@ -438,9 +394,6 @@ public bool ToggleLink( if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent)) return false; - if (sourceComponent.Ports == null || sinkComponent.Ports == null) - return false; - var outputs = sourceComponent.Outputs.GetOrNew(source); var linkedPorts = sourceComponent.LinkedPorts.GetOrNew(sinkUid); diff --git a/Resources/Maps/core.yml b/Resources/Maps/core.yml index 48ee0290fa2..09bb60c4eb9 100644 --- a/Resources/Maps/core.yml +++ b/Resources/Maps/core.yml @@ -115412,9 +115412,6 @@ entities: rot: 3.141592653589793 rad pos: 50.5,-2.5 parent: 2 - - type: DeviceLinkSource - linkedPorts: - 6516: [] - proto: SignalButtonDirectional entities: - uid: 2100 From 024521b66bad06af8d8e2626fc02a5cb892ac216 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Wed, 19 Jun 2024 11:04:30 -0400 Subject: [PATCH 029/109] Add prediction for Tech Disks, cleanup (#29061) * Add prediction for Tech Disks, cleanup * Remove IsServer check in OnMapInit * Use HashSet for techs, remove LINQ --- .../Systems/ResearchSystem.Technology.cs | 19 ---- .../Components/TechnologyDiskComponent.cs | 20 ---- .../Systems/TechnologyDiskSystem.cs | 92 ------------------ .../Research/Systems/SharedResearchSystem.cs | 19 ++++ .../Components/TechnologyDiskComponent.cs | 24 +++++ .../Systems/TechnologyDiskSystem.cs | 94 +++++++++++++++++++ 6 files changed, 137 insertions(+), 131 deletions(-) delete mode 100644 Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs delete mode 100644 Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs create mode 100644 Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs create mode 100644 Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs diff --git a/Content.Server/Research/Systems/ResearchSystem.Technology.cs b/Content.Server/Research/Systems/ResearchSystem.Technology.cs index 9bd71cf7c6e..7578d316c5e 100644 --- a/Content.Server/Research/Systems/ResearchSystem.Technology.cs +++ b/Content.Server/Research/Systems/ResearchSystem.Technology.cs @@ -131,25 +131,6 @@ public void AddTechnology(EntityUid uid, TechnologyPrototype technology, Technol RaiseLocalEvent(uid, ref ev); } - /// - /// Adds a lathe recipe to the specified technology database - /// without checking if it can be unlocked. - /// - public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null) - { - if (!Resolve(uid, ref component)) - return; - - if (component.UnlockedRecipes.Contains(recipe)) - return; - - component.UnlockedRecipes.Add(recipe); - Dirty(uid, component); - - var ev = new TechnologyDatabaseModifiedEvent(); - RaiseLocalEvent(uid, ref ev); - } - /// /// Returns whether a technology can be unlocked on this database, /// taking parent technologies into account. diff --git a/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs deleted file mode 100644 index eb5a0623a08..00000000000 --- a/Content.Server/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Shared.Random; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Research.TechnologyDisk.Components; - -[RegisterComponent] -public sealed partial class TechnologyDiskComponent : Component -{ - /// - /// The recipe that will be added. If null, one will be randomly generated - /// - [DataField("recipes")] - public List? Recipes; - - /// - /// A weighted random prototype for how rare each tier should be. - /// - [DataField("tierWeightPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string TierWeightPrototype = "TechDiskTierWeights"; -} diff --git a/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs b/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs deleted file mode 100644 index 176b2b68bc9..00000000000 --- a/Content.Server/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System.Linq; -using Content.Server.Popups; -using Content.Server.Research.Systems; -using Content.Server.Research.TechnologyDisk.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; -using Content.Shared.Random; -using Content.Shared.Random.Helpers; -using Content.Shared.Research.Components; -using Content.Shared.Research.Prototypes; -using Robust.Shared.Prototypes; -using Robust.Shared.Random; - -namespace Content.Server.Research.TechnologyDisk.Systems; - -public sealed class TechnologyDiskSystem : EntitySystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - [Dependency] private readonly ResearchSystem _research = default!; - [Dependency] private readonly IPrototypeManager _prototype = default!; - [Dependency] private readonly IRobustRandom _random = default!; - - /// - public override void Initialize() - { - SubscribeLocalEvent(OnAfterInteract); - SubscribeLocalEvent(OnExamine); - SubscribeLocalEvent(OnMapInit); - } - - private void OnAfterInteract(EntityUid uid, TechnologyDiskComponent component, AfterInteractEvent args) - { - if (args.Handled || !args.CanReach || args.Target is not { } target) - return; - - if (!HasComp(target) || !TryComp(target, out var database)) - return; - - if (component.Recipes != null) - { - foreach (var recipe in component.Recipes) - { - _research.AddLatheRecipe(target, recipe, database); - } - } - _popup.PopupEntity(Loc.GetString("tech-disk-inserted"), target, args.User); - QueueDel(uid); - args.Handled = true; - } - - private void OnExamine(EntityUid uid, TechnologyDiskComponent component, ExaminedEvent args) - { - var message = Loc.GetString("tech-disk-examine-none"); - if (component.Recipes != null && component.Recipes.Any()) - { - var prototype = _prototype.Index(component.Recipes[0]); - var resultPrototype = _prototype.Index(prototype.Result); - message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name)); - - if (component.Recipes.Count > 1) //idk how to do this well. sue me. - message += " " + Loc.GetString("tech-disk-examine-more"); - } - args.PushMarkup(message); - } - - private void OnMapInit(EntityUid uid, TechnologyDiskComponent component, MapInitEvent args) - { - if (component.Recipes != null) - return; - - var weightedRandom = _prototype.Index(component.TierWeightPrototype); - var tier = int.Parse(weightedRandom.Pick(_random)); - - //get a list of every distinct recipe in all the technologies. - var techs = new List>(); - foreach (var tech in _prototype.EnumeratePrototypes()) - { - if (tech.Tier != tier) - continue; - - techs.AddRange(tech.RecipeUnlocks); - } - techs = techs.Distinct().ToList(); - - if (!techs.Any()) - return; - - //pick one - component.Recipes = new(); - component.Recipes.Add(_random.Pick(techs)); - } -} diff --git a/Content.Shared/Research/Systems/SharedResearchSystem.cs b/Content.Shared/Research/Systems/SharedResearchSystem.cs index 9819e949b8b..81c6950f283 100644 --- a/Content.Shared/Research/Systems/SharedResearchSystem.cs +++ b/Content.Shared/Research/Systems/SharedResearchSystem.cs @@ -280,4 +280,23 @@ public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null) comp.UnlockedTechnologies.Clear(); Dirty(uid, comp); } + + /// + /// Adds a lathe recipe to the specified technology database + /// without checking if it can be unlocked. + /// + public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + if (component.UnlockedRecipes.Contains(recipe)) + return; + + component.UnlockedRecipes.Add(recipe); + Dirty(uid, component); + + var ev = new TechnologyDatabaseModifiedEvent(); + RaiseLocalEvent(uid, ref ev); + } } diff --git a/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs new file mode 100644 index 00000000000..ce8a138bdbd --- /dev/null +++ b/Content.Shared/Research/TechnologyDisk/Components/TechnologyDiskComponent.cs @@ -0,0 +1,24 @@ +using Content.Shared.Random; +using Content.Shared.Research.Prototypes; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Research.TechnologyDisk.Components; + +[RegisterComponent, NetworkedComponent] +[AutoGenerateComponentState] +public sealed partial class TechnologyDiskComponent : Component +{ + /// + /// The recipe that will be added. If null, one will be randomly generated + /// + [DataField] + [AutoNetworkedField] + public List>? Recipes; + + /// + /// A weighted random prototype for how rare each tier should be. + /// + [DataField] + public ProtoId TierWeightPrototype = "TechDiskTierWeights"; +} diff --git a/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs b/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs new file mode 100644 index 00000000000..b0d615fcb3b --- /dev/null +++ b/Content.Shared/Research/TechnologyDisk/Systems/TechnologyDiskSystem.cs @@ -0,0 +1,94 @@ +using Content.Shared.Examine; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Random.Helpers; +using Content.Shared.Research.Components; +using Content.Shared.Research.Prototypes; +using Content.Shared.Research.Systems; +using Content.Shared.Research.TechnologyDisk.Components; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; + +namespace Content.Shared.Research.TechnologyDisk.Systems; + +public sealed class TechnologyDiskSystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IPrototypeManager _protoMan = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedResearchSystem _research = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnAfterInteract); + SubscribeLocalEvent(OnExamine); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (ent.Comp.Recipes != null) + return; + + var weightedRandom = _protoMan.Index(ent.Comp.TierWeightPrototype); + var tier = int.Parse(weightedRandom.Pick(_random)); + + //get a list of every distinct recipe in all the technologies. + var techs = new HashSet>(); + foreach (var tech in _protoMan.EnumeratePrototypes()) + { + if (tech.Tier != tier) + continue; + + techs.UnionWith(tech.RecipeUnlocks); + } + + if (techs.Count == 0) + return; + + //pick one + ent.Comp.Recipes = []; + ent.Comp.Recipes.Add(_random.Pick(techs)); + Dirty(ent); + } + + private void OnAfterInteract(Entity ent, ref AfterInteractEvent args) + { + if (args.Handled || !args.CanReach || args.Target is not { } target) + return; + + if (!HasComp(target) || !TryComp(target, out var database)) + return; + + if (ent.Comp.Recipes != null) + { + foreach (var recipe in ent.Comp.Recipes) + { + _research.AddLatheRecipe(target, recipe, database); + } + } + _popup.PopupClient(Loc.GetString("tech-disk-inserted"), target, args.User); + if (_net.IsServer) + QueueDel(ent); + args.Handled = true; + } + + private void OnExamine(Entity ent, ref ExaminedEvent args) + { + var message = Loc.GetString("tech-disk-examine-none"); + if (ent.Comp.Recipes != null && ent.Comp.Recipes.Count > 0) + { + var prototype = _protoMan.Index(ent.Comp.Recipes[0]); + var resultPrototype = _protoMan.Index(prototype.Result); + message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name)); + + if (ent.Comp.Recipes.Count > 1) //idk how to do this well. sue me. + message += " " + Loc.GetString("tech-disk-examine-more"); + } + args.PushMarkup(message); + } +} From e33f0341ada2d8bd6ed4195ac3f0578b23e4ba8c Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Wed, 19 Jun 2024 11:13:40 -0400 Subject: [PATCH 030/109] Code cleanup: radio jammer (#29052) * Code cleanup for radio jammer * More Entity for the people, and fix an accidental variable reuse --- .../Systems/DeviceNetworkJammerSystem.cs | 6 ++ .../Systems/DeviceNetworkJammerSystem.cs | 19 +++-- .../Components/ActiveRadioJammerComponent.cs | 12 --- .../Radio/EntitySystems/JammerSystem.cs | 79 +++++++------------ .../DeviceNetworkJammerComponent.cs | 2 + .../SharedDeviceNetworkJammerSystem.cs | 63 +++++++++++++++ .../Components/ActiveRadioJammerComponent.cs | 13 +++ ...erComponent.cs => RadioJammerComponent.cs} | 6 +- .../Radio/EntitySystems/SharedJammerSystem.cs | 48 +++++++---- 9 files changed, 158 insertions(+), 90 deletions(-) create mode 100644 Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs delete mode 100644 Content.Server/Radio/Components/ActiveRadioJammerComponent.cs create mode 100644 Content.Shared/DeviceNetwork/Systems/SharedDeviceNetworkJammerSystem.cs create mode 100644 Content.Shared/Radio/Components/ActiveRadioJammerComponent.cs rename Content.Shared/Radio/Components/{SharedRadioJammerComponent.cs => RadioJammerComponent.cs} (91%) diff --git a/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs b/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs new file mode 100644 index 00000000000..39e03a17dae --- /dev/null +++ b/Content.Client/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs @@ -0,0 +1,6 @@ +using Content.Shared.DeviceNetwork.Systems; + +namespace Content.Client.DeviceNetwork.Systems; + +/// +public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem; diff --git a/Content.Server/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs b/Content.Server/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs index 3d3820562d6..0702a720424 100644 --- a/Content.Server/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs +++ b/Content.Server/DeviceNetwork/Systems/DeviceNetworkJammerSystem.cs @@ -1,12 +1,15 @@ -using Content.Server.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Components; +using Content.Shared.DeviceNetwork.Systems; using Robust.Server.GameObjects; namespace Content.Server.DeviceNetwork.Systems; -public sealed class DeviceNetworkJammerSystem : EntitySystem +/// +public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem { - [Dependency] private TransformSystem _transform = default!; + [Dependency] private readonly TransformSystem _transform = default!; + [Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!; + public override void Initialize() { base.Initialize(); @@ -14,20 +17,20 @@ public override void Initialize() SubscribeLocalEvent(BeforePacketSent); } - private void BeforePacketSent(EntityUid uid, TransformComponent xform, BeforePacketSentEvent ev) + private void BeforePacketSent(Entity xform, ref BeforePacketSentEvent ev) { if (ev.Cancelled) return; var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out var jammerComp, out var jammerXform)) + while (query.MoveNext(out var uid, out var jammerComp, out var jammerXform)) { - if (!jammerComp.JammableNetworks.Contains(ev.NetworkId)) + if (!_jammer.GetJammableNetworks((uid, jammerComp)).Contains(ev.NetworkId)) continue; - if (jammerXform.Coordinates.InRange(EntityManager, _transform, ev.SenderTransform.Coordinates, jammerComp.Range) - || jammerXform.Coordinates.InRange(EntityManager, _transform, xform.Coordinates, jammerComp.Range)) + if (_transform.InRange(jammerXform.Coordinates, ev.SenderTransform.Coordinates, jammerComp.Range) + || _transform.InRange(jammerXform.Coordinates, xform.Comp.Coordinates, jammerComp.Range)) { ev.Cancel(); return; diff --git a/Content.Server/Radio/Components/ActiveRadioJammerComponent.cs b/Content.Server/Radio/Components/ActiveRadioJammerComponent.cs deleted file mode 100644 index ff3e6271bef..00000000000 --- a/Content.Server/Radio/Components/ActiveRadioJammerComponent.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Content.Server.Radio.EntitySystems; - -namespace Content.Server.Radio.Components; - -/// -/// Prevents all radio in range from sending messages -/// -[RegisterComponent] -[Access(typeof(JammerSystem))] -public sealed partial class ActiveRadioJammerComponent : Component -{ -} diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs index 223d0e47c02..1fe48d22b45 100644 --- a/Content.Server/Radio/EntitySystems/JammerSystem.cs +++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs @@ -1,14 +1,12 @@ using Content.Server.DeviceNetwork.Components; -using Content.Server.Popups; using Content.Server.Power.EntitySystems; using Content.Server.PowerCell; -using Content.Server.Radio.Components; using Content.Shared.DeviceNetwork.Components; -using Content.Shared.Examine; using Content.Shared.Interaction; using Content.Shared.PowerCell.Components; -using Content.Shared.RadioJammer; using Content.Shared.Radio.EntitySystems; +using Content.Shared.Radio.Components; +using Content.Shared.DeviceNetwork.Systems; namespace Content.Server.Radio.EntitySystems; @@ -17,6 +15,7 @@ public sealed class JammerSystem : SharedJammerSystem [Dependency] private readonly PowerCellSystem _powerCell = default!; [Dependency] private readonly BatterySystem _battery = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!; public override void Initialize() { @@ -24,7 +23,6 @@ public override void Initialize() SubscribeLocalEvent(OnActivate); SubscribeLocalEvent(OnPowerCellChanged); - SubscribeLocalEvent(OnExamine); SubscribeLocalEvent(OnRadioSendAttempt); } @@ -37,27 +35,22 @@ public override void Update(float frameTime) if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery)) { - if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery)) + if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery)) { - ChangeLEDState(false, uid); + ChangeLEDState(uid, false); RemComp(uid); RemComp(uid); } else { var percentCharged = battery.CurrentCharge / battery.MaxCharge; - if (percentCharged > .50) + var chargeLevel = percentCharged switch { - ChangeChargeLevel(RadioJammerChargeLevel.High, uid); - } - else if (percentCharged < .15) - { - ChangeChargeLevel(RadioJammerChargeLevel.Low, uid); - } - else - { - ChangeChargeLevel(RadioJammerChargeLevel.Medium, uid); - } + > 0.50f => RadioJammerChargeLevel.High, + < 0.15f => RadioJammerChargeLevel.Low, + _ => RadioJammerChargeLevel.Medium, + }; + ChangeChargeLevel(uid, chargeLevel); } } @@ -65,28 +58,27 @@ public override void Update(float frameTime) } } - private void OnActivate(EntityUid uid, RadioJammerComponent comp, ActivateInWorldEvent args) + private void OnActivate(Entity ent, ref ActivateInWorldEvent args) { if (args.Handled || !args.Complex) return; - var activated = !HasComp(uid) && - _powerCell.TryGetBatteryFromSlot(uid, out var battery) && - battery.CurrentCharge > GetCurrentWattage(comp); + var activated = !HasComp(ent) && + _powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) && + battery.CurrentCharge > GetCurrentWattage(ent); if (activated) { - ChangeLEDState(true, uid); - EnsureComp(uid); - EnsureComp(uid, out var jammingComp); - jammingComp.Range = GetCurrentRange(comp); - jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString()); - Dirty(uid, jammingComp); + ChangeLEDState(ent.Owner, true); + EnsureComp(ent); + EnsureComp(ent, out var jammingComp); + _jammer.SetRange((ent, jammingComp), GetCurrentRange(ent)); + _jammer.AddJammableNetwork((ent, jammingComp), DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString()); } else { - ChangeLEDState(false, uid); - RemCompDeferred(uid); - RemCompDeferred(uid); + ChangeLEDState(ent.Owner, false); + RemCompDeferred(ent); + RemCompDeferred(ent); } var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state"); var message = Loc.GetString("radio-jammer-component-on-use", ("state", state)); @@ -94,27 +86,12 @@ private void OnActivate(EntityUid uid, RadioJammerComponent comp, ActivateInWorl args.Handled = true; } - private void OnPowerCellChanged(EntityUid uid, ActiveRadioJammerComponent comp, PowerCellChangedEvent args) + private void OnPowerCellChanged(Entity ent, ref PowerCellChangedEvent args) { if (args.Ejected) { - ChangeLEDState(false, uid); - RemCompDeferred(uid); - } - } - - private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args) - { - if (args.IsInDetailsRange) - { - var powerIndicator = HasComp(uid) - ? Loc.GetString("radio-jammer-component-examine-on-state") - : Loc.GetString("radio-jammer-component-examine-off-state"); - args.PushMarkup(powerIndicator); - - var powerLevel = Loc.GetString(comp.Settings[comp.SelectedPowerLevel].Name); - var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel)); - args.PushMarkup(switchIndicator); + ChangeLEDState(ent.Owner, false); + RemCompDeferred(ent); } } @@ -131,9 +108,9 @@ private bool ShouldCancelSend(EntityUid sourceUid) var source = Transform(sourceUid).Coordinates; var query = EntityQueryEnumerator(); - while (query.MoveNext(out _, out _, out var jam, out var transform)) + while (query.MoveNext(out var uid, out _, out var jam, out var transform)) { - if (source.InRange(EntityManager, _transform, transform.Coordinates, GetCurrentRange(jam))) + if (_transform.InRange(source, transform.Coordinates, GetCurrentRange((uid, jam)))) { return true; } diff --git a/Content.Shared/DeviceNetwork/Components/DeviceNetworkJammerComponent.cs b/Content.Shared/DeviceNetwork/Components/DeviceNetworkJammerComponent.cs index 75de0cb8a25..ab320d6d3e4 100644 --- a/Content.Shared/DeviceNetwork/Components/DeviceNetworkJammerComponent.cs +++ b/Content.Shared/DeviceNetwork/Components/DeviceNetworkJammerComponent.cs @@ -1,3 +1,4 @@ +using Content.Shared.DeviceNetwork.Systems; using Robust.Shared.GameStates; namespace Content.Shared.DeviceNetwork.Components; @@ -6,6 +7,7 @@ namespace Content.Shared.DeviceNetwork.Components; /// Allow entities to jam DeviceNetwork packets. /// [RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedDeviceNetworkJammerSystem))] public sealed partial class DeviceNetworkJammerComponent : Component { /// diff --git a/Content.Shared/DeviceNetwork/Systems/SharedDeviceNetworkJammerSystem.cs b/Content.Shared/DeviceNetwork/Systems/SharedDeviceNetworkJammerSystem.cs new file mode 100644 index 00000000000..fc714ea34f7 --- /dev/null +++ b/Content.Shared/DeviceNetwork/Systems/SharedDeviceNetworkJammerSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared.DeviceNetwork.Components; + +namespace Content.Shared.DeviceNetwork.Systems; + +/// +public abstract class SharedDeviceNetworkJammerSystem : EntitySystem +{ + /// + /// Sets the range of the jamming effect. + /// + public void SetRange(Entity ent, float value) + { + ent.Comp.Range = value; + Dirty(ent); + } + + /// + public bool TrySetRange(Entity ent, float value) + { + if (!Resolve(ent, ref ent.Comp, logMissing: false)) + return false; + + SetRange((ent, ent.Comp), value); + return true; + } + + /// + /// Returns the set of networks that this entity can jam. + public IReadOnlySet GetJammableNetworks(Entity ent) + { + return ent.Comp.JammableNetworks; + } + + /// + /// Enables this entity to jam packets on the specified network. + /// + public void AddJammableNetwork(Entity ent, string networkId) + { + if (ent.Comp.JammableNetworks.Add(networkId)) + Dirty(ent); + } + + /// + /// Stops this entity from jamming packets on the specified network. + /// + public void RemoveJammableNetwork(Entity ent, string networkId) + { + if (ent.Comp.JammableNetworks.Remove(networkId)) + Dirty(ent); + } + + /// + /// Stops this entity from jamming packets on any networks. + /// + public void ClearJammableNetworks(Entity ent) + { + if (ent.Comp.JammableNetworks.Count == 0) + return; + + ent.Comp.JammableNetworks.Clear(); + Dirty(ent); + } +} diff --git a/Content.Shared/Radio/Components/ActiveRadioJammerComponent.cs b/Content.Shared/Radio/Components/ActiveRadioJammerComponent.cs new file mode 100644 index 00000000000..d5679f11895 --- /dev/null +++ b/Content.Shared/Radio/Components/ActiveRadioJammerComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Radio.EntitySystems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Radio.Components; + +/// +/// Prevents all radio in range from sending messages +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedJammerSystem))] +public sealed partial class ActiveRadioJammerComponent : Component +{ +} diff --git a/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs b/Content.Shared/Radio/Components/RadioJammerComponent.cs similarity index 91% rename from Content.Shared/Radio/Components/SharedRadioJammerComponent.cs rename to Content.Shared/Radio/Components/RadioJammerComponent.cs index e5e52a3e475..8f3519cf7d3 100644 --- a/Content.Shared/Radio/Components/SharedRadioJammerComponent.cs +++ b/Content.Shared/Radio/Components/RadioJammerComponent.cs @@ -1,13 +1,14 @@ using Robust.Shared.Serialization; using Robust.Shared.GameStates; -namespace Content.Shared.RadioJammer; +namespace Content.Shared.Radio.Components; /// /// When activated () prevents from sending messages in range /// Suit sensors will also stop working. /// [NetworkedComponent, RegisterComponent] +[AutoGenerateComponentState] public sealed partial class RadioJammerComponent : Component { [DataDefinition] @@ -26,7 +27,7 @@ public partial struct RadioJamSetting public float Range; /// - /// The message that is displayed when switched + /// The message that is displayed when switched. /// to this setting. /// [DataField(required: true)] @@ -49,6 +50,7 @@ public partial struct RadioJamSetting /// Index of the currently selected setting. /// [DataField] + [AutoNetworkedField] public int SelectedPowerLevel = 1; } diff --git a/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs index e1f632735c2..8c5baf93f5d 100644 --- a/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs +++ b/Content.Shared/Radio/EntitySystems/SharedJammerSystem.cs @@ -1,13 +1,15 @@ using Content.Shared.Popups; -using Content.Shared.DeviceNetwork.Components; using Content.Shared.Verbs; -using Content.Shared.RadioJammer; +using Content.Shared.Examine; +using Content.Shared.Radio.Components; +using Content.Shared.DeviceNetwork.Systems; namespace Content.Shared.Radio.EntitySystems; public abstract class SharedJammerSystem : EntitySystem { [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; public override void Initialize() @@ -15,6 +17,7 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent>(OnGetVerb); + SubscribeLocalEvent(OnExamine); } private void OnGetVerb(Entity entity, ref GetVerbsEvent args) @@ -38,13 +41,11 @@ private void OnGetVerb(Entity entity, ref GetVerbsEvent { entity.Comp.SelectedPowerLevel = currIndex; - if (TryComp(entity.Owner, out var jammerComp)) + Dirty(entity); + if (_jammer.TrySetRange(entity.Owner, GetCurrentRange(entity))) { - // This is a little sketcy but only way to do it. - jammerComp.Range = GetCurrentRange(entity.Comp); - Dirty(entity.Owner, jammerComp); + Popup.PopupPredicted(Loc.GetString(setting.Message), user, user); } - Popup.PopupPredicted(Loc.GetString(setting.Message), user, user); }, Text = Loc.GetString(setting.Name), }; @@ -53,26 +54,39 @@ private void OnGetVerb(Entity entity, ref GetVerbsEvent ent, ref ExaminedEvent args) { - return jammer.Settings[jammer.SelectedPowerLevel].Wattage; + if (args.IsInDetailsRange) + { + var powerIndicator = HasComp(ent) + ? Loc.GetString("radio-jammer-component-examine-on-state") + : Loc.GetString("radio-jammer-component-examine-off-state"); + args.PushMarkup(powerIndicator); + + var powerLevel = Loc.GetString(ent.Comp.Settings[ent.Comp.SelectedPowerLevel].Name); + var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel)); + args.PushMarkup(switchIndicator); + } + } + + public float GetCurrentWattage(Entity jammer) + { + return jammer.Comp.Settings[jammer.Comp.SelectedPowerLevel].Wattage; } - public float GetCurrentRange(RadioJammerComponent jammer) + public float GetCurrentRange(Entity jammer) { - return jammer.Settings[jammer.SelectedPowerLevel].Range; + return jammer.Comp.Settings[jammer.Comp.SelectedPowerLevel].Range; } - protected void ChangeLEDState(bool isLEDOn, EntityUid uid, - AppearanceComponent? appearance = null) + protected void ChangeLEDState(Entity ent, bool isLEDOn) { - _appearance.SetData(uid, RadioJammerVisuals.LEDOn, isLEDOn, appearance); + _appearance.SetData(ent, RadioJammerVisuals.LEDOn, isLEDOn, ent.Comp); } - protected void ChangeChargeLevel(RadioJammerChargeLevel chargeLevel, EntityUid uid, - AppearanceComponent? appearance = null) + protected void ChangeChargeLevel(Entity ent, RadioJammerChargeLevel chargeLevel) { - _appearance.SetData(uid, RadioJammerVisuals.ChargeLevel, chargeLevel, appearance); + _appearance.SetData(ent, RadioJammerVisuals.ChargeLevel, chargeLevel, ent.Comp); } } From fa3c89a521e14878b8890afdbd4feaef5b9a9a12 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Thu, 20 Jun 2024 03:14:18 +1200 Subject: [PATCH 031/109] Partial buckling refactor (#29031) * partial buckling refactor * git mv test * change test namespace * git mv test * Update test namespace * Add pulling test * Network BuckleTime * Add two more tests * smelly --- Content.Client/Buckle/BuckleSystem.cs | 66 ++- .../Tests/Buckle/BuckleDragTest.cs | 56 ++ .../Tests/Buckle/BuckleTest.cs | 20 +- .../Tests/Climbing/ClimbingTest.cs | 1 + .../Construction/Interaction/CraftingTests.cs | 56 +- .../Interaction/InteractionTest.Helpers.cs | 20 +- .../Tests/Interaction/InteractionTest.cs | 8 +- .../Tests/Movement/BuckleMovementTest.cs | 63 ++ .../{Interaction => Movement}/MovementTest.cs | 3 +- .../Tests/Movement/PullingTest.cs | 73 +++ .../{Slipping => Movement}/SlippingTest.cs | 6 +- Content.Server/Bed/BedSystem.cs | 49 +- .../Bed/Components/HealOnBuckleComponent.cs | 21 +- .../Bed/Components/HealOnBuckleHealing.cs | 1 + .../Bed/Components/StasisBedComponent.cs | 3 +- .../Body/Systems/BloodstreamSystem.cs | 3 + .../Body/Systems/MetabolizerSystem.cs | 6 + .../Body/Systems/RespiratorSystem.cs | 3 + .../Operators/Combat/UnbuckleOperator.cs | 7 +- Content.Shared/Bed/Sleep/SleepingSystem.cs | 10 + .../Buckle/Components/BuckleComponent.cs | 113 +++- .../Buckle/Components/StrapComponent.cs | 50 +- .../Buckle/SharedBuckleSystem.Buckle.cs | 556 +++++++++--------- .../Buckle/SharedBuckleSystem.Interaction.cs | 171 ++++++ .../Buckle/SharedBuckleSystem.Strap.cs | 245 +------- Content.Shared/Buckle/SharedBuckleSystem.cs | 49 +- .../Climbing/Systems/ClimbSystem.cs | 6 +- Content.Shared/Cuffs/SharedCuffableSystem.cs | 27 +- Content.Shared/Foldable/FoldableSystem.cs | 6 +- .../Interaction/RotateToFaceSystem.cs | 32 +- .../Systems/MobStateSystem.Subscribers.cs | 14 +- .../Pulling/Events/PullStartedMessage.cs | 11 +- .../Pulling/Events/PullStoppedMessage.cs | 13 +- .../Movement/Pulling/Systems/PullingSystem.cs | 58 +- .../Systems/SharedWaddleAnimationSystem.cs | 2 +- .../Traits/Assorted/LegsParalyzedSystem.cs | 19 +- .../Entities/Structures/Furniture/chairs.yml | 2 + .../Structures/Furniture/rollerbeds.yml | 6 + 38 files changed, 1009 insertions(+), 846 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs create mode 100644 Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs rename Content.IntegrationTests/Tests/{Interaction => Movement}/MovementTest.cs (95%) create mode 100644 Content.IntegrationTests/Tests/Movement/PullingTest.cs rename Content.IntegrationTests/Tests/{Slipping => Movement}/SlippingTest.cs (91%) create mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index fea18e5cf3c..6770899e0aa 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Buckle.Components; using Content.Shared.Rotation; using Robust.Client.GameObjects; +using Robust.Shared.GameStates; namespace Content.Client.Buckle; @@ -14,40 +15,63 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBuckleAfterAutoHandleState); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnStrapMoveEvent); } - private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args) + private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) { - ActionBlocker.UpdateCanMove(uid); + // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment: + // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride - if (!TryComp(uid, out var ownerSprite)) + // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore. + // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking + // sprite rendering for entity layers & direction dependent sorting. + + if (args.NewRotation == args.OldRotation) return; - // Adjust draw depth when the chair faces north so that the seat back is drawn over the player. - // Reset the draw depth when rotated in any other direction. - // TODO when ECSing, make this a visualizer - // This code was written before rotatable viewports were introduced, so hard-coding Direction.North - // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but - // better to get it working for most people before we look at a more permanent solution. - if (component is { Buckled: true, LastEntityBuckledTo: { } } && - Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North && - TryComp(component.LastEntityBuckledTo, out var buckledSprite)) - { - component.OriginalDrawDepth ??= ownerSprite.DrawDepth; - ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1; + if (!TryComp(uid, out var strapSprite)) return; - } - // If here, we're not turning north and should restore the saved draw depth. - if (component.OriginalDrawDepth.HasValue) + var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North; + foreach (var buckledEntity in component.BuckledEntities) { - ownerSprite.DrawDepth = component.OriginalDrawDepth.Value; - component.OriginalDrawDepth = null; + if (!TryComp(buckledEntity, out var buckle)) + continue; + + if (!TryComp(buckledEntity, out var buckledSprite)) + continue; + + if (isNorth) + { + buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth; + buckledSprite.DrawDepth = strapSprite.DrawDepth - 1; + } + else if (buckle.OriginalDrawDepth.HasValue) + { + buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value; + buckle.OriginalDrawDepth = null; + } } } + private void OnHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not BuckleState state) + return; + + ent.Comp.DontCollide = state.DontCollide; + ent.Comp.BuckleTime = state.BuckleTime; + var strapUid = EnsureEntity(state.BuckledTo, ent); + + SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null)); + + var (uid, component) = ent; + + } + private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { if (!TryComp(uid, out var rotVisuals)) diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs new file mode 100644 index 00000000000..8df151d5a0e --- /dev/null +++ b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs @@ -0,0 +1,56 @@ +using Content.IntegrationTests.Tests.Interaction; +using Content.Shared.Buckle; +using Content.Shared.Buckle.Components; +using Content.Shared.Input; +using Content.Shared.Movement.Pulling.Components; + +namespace Content.IntegrationTests.Tests.Buckle; + +public sealed class BuckleDragTest : InteractionTest +{ + // Check that dragging a buckled player unbuckles them. + [Test] + public async Task BucklePullTest() + { + var urist = await SpawnTarget("MobHuman"); + var sUrist = ToServer(urist); + await SpawnTarget("Chair"); + + var buckle = Comp(urist); + var strap = Comp(Target); + var puller = Comp(Player); + var pullable = Comp(urist); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Initially not buckled to the chair and not pulling anything + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + + // Strap the human to the chair + Assert.That(Server.System().TryBuckle(sUrist, SPlayer, STarget.Value)); + await RunTicks(5); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{sUrist})); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + + // Start pulling, and thus unbuckle them + await PressKey(ContentKeyFunctions.TryPullObject, cursorEntity:urist); + await RunTicks(5); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(puller.Pulling, Is.EqualTo(sUrist)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + } +} diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs index 57ac63b1247..156f42aac33 100644 --- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs +++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs @@ -91,7 +91,6 @@ await server.WaitAssertion(() => { Assert.That(strap, Is.Not.Null); Assert.That(strap.BuckledEntities, Is.Empty); - Assert.That(strap.OccupiedSize, Is.Zero); }); // Side effects of buckling @@ -111,8 +110,6 @@ await server.WaitAssertion(() => // Side effects of buckling for the strap Assert.That(strap.BuckledEntities, Does.Contain(human)); - Assert.That(strap.OccupiedSize, Is.EqualTo(buckle.Size)); - Assert.That(strap.OccupiedSize, Is.Positive); }); #pragma warning disable NUnit2045 // Interdependent asserts. @@ -122,7 +119,7 @@ await server.WaitAssertion(() => // Trying to unbuckle too quickly fails Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -149,7 +146,6 @@ await server.WaitAssertion(() => // Unbuckle, strap Assert.That(strap.BuckledEntities, Is.Empty); - Assert.That(strap.OccupiedSize, Is.Zero); }); #pragma warning disable NUnit2045 // Interdependent asserts. @@ -160,9 +156,9 @@ await server.WaitAssertion(() => // On cooldown Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); + Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False); Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 }); @@ -189,7 +185,6 @@ await server.WaitAssertion(() => #pragma warning disable NUnit2045 // Interdependent asserts. Assert.That(buckleSystem.TryBuckle(human, human, chair, buckleComp: buckle), Is.False); Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); #pragma warning restore NUnit2045 // Move near the chair @@ -202,12 +197,10 @@ await server.WaitAssertion(() => Assert.That(buckle.Buckled); Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False); Assert.That(buckle.Buckled); - Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False); - Assert.That(buckle.Buckled); #pragma warning restore NUnit2045 // Force unbuckle - Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle)); + buckleSystem.Unbuckle(human, human); Assert.Multiple(() => { Assert.That(buckle.Buckled, Is.False); @@ -311,7 +304,7 @@ await server.WaitAssertion(() => // Break our guy's kneecaps foreach (var leg in legs) { - xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent(leg.Id)); + entityManager.DeleteEntity(leg.Id); } }); @@ -328,7 +321,8 @@ await server.WaitAssertion(() => Assert.That(hand.HeldEntity, Is.Null); } - buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle); + buckleSystem.Unbuckle(human, human); + Assert.That(buckle.Buckled, Is.False); }); await pair.CleanReturnAsync(); diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs index d8d3086520e..2db0a9acd3d 100644 --- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs +++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs @@ -1,5 +1,6 @@ #nullable enable using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Movement; using Robust.Shared.Maths; using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent; using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem; diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs index 76911eba5f7..74d0e924217 100644 --- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs +++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs @@ -59,11 +59,6 @@ public async Task CraftSpear() await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1)); } - // The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize - // net messages and just copy objects by reference. This means that the server will directly modify cached server - // states on the client's end. Crude fix at the moment is to used modified state handling while in debug mode - // Otherwise, this test cannot work. -#if DEBUG /// /// Cancel crafting a complex recipe. /// @@ -93,28 +88,22 @@ public async Task CancelCraft() await RunTicks(1); // DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container. - Assert.Multiple(async () => - { - Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); - Assert.That(sys.IsEntityInContainer(shard), Is.True); - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(rodStack, Has.Count.EqualTo(8)); - Assert.That(wireStack, Has.Count.EqualTo(7)); + Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); + Assert.That(sys.IsEntityInContainer(shard), Is.True); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(rodStack, Has.Count.EqualTo(8)); + Assert.That(wireStack, Has.Count.EqualTo(7)); - await FindEntity(Spear, shouldSucceed: false); - }); + await FindEntity(Spear, shouldSucceed: false); // Cancel the DoAfter. Should drop ingredients to the floor. await CancelDoAfters(); - Assert.Multiple(async () => - { - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(sys.IsEntityInContainer(shard), Is.False); - await FindEntity(Spear, shouldSucceed: false); - await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1)); - }); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(sys.IsEntityInContainer(shard), Is.False); + await FindEntity(Spear, shouldSucceed: false); + await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1)); // Re-attempt the do-after #pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above. @@ -123,24 +112,17 @@ public async Task CancelCraft() await RunTicks(1); // DoAfter is in progress. Entity not spawned, ingredients are in a container. - Assert.Multiple(async () => - { - Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); - Assert.That(sys.IsEntityInContainer(shard), Is.True); - await FindEntity(Spear, shouldSucceed: false); - }); + Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1)); + Assert.That(sys.IsEntityInContainer(shard), Is.True); + await FindEntity(Spear, shouldSucceed: false); // Finish the DoAfter await AwaitDoAfters(); // Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed. - Assert.Multiple(async () => - { - await FindEntity(Spear); - Assert.That(sys.IsEntityInContainer(rods), Is.False); - Assert.That(sys.IsEntityInContainer(wires), Is.False); - Assert.That(SEntMan.Deleted(shard)); - }); + await FindEntity(Spear); + Assert.That(sys.IsEntityInContainer(rods), Is.False); + Assert.That(sys.IsEntityInContainer(wires), Is.False); + Assert.That(SEntMan.Deleted(shard)); } -#endif } diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index f4826cb2495..a61a0593017 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -84,8 +84,9 @@ protected async Task CraftItem(string prototype, bool shouldSucceed = true) /// /// Spawn an entity entity and set it as the target. /// - [MemberNotNull(nameof(Target))] - protected async Task SpawnTarget(string prototype) + [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))] +#pragma warning disable CS8774 // Member must have a non-null value when exiting. + protected async Task SpawnTarget(string prototype) { Target = NetEntity.Invalid; await Server.WaitPost(() => @@ -95,7 +96,9 @@ await Server.WaitPost(() => await RunTicks(5); AssertPrototype(prototype); + return Target!.Value; } +#pragma warning restore CS8774 // Member must have a non-null value when exiting. /// /// Spawn an entity in preparation for deconstruction @@ -1170,14 +1173,17 @@ await Server.WaitPost(() => #region Inputs + + /// /// Make the client press and then release a key. This assumes the key is currently released. + /// This will default to using the entity and coordinates. /// protected async Task PressKey( BoundKeyFunction key, int ticks = 1, NetCoordinates? coordinates = null, - NetEntity cursorEntity = default) + NetEntity? cursorEntity = null) { await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity); await RunTicks(ticks); @@ -1186,15 +1192,17 @@ protected async Task PressKey( } /// - /// Make the client press or release a key + /// Make the client press or release a key. + /// This will default to using the entity and coordinates. /// protected async Task SetKey( BoundKeyFunction key, BoundKeyState state, NetCoordinates? coordinates = null, - NetEntity cursorEntity = default) + NetEntity? cursorEntity = null) { var coords = coordinates ?? TargetCoords; + var target = cursorEntity ?? Target ?? default; ScreenCoordinates screen = default; var funcId = InputManager.NetworkBindMap.KeyFunctionID(key); @@ -1203,7 +1211,7 @@ protected async Task SetKey( State = state, Coordinates = CEntMan.GetCoordinates(coords), ScreenCoordinates = screen, - Uid = CEntMan.GetEntity(cursorEntity), + Uid = CEntMan.GetEntity(target), }; await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message)); diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs index 089addfaefd..37102481ed0 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs @@ -84,6 +84,7 @@ public abstract partial class InteractionTest protected NetEntity? Target; protected EntityUid? STarget => ToServer(Target); + protected EntityUid? CTarget => ToClient(Target); /// @@ -128,7 +129,6 @@ public abstract partial class InteractionTest public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds; - // Simple mob that has one hand and can perform misc interactions. [TestPrototypes] private const string TestPrototypes = @" @@ -142,6 +142,8 @@ public abstract partial class InteractionTest - type: ComplexInteraction - type: MindContainer - type: Stripping + - type: Puller + - type: Physics - type: Tag tags: - CanPilot @@ -207,8 +209,8 @@ await Server.WaitPost(() => SEntMan.System().WipeMind(ServerSession.ContentData()?.Mind); old = cPlayerMan.LocalEntity; - Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords))); - SPlayer = SEntMan.GetEntity(Player); + SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)); + Player = SEntMan.GetNetEntity(SPlayer); Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer); Hands = SEntMan.GetComponent(SPlayer); DoAfters = SEntMan.GetComponent(SPlayer); diff --git a/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs new file mode 100644 index 00000000000..8d91855098f --- /dev/null +++ b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs @@ -0,0 +1,63 @@ +using Content.Shared.Alert; +using Content.Shared.Buckle.Components; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests.Movement; + +public sealed class BuckleMovementTest : MovementTest +{ + // Check that interacting with a chair straps you to it and prevents movement. + [Test] + public async Task ChairTest() + { + await SpawnTarget("Chair"); + + var cAlert = Client.System(); + var sAlert = Server.System(); + var buckle = Comp(Player); + var strap = Comp(Target); + +#pragma warning disable RA0002 + buckle.Delay = TimeSpan.Zero; +#pragma warning restore RA0002 + + // Initially not buckled to the chair, and standing off to the side + Assert.That(Delta(), Is.InRange(0.9f, 1.1f)); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False); + + // Interact results in being buckled to the chair + await Interact(); + Assert.That(Delta(), Is.InRange(-0.01f, 0.01f)); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer})); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True); + + // Attempting to walk away does nothing + await Move(DirectionFlag.East, 1); + Assert.That(Delta(), Is.InRange(-0.01f, 0.01f)); + Assert.That(buckle.Buckled, Is.True); + Assert.That(buckle.BuckledTo, Is.EqualTo(STarget)); + Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer})); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True); + + // Interacting again will unbuckle the player + await Interact(); + Assert.That(Delta(), Is.InRange(-0.5f, 0.5f)); + Assert.That(buckle.Buckled, Is.False); + Assert.That(buckle.BuckledTo, Is.Null); + Assert.That(strap.BuckledEntities, Is.Empty); + Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False); + + // And now they can move away + await Move(DirectionFlag.SouthEast, 1); + Assert.That(Delta(), Is.LessThan(-1)); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs similarity index 95% rename from Content.IntegrationTests/Tests/Interaction/MovementTest.cs rename to Content.IntegrationTests/Tests/Movement/MovementTest.cs index dc5aec92cfc..ad7b1d0459f 100644 --- a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs +++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs @@ -1,8 +1,9 @@ #nullable enable using System.Numerics; +using Content.IntegrationTests.Tests.Interaction; using Robust.Shared.GameObjects; -namespace Content.IntegrationTests.Tests.Interaction; +namespace Content.IntegrationTests.Tests.Movement; /// /// This is a variation of that sets up the player with a normal human entity and a simple diff --git a/Content.IntegrationTests/Tests/Movement/PullingTest.cs b/Content.IntegrationTests/Tests/Movement/PullingTest.cs new file mode 100644 index 00000000000..d96c4ea0e56 --- /dev/null +++ b/Content.IntegrationTests/Tests/Movement/PullingTest.cs @@ -0,0 +1,73 @@ +#nullable enable +using Content.Shared.Alert; +using Content.Shared.Input; +using Content.Shared.Movement.Pulling.Components; +using Robust.Shared.Maths; + +namespace Content.IntegrationTests.Tests.Movement; + +public sealed class PullingTest : MovementTest +{ + protected override int Tiles => 4; + + [Test] + public async Task PullTest() + { + var cAlert = Client.System(); + var sAlert = Server.System(); + await SpawnTarget("MobHuman"); + + var puller = Comp(Player); + var pullable = Comp(Target); + + // Player is initially to the left of the target and not pulling anything + Assert.That(Delta(), Is.InRange(0.9f, 1.1f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False); + + // Start pulling + await PressKey(ContentKeyFunctions.TryPullObject); + await RunTicks(5); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Move to the left and check that the target moves with the player and is still being pulled. + await Move(DirectionFlag.West, 1); + Assert.That(Delta(), Is.InRange(0.9f, 1.3f)); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Move in the other direction + await Move(DirectionFlag.East, 2); + Assert.That(Delta(), Is.InRange(-1.3f, -0.9f)); + Assert.That(puller.Pulling, Is.EqualTo(STarget)); + Assert.That(pullable.Puller, Is.EqualTo(SPlayer)); + Assert.That(pullable.BeingPulled, Is.True); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True); + + // Stop pulling + await PressKey(ContentKeyFunctions.ReleasePulledObject); + await RunTicks(5); + Assert.That(Delta(), Is.InRange(-1.3f, -0.9f)); + Assert.That(puller.Pulling, Is.Null); + Assert.That(pullable.Puller, Is.Null); + Assert.That(pullable.BeingPulled, Is.False); + Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False); + Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False); + + // Move back to the left and ensure the target is no longer following us. + await Move(DirectionFlag.West, 2); + Assert.That(Delta(), Is.GreaterThan(2f)); + } +} + diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs similarity index 91% rename from Content.IntegrationTests/Tests/Slipping/SlippingTest.cs rename to Content.IntegrationTests/Tests/Movement/SlippingTest.cs index 7f77146f455..7ee895d7c27 100644 --- a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs @@ -8,7 +8,7 @@ using Robust.Shared.Input; using Robust.Shared.Maths; -namespace Content.IntegrationTests.Tests.Slipping; +namespace Content.IntegrationTests.Tests.Movement; public sealed class SlippingTest : MovementTest { @@ -36,18 +36,14 @@ public async Task BananaSlipTest() Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); // Player is to the left of the banana peel and has not slipped. -#pragma warning disable NUnit2045 Assert.That(Delta(), Is.GreaterThan(0.5f)); Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); -#pragma warning restore NUnit2045 // Walking over the banana slowly does not trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down); await Move(DirectionFlag.East, 1f); -#pragma warning disable NUnit2045 Assert.That(Delta(), Is.LessThan(0.5f)); Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); -#pragma warning restore NUnit2045 AssertComp(false, Player); // Moving at normal speeds does trigger a slip. diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs index ee43cff26de..a6b61da591f 100644 --- a/Content.Server/Bed/BedSystem.cs +++ b/Content.Server/Bed/BedSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.Emag.Systems; using Content.Shared.Mobs.Systems; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Server.Bed { @@ -26,25 +27,29 @@ public sealed class BedSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(ManageUpdateList); - SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnStrapped); + SubscribeLocalEvent(OnUnstrapped); + SubscribeLocalEvent(OnStasisStrapped); + SubscribeLocalEvent(OnStasisUnstrapped); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnEmagged); } - private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args) + private void OnStrapped(Entity bed, ref StrappedEvent args) { - if (args.Buckling) - { - AddComp(uid); - component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime); - _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid); - return; - } + EnsureComp(bed); + bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime); + _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed); - _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction); - _sleepingSystem.TryWaking(args.BuckledEntity); - RemComp(uid); + // Single action entity, cannot strap multiple entities to the same bed. + DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1); + } + + private void OnUnstrapped(Entity bed, ref UnstrappedEvent args) + { + _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction); + _sleepingSystem.TryWaking(args.Buckle.Owner); + RemComp(bed); } public override void Update(float frameTime) @@ -82,18 +87,22 @@ private void UpdateAppearance(EntityUid uid, bool isOn) _appearance.SetData(uid, StasisBedVisuals.IsOn, isOn); } - private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args) + private void OnStasisStrapped(Entity bed, ref StrappedEvent args) { - // In testing this also received an unbuckle event when the bed is destroyed - // So don't worry about that - if (!HasComp(args.BuckledEntity)) + if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - if (!this.IsPowered(uid, EntityManager)) + var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true); + RaiseLocalEvent(args.Buckle, ref metabolicEvent); + } + + private void OnStasisUnstrapped(Entity bed, ref UnstrappedEvent args) + { + if (!HasComp(args.Buckle) || !this.IsPowered(bed, EntityManager)) return; - var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling); - RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent); + var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false); + RaiseLocalEvent(args.Buckle, ref metabolicEvent); } private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args) diff --git a/Content.Server/Bed/Components/HealOnBuckleComponent.cs b/Content.Server/Bed/Components/HealOnBuckleComponent.cs index f29fe30429f..3c6f3a4382b 100644 --- a/Content.Server/Bed/Components/HealOnBuckleComponent.cs +++ b/Content.Server/Bed/Components/HealOnBuckleComponent.cs @@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components [RegisterComponent] public sealed partial class HealOnBuckleComponent : Component { - [DataField("damage", required: true)] - [ViewVariables(VVAccess.ReadWrite)] + /// + /// Damage to apply to entities that are strapped to this entity. + /// + [DataField(required: true)] public DamageSpecifier Damage = default!; - [DataField("healTime", required: false)] - [ViewVariables(VVAccess.ReadWrite)] - public float HealTime = 1f; // How often the bed applies the damage + /// + /// How frequently the damage should be applied, in seconds. + /// + [DataField(required: false)] + public float HealTime = 1f; - [DataField("sleepMultiplier")] + /// + /// Damage multiplier that gets applied if the entity is sleeping. + /// + [DataField] public float SleepMultiplier = 3f; public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal - [DataField("sleepAction")] public EntityUid? SleepAction; + [DataField] public EntityUid? SleepAction; } } diff --git a/Content.Server/Bed/Components/HealOnBuckleHealing.cs b/Content.Server/Bed/Components/HealOnBuckleHealing.cs index a944e67e12d..aaa82c737c5 100644 --- a/Content.Server/Bed/Components/HealOnBuckleHealing.cs +++ b/Content.Server/Bed/Components/HealOnBuckleHealing.cs @@ -1,5 +1,6 @@ namespace Content.Server.Bed.Components { + // TODO rename this component [RegisterComponent] public sealed partial class HealOnBuckleHealingComponent : Component {} diff --git a/Content.Server/Bed/Components/StasisBedComponent.cs b/Content.Server/Bed/Components/StasisBedComponent.cs index e2175d6e646..160bd0ac678 100644 --- a/Content.Server/Bed/Components/StasisBedComponent.cs +++ b/Content.Server/Bed/Components/StasisBedComponent.cs @@ -6,7 +6,8 @@ public sealed partial class StasisBedComponent : Component /// /// What the metabolic update rate will be multiplied by (higher = slower metabolism) /// - [ViewVariables(VVAccess.ReadWrite)] + [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first + [DataField] public float Multiplier = 10f; } } diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs index f961307fc6a..eaa7b62f25a 100644 --- a/Content.Server/Body/Systems/BloodstreamSystem.cs +++ b/Content.Server/Body/Systems/BloodstreamSystem.cs @@ -268,6 +268,9 @@ private void OnApplyMetabolicMultiplier( Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs index 45cba5a195f..8394d9999bc 100644 --- a/Content.Server/Body/Systems/MetabolizerSystem.cs +++ b/Content.Server/Body/Systems/MetabolizerSystem.cs @@ -67,6 +67,9 @@ private void OnApplyMetabolicMultiplier( Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; @@ -229,6 +232,9 @@ private void TryMetabolize(Entity ent, ref ApplyMetabolicMultiplierEvent args) { + // TODO REFACTOR THIS + // This will slowly drift over time due to floating point errors. + // Instead, raise an event with the base rates and allow modifiers to get applied to it. if (args.Apply) { ent.Comp.UpdateInterval *= args.Multiplier; diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs index 207665d786f..116e8fe7c7f 100644 --- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs +++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs @@ -1,11 +1,9 @@ using Content.Server.Buckle.Systems; -using Content.Shared.Buckle.Components; namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat; public sealed partial class UnbuckleOperator : HTNOperator { - [Dependency] private readonly IEntityManager _entManager = default!; private BuckleSystem _buckle = default!; [DataField("shutdownState")] @@ -21,10 +19,7 @@ public override void Startup(NPCBlackboard blackboard) { base.Startup(blackboard); var owner = blackboard.GetValue(NPCBlackboard.Owner); - if (!_entManager.TryGetComponent(owner, out var buckle) || !buckle.Buckled) - return; - - _buckle.TryUnbuckle(owner, owner, true, buckle); + _buckle.Unbuckle(owner, null); } public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime) diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index 6008e301cfe..581924c053b 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Actions; +using Content.Shared.Buckle.Components; using Content.Shared.Damage; using Content.Shared.Damage.ForceSay; using Content.Shared.Examine; @@ -59,6 +60,15 @@ public override void Initialize() SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnUnbuckleAttempt); + } + + private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) + { + // TODO is this necessary? + // Shouldn't the interaction have already been blocked by a general interaction check? + if (ent.Owner == args.User) + args.Cancelled = true; } private void OnBedSleepAction(Entity ent, ref SleepActionEvent args) diff --git a/Content.Shared/Buckle/Components/BuckleComponent.cs b/Content.Shared/Buckle/Components/BuckleComponent.cs index cf28b56d51f..ee86e6d4de0 100644 --- a/Content.Shared/Buckle/Components/BuckleComponent.cs +++ b/Content.Shared/Buckle/Components/BuckleComponent.cs @@ -1,10 +1,15 @@ +using System.Diagnostics.CodeAnalysis; using Content.Shared.Interaction; using Robust.Shared.GameStates; using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; namespace Content.Shared.Buckle.Components; -[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +/// +/// This component allows an entity to be buckled to an entity with a . +/// +[RegisterComponent, NetworkedComponent] [Access(typeof(SharedBuckleSystem))] public sealed partial class BuckleComponent : Component { @@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component /// across a table two tiles away" problem. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public float Range = SharedInteractionSystem.InteractionRange / 1.4f; /// /// True if the entity is buckled, false otherwise. /// - [ViewVariables(VVAccess.ReadWrite)] - [AutoNetworkedField] - public bool Buckled; - - [ViewVariables] - [AutoNetworkedField] - public EntityUid? LastEntityBuckledTo; + [MemberNotNullWhen(true, nameof(BuckledTo))] + public bool Buckled => BuckledTo != null; /// /// Whether or not collisions should be possible with the entity we are strapped to /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField, AutoNetworkedField] + [DataField] public bool DontCollide; /// /// Whether or not we should be allowed to pull the entity we are strapped to /// - [ViewVariables(VVAccess.ReadWrite)] [DataField] public bool PullStrap; @@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component /// be able to unbuckle after recently buckling. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public TimeSpan Delay = TimeSpan.FromSeconds(0.25f); /// /// The time that this entity buckled at. /// - [ViewVariables] - public TimeSpan BuckleTime; + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))] + public TimeSpan? BuckleTime; /// /// The strap that this component is buckled to. /// - [ViewVariables] - [AutoNetworkedField] + [DataField] public EntityUid? BuckledTo; /// @@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component /// . /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public int Size = 100; /// @@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component [ViewVariables] public int? OriginalDrawDepth; } +[Serializable, NetSerializable] +public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState +{ + public readonly NetEntity? BuckledTo = buckledTo; + public readonly bool DontCollide = dontCollide; + public readonly TimeSpan? BuckleTime = buckleTime; +} + + +/// +/// Event raised directed at a strap entity before some entity gets buckled to it. +/// +[ByRefEvent] +public record struct StrapAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a buckle entity before it gets buckled to some strap entity. +/// +[ByRefEvent] +public record struct BuckleAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a strap entity before some entity gets unbuckled from it. +/// +[ByRefEvent] +public record struct UnstrapAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a buckle entity before it gets unbuckled. +/// +[ByRefEvent] +public record struct UnbuckleAttemptEvent( + Entity Strap, + Entity Buckle, + EntityUid? User, + bool Popup) +{ + public bool Cancelled; +} + +/// +/// Event raised directed at a strap entity after something has been buckled to it. +/// +[ByRefEvent] +public readonly record struct StrappedEvent(Entity Strap, Entity Buckle); + +/// +/// Event raised directed at a buckle entity after it has been buckled. +/// +[ByRefEvent] +public readonly record struct BuckledEvent(Entity Strap, Entity Buckle); + +/// +/// Event raised directed at a strap entity after something has been unbuckled from it. +/// [ByRefEvent] -public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false); +public readonly record struct UnstrappedEvent(Entity Strap, Entity Buckle); +/// +/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity. +/// [ByRefEvent] -public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling); +public readonly record struct UnbuckledEvent(Entity Strap, Entity Buckle); [Serializable, NetSerializable] public enum BuckleVisuals diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs index 9a19cea0c9a..a16d15f8a2c 100644 --- a/Content.Shared/Buckle/Components/StrapComponent.cs +++ b/Content.Shared/Buckle/Components/StrapComponent.cs @@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components; public sealed partial class StrapComponent : Component { /// - /// The entities that are currently buckled + /// The entities that are currently buckled to this strap. /// - [AutoNetworkedField] - [ViewVariables] // TODO serialization + [ViewVariables] public HashSet BuckledEntities = new(); /// /// Entities that this strap accepts and can buckle /// If null it accepts any entity /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntityWhitelist? Whitelist; /// /// Entities that this strap does not accept and cannot buckle. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public EntityWhitelist? Blacklist; /// /// The change in position to the strapped mob /// [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] public StrapPosition Position = StrapPosition.None; - /// - /// The distance above which a buckled entity will be automatically unbuckled. - /// Don't change it unless you really have to - /// - /// - /// Dont set this below 0.2 because that causes audio issues with - /// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason - /// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check - /// This is why this needs to be above 0.1 to make the InRange check fail in both client and server. - /// - [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] - public float MaxBuckleDistance = 0.2f; - - /// - /// Gets and clamps the buckle offset to MaxBuckleDistance - /// - [ViewVariables] - public Vector2 BuckleOffsetClamped => Vector2.Clamp( - BuckleOffset, - Vector2.One * -MaxBuckleDistance, - Vector2.One * MaxBuckleDistance); - /// /// The buckled entity will be offset by this amount from the center of the strap object. - /// If this offset it too big, it will be clamped to /// [DataField, AutoNetworkedField] - [ViewVariables(VVAccess.ReadWrite)] public Vector2 BuckleOffset = Vector2.Zero; /// /// The angle to rotate the player by when they get strapped /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public Angle Rotation; /// /// The size of the strap which is compared against when buckling entities /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public int Size = 100; /// /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled /// - [ViewVariables] + [DataField, AutoNetworkedField] public bool Enabled = true; /// /// You can specify the offset the entity will have after unbuckling. /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public Vector2 UnbuckleOffset = Vector2.Zero; /// /// The sound to be played when a mob is buckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier BuckleSound = new SoundPathSpecifier("/Audio/Effects/buckle.ogg"); /// /// The sound to be played when a mob is unbuckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg"); /// /// ID of the alert to show when buckled /// [DataField] - [ViewVariables(VVAccess.ReadWrite)] public ProtoId BuckledAlertType = "Buckled"; - - /// - /// The sum of the sizes of all the buckled entities in this strap - /// - [AutoNetworkedField] - [ViewVariables] - public int OccupiedSize; } public enum StrapPosition diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs index e7bfb53f08c..fd6ffd30e39 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs @@ -1,39 +1,46 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Alert; -using Content.Shared.Bed.Sleep; using Content.Shared.Buckle.Components; using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.IdentityManagement; -using Content.Shared.Interaction; -using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; +using Content.Shared.Movement.Pulling.Events; using Content.Shared.Popups; +using Content.Shared.Pulling.Events; using Content.Shared.Standing; using Content.Shared.Storage.Components; using Content.Shared.Stunnable; using Content.Shared.Throwing; -using Content.Shared.Verbs; using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.GameStates; +using Robust.Shared.Map; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; -using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent; namespace Content.Shared.Buckle; public abstract partial class SharedBuckleSystem { + public static ProtoId BuckledAlertCategory = "Buckled"; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; private void InitializeBuckle() { - SubscribeLocalEvent(OnBuckleComponentStartup); SubscribeLocalEvent(OnBuckleComponentShutdown); SubscribeLocalEvent(OnBuckleMove); - SubscribeLocalEvent(OnBuckleInteractHand); - SubscribeLocalEvent>(AddUnbuckleVerb); + SubscribeLocalEvent(OnParentChanged); + SubscribeLocalEvent(OnInserted); + + SubscribeLocalEvent(OnPullAttempt); + SubscribeLocalEvent(OnBeingPulledAttempt); + SubscribeLocalEvent(OnPullStarted); + SubscribeLocalEvent(OnBuckleInsertIntoEntityStorageAttempt); SubscribeLocalEvent(OnBucklePreventCollide); @@ -41,69 +48,93 @@ private void InitializeBuckle() SubscribeLocalEvent(OnBuckleStandAttempt); SubscribeLocalEvent(OnBuckleThrowPushbackAttempt); SubscribeLocalEvent(OnBuckleUpdateCanMove); - } - [ValidatePrototypeId] - public const string BuckledAlertCategory = "Buckled"; + SubscribeLocalEvent(OnGetState); + } - private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args) + private void OnGetState(Entity ent, ref ComponentGetState args) { - UpdateBuckleStatus(uid, component); + args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime); } - private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args) + private void OnBuckleComponentShutdown(Entity ent, ref ComponentShutdown args) { - TryUnbuckle(uid, uid, true, component); + Unbuckle(ent!, null); + } + + #region Pulling - component.BuckleTime = default; + private void OnPullAttempt(Entity ent, ref StartPullAttemptEvent args) + { + // Prevent people pulling the chair they're on, etc. + if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap) + args.Cancel(); } - private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev) + private void OnBeingPulledAttempt(Entity ent, ref BeingPulledAttemptEvent args) { - if (component.BuckledTo is not { } strapUid) + if (args.Cancelled || !ent.Comp.Buckled) return; - if (!TryComp(strapUid, out var strapComp)) - return; + if (!CanUnbuckle(ent!, args.Puller, false)) + args.Cancel(); + } - var strapPosition = Transform(strapUid).Coordinates; - if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance)) - return; + private void OnPullStarted(Entity ent, ref PullStartedMessage args) + { + Unbuckle(ent!, args.PullerUid); + } + + #endregion - TryUnbuckle(uid, uid, true, component); + #region Transform + + private void OnParentChanged(Entity ent, ref EntParentChangedMessage args) + { + BuckleTransformCheck(ent, args.Transform); } - private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args) + private void OnInserted(Entity ent, ref EntGotInsertedIntoContainerMessage args) { - if (!component.Buckled) - return; + BuckleTransformCheck(ent, Transform(ent)); + } - if (TryUnbuckle(uid, args.User, buckleComp: component)) - args.Handled = true; + private void OnBuckleMove(Entity ent, ref MoveEvent ev) + { + BuckleTransformCheck(ent, ev.Component); } - private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) + /// + /// Check if the entity should get unbuckled as a result of transform or container changes. + /// + private void BuckleTransformCheck(Entity buckle, TransformComponent xform) { - if (!args.CanAccess || !args.CanInteract || !component.Buckled) + if (_gameTiming.ApplyingState) return; - InteractionVerb verb = new() + if (buckle.Comp.BuckledTo is not { } strapUid) + return; + + if (!TryComp(strapUid, out var strapComp)) { - Act = () => TryUnbuckle(uid, args.User, buckleComp: component), - Text = Loc.GetString("verb-categories-unbuckle"), - Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png")) - }; + Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}"); + SetBuckledTo(buckle, null); + return; + } - if (args.Target == args.User && args.Using == null) + if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle)) { - // A user is left clicking themselves with an empty hand, while buckled. - // It is very likely they are trying to unbuckle themselves. - verb.Priority = 1; + Unbuckle(buckle, (strapUid, strapComp), null); + return; } - args.Verbs.Add(verb); + var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared(); + if (delta > 1e-5) + Unbuckle(buckle, (strapUid, strapComp), null); } + #endregion + private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args) { if (component.Buckled) @@ -112,10 +143,7 @@ private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleCompone private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args) { - if (args.OtherEntity != component.BuckledTo) - return; - - if (component.Buckled || component.DontCollide) + if (args.OtherEntity == component.BuckledTo && component.DontCollide) args.Cancelled = true; } @@ -139,10 +167,7 @@ private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent compone private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args) { - if (component.LifeStage > ComponentLifeStage.Running) - return; - - if (component.Buckled) // buckle shitcode + if (component.Buckled) args.Cancel(); } @@ -151,162 +176,139 @@ public bool IsBuckled(EntityUid uid, BuckleComponent? component = null) return Resolve(uid, ref component, false) && component.Buckled; } - /// - /// Shows or hides the buckled status effect depending on if the - /// entity is buckled or not. - /// - /// Entity that we want to show the alert - /// buckle component of the entity - /// strap component of the thing we are strapping to - private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null) + protected void SetBuckledTo(Entity buckle, Entity? strap) { - Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled); - if (buckleComp.BuckledTo != null) - { - if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp)) - return; + if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old)) + old.BuckledEntities.Remove(buckle); - var alertType = strapComp.BuckledAlertType; - _alerts.ShowAlert(uid, alertType); - } - else + if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp)) { - _alerts.ClearAlertCategory(uid, BuckledAlertCategory); - } - } - - /// - /// Sets the field in the component to a value - /// - /// Value tat with be assigned to the field - private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp) - { - buckleComp.BuckledTo = strapUid; - - if (strapUid == null) - { - buckleComp.Buckled = false; + strapEnt.Comp.BuckledEntities.Add(buckle); + _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType); } else { - buckleComp.LastEntityBuckledTo = strapUid; - buckleComp.DontCollide = true; - buckleComp.Buckled = true; - buckleComp.BuckleTime = _gameTiming.CurTime; + _alerts.ClearAlertCategory(buckle, BuckledAlertCategory); } - ActionBlocker.UpdateCanMove(buckleUid); - UpdateBuckleStatus(buckleUid, buckleComp, strapComp); - Dirty(buckleUid, buckleComp); + buckle.Comp.BuckledTo = strap; + buckle.Comp.BuckleTime = _gameTiming.CurTime; + ActionBlocker.UpdateCanMove(buckle); + Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled); + Dirty(buckle); } /// /// Checks whether or not buckling is possible /// /// Uid of the owner of BuckleComponent - /// - /// Uid of a third party entity, - /// i.e, the uid of someone else you are dragging to a chair. - /// Can equal buckleUid sometimes + /// + /// Uid of a third party entity, + /// i.e, the uid of someone else you are dragging to a chair. + /// Can equal buckleUid sometimes /// /// Uid of the owner of strap component - private bool CanBuckle( - EntityUid buckleUid, - EntityUid userUid, + /// + /// + private bool CanBuckle(EntityUid buckleUid, + EntityUid? user, EntityUid strapUid, + bool popup, [NotNullWhen(true)] out StrapComponent? strapComp, - BuckleComponent? buckleComp = null) + BuckleComponent buckleComp) { strapComp = null; - - if (userUid == strapUid || - !Resolve(buckleUid, ref buckleComp, false) || - !Resolve(strapUid, ref strapComp, false)) - { + if (!Resolve(strapUid, ref strapComp, false)) return false; - } // Does it pass the Whitelist if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) || _whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid)) { - if (_netManager.IsServer) - _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium); + if (_netManager.IsServer && popup && user != null) + _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium); return false; } - // Is it within range - bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid; - - if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored, + if (!_interaction.InRangeUnobstructed(buckleUid, + strapUid, + buckleComp.Range, + predicate: entity => entity == buckleUid || entity == user || entity == strapUid, popup: true)) { return false; } - // If in a container - if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer)) - { - // And not in the same container as the strap - if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) || - ownerContainer != strapContainer) - { - return false; - } - } + if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null))) + return false; - if (!HasComp(userUid)) + if (user != null && !HasComp(user)) { // PopupPredicted when - if (_netManager.IsServer) - _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid); + if (_netManager.IsServer && popup) + _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value); return false; } if (buckleComp.Buckled) { - var message = Loc.GetString(buckleUid == userUid + if (_netManager.IsClient || popup || user == null) + return false; + + var message = Loc.GetString(buckleUid == user ? "buckle-component-already-buckled-message" : "buckle-component-other-already-buckled-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + _popup.PopupEntity(message, user.Value, user.Value); return false; } + // Check whether someone is attempting to buckle something to their own child var parent = Transform(strapUid).ParentUid; while (parent.IsValid()) { - if (parent == userUid) + if (parent != buckleUid) { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + parent = Transform(parent).ParentUid; + continue; + } + if (_netManager.IsClient || popup || user == null) return false; - } - parent = Transform(parent).ParentUid; + var message = Loc.GetString(buckleUid == user + ? "buckle-component-cannot-buckle-message" + : "buckle-component-other-cannot-buckle-message", + ("owner", Identity.Entity(buckleUid, EntityManager))); + + _popup.PopupEntity(message, user.Value, user.Value); + return false; } if (!StrapHasSpace(strapUid, buckleComp, strapComp)) { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-fit-message" - : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); + if (_netManager.IsClient || popup || user == null) + return false; + + var message = Loc.GetString(buckleUid == user + ? "buckle-component-cannot-fit-message" + : "buckle-component-other-cannot-fit-message", + ("owner", Identity.Entity(buckleUid, EntityManager))); + + _popup.PopupEntity(message, user.Value, user.Value); return false; } - var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true); - RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); - RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); - if (attemptEvent.Cancelled) + var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup); + RaiseLocalEvent(buckleUid, ref buckleAttempt); + if (buckleAttempt.Cancelled) + return false; + + var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup); + RaiseLocalEvent(strapUid, ref strapAttempt); + if (strapAttempt.Cancelled) return false; return true; @@ -315,216 +317,194 @@ private bool CanBuckle( /// /// Attempts to buckle an entity to a strap /// - /// Uid of the owner of BuckleComponent - /// + /// Uid of the owner of BuckleComponent + /// /// Uid of a third party entity, /// i.e, the uid of someone else you are dragging to a chair. /// Can equal buckleUid sometimes /// - /// Uid of the owner of strap component - public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null) + /// Uid of the owner of strap component + public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true) { - if (!Resolve(buckleUid, ref buckleComp, false)) + if (!Resolve(buckle, ref buckleComp, false)) return false; - if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp)) + if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp)) return false; - if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp)) - { - var message = Loc.GetString(buckleUid == userUid - ? "buckle-component-cannot-buckle-message" - : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager))); - if (_netManager.IsServer) - _popup.PopupEntity(message, userUid, userUid); - return false; - } + Buckle((buckle, buckleComp), (strap, strapComp), user); + return true; + } - if (TryComp(buckleUid, out var appearance)) - Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance); + private void Buckle(Entity buckle, Entity strap, EntityUid? user) + { + if (user == buckle.Owner) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}"); + else if (user != null) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}"); - _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation); + _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user); - ReAttach(buckleUid, strapUid, buckleComp, strapComp); - SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp); - // TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that - _audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid); + SetBuckledTo(buckle, strap!); + Appearance.SetData(strap, StrapVisuals.State, true); + Appearance.SetData(buckle, BuckleVisuals.Buckled, true); - var ev = new BuckleChangeEvent(strapUid, buckleUid, true); - RaiseLocalEvent(ev.BuckledEntity, ref ev); - RaiseLocalEvent(ev.StrapEntity, ref ev); + _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation); - if (TryComp(buckleUid, out var ownerPullable)) - { - if (ownerPullable.Puller != null) - { - _pulling.TryStopPull(buckleUid, ownerPullable); - } - } + var xform = Transform(buckle); + var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset); + _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero); - if (TryComp(buckleUid, out var physics)) - { - _physics.ResetDynamics(buckleUid, physics); - } + _joints.SetRelay(buckle, strap); - if (!buckleComp.PullStrap && TryComp(strapUid, out var toPullable)) + switch (strap.Comp.Position) { - if (toPullable.Puller == buckleUid) - { - // can't pull it and buckle to it at the same time - _pulling.TryStopPull(strapUid, toPullable); - } + case StrapPosition.Stand: + _standing.Stand(buckle); + break; + case StrapPosition.Down: + _standing.Down(buckle, false, false); + break; } - // Logging - if (userUid != buckleUid) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}"); - else - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}"); + var ev = new StrappedEvent(strap, buckle); + RaiseLocalEvent(strap, ref ev); - return true; + var gotEv = new BuckledEvent(strap, buckle); + RaiseLocalEvent(buckle, ref gotEv); + + if (TryComp(buckle, out var physics)) + _physics.ResetDynamics(buckle, physics); + + DebugTools.AssertEqual(xform.ParentUid, strap.Owner); } /// /// Tries to unbuckle the Owner of this component from its current strap. /// /// The entity to unbuckle. - /// The entity doing the unbuckling. - /// - /// Whether to force the unbuckling or not. Does not guarantee true to - /// be returned, but guarantees the owner to be unbuckled afterwards. - /// + /// The entity doing the unbuckling. /// The buckle component of the entity to unbuckle. /// /// true if the owner was unbuckled, otherwise false even if the owner /// was previously already unbuckled. /// - public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null) + public bool TryUnbuckle(EntityUid buckleUid, + EntityUid? user, + BuckleComponent? buckleComp = null, + bool popup = true) + { + return TryUnbuckle((buckleUid, buckleComp), user, popup); + } + + public bool TryUnbuckle(Entity buckle, EntityUid? user, bool popup) { - if (!Resolve(buckleUid, ref buckleComp, false) || - buckleComp.BuckledTo is not { } strapUid) + if (!Resolve(buckle.Owner, ref buckle.Comp)) return false; - if (!force) - { - var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false); - RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent); - RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent); - if (attemptEvent.Cancelled) - return false; + if (!CanUnbuckle(buckle, user, popup, out var strap)) + return false; - if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay) - return false; + Unbuckle(buckle!, strap, user); + return true; + } - if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true)) - return false; + public void Unbuckle(Entity buckle, EntityUid? user) + { + if (!Resolve(buckle.Owner, ref buckle.Comp, false)) + return; - if (HasComp(buckleUid) && buckleUid == userUid) - return false; + if (buckle.Comp.BuckledTo is not { } strap) + return; - // If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated. - if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid) - return false; + if (!TryComp(strap, out StrapComponent? strapComp)) + { + Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}"); + SetBuckledTo(buckle!, null); + return; } - // Logging - if (userUid != buckleUid) - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}"); - else - _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}"); + Unbuckle(buckle!, (strap, strapComp), user); + } + + private void Unbuckle(Entity buckle, Entity strap, EntityUid? user) + { + if (user == buckle.Owner) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}"); + else if (user != null) + _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}"); - SetBuckledTo(buckleUid, null, null, buckleComp); + _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user); - if (!TryComp(strapUid, out var strapComp)) - return false; + SetBuckledTo(buckle, null); - var buckleXform = Transform(buckleUid); - var oldBuckledXform = Transform(strapUid); + var buckleXform = Transform(buckle); + var oldBuckledXform = Transform(strap); - if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid)) + if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid)) { - _container.AttachParentToContainerOrGrid((buckleUid, buckleXform)); + _container.AttachParentToContainerOrGrid((buckle, buckleXform)); - var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid); + var oldBuckledToWorldRot = _transform.GetWorldRotation(strap); _transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot); - if (strapComp.UnbuckleOffset != Vector2.Zero) - buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset); + if (strap.Comp.UnbuckleOffset != Vector2.Zero) + buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset); } - if (TryComp(buckleUid, out AppearanceComponent? appearance)) - Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance); - _rotationVisuals.ResetHorizontalAngle(buckleUid); + _rotationVisuals.ResetHorizontalAngle(buckle.Owner); + Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0); + Appearance.SetData(buckle, BuckleVisuals.Buckled, false); - if (TryComp(buckleUid, out var mobState) - && _mobState.IsIncapacitated(buckleUid, mobState) - || HasComp(buckleUid)) - { - _standing.Down(buckleUid); - } + if (HasComp(buckle) || _mobState.IsIncapacitated(buckle)) + _standing.Down(buckle); else - { - _standing.Stand(buckleUid); - } + _standing.Stand(buckle); - if (_mobState.IsIncapacitated(buckleUid, mobState)) - { - _standing.Down(buckleUid); - } - if (strapComp.BuckledEntities.Remove(buckleUid)) - { - strapComp.OccupiedSize -= buckleComp.Size; - Dirty(strapUid, strapComp); - } - - _joints.RefreshRelay(buckleUid); - Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0); + _joints.RefreshRelay(buckle); - // TODO: Buckle listening to moveevents is sussy anyway. - if (!TerminatingOrDeleted(strapUid)) - _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid); + var buckleEv = new UnbuckledEvent(strap, buckle); + RaiseLocalEvent(buckle, ref buckleEv); - var ev = new BuckleChangeEvent(strapUid, buckleUid, false); - RaiseLocalEvent(buckleUid, ref ev); - RaiseLocalEvent(strapUid, ref ev); + var strapEv = new UnstrappedEvent(strap, buckle); + RaiseLocalEvent(strap, ref strapEv); + } - return true; + public bool CanUnbuckle(Entity buckle, EntityUid user, bool popup) + { + return CanUnbuckle(buckle, user, popup, out _); } - /// - /// Makes an entity toggle the buckling status of the owner to a - /// specific entity. - /// - /// The entity to buckle/unbuckle from . - /// The entity doing the buckling/unbuckling. - /// - /// The entity to toggle the buckle status of the owner to. - /// - /// - /// Whether to force the unbuckling or not, if it happens. Does not - /// guarantee true to be returned, but guarantees the owner to be - /// unbuckled afterwards. - /// - /// The buckle component of the entity to buckle/unbuckle from . - /// true if the buckling status was changed, false otherwise. - public bool ToggleBuckle( - EntityUid buckleUid, - EntityUid userUid, - EntityUid strapUid, - bool force = false, - BuckleComponent? buckle = null) + private bool CanUnbuckle(Entity buckle, EntityUid? user, bool popup, out Entity strap) { - if (!Resolve(buckleUid, ref buckle, false)) + strap = default; + if (!Resolve(buckle.Owner, ref buckle.Comp)) return false; - if (!buckle.Buckled) - { - return TryBuckle(buckleUid, userUid, strapUid, buckle); - } - else + if (buckle.Comp.BuckledTo is not { } strapUid) + return false; + + if (!TryComp(strapUid, out StrapComponent? strapComp)) { - return TryUnbuckle(buckleUid, userUid, force, buckle); + Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}"); + SetBuckledTo(buckle!, null); + return false; } + strap = (strapUid, strapComp); + if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay) + return false; + + if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup)) + return false; + + var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup); + RaiseLocalEvent(buckle, ref unbuckleAttempt); + if (unbuckleAttempt.Cancelled) + return false; + + var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup); + RaiseLocalEvent(strap, ref unstrapAttempt); + return !unstrapAttempt.Cancelled; } } diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs new file mode 100644 index 00000000000..8c2d0b8ee18 --- /dev/null +++ b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs @@ -0,0 +1,171 @@ +using Content.Shared.Buckle.Components; +using Content.Shared.DragDrop; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Verbs; +using Robust.Shared.Utility; + +namespace Content.Shared.Buckle; + +// Partial class containing interaction & verb event handlers +public abstract partial class SharedBuckleSystem +{ + private void InitializeInteraction() + { + SubscribeLocalEvent>(AddStrapVerbs); + SubscribeLocalEvent(OnStrapInteractHand); + SubscribeLocalEvent(OnStrapDragDropTarget); + SubscribeLocalEvent(OnCanDropTarget); + + SubscribeLocalEvent>(AddUnbuckleVerb); + } + + private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args) + { + args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component); + args.Handled = true; + } + + private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args) + { + if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component)) + return; + + args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false); + } + + private bool StrapCanDragDropOn( + EntityUid strapUid, + EntityUid userUid, + EntityUid targetUid, + EntityUid buckleUid, + StrapComponent? strapComp = null, + BuckleComponent? buckleComp = null) + { + if (!Resolve(strapUid, ref strapComp, false) || + !Resolve(buckleUid, ref buckleComp, false)) + { + return false; + } + + bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid; + + return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored); + } + + private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) + { + if (args.Handled) + return; + + if (!TryComp(args.User, out BuckleComponent? buckle)) + return; + + if (buckle.BuckledTo == null) + TryBuckle(args.User, args.User, uid, buckle, popup: true); + else if (buckle.BuckledTo == uid) + TryUnbuckle(args.User, args.User, buckle, popup: true); + else + return; + + args.Handled = true; // This generate popups on failure. + } + + private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) + { + if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled) + return; + + // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this + // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb. + + // Add unstrap verbs for every strapped entity. + foreach (var entity in component.BuckledEntities) + { + var buckledComp = Comp(entity); + + if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) + continue; + + var verb = new InteractionVerb() + { + Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp), + Category = VerbCategory.Unbuckle, + Text = entity == args.User + ? Loc.GetString("verb-self-target-pronoun") + : Identity.Name(entity, EntityManager) + }; + + // In the event that you have more than once entity with the same name strapped to the same object, + // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to + // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by + // appending an integer to verb.Text to distinguish the verbs. + + args.Verbs.Add(verb); + } + + // Add a verb to buckle the user. + if (TryComp(args.User, out var buckle) && + buckle.BuckledTo != uid && + args.User != uid && + StrapHasSpace(uid, buckle, component) && + _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) + { + InteractionVerb verb = new() + { + Act = () => TryBuckle(args.User, args.User, args.Target, buckle), + Category = VerbCategory.Buckle, + Text = Loc.GetString("verb-self-target-pronoun") + }; + args.Verbs.Add(verb); + } + + // If the user is currently holding/pulling an entity that can be buckled, add a verb for that. + if (args.Using is { Valid: true } @using && + TryComp(@using, out var usingBuckle) && + StrapHasSpace(uid, usingBuckle, component) && + _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range)) + { + // Check that the entity is unobstructed from the target (ignoring the user). + bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using; + if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) + return; + + var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _); + InteractionVerb verb = new() + { + Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle), + Category = VerbCategory.Buckle, + Text = Identity.Name(@using, EntityManager), + // just a held object, the user is probably just trying to sit down. + // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is + Priority = isPlayer ? 1 : -1 + }; + + args.Verbs.Add(verb); + } + } + + private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || !component.Buckled) + return; + + InteractionVerb verb = new() + { + Act = () => TryUnbuckle(uid, args.User, buckleComp: component), + Text = Loc.GetString("verb-categories-unbuckle"), + Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png")) + }; + + if (args.Target == args.User && args.Using == null) + { + // A user is left clicking themselves with an empty hand, while buckled. + // It is very likely they are trying to unbuckle themselves. + verb.Priority = 1; + } + + args.Verbs.Add(verb); + } + +} diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs index 147af42e728..eb23aa973b4 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs @@ -2,39 +2,25 @@ using Content.Shared.Buckle.Components; using Content.Shared.Construction; using Content.Shared.Destructible; -using Content.Shared.DragDrop; using Content.Shared.Foldable; -using Content.Shared.Interaction; -using Content.Shared.Rotation; using Content.Shared.Storage; -using Content.Shared.Verbs; using Robust.Shared.Containers; namespace Content.Shared.Buckle; public abstract partial class SharedBuckleSystem { - [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!; - private void InitializeStrap() { SubscribeLocalEvent(OnStrapStartup); SubscribeLocalEvent(OnStrapShutdown); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); - SubscribeLocalEvent(OnStrapEntModifiedFromContainer); - SubscribeLocalEvent(OnStrapEntModifiedFromContainer); - SubscribeLocalEvent>(AddStrapVerbs); SubscribeLocalEvent(OnStrapContainerGettingInsertedAttempt); - SubscribeLocalEvent(OnStrapInteractHand); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); - SubscribeLocalEvent(OnStrapDragDropTarget); - SubscribeLocalEvent(OnCanDropTarget); SubscribeLocalEvent(OnAttemptFold); - - SubscribeLocalEvent(OnStrapMoveEvent); SubscribeLocalEvent((e, c, _) => StrapRemoveAll(e, c)); } @@ -45,145 +31,17 @@ private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentSt private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args) { - if (LifeStage(uid) > EntityLifeStage.MapInitialized) - return; - - StrapRemoveAll(uid, component); - } - - private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message) - { - if (_gameTiming.ApplyingState) - return; - - foreach (var buckledEntity in component.BuckledEntities) - { - if (!TryComp(buckledEntity, out var buckleComp)) - { - continue; - } - - ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component); - } - } - - private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null) - { - if (!Resolve(buckleUid, ref buckleComp, false) || - !Resolve(strapUid, ref strapComp, false)) - return; - - var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer); - var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer); - - if (contained != strapContained || ownContainer != strapContainer) - { - TryUnbuckle(buckleUid, buckleUid, true, buckleComp); - return; - } - - if (!contained) - { - ReAttach(buckleUid, strapUid, buckleComp, strapComp); - } + if (!TerminatingOrDeleted(uid)) + StrapRemoveAll(uid, component); } private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args) { // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it. - if (HasComp(args.Container.Owner) && component.BuckledEntities.Count != 0) + if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0) args.Cancel(); } - private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args) - { - if (args.Handled) - return; - - args.Handled = ToggleBuckle(args.User, args.User, uid); - } - - private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) - { - if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled) - return; - - // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this - // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb. - - // Add unstrap verbs for every strapped entity. - foreach (var entity in component.BuckledEntities) - { - var buckledComp = Comp(entity); - - if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range)) - continue; - - var verb = new InteractionVerb() - { - Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp), - Category = VerbCategory.Unbuckle, - Text = entity == args.User - ? Loc.GetString("verb-self-target-pronoun") - : Comp(entity).EntityName - }; - - // In the event that you have more than once entity with the same name strapped to the same object, - // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to - // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by - // appending an integer to verb.Text to distinguish the verbs. - - args.Verbs.Add(verb); - } - - // Add a verb to buckle the user. - if (TryComp(args.User, out var buckle) && - buckle.BuckledTo != uid && - args.User != uid && - StrapHasSpace(uid, buckle, component) && - _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range)) - { - InteractionVerb verb = new() - { - Act = () => TryBuckle(args.User, args.User, args.Target, buckle), - Category = VerbCategory.Buckle, - Text = Loc.GetString("verb-self-target-pronoun") - }; - args.Verbs.Add(verb); - } - - // If the user is currently holding/pulling an entity that can be buckled, add a verb for that. - if (args.Using is { Valid: true } @using && - TryComp(@using, out var usingBuckle) && - StrapHasSpace(uid, usingBuckle, component) && - _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range)) - { - // Check that the entity is unobstructed from the target (ignoring the user). - bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using; - if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored)) - return; - - var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _); - InteractionVerb verb = new() - { - Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle), - Category = VerbCategory.Buckle, - Text = Comp(@using).EntityName, - // just a held object, the user is probably just trying to sit down. - // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is - Priority = isPlayer ? 1 : -1 - }; - - args.Verbs.Add(verb); - } - } - - private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args) - { - args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component); - args.Handled = true; - } - private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args) { if (args.Cancelled) @@ -192,69 +50,6 @@ private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAtte args.Cancelled = component.BuckledEntities.Count != 0; } - private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args) - { - if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component)) - return; - - args.Handled = TryBuckle(args.Dragged, args.User, uid); - } - - private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) - { - // TODO: This looks dirty af. - // On rotation of a strap, reattach all buckled entities. - // This fixes buckle offsets and draw depths. - // This is mega cursed. Please somebody save me from Mr Buckle's wild ride. - // Oh god I'm back here again. Send help. - - // Consider a chair that has a player strapped to it. Then the client receives a new server state, showing - // that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player - // state, then the chairs transform comp state, and then the buckle state. The transform state will - // forcefully teleport the player back to the chair (client-side only). This causes even more issues if the - // chair was teleporting in from nullspace after having left PVS. - // - // One option is to just never trigger re-buckles during state application. - // another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm. - - if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation) - return; - - foreach (var buckledEntity in component.BuckledEntities) - { - if (!TryComp(buckledEntity, out var buckled)) - continue; - - if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid) - { - Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}"); - continue; - } - - ReAttach(buckledEntity, uid, buckled, component); - Dirty(buckledEntity, buckled); - } - } - - private bool StrapCanDragDropOn( - EntityUid strapUid, - EntityUid userUid, - EntityUid targetUid, - EntityUid buckleUid, - StrapComponent? strapComp = null, - BuckleComponent? buckleComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) || - !Resolve(buckleUid, ref buckleComp, false)) - { - return false; - } - - bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid; - - return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored); - } - /// /// Remove everything attached to the strap /// @@ -264,10 +59,6 @@ private void StrapRemoveAll(EntityUid uid, StrapComponent strapComp) { TryUnbuckle(entity, entity, true); } - - strapComp.BuckledEntities.Clear(); - strapComp.OccupiedSize = 0; - Dirty(uid, strapComp); } private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null) @@ -275,30 +66,13 @@ private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, Strap if (!Resolve(strapUid, ref strapComp, false)) return false; - return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size; - } - - /// - /// Try to add an entity to the strap - /// - private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) || - !strapComp.Enabled) - return false; - - if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp)) - return false; - - if (!strapComp.BuckledEntities.Add(buckleUid)) - return false; - - strapComp.OccupiedSize += buckleComp.Size; - - Appearance.SetData(strapUid, StrapVisuals.State, true); + var avail = strapComp.Size; + foreach (var buckle in strapComp.BuckledEntities) + { + avail -= CompOrNull(buckle)?.Size ?? 0; + } - Dirty(strapUid, strapComp); - return true; + return avail >= buckleComp.Size; } /// @@ -311,6 +85,7 @@ public void StrapSetEnabled(EntityUid strapUid, bool enabled, StrapComponent? st return; strapComp.Enabled = enabled; + Dirty(strapUid, strapComp); if (!enabled) StrapRemoveAll(strapUid, strapComp); diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs index 67218657e52..770fababded 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.cs @@ -1,21 +1,17 @@ using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; -using Content.Shared.Buckle.Components; using Content.Shared.Interaction; using Content.Shared.Mobs.Systems; using Content.Shared.Popups; -using Content.Shared.Pulling; +using Content.Shared.Rotation; using Content.Shared.Standing; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; -using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Physics.Systems; using Robust.Shared.Player; using Robust.Shared.Timing; -using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem; namespace Content.Shared.Buckle; @@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedJointSystem _joints = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly PullingSystem _pulling = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly StandingStateSystem _standing = default!; [Dependency] private readonly SharedPhysicsSystem _physics = default!; + [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!; /// public override void Initialize() @@ -51,45 +47,6 @@ public override void Initialize() InitializeBuckle(); InitializeStrap(); - } - - /// - /// Reattaches this entity to the strap, modifying its position and rotation. - /// - /// The entity to reattach. - /// The entity to reattach the buckleUid entity to. - private void ReAttach( - EntityUid buckleUid, - EntityUid strapUid, - BuckleComponent? buckleComp = null, - StrapComponent? strapComp = null) - { - if (!Resolve(strapUid, ref strapComp, false) - || !Resolve(buckleUid, ref buckleComp, false)) - return; - - _transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped)); - - var buckleTransform = Transform(buckleUid); - - // Buckle subscribes to move for so this might fail. - // TODO: Make buckle not do that. - if (buckleTransform.ParentUid != strapUid) - return; - - _transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform); - _joints.SetRelay(buckleUid, strapUid); - - switch (strapComp.Position) - { - case StrapPosition.None: - break; - case StrapPosition.Stand: - _standing.Stand(buckleUid); - break; - case StrapPosition.Down: - _standing.Down(buckleUid, false, false); - break; - } + InitializeInteraction(); } } diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs index ac01c4e9acb..726cdc24687 100644 --- a/Content.Shared/Climbing/Systems/ClimbSystem.cs +++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs @@ -58,7 +58,7 @@ public override void Initialize() SubscribeLocalEvent(OnParentChange); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnClimbEndCollide); - SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckled); SubscribeLocalEvent(OnCanDragDropOn); SubscribeLocalEvent>(AddClimbableVerb); @@ -468,10 +468,8 @@ public void ForciblySetClimbing(EntityUid uid, EntityUid climbable, ClimbingComp Climb(uid, uid, climbable, true, component); } - private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args) + private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args) { - if (!args.Buckling) - return; StopClimb(uid, component); } diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs index be169deb0e5..b9f287f1ce4 100644 --- a/Content.Shared/Cuffs/SharedCuffableSystem.cs +++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs @@ -71,6 +71,7 @@ public override void Initialize() SubscribeLocalEvent(OnUnequipAttempt); SubscribeLocalEvent(OnBeingPulledAttempt); SubscribeLocalEvent(OnBuckleAttemptEvent); + SubscribeLocalEvent(OnUnbuckleAttemptEvent); SubscribeLocalEvent>(AddUncuffVerb); SubscribeLocalEvent(OnCuffableDoAfter); SubscribeLocalEvent(OnPull); @@ -195,21 +196,33 @@ private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, Be args.Cancel(); } - private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args) + private void OnBuckleAttempt(Entity ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup) { - // if someone else is doing it, let it pass. - if (args.UserEntity != uid) + if (cancelled || user != ent.Owner) return; - if (!TryComp(uid, out var hands) || component.CuffedHandCount != hands.Count) + if (!TryComp(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count) return; - args.Cancelled = true; - var message = args.Buckling + cancelled = true; + if (!popup) + return; + + var message = buckling ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message") : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message"); - _popup.PopupClient(message, uid, args.UserEntity); + _popup.PopupClient(message, ent, user); + } + + private void OnBuckleAttemptEvent(Entity ent, ref BuckleAttemptEvent args) + { + OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup); + } + + private void OnUnbuckleAttemptEvent(Entity ent, ref UnbuckleAttemptEvent args) + { + OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup); } private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args) diff --git a/Content.Shared/Foldable/FoldableSystem.cs b/Content.Shared/Foldable/FoldableSystem.cs index 10baf8165b5..2a846f4f234 100644 --- a/Content.Shared/Foldable/FoldableSystem.cs +++ b/Content.Shared/Foldable/FoldableSystem.cs @@ -26,7 +26,7 @@ public override void Initialize() SubscribeLocalEvent(OnStoreThisAttempt); SubscribeLocalEvent(OnFoldableOpenAttempt); - SubscribeLocalEvent(OnBuckleAttempt); + SubscribeLocalEvent(OnStrapAttempt); } private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args) @@ -53,9 +53,9 @@ public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref StoreM args.Cancelled = true; } - public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args) + public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args) { - if (args.Buckling && comp.IsFolded) + if (comp.IsFolded) args.Cancelled = true; } diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs index 7f73d3190f9..6fe5582bb17 100644 --- a/Content.Shared/Interaction/RotateToFaceSystem.cs +++ b/Content.Shared/Interaction/RotateToFaceSystem.cs @@ -1,7 +1,6 @@ using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Buckle.Components; -using Content.Shared.Mobs.Systems; using Content.Shared.Rotatable; using JetBrains.Annotations; @@ -83,24 +82,21 @@ public bool TryFaceAngle(EntityUid user, Angle diffAngle, TransformComponent? xf if (!_actionBlockerSystem.CanChangeDirection(user)) return false; - if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled) + if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap) { - var suid = buckle.LastEntityBuckledTo; - if (suid != null) - { - // We're buckled to another object. Is that object rotatable? - if (TryComp(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored) - { - // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel". - // (Since the user being buckled to it holds it down with their weight.) - // This is logically equivalent to RotateWhileAnchored. - // Barstools and office chairs have independent wheels, while regular chairs don't. - _transform.SetWorldRotation(Transform(suid.Value), diffAngle); - return true; - } - } - - return false; + // What if a person is strapped to a borg? + // I'm pretty sure this would allow them to be partially ratatouille'd + + // We're buckled to another object. Is that object rotatable? + if (!TryComp(strap, out var rotatable) || !rotatable.RotateWhileAnchored) + return false; + + // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel". + // (Since the user being buckled to it holds it down with their weight.) + // This is logically equivalent to RotateWhileAnchored. + // Barstools and office chairs have independent wheels, while regular chairs don't. + _transform.SetWorldRotation(Transform(strap), diffAngle); + return true; } // user is not buckled in; apply to their transform diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs index 155cfede015..9ee8a064e54 100644 --- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs +++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs @@ -1,4 +1,5 @@ using Content.Shared.Bed.Sleep; +using Content.Shared.Buckle.Components; using Content.Shared.CombatMode.Pacification; using Content.Shared.Damage.ForceSay; using Content.Shared.Emoting; @@ -10,15 +11,12 @@ using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; using Content.Shared.Pointing; -using Content.Shared.Projectiles; using Content.Shared.Pulling.Events; using Content.Shared.Speech; using Content.Shared.Standing; using Content.Shared.Strip.Components; using Content.Shared.Throwing; -using Content.Shared.Weapons.Ranged.Components; using Robust.Shared.Physics.Components; -using Robust.Shared.Physics.Events; namespace Content.Shared.Mobs.Systems; @@ -46,6 +44,16 @@ private void SubscribeEvents() SubscribeLocalEvent(OnSleepAttempt); SubscribeLocalEvent(OnCombatModeShouldHandInteract); SubscribeLocalEvent(OnAttemptPacifiedAttack); + + SubscribeLocalEvent(OnUnbuckleAttempt); + } + + private void OnUnbuckleAttempt(Entity ent, ref UnbuckleAttemptEvent args) + { + // TODO is this necessary? + // Shouldn't the interaction have already been blocked by a general interaction check? + if (args.User == ent.Owner && IsIncapacitated(ent)) + args.Cancelled = true; } private void CheckConcious(Entity ent, ref ConsciousAttemptEvent args) diff --git a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs index 29460e1dfc1..c0775b4ce2d 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs @@ -1,9 +1,6 @@ namespace Content.Shared.Movement.Pulling.Events; -public sealed class PullStartedMessage : PullMessage -{ - public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : - base(pullerUid, pullableUid) - { - } -} +/// +/// Event raised directed BOTH at the puller and pulled entity when a pull starts. +/// +public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid); diff --git a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs index 47aa34562fb..6df4d174839 100644 --- a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs +++ b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs @@ -1,13 +1,6 @@ -using Robust.Shared.Physics.Components; - -namespace Content.Shared.Movement.Pulling.Events; +namespace Content.Shared.Movement.Pulling.Events; /// -/// Raised directed on both puller and pullable. +/// Event raised directed BOTH at the puller and pulled entity when a pull starts. /// -public sealed class PullStoppedMessage : PullMessage -{ - public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid) - { - } -} +public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid); diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 72b87476bb0..eb2872df9c1 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,4 +1,3 @@ -using System.Numerics; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; @@ -16,11 +15,9 @@ using Content.Shared.Popups; using Content.Shared.Pulling.Events; using Content.Shared.Standing; -using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Containers; using Robust.Shared.Input.Binding; -using Robust.Shared.Map; using Robust.Shared.Physics; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Events; @@ -68,11 +65,26 @@ public override void Initialize() SubscribeLocalEvent(OnRefreshMovespeed); SubscribeLocalEvent(OnDropHandItems); + SubscribeLocalEvent(OnBuckled); + SubscribeLocalEvent(OnGotBuckled); + CommandBinds.Builder .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false)) .Register(); } + private void OnBuckled(Entity ent, ref StrappedEvent args) + { + // Prevent people from pulling the entity they are buckled to + if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap) + StopPulling(ent, ent); + } + + private void OnGotBuckled(Entity ent, ref BuckledEvent args) + { + StopPulling(ent, ent); + } + private void OnAfterState(Entity ent, ref AfterAutoHandleStateEvent args) { if (ent.Comp.Pulling == null) @@ -94,7 +106,8 @@ private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHand private void OnPullerContainerInsert(Entity ent, ref EntGotInsertedIntoContainerMessage args) { - if (ent.Comp.Pulling == null) return; + if (ent.Comp.Pulling == null) + return; if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling)) return; @@ -228,8 +241,18 @@ private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRem /// private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp) { + if (pullableComp.Puller == null) + return; + if (!_timing.ApplyingState) { + // Joint shutdown + if (pullableComp.PullJointId != null) + { + _joints.RemoveJoint(pullableUid, pullableComp.PullJointId); + pullableComp.PullJointId = null; + } + if (TryComp(pullableUid, out var pullablePhysics)) { _physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics); @@ -330,15 +353,6 @@ public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pu return false; } - if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle)) - { - // Prevent people pulling the chair they're on, etc. - if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid)) - { - return false; - } - } - var getPulled = new BeingPulledAttemptEvent(puller, pullableUid); RaiseLocalEvent(pullableUid, getPulled, true); var startPull = new StartPullAttemptEvent(puller, pullableUid); @@ -382,11 +396,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, if (!CanPull(pullerUid, pullableUid)) return false; - if (!EntityManager.TryGetComponent(pullerUid, out var pullerPhysics) || - !EntityManager.TryGetComponent(pullableUid, out var pullablePhysics)) - { + if (!HasComp(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics)) return false; - } // Ensure that the puller is not currently pulling anything. if (TryComp(pullerComp.Pulling, out var oldPullable) @@ -431,7 +442,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid, { // Joint startup var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics)); - var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f; + var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId); joint.CollideConnected = false; @@ -475,17 +486,6 @@ public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, Entit if (msg.Cancelled) return false; - // Stop pulling confirmed! - if (!_timing.ApplyingState) - { - // Joint shutdown - if (pullable.PullJointId != null) - { - _joints.RemoveJoint(pullableUid, pullable.PullJointId); - pullable.PullJointId = null; - } - } - StopPulling(pullableUid, pullable); return true; } diff --git a/Content.Shared/Movement/Systems/SharedWaddleAnimationSystem.cs b/Content.Shared/Movement/Systems/SharedWaddleAnimationSystem.cs index 2fcb4fc60bb..cebae8093b4 100644 --- a/Content.Shared/Movement/Systems/SharedWaddleAnimationSystem.cs +++ b/Content.Shared/Movement/Systems/SharedWaddleAnimationSystem.cs @@ -25,7 +25,7 @@ public override void Initialize() // Stop moving possibilities SubscribeLocalEvent((Entity ent, ref StunnedEvent _) => StopWaddling(ent)); SubscribeLocalEvent((Entity ent, ref DownedEvent _) => StopWaddling(ent)); - SubscribeLocalEvent((Entity ent, ref BuckleChangeEvent _) => StopWaddling(ent)); + SubscribeLocalEvent((Entity ent, ref BuckledEvent _) => StopWaddling(ent)); SubscribeLocalEvent(OnGravityChanged); } diff --git a/Content.Shared/Traits/Assorted/LegsParalyzedSystem.cs b/Content.Shared/Traits/Assorted/LegsParalyzedSystem.cs index 7c91366937c..929ee3a19fb 100644 --- a/Content.Shared/Traits/Assorted/LegsParalyzedSystem.cs +++ b/Content.Shared/Traits/Assorted/LegsParalyzedSystem.cs @@ -17,7 +17,8 @@ public override void Initialize() { SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); - SubscribeLocalEvent(OnBuckleChange); + SubscribeLocalEvent(OnBuckled); + SubscribeLocalEvent(OnUnbuckled); SubscribeLocalEvent(OnThrowPushbackAttempt); SubscribeLocalEvent(OnUpdateCanMoveEvent); } @@ -34,16 +35,14 @@ private void OnShutdown(EntityUid uid, LegsParalyzedComponent component, Compone _bodySystem.UpdateMovementSpeed(uid); } - private void OnBuckleChange(EntityUid uid, LegsParalyzedComponent component, ref BuckleChangeEvent args) + private void OnBuckled(EntityUid uid, LegsParalyzedComponent component, ref BuckledEvent args) { - if (args.Buckling) - { - _standingSystem.Stand(args.BuckledEntity); - } - else - { - _standingSystem.Down(args.BuckledEntity); - } + _standingSystem.Stand(uid); + } + + private void OnUnbuckled(EntityUid uid, LegsParalyzedComponent component, ref UnbuckledEvent args) + { + _standingSystem.Down(uid); } private void OnUpdateCanMoveEvent(EntityUid uid, LegsParalyzedComponent component, UpdateCanMoveEvent args) diff --git a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml index 0fb69b4fdbd..ac5ec1e83e3 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml @@ -357,6 +357,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity name: steel bench diff --git a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml index 161ea25bc43..b3cfe6ade3f 100644 --- a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml +++ b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml @@ -79,6 +79,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity id: CheapRollerBed @@ -105,6 +107,8 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False - type: entity id: EmergencyRollerBed @@ -131,3 +135,5 @@ components: - type: Foldable folded: true + - type: Strap + enabled: False From 39ccab6718000954fdb3601dc76c26e72752c2ac Mon Sep 17 00:00:00 2001 From: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com> Date: Wed, 19 Jun 2024 18:15:26 +0200 Subject: [PATCH 032/109] Fix documentation typo (#29209) Fix everything. --- Content.Shared/Paper/StampComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Content.Shared/Paper/StampComponent.cs b/Content.Shared/Paper/StampComponent.cs index 04e2410df7c..6a8c3d7b401 100644 --- a/Content.Shared/Paper/StampComponent.cs +++ b/Content.Shared/Paper/StampComponent.cs @@ -34,7 +34,7 @@ public sealed partial class StampComponent : Component public string StampedName { get; set; } = "stamp-component-stamped-name-default"; /// - /// Tne sprite state of the stamp to display on the paper from paper Sprite path. + /// The sprite state of the stamp to display on the paper from paper Sprite path. /// [DataField("stampState")] public string StampState { get; set; } = "paper_stamp-generic"; From 94995460fd0eec432b1a6468af47ad415063c6af Mon Sep 17 00:00:00 2001 From: Flareguy <78941145+Flareguy@users.noreply.github.com> Date: Wed, 19 Jun 2024 13:13:27 -0500 Subject: [PATCH 033/109] Emergency toolbox fill rework (#29202) * emergency toolbox fill rework * Fuck --- .../Catalog/Fills/Items/toolboxes.yml | 28 +++++++-------- .../Objects/Misc/fire_extinguisher.yml | 33 ++++++++++++++++++ .../fire_extinguisher_closed.png | Bin 0 -> 559 bytes .../fire_extinguisher_open.png | Bin 0 -> 590 bytes .../inhand-left.png | Bin 0 -> 320 bytes .../inhand-right.png | Bin 0 -> 313 bytes .../Misc/fire_extinguisher_mini.rsi/meta.json | 25 +++++++++++++ 7 files changed, 71 insertions(+), 15 deletions(-) create mode 100644 Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/fire_extinguisher_closed.png create mode 100644 Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/fire_extinguisher_open.png create mode 100644 Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/meta.json diff --git a/Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml b/Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml index 87921a71d4f..1091207bba4 100644 --- a/Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml +++ b/Resources/Prototypes/Catalog/Fills/Items/toolboxes.yml @@ -7,24 +7,22 @@ - type: StorageFill contents: - id: CrowbarRed - - id: ClothingMaskBreath - - id: ClothingMaskBreath - prob: 0.5 - - id: EmergencyOxygenTankFilled - - id: EmergencyOxygenTankFilled - prob: 0.5 - - id: FoodTinMRE - - id: FoodTinMRE - prob: 0.5 - - id: DrinkWaterBottleFull - - id: DrinkWaterBottleFull - prob: 0.5 + - id: RadioHandheld + - id: WelderMini + - id: FireExtinguisherMini + # Random lighting item orGroup - id: FlashlightLantern - - id: FlashlightLantern - prob: 0.5 + orGroup: LightingItem + - id: Flare + orGroup: LightingItem + - id: GlowstickBase + orGroup: LightingItem + # Low-chance items + - id: FoodSnackChocolate + prob: 0.15 - id: HarmonicaInstrument prob: 0.15 - + - type: entity id: ToolboxElectricalFilled name: electrical toolbox diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml index 71629919ae4..5aaa58dbbee 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml @@ -75,3 +75,36 @@ - type: VaporVisuals animationTime: 0.8 animationState: extinguish + +- type: entity + name: pocket fire extinguisher + parent: FireExtinguisher + id: FireExtinguisherMini + description: A light and compact fibreglass-framed model fire extinguisher. It holds less water then its bigger brother. + components: + - type: Sprite + sprite: Objects/Misc/fire_extinguisher_mini.rsi + layers: + - state: fire_extinguisher_closed + map: [ "enabled" ] + - type: Item + sprite: Objects/Misc/fire_extinguisher_mini.rsi + size: Small + - type: SolutionContainerManager + solutions: + spray: + maxVol: 30 + reagents: + - ReagentId: Water + Quantity: 30 + - type: MeleeWeapon + wideAnimationRotation: 180 + damage: + types: + Blunt: 3 #completely ineffective; your fists are better + soundHit: + path: /Audio/Effects/weak_hit1.ogg + - type: PhysicalComposition + materialComposition: + Steel: 50 + Glass: 40 \ No newline at end of file diff --git a/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/fire_extinguisher_closed.png b/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/fire_extinguisher_closed.png new file mode 100644 index 0000000000000000000000000000000000000000..3ae6804597bebd167d30421596e213ca5a9bf26a GIT binary patch literal 559 zcmV+~0?_@5P)#g4ix*KrG1QZyR^X*a3<0q8?75Ps7C_v8SY^$_68*Y6xYaES8Cikg=WK++tx&6UeN9Ph>84N=1uI$v1CE`nAhNYjLuzIPoY!C(@-cW9UVrhD$?eDB9O_q+>Tb=B1tOPC~` znV3+m_8%`SX!EoaMk5im`1TEje3o1xPo+}f?70i{?d%00{^F$;)5t*L^(#K6KH<1t z)7hmf*VsEetW}9^x&{C|&qD~olV{HX80Zi2Be%-@+$;|sJ^~%I3p52b!H@5N%``eao2eHLzQ1Aw1BJqjS~x+1ie>&VnHkWPOAz;#^|;P+XL zvw?89u@1YgC=3m8|G;6jx}(1%Q>z8Zckjh$B+_yM0)aqd9k(nsmdOan{FA9wfx3hc zNZCGhtXBK)OK4<3N=d-)Yuc;UswOD2DUecP+cs6F#*N!I6~Ncbca9u9M0$Byk7Z)O z7ub$%2N~QICYvh~vV$BycGLuAcn73~lyLggIHA5?HawSLFi1AvdE{Ejs0HdroKUFLi*)ugw>heYQ&)nI30{|aZRv3&<8s!=p za2y9`z0!!S`550EhtJWg+AO0>xR_k}8+3Qh1Z8F*SI9TT_2=0{Mp^A1B!FZhVdkW( cuC_|Q0LkpD(fKtcBme*a07*qoM6N<$f>m4%00000 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-left.png b/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..570b1dbcc44a8746b2cedc1c172de13fa37dd8c8 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1k1^9%xPMbDm$Bu0;Up_arv@o@{K6UCuZgKIg+t-e(sR5PjR=Xqtq_|3g z{DS|(0K=v;I_H6+oCO|{#S9F5he4R}c>anMpx{(b7srqY_qW#!g$^t5xCC-?3%{#> z*pn0EvZhd?YsroKGX&2ZRt*wYb~pe;zr!7V8A`|=+?df->{(SD{<}_P*IK62^&hei zm_$a$MP9P7I=tp<`@t6+%)){S4kiRYmDi|l8mY! wJ|B23sFAGjrfGAzexot>?dH?_x*gW$AE@pvYBhZSALu{^Pgg&ebxsLQ0Lu@4?f?J) literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-right.png b/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..3a62d5a9f921670025feb2dbd6dcb9b8b992e0d9 GIT binary patch literal 313 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+ru!7>k44ofy`glX(f`u%tWsIx;Y9 z?C1WI$O`1k1^9%xPMbF6<;&+gc5E}Xv@o@{K6UCuZgKIg+t-e(sR5N-o%oy=NO6?} z`33)n0ftRybj|}sISV`@iy0XB4ude`@%$AjK*2sw7srqY_qW%)`5F{>TrW1cru>&S z^9}eJnJw|+!QD>ZHSBFcvv`2I85mx=Gwfa1^)y-O`CF#Sa@iW2>5&P)*yc08v3dT{ z^4q%@JJoIV@w&I)*9tQ*2!T-$`ySETe;u;t?F!?n)LPBBXu+KWOf4%I3l&Tbm~p$v qc2sJu`X2e;TVii0-xgMdrLqTEdyC7jHfsZ2#^CAd=d#Wzp$Py9+jH{( literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/meta.json b/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/meta.json new file mode 100644 index 00000000000..db1885634a5 --- /dev/null +++ b/Resources/Textures/Objects/Misc/fire_extinguisher_mini.rsi/meta.json @@ -0,0 +1,25 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from vgstation13 at commit https://github.com/vgstation-coders/vgstation13/commit/31d6576ba8102135d058ef49c3cb6ecbe8db8a79. Edited by Flareguy for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "fire_extinguisher_open" + }, + { + "name": "fire_extinguisher_closed" + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + } + ] +} From 912091e24ef1f9f836fc7392a88253183e3b8b42 Mon Sep 17 00:00:00 2001 From: lzk <124214523+lzk228@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:13:55 +0200 Subject: [PATCH 034/109] Add wet floor sign & warning cone to autolathe (#29205) * Add wet floor sign & warning cone to autolathe * removing --- .../Prototypes/Entities/Structures/Machines/lathe.yml | 2 ++ Resources/Prototypes/Recipes/Lathes/misc.yml | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 66af73088dc..67cf6f767da 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -196,6 +196,8 @@ - WeaponCapacitorRechargerCircuitboard - HandheldStationMap - ClothingHeadHatWelding + - WetFloorSign + - ClothingHeadHatCone - type: EmagLatheRecipes emagStaticRecipes: - BoxLethalshot diff --git a/Resources/Prototypes/Recipes/Lathes/misc.yml b/Resources/Prototypes/Recipes/Lathes/misc.yml index 378392bae17..0f043df9f8f 100644 --- a/Resources/Prototypes/Recipes/Lathes/misc.yml +++ b/Resources/Prototypes/Recipes/Lathes/misc.yml @@ -214,3 +214,10 @@ materials: Steel: 400 Glass: 200 + +- type: latheRecipe + id: ClothingHeadHatCone + result: ClothingHeadHatCone + completetime: 3 + materials: + Plastic: 200 From 674409d3bf6a937e40081be2bad21a96025e816d Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 19 Jun 2024 18:15:02 +0000 Subject: [PATCH 035/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 33 +++++++++++++++++-------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1fea991f55e..43d309e1327 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,19 +1,4 @@ Entries: -- author: Keer-Sar - changes: - - message: Cyborgs now have audio for some emotes. - type: Add - id: 6282 - time: '2024-04-01T04:35:21.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26594 -- author: SlamBamActionman - changes: - - message: Added Coordinates Disks to the Salvage expeditions console, which are - now a requirement for expedition FTL. - type: Add - id: 6283 - time: '2024-04-01T04:50:00.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/23240 - author: kseandi changes: - message: NT has declassified the documentation for door electronics, now anyone @@ -3845,3 +3830,21 @@ id: 6781 time: '2024-06-19T14:49:19.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29208 +- author: Flareguy + changes: + - message: Emergency Toolboxes now have a different fill that contains more utility + items then before. + type: Tweak + - message: Added the pocket fire extinguisher. You can currently only find them + in the emergency toolbox. + type: Add + id: 6782 + time: '2024-06-19T18:13:27.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29202 +- author: lzk228 + changes: + - message: Warning cone and wet floor sign recipes added to the autolathe. + type: Add + id: 6783 + time: '2024-06-19T18:13:55.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29205 From 9759747176232cc1fb96978fec67a23b7ba445c2 Mon Sep 17 00:00:00 2001 From: Vasilis Date: Wed, 19 Jun 2024 22:33:02 +0300 Subject: [PATCH 036/109] Tools batch files (#29179) * Tools batch files * fine --- runclient-Tools.bat | 2 ++ runserver-Tools.bat | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 runclient-Tools.bat create mode 100644 runserver-Tools.bat diff --git a/runclient-Tools.bat b/runclient-Tools.bat new file mode 100644 index 00000000000..21973feb6f7 --- /dev/null +++ b/runclient-Tools.bat @@ -0,0 +1,2 @@ +@echo off +dotnet run --project Content.Client --configuration Tools diff --git a/runserver-Tools.bat b/runserver-Tools.bat new file mode 100644 index 00000000000..8ea3d7c04b5 --- /dev/null +++ b/runserver-Tools.bat @@ -0,0 +1,3 @@ +@echo off +dotnet run --project Content.Server --configuration Tools +pause From 67cc089dd0fcd04f8f72fc18358021ddff5e420a Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 20 Jun 2024 00:14:47 +0200 Subject: [PATCH 037/109] Fix terrible portable scrubber unlit layers (#29232) Jesus fucking christ man --- .../portable_scrubber.rsi/unlit-full.png | Bin 705 -> 535 bytes .../Portable/portable_scrubber.rsi/unlit.png | Bin 706 -> 607 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png b/Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png index b688c5b73a4ad84fece19b6f25cb96399ef86543..1ed8c5ed4bc67d8b11dfd75c2671e4c56be0ac84 100644 GIT binary patch delta 107 zcmV-x0F?j11(yV{v;kvINklY1~B;Pyt^LA;4JWnEM{QfI|9OtQ?>b|fr9KMp1!W^&)7uS zO@!}FT`~_SBw6AbQR1ARo12Xd)v;lvXNklY1~B;Pyt^LA;4JWnEM{QfI|9OtQ?>b|fr9KMp1!W^&)7uS zjYPy&PumR?k}PqJC~?lu%}vcKVQ?-=O)N=GQ7F$W$xv|j^bH7a+%J Date: Thu, 20 Jun 2024 01:51:07 +0200 Subject: [PATCH 038/109] Prevent fly-by fixture from powering containment field generator (#29225) * Prevent fly-by fixture from powering containment field generator * Update according to review --- .../EntitySystems/ContainmentFieldGeneratorSystem.cs | 3 ++- .../Components/ContainmentFieldGeneratorComponent.cs | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs index d58458527f6..e575b0403b6 100644 --- a/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs +++ b/Content.Server/Singularity/EntitySystems/ContainmentFieldGeneratorSystem.cs @@ -65,7 +65,8 @@ public override void Update(float frameTime) /// private void HandleGeneratorCollide(Entity generator, ref StartCollideEvent args) { - if (_tags.HasTag(args.OtherEntity, generator.Comp.IDTag)) + if (args.OtherFixtureId == generator.Comp.SourceFixtureId && + _tags.HasTag(args.OtherEntity, generator.Comp.IDTag)) { ReceivePower(generator.Comp.PowerReceived, generator); generator.Comp.Accumulator = 0f; diff --git a/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs b/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs index c704c8bf77b..938b34f354a 100644 --- a/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs +++ b/Content.Shared/Singularity/Components/ContainmentFieldGeneratorComponent.cs @@ -69,6 +69,13 @@ public int PowerBuffer [DataField("idTag", customTypeSerializer: typeof(PrototypeIdSerializer))] public string IDTag = "EmitterBolt"; + /// + /// Which fixture ID should test collision with from the entity that powers the generator? + /// Prevents the generator from being powered by fly-by fixtures. + /// + [DataField] + public string SourceFixtureId = "projectile"; + /// /// Is the generator toggled on? /// From a8ad09ebf9a1918ef121f797f21f717ac2ab2ff4 Mon Sep 17 00:00:00 2001 From: PJBot Date: Wed, 19 Jun 2024 23:52:13 +0000 Subject: [PATCH 039/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 43d309e1327..5f436812ad3 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: kseandi - changes: - - message: NT has declassified the documentation for door electronics, now anyone - can configure its access using network configurator or multitool. - type: Add - id: 6284 - time: '2024-04-01T06:06:14.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/17778 - author: musicmanvr changes: - message: Added Sol Dry, 8 new cocktails and coconut water. @@ -3848,3 +3840,11 @@ id: 6783 time: '2024-06-19T18:13:55.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29205 +- author: arimah + changes: + - message: Containment field generators can no longer be powered by an emitter bolt + flying past them. The bolt must hit them directly. + type: Fix + id: 6784 + time: '2024-06-19T23:51:07.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29225 From 872e927cc7776589589a567b332d9fca3b1ade68 Mon Sep 17 00:00:00 2001 From: "Mr. 27" <45323883+Dutch-VanDerLinde@users.noreply.github.com> Date: Wed, 19 Jun 2024 20:09:44 -0400 Subject: [PATCH 040/109] Hide moth antenna and lizard frills with hardsuit helmets, fix lizard snouts not being hidden (#29214) * inital * Update ClothingSystem.cs * Update helmets.yml --- .../Clothing/EntitySystems/ClothingSystem.cs | 2 +- .../Entities/Clothing/Head/base_clothinghead.yml | 4 ++++ .../Entities/Clothing/Head/hardsuit-helmets.yml | 3 +++ Resources/Prototypes/Entities/Clothing/Head/hats.yml | 2 ++ .../Prototypes/Entities/Clothing/Head/helmets.yml | 12 ++++++++++++ .../Prototypes/Entities/Clothing/Head/hoods.yml | 6 ++++++ Resources/Prototypes/Entities/Mobs/Species/moth.yml | 2 ++ .../Prototypes/Entities/Mobs/Species/reptilian.yml | 4 ++++ 8 files changed, 34 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs index bdcb2c82042..381edc68baa 100644 --- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs +++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs @@ -87,7 +87,7 @@ private void ToggleVisualLayers(EntityUid equipee, HashSet foreach (HumanoidVisualLayers layer in layers) { if (!appearanceLayers.Contains(layer)) - break; + continue; InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee); diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index 866fe962ca5..07c3e12dd85 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -143,6 +143,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: entity abstract: true @@ -187,6 +189,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: entity abstract: true diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml index 0c660e09b80..3c50319836d 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml @@ -18,7 +18,10 @@ sprite: Clothing/Head/Hardsuits/basic.rsi - type: HideLayerClothing slots: + - Hair - Snout + - HeadTop + - HeadSide #Atmospherics Hardsuit - type: entity diff --git a/Resources/Prototypes/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Entities/Clothing/Head/hats.yml index 462b1cb8895..e2786f70f06 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hats.yml @@ -392,6 +392,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: entity parent: ClothingHeadHatWizardBase diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index d585518ece8..a01d6fae519 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -22,6 +22,10 @@ - type: Tag tags: - WhitelistChameleon + - type: HideLayerClothing + slots: + - HeadTop + - HeadSide #Mercenary Helmet - type: entity @@ -114,6 +118,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide #Janitorial Bombsuit Helmet - type: entity @@ -168,6 +174,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide #Templar Helmet - type: entity @@ -234,6 +242,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide #Atmos Fire Helmet - type: entity @@ -263,6 +273,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: BreathMask #Chitinous Helmet diff --git a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml index db187069123..4dec89e75ad 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml @@ -18,6 +18,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: entity parent: ClothingHeadHatHoodBioGeneral @@ -158,6 +160,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide - type: entity parent: ClothingHeadBase @@ -240,6 +244,8 @@ slots: - Hair - Snout + - HeadTop + - HeadSide #Winter Coat Hoods - type: entity diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index f2a0194b7a4..f6fde849efe 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -7,6 +7,8 @@ components: - type: HumanoidAppearance species: Moth + hideLayersOnEquip: + - HeadTop - type: Hunger - type: Thirst - type: Icon diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml index f099ffdc6a9..ad543620cf8 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml @@ -7,6 +7,10 @@ components: - type: HumanoidAppearance species: Reptilian + hideLayersOnEquip: + - Snout + - HeadTop + - HeadSide - type: Hunger - type: Puller needsHands: false From 625fef00c1556191918da6e919fd55942ae06de3 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 00:10:50 +0000 Subject: [PATCH 041/109] 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 5f436812ad3..c23d63e0ab5 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: musicmanvr - changes: - - message: Added Sol Dry, 8 new cocktails and coconut water. - type: Add - id: 6285 - time: '2024-04-01T06:41:14.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25367 - author: f0x-n3rd changes: - message: Added pages for each categories from the chemical page in the guidebook, @@ -3848,3 +3841,10 @@ id: 6784 time: '2024-06-19T23:51:07.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29225 +- author: Dutch-VanDerLinde + changes: + - message: Lizard frills and moth antenna are now hidden by hardsuit helmets. + type: Tweak + id: 6785 + time: '2024-06-20T00:09:44.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29214 From 40bcf66fc7fa56774bf8656546790d4aedaf5ae0 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Wed, 19 Jun 2024 20:57:52 -0400 Subject: [PATCH 042/109] Fix and enable TestEmptyLoadout (#29228) * Fix and enabled TestEmptyLoadout * Fine, have a real name --- .../Tests/Preferences/LoadoutTests.cs | 75 ++++++++++++++++--- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs index 72e35dac057..a66cecc90f9 100644 --- a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs @@ -1,15 +1,52 @@ +using System.Collections.Generic; using Content.Server.Station.Systems; +using Content.Shared.Inventory; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles.Jobs; using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; namespace Content.IntegrationTests.Tests.Preferences; [TestFixture] -[Ignore("HumanoidAppearance crashes upon loading default profiles.")] public sealed class LoadoutTests { + [TestPrototypes] + private const string Prototypes = @" +- type: playTimeTracker + id: PlayTimeLoadoutTester + +- type: loadout + id: TestJumpsuit + equipment: TestJumpsuit + +- type: startingGear + id: TestJumpsuit + equipment: + jumpsuit: ClothingUniformJumpsuitColorGrey + +- type: loadoutGroup + id: LoadoutTesterJumpsuit + name: generic-unknown + loadouts: + - TestJumpsuit + +- type: roleLoadout + id: JobLoadoutTester + groups: + - LoadoutTesterJumpsuit + +- type: job + id: LoadoutTester + playTimeTracker: PlayTimeLoadoutTester +"; + + private readonly Dictionary _expectedEquipment = new() + { + ["jumpsuit"] = "ClothingUniformJumpsuitColorGrey" + }; + /// /// Checks that an empty loadout still spawns with default gear and not naked. /// @@ -26,18 +63,38 @@ public async Task TestEmptyLoadout() // Check that an empty role loadout spawns gear var stationSystem = entManager.System(); + var inventorySystem = entManager.System(); var testMap = await pair.CreateTestMap(); - // That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing. - var profile = new HumanoidCharacterProfile(); + await server.WaitAssertion(() => + { + var profile = new HumanoidCharacterProfile(); - profile.SetLoadout(new RoleLoadout("TestRoleLoadout")); + profile.SetLoadout(new RoleLoadout("LoadoutTester")); - stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent() - { - // Sue me, there's so much involved in setting up jobs - Prototype = "CargoTechnician" - }, profile, station: null); + var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent() + { + Prototype = "LoadoutTester" + }, profile, station: null); + + var slotQuery = inventorySystem.GetSlotEnumerator(tester); + var checkedCount = 0; + while (slotQuery.NextItem(out var item, out var slot)) + { + // Make sure the slot is valid + Assert.That(_expectedEquipment.TryGetValue(slot.Name, out var expectedItem), $"Spawned item in unexpected slot: {slot.Name}"); + + // Make sure that the item is the right one + var meta = entManager.GetComponent(item); + Assert.That(meta.EntityPrototype.ID, Is.EqualTo(expectedItem.Id), $"Spawned wrong item in slot {slot.Name}!"); + + checkedCount++; + } + // Make sure the number of items is the same + Assert.That(checkedCount, Is.EqualTo(_expectedEquipment.Count), "Number of items does not match expected!"); + + entManager.DeleteEntity(tester); + }); await pair.CleanReturnAsync(); } From d438925d3b0d6875b8bf7effa938e5589823fb5b Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:15:40 +1000 Subject: [PATCH 043/109] Fix entities getting stuck red (#28981) --- .../Effects/ColorFlashEffectSystem.cs | 68 ++++++++++--------- .../Gravity/FloatingVisualizerSystem.cs | 2 +- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index af0bd4b600d..0570b4951ec 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -2,8 +2,10 @@ using Robust.Client.Animations; using Robust.Client.GameObjects; using Robust.Shared.Animations; +using Robust.Shared.Collections; using Robust.Shared.Player; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Content.Client.Effects; @@ -11,13 +13,13 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem { [Dependency] private readonly IGameTiming _timing = default!; [Dependency] private readonly AnimationPlayerSystem _animation = default!; - [Dependency] private readonly IComponentFactory _factory = default!; /// /// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register. /// private const float AnimationLength = 0.30f; private const string AnimationKey = "color-flash-effect"; + private ValueList _toRemove = new(); public override void Initialize() { @@ -44,8 +46,28 @@ private void OnEffectAnimationCompleted(EntityUid uid, ColorFlashEffectComponent { sprite.Color = component.Color; } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = AllEntityQuery(); + _toRemove.Clear(); + + // Can't use deferred removal on animation completion or it will cause issues. + while (query.MoveNext(out var uid, out _)) + { + if (_animation.HasRunningAnimation(uid, AnimationKey)) + continue; - RemCompDeferred(uid); + _toRemove.Add(uid); + } + + foreach (var ent in _toRemove) + { + RemComp(ent); + } } private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null) @@ -82,51 +104,31 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) { var ent = GetEntity(nent); - if (Deleted(ent)) + if (Deleted(ent) || !TryComp(ent, out SpriteComponent? sprite)) { continue; } - if (!TryComp(ent, out AnimationPlayerComponent? player)) - { - player = (AnimationPlayerComponent) _factory.GetComponent(typeof(AnimationPlayerComponent)); - player.Owner = ent; - player.NetSyncEnabled = false; - AddComp(ent, player); - } - - // Need to stop the existing animation first to ensure the sprite color is fixed. - // Otherwise we might lerp to a red colour instead. - if (_animation.HasRunningAnimation(ent, player, AnimationKey)) - { - _animation.Stop(ent, player, AnimationKey); - } - - if (!TryComp(ent, out var sprite)) - { - continue; - } - - if (TryComp(ent, out var effect)) +#if DEBUG + if (!TryComp(ent, out ColorFlashEffectComponent? comp)) { - sprite.Color = effect.Color; + DebugTools.Assert(!_animation.HasRunningAnimation(ent, AnimationKey)); } +#endif + _animation.Stop(ent, AnimationKey); var animation = GetDamageAnimation(ent, color, sprite); if (animation == null) - continue; - - if (!TryComp(ent, out ColorFlashEffectComponent? comp)) { - comp = (ColorFlashEffectComponent) _factory.GetComponent(typeof(ColorFlashEffectComponent)); - comp.Owner = ent; - comp.NetSyncEnabled = false; - AddComp(ent, comp); + continue; } + comp = EnsureComp(ent); + comp.NetSyncEnabled = false; comp.Color = sprite.Color; - _animation.Play((ent, player), animation, AnimationKey); + + _animation.Play(ent, animation, AnimationKey); } } } diff --git a/Content.Client/Gravity/FloatingVisualizerSystem.cs b/Content.Client/Gravity/FloatingVisualizerSystem.cs index f489237d107..a7cb3333b27 100644 --- a/Content.Client/Gravity/FloatingVisualizerSystem.cs +++ b/Content.Client/Gravity/FloatingVisualizerSystem.cs @@ -57,6 +57,6 @@ private void OnAnimationCompleted(EntityUid uid, FloatingVisualsComponent compon if (args.Key != component.AnimationKey) return; - FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime, !component.CanFloat); + FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime, stop: !component.CanFloat); } } From f2c0991467b24a775d9f78c4013dbec1bdcda8ef Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 07:16:46 +0000 Subject: [PATCH 044/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index c23d63e0ab5..b29db0a9b3a 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: f0x-n3rd - changes: - - message: Added pages for each categories from the chemical page in the guidebook, - no more infinite scrolling when looking up certain chemicals. - type: Add - id: 6286 - time: '2024-04-01T07:20:38.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25831 - author: TheShuEd changes: - message: Added new transformation particles types for APE and CHIMP @@ -3848,3 +3840,10 @@ id: 6785 time: '2024-06-20T00:09:44.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29214 +- author: metalgearsloth + changes: + - message: Fix entities getting stuck red after getting hit. + type: Fix + id: 6786 + time: '2024-06-20T07:15:40.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/28981 From c1948d88d81dabf180c7659c07dd6b35ad5612e6 Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 20 Jun 2024 18:19:00 +1000 Subject: [PATCH 045/109] Update submodule to 226.2.0 (#29247) --- RobustToolbox | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RobustToolbox b/RobustToolbox index bf8054b1813..da5416a2da2 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit bf8054b181392ec9a7eb9f4fea94f66837ed4a71 +Subproject commit da5416a2da2745044a1ed4f2aa5d0af294e8b705 From 4bf6d9e1711482348fc838d08ea78300a6a476d0 Mon Sep 17 00:00:00 2001 From: IProduceWidgets <107586145+IProduceWidgets@users.noreply.github.com> Date: Thu, 20 Jun 2024 06:04:07 -0400 Subject: [PATCH 046/109] add a type specifier where one was forgor (#29250) * add a type specifier where one was forgor * Fix other way because degub conditions * okay this feels kinda dumb but it does fix it. * Update Content.Client/Effects/ColorFlashEffectSystem.cs --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- Content.Client/Effects/ColorFlashEffectSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Content.Client/Effects/ColorFlashEffectSystem.cs b/Content.Client/Effects/ColorFlashEffectSystem.cs index 0570b4951ec..956c9465244 100644 --- a/Content.Client/Effects/ColorFlashEffectSystem.cs +++ b/Content.Client/Effects/ColorFlashEffectSystem.cs @@ -109,12 +109,12 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) continue; } -#if DEBUG if (!TryComp(ent, out ColorFlashEffectComponent? comp)) { +#if DEBUG DebugTools.Assert(!_animation.HasRunningAnimation(ent, AnimationKey)); - } #endif + } _animation.Stop(ent, AnimationKey); var animation = GetDamageAnimation(ent, color, sprite); @@ -124,7 +124,7 @@ private void OnColorFlashEffect(ColorFlashEffectEvent ev) continue; } - comp = EnsureComp(ent); + EnsureComp(ent, out comp); comp.NetSyncEnabled = false; comp.Color = sprite.Color; From ad89444c585e77e86977e2750a4df9da10c00720 Mon Sep 17 00:00:00 2001 From: "Mr. 27" <45323883+Dutch-VanDerLinde@users.noreply.github.com> Date: Thu, 20 Jun 2024 06:20:20 -0400 Subject: [PATCH 047/109] Make winter coat hood hide certain markings (#29238) Update base_clothinghead.yml --- .../Prototypes/Entities/Clothing/Head/base_clothinghead.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml index 07c3e12dd85..f9ff59d6b8b 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml @@ -265,3 +265,5 @@ - type: HideLayerClothing slots: - Hair + - HeadTop + - HeadSide \ No newline at end of file From 8c9fcd4ca75a88374775ca01aca86db549636121 Mon Sep 17 00:00:00 2001 From: "Mr. 27" <45323883+Dutch-VanDerLinde@users.noreply.github.com> Date: Thu, 20 Jun 2024 07:12:58 -0400 Subject: [PATCH 048/109] Fix ERT becoming sleeper agents and add sleeper agent preferences (#27817) * b * Update antags.ftl * m * ok * Update events.yml * antag * a * Create InitialInfectedExemptComponent.cs * Delete InitialInfectedExemptComponent.cs * yes * Delete InitialInfectedExemptComponent.cs * Create AntagImmuneComponent.cs --- .../Antag/Components/AntagImmuneComponent.cs | 7 +++++++ .../Zombies/InitialInfectedExemptComponent.cs | 7 ------- Resources/Locale/en-US/prototypes/roles/antags.ftl | 3 +++ .../Prototypes/Entities/Mobs/Player/humanoid.yml | 9 ++------- Resources/Prototypes/GameRules/events.yml | 12 ++++++++---- Resources/Prototypes/GameRules/roundstart.yml | 5 ++++- Resources/Prototypes/Roles/Antags/traitor.yml | 7 +++++++ 7 files changed, 31 insertions(+), 19 deletions(-) create mode 100644 Content.Server/Antag/Components/AntagImmuneComponent.cs delete mode 100644 Content.Server/Zombies/InitialInfectedExemptComponent.cs diff --git a/Content.Server/Antag/Components/AntagImmuneComponent.cs b/Content.Server/Antag/Components/AntagImmuneComponent.cs new file mode 100644 index 00000000000..3ae8e049f60 --- /dev/null +++ b/Content.Server/Antag/Components/AntagImmuneComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Antag.Components; + +[RegisterComponent] +public partial class AntagImmuneComponent : Component +{ + +} diff --git a/Content.Server/Zombies/InitialInfectedExemptComponent.cs b/Content.Server/Zombies/InitialInfectedExemptComponent.cs deleted file mode 100644 index f2dfda3f872..00000000000 --- a/Content.Server/Zombies/InitialInfectedExemptComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Zombies; - -[RegisterComponent] -public sealed partial class InitialInfectedExemptComponent : Component -{ - -} diff --git a/Resources/Locale/en-US/prototypes/roles/antags.ftl b/Resources/Locale/en-US/prototypes/roles/antags.ftl index f0038b0eb96..ba43d4ff85b 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -1,6 +1,9 @@ roles-antag-syndicate-agent-name = Syndicate agent roles-antag-syndicate-agent-objective = Complete your objectives without being caught. +roles-antag-syndicate-agent-sleeper-name = Syndicate sleeper agent +roles-antag-syndicate-agent-sleeper-objective = A form of syndicate agent that can activate at any point in the middle of the shift. + roles-antag-initial-infected-name = Initial Infected roles-antag-initial-infected-objective = Once you turn, infect as many other crew members as possible. diff --git a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml index e365ee3a1cf..6067c95fd3c 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml @@ -12,7 +12,7 @@ parent: EventHumanoid components: - type: MindShield - - type: InitialInfectedExempt + - type: AntagImmune ## Death Squad @@ -30,7 +30,6 @@ - NamesLastMilitary - type: RandomHumanoidSpawner settings: DeathSquad - - type: InitialInfectedExempt - type: randomHumanoidSettings id: DeathSquad @@ -66,7 +65,6 @@ - NamesLastMilitary - type: RandomHumanoidSpawner settings: ERTLeader - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeader @@ -96,7 +94,6 @@ state: ertleadereva - type: RandomHumanoidSpawner settings: ERTLeaderEVA - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeaderEVA @@ -117,7 +114,6 @@ components: - type: RandomHumanoidSpawner settings: ERTLeaderEVALecter - - type: InitialInfectedExempt - type: randomHumanoidSettings id: ERTLeaderEVALecter @@ -453,7 +449,6 @@ state: cburn - type: RandomHumanoidSpawner settings: CBURNAgent - - type: InitialInfectedExempt - type: randomHumanoidSettings id: CBURNAgent @@ -470,6 +465,7 @@ nameSegments: - NamesFirstMilitary - NamesLastMilitary + ## Central Command - type: entity @@ -481,7 +477,6 @@ state: centcom - type: RandomHumanoidSpawner settings: CentcomOfficial - - type: InitialInfectedExempt - type: randomHumanoidSettings id: CentcomOfficial diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index a443ff50277..f8f2ef49e70 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -393,7 +393,7 @@ blacklist: components: - ZombieImmune - - InitialInfectedExempt + - AntagImmune briefing: text: zombie-patientzero-role-greeting color: Plum @@ -444,7 +444,7 @@ - type: entity parent: BaseTraitorRule - id: SleeperAgentsRule + id: SleeperAgents components: - type: StationEvent earliestStart: 30 @@ -457,13 +457,17 @@ - type: AlertLevelInterceptionRule - type: AntagSelection definitions: - - prefRoles: [ Traitor ] + - prefRoles: [ TraitorSleeper ] + fallbackRoles: [ Traitor ] min: 1 max: 2 playerRatio: 10 + blacklist: + components: + - AntagImmune mindComponents: - type: TraitorRole - prototype: Traitor + prototype: TraitorSleeper - type: entity id: MassHallucinations diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 88f80fd4f6a..923a0114606 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -173,6 +173,9 @@ - prefRoles: [ Traitor ] max: 8 playerRatio: 10 + blacklist: + components: + - AntagImmune lateJoinAdditional: true mindComponents: - type: TraitorRole @@ -232,7 +235,7 @@ blacklist: components: - ZombieImmune - - InitialInfectedExempt + - AntagImmune briefing: text: zombie-patientzero-role-greeting color: Plum diff --git a/Resources/Prototypes/Roles/Antags/traitor.yml b/Resources/Prototypes/Roles/Antags/traitor.yml index e97ced1795e..205f04b05ea 100644 --- a/Resources/Prototypes/Roles/Antags/traitor.yml +++ b/Resources/Prototypes/Roles/Antags/traitor.yml @@ -6,6 +6,13 @@ objective: roles-antag-syndicate-agent-objective guides: [ Traitors ] +- type: antag + id: TraitorSleeper + name: roles-antag-syndicate-agent-sleeper-name + antagonist: true + setPreference: true + objective: roles-antag-syndicate-agent-sleeper-objective + # Syndicate Operative Outfit - Monkey - type: startingGear id: SyndicateOperativeGearMonkey From 4ccdc9070f799086d05ffc8c2acebac6999a0e2f Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 11:14:04 +0000 Subject: [PATCH 049/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index b29db0a9b3a..d12ec6869b2 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,15 +1,4 @@ Entries: -- author: TheShuEd - changes: - - message: Added new transformation particles types for APE and CHIMP - type: Add - - message: Added new anomaly behaviour mechanic. Different behaviors can change - the gameplay of an anomaly. Players can get random behaviors by bombarding the - anomaly with new particles - type: Add - id: 6287 - time: '2024-04-01T08:29:13.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24683 - author: Sk1tch changes: - message: Added chat window opacity slider to options. @@ -3847,3 +3836,13 @@ id: 6786 time: '2024-06-20T07:15:40.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/28981 +- author: Dutch-VanDerLinde + changes: + - message: Syndicate sleeper agents are now opt-in. You can opt-in with the antagonist + preferences menu. + type: Tweak + - message: ERT are no longer able to become sleeper agents. + type: Fix + id: 6787 + time: '2024-06-20T11:12:58.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27817 From 9bf4a28f4d357a6ca7a8187ca930d114c99e2ca4 Mon Sep 17 00:00:00 2001 From: neutrino <67447925+neutrino-laser@users.noreply.github.com> Date: Thu, 20 Jun 2024 04:16:06 -0700 Subject: [PATCH 050/109] fixed Syndicate smokes pack being half-filled (#28371) --- .../Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml index e2874093c5b..cb54b4c957f 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Cigarettes/packs.yml @@ -191,7 +191,7 @@ - type: StorageFill contents: - id: CigaretteSyndicate - amount: 5 + amount: 10 - type: entity id: CigPackMixedMedical From 9fbcec1c7453954bfab14e095ac8ed02c12e8425 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 11:17:18 +0000 Subject: [PATCH 051/109] 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 d12ec6869b2..8551cf13e8d 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Sk1tch - changes: - - message: Added chat window opacity slider to options. - type: Add - id: 6288 - time: '2024-04-01T20:48:02.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/24990 - author: TheShuEd changes: - message: added procedurally generating books to library. @@ -3846,3 +3839,10 @@ id: 6787 time: '2024-06-20T11:12:58.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27817 +- author: neutrino-laser + changes: + - message: Syndicate smokes pack now has 10 cigarettes as intended. + type: Fix + id: 6788 + time: '2024-06-20T11:16:06.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/28371 From 4fe5235e3707e84d5a7c4cba2148970800d059bc Mon Sep 17 00:00:00 2001 From: lzk <124214523+lzk228@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:58:51 +0200 Subject: [PATCH 052/109] Buff cube boxes (#29251) --- .../Entities/Objects/Misc/monkeycube.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml b/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml index 1490af88ca8..cf2dad1ce58 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/monkeycube.yml @@ -7,10 +7,7 @@ - type: StorageFill contents: - id: MonkeyCubeWrapped - amount: 6 - - type: Storage - grid: - - 0,0,2,1 + amount: 8 - type: Sprite sprite: Objects/Misc/monkeycube.rsi state: box @@ -45,7 +42,7 @@ - type: StorageFill contents: - id: KoboldCubeWrapped - amount: 6 + amount: 8 - type: Sprite sprite: Objects/Misc/monkeycube.rsi state: box_kobold @@ -59,9 +56,9 @@ - type: StorageFill contents: - id: KoboldCubeWrapped - amount: 3 + amount: 4 - id: MonkeyCubeWrapped - amount: 3 + amount: 4 - type: Sprite sprite: Objects/Misc/monkeycube.rsi state: box_variant @@ -89,10 +86,6 @@ id: SyndicateSpongeBox description: Drymate brand monkey cubes. Just add water! components: - - type: Storage - whitelist: - tags: - - MonkeyCube - type: StorageFill contents: - id: SyndicateSpongeWrapped From 0ac1b31e3622fd106486d40330ad508901a1ee26 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 11:59:57 +0000 Subject: [PATCH 053/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 8551cf13e8d..8af64db532f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: TheShuEd - changes: - - message: added procedurally generating books to library. - type: Add - - message: full books resprite - type: Add - id: 6289 - time: '2024-04-01T21:00:10.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/25840 - author: osjarw changes: - message: Ambuzol plus pills now have different sprites from ambuzol pills. Now @@ -3846,3 +3837,10 @@ id: 6788 time: '2024-06-20T11:16:06.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/28371 +- author: lzk228 + changes: + - message: Cube boxes buffed 6 -> 8 cubes. + type: Tweak + id: 6789 + time: '2024-06-20T11:58:51.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29251 From a74ca54cf8c523fcf9fd58a378247f6c42e50713 Mon Sep 17 00:00:00 2001 From: Boaz1111 <149967078+Boaz1111@users.noreply.github.com> Date: Thu, 20 Jun 2024 14:22:04 +0200 Subject: [PATCH 054/109] moves explosive tech to T1 (#29227) moves explo tech to T1 --- Resources/Prototypes/Research/arsenal.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Prototypes/Research/arsenal.yml b/Resources/Prototypes/Research/arsenal.yml index 5a55a34cdfa..1cfa1fec80f 100644 --- a/Resources/Prototypes/Research/arsenal.yml +++ b/Resources/Prototypes/Research/arsenal.yml @@ -97,8 +97,6 @@ - HoloprojectorSecurity - WeaponDisablerSMG -# Tier 2 - - type: technology id: ExplosiveTechnology name: research-technology-explosive-technology @@ -106,7 +104,7 @@ sprite: Objects/Devices/payload.rsi state: payload-explosive-armed discipline: Arsenal - tier: 2 + tier: 1 cost: 10000 recipeUnlocks: - SignallerAdvanced @@ -117,6 +115,8 @@ - ExplosivePayload - ChemicalPayload +# Tier 2 + - type: technology id: ConcentratedLaserWeaponry name: research-technology-concentrated-laser-weaponry From 514e7600c65e2aa6b2fc0403f5f11679ad38c413 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 12:23:10 +0000 Subject: [PATCH 055/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 8af64db532f..53c12e78f73 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: osjarw - changes: - - message: Ambuzol plus pills now have different sprites from ambuzol pills. Now - CBURN agents and Nukies who bought the syndicate zombie bundle might realize - that they also have some ambuzol plus. - type: Tweak - id: 6290 - time: '2024-04-02T00:50:43.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26651 - author: osjarw changes: - message: Air injectors finally have working indicator lights while enabled. @@ -3844,3 +3835,10 @@ id: 6789 time: '2024-06-20T11:58:51.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29251 +- author: Boaz1111 + changes: + - message: Explosive technology has been moved to T1 arsenal + type: Tweak + id: 6790 + time: '2024-06-20T12:22:04.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29227 From 68c414e8ed424b8801ad19a9087af9b581fc6030 Mon Sep 17 00:00:00 2001 From: Redfire1331 <125223432+Redfire1331@users.noreply.github.com> Date: Thu, 20 Jun 2024 08:34:46 -0400 Subject: [PATCH 056/109] made cup ramen eatable with anything with the fork component (#27826) * made cup ramen eatable with anything with the fork component * removed extra png * made cupramen fillable with water, and made hot ramen dry ramen. --------- Co-authored-by: redfire1331 --- .../Objects/Consumable/Drinks/drinks.yml | 38 -------------- .../Objects/Consumable/Food/snacks.yml | 49 ++++++++++++++++++ .../Consumable/Drinks/ramen.rsi/meta.json | 1 - .../Consumable/Food/snacks.rsi/meta.json | 5 +- .../icon.png => Food/snacks.rsi/ramen.png} | Bin 5 files changed, 53 insertions(+), 40 deletions(-) delete mode 100644 Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json rename Resources/Textures/Objects/Consumable/{Drinks/ramen.rsi/icon.png => Food/snacks.rsi/ramen.png} (100%) diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml index 42814f7fd87..5c55f9ff17d 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks.yml @@ -2328,44 +2328,6 @@ - type: Sprite sprite: Objects/Consumable/Drinks/shake-white.rsi -- type: entity - parent: DrinkGlassBase - id: DrinkRamen - name: cup ramen - description: Just add 10ml boiling water. A taste that reminds you of your school years. - components: - - type: SolutionContainerManager - solutions: - drink: - maxVol: 40 #big cup - reagents: - - ReagentId: DryRamen - Quantity: 25 - - ReagentId: Soysauce - Quantity: 5 - - type: Sprite - sprite: Objects/Consumable/Drinks/ramen.rsi - - type: Tag - tags: - - Trash - - type: SpaceGarbage - -- type: entity - parent: DrinkRamen - id: DrinkHellRamen - name: hell ramen - description: Just add 10ml boiling water. Super spicy flavor. - components: - - type: SolutionContainerManager - solutions: - drink: - maxVol: 40 - reagents: - - ReagentId: DryRamen - Quantity: 25 - - ReagentId: CapsaicinOil - Quantity: 5 - - type: entity parent: DrinkGlass id: DrinkBloodGlass diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml index 223ac6376ca..d620dbda6f4 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml @@ -272,6 +272,46 @@ - type: Food trash: FoodPacketSyndiTrash +- type: entity + parent: FoodSnackBase + id: DrinkRamen + name: cup ramen + description: A cheap food with a taste that reminds you of your school years. + components: + - type: RefillableSolution + solution: food + - type: InjectableSolution + solution: food + - type: SolutionContainerManager + solutions: + food: + maxVol: 50 #big cup + reagents: + - ReagentId: DryRamen + Quantity: 30 + - ReagentId: Soysauce + Quantity: 5 + - type: Sprite + state: ramen + - type: Food + trash: FoodPacketCupRamenTrash + +- type: entity + parent: DrinkRamen + id: DrinkHellRamen + name: hell ramen + description: Super spicy flavor! + components: + - type: SolutionContainerManager + solutions: + food: + maxVol: 50 + reagents: + - ReagentId: DryRamen + Quantity: 30 + - ReagentId: CapsaicinOil + Quantity: 5 + - type: entity name: chow mein parent: FoodSnackBase @@ -573,6 +613,15 @@ - type: Sprite state: syndicakes-trash +- type: entity + noSpawn: true + parent: FoodPacketTrash + id: FoodPacketCupRamenTrash + name: empty cup ramen + components: + - type: Sprite + state: ramen + - type: entity noSpawn: true parent: FoodPacketTrash diff --git a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json b/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json deleted file mode 100644 index db0ac608ed0..00000000000 --- a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/meta.json +++ /dev/null @@ -1 +0,0 @@ -{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "https://github.com/discordia-space/CEV-Eris/raw/f7aa28fd4b4d0386c3393d829681ebca526f1d2d/icons/obj/drinks.dmi", "states": [{"name": "icon"}]} \ No newline at end of file diff --git a/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json index 6f699103840..4ff3230cae7 100644 --- a/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json +++ b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/meta.json @@ -1,12 +1,15 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24, chinese from paradise, ticket by peptide, cnds-trash based on boritos-trash and syndicakes modified by potato1234x", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/c6e3401f2e7e1e55c57060cdf956a98ef1fefc24, chinese from paradise, ticket by peptide, cnds-trash based on boritos-trash and syndicakes modified by potato1234x, ramen from https://github.com/discordia-space/CEV-Eris/raw/f7aa28fd4b4d0386c3393d829681ebca526f1d2d/icons/obj/drinks.dmi", "size": { "x": 32, "y": 32 }, "states": [ + { + "name": "ramen" + }, { "name": "boritos" }, diff --git a/Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/icon.png b/Resources/Textures/Objects/Consumable/Food/snacks.rsi/ramen.png similarity index 100% rename from Resources/Textures/Objects/Consumable/Drinks/ramen.rsi/icon.png rename to Resources/Textures/Objects/Consumable/Food/snacks.rsi/ramen.png From 7994175aba4535dd6defc3f1f660ba6891d4b7e1 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 12:35:52 +0000 Subject: [PATCH 057/109] 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 53c12e78f73..a9c24ddfeb3 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: osjarw - changes: - - message: Air injectors finally have working indicator lights while enabled. - type: Fix - id: 6291 - time: '2024-04-02T05:17:26.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26654 - author: Simyon changes: - message: The hands of cyborgs are now explosion proof. Your beakers wont ever @@ -3842,3 +3835,10 @@ id: 6790 time: '2024-06-20T12:22:04.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29227 +- author: Redfire1331 + changes: + - message: You can now eat cup ramen with chopsticks. + type: Tweak + id: 6791 + time: '2024-06-20T12:34:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/27826 From d15eafe13301960ac698668e56ac1ebcb5c24609 Mon Sep 17 00:00:00 2001 From: Errant <35878406+Errant-4@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:05:01 +0200 Subject: [PATCH 058/109] Survival Box Loadouts, Nitrogen Emergency tanks (#29131) * Nitrogen survival boxes * Zero-setup workaround * clown box * cleanup and universal tanks * cleanup * more cleanup * hide loadoutgroups * remaining survival boxes * space ninja * Revert "space ninja" This reverts commit a650f417f83f5861e0001b25418e4a361927c6c6. * weh * weh * undo appearance change of syndicate survival boxes * indentation fix and missing label --- .../en-US/preferences/loadout-groups.ftl | 6 + .../Catalog/Fills/Boxes/emergency.yml | 213 +++++++++++++----- .../Loadouts/Miscellaneous/survival.yml | 182 +++++++++++++++ .../Prototypes/Loadouts/loadout_groups.yml | 61 +++++ .../Prototypes/Loadouts/role_loadouts.yml | 70 ++++++ .../Roles/Jobs/Cargo/cargo_technician.yml | 6 +- .../Roles/Jobs/Cargo/quartermaster.yml | 3 +- .../Roles/Jobs/Cargo/salvage_specialist.yml | 6 +- .../Roles/Jobs/Civilian/assistant.yml | 6 +- .../Roles/Jobs/Civilian/bartender.yml | 6 +- .../Roles/Jobs/Civilian/botanist.yml | 6 +- .../Roles/Jobs/Civilian/chaplain.yml | 1 - .../Prototypes/Roles/Jobs/Civilian/chef.yml | 6 +- .../Prototypes/Roles/Jobs/Civilian/clown.yml | 3 +- .../Roles/Jobs/Civilian/janitor.yml | 6 +- .../Prototypes/Roles/Jobs/Civilian/lawyer.yml | 1 - .../Roles/Jobs/Civilian/librarian.yml | 1 - .../Prototypes/Roles/Jobs/Civilian/mime.yml | 1 - .../Roles/Jobs/Civilian/musician.yml | 6 +- .../Roles/Jobs/Civilian/service_worker.yml | 6 +- .../Prototypes/Roles/Jobs/Command/captain.yml | 3 +- .../Roles/Jobs/Command/head_of_personnel.yml | 3 +- .../Engineering/atmospheric_technician.yml | 6 +- .../Roles/Jobs/Engineering/chief_engineer.yml | 3 +- .../Jobs/Engineering/station_engineer.yml | 6 +- .../Jobs/Engineering/technical_assistant.yml | 6 +- .../Roles/Jobs/Fun/emergencyresponseteam.yml | 6 +- .../Roles/Jobs/Fun/wizard_startinggear.yml | 2 +- .../Prototypes/Roles/Jobs/Medical/chemist.yml | 6 +- .../Jobs/Medical/chief_medical_officer.yml | 1 - .../Roles/Jobs/Medical/medical_doctor.yml | 6 +- .../Roles/Jobs/Medical/medical_intern.yml | 6 +- .../Roles/Jobs/Medical/paramedic.yml | 3 +- .../Roles/Jobs/Science/research_assistant.yml | 6 +- .../Roles/Jobs/Science/research_director.yml | 3 +- .../Roles/Jobs/Science/scientist.yml | 6 +- .../Roles/Jobs/Security/detective.yml | 3 +- .../Roles/Jobs/Security/head_of_security.yml | 3 +- .../Roles/Jobs/Security/security_cadet.yml | 3 +- .../Roles/Jobs/Security/security_officer.yml | 3 +- .../Prototypes/Roles/Jobs/Security/warden.yml | 3 +- .../Prototypes/Roles/Jobs/Wildcards/boxer.yml | 6 +- .../Roles/Jobs/Wildcards/psychologist.yml | 6 +- .../Roles/Jobs/Wildcards/reporter.yml | 6 +- .../Roles/Jobs/Wildcards/zookeeper.yml | 6 +- .../Objects/Storage/boxes.rsi/meta.json | 5 +- .../Storage/boxes.rsi/nitrogentank.png | Bin 0 -> 214 bytes 47 files changed, 560 insertions(+), 152 deletions(-) create mode 100644 Resources/Prototypes/Loadouts/Miscellaneous/survival.yml create mode 100644 Resources/Textures/Objects/Storage/boxes.rsi/nitrogentank.png diff --git a/Resources/Locale/en-US/preferences/loadout-groups.ftl b/Resources/Locale/en-US/preferences/loadout-groups.ftl index ef0937c9109..793a11d690f 100644 --- a/Resources/Locale/en-US/preferences/loadout-groups.ftl +++ b/Resources/Locale/en-US/preferences/loadout-groups.ftl @@ -3,6 +3,12 @@ loadout-group-trinkets = Trinkets loadout-group-glasses = Glasses loadout-group-backpack = Backpack loadout-group-instruments = Instruments +loadout-group-survival-basic = Survival Box +loadout-group-survival-extended = Extended Survival Box +loadout-group-survival-clown = Clown Survival Box +loadout-group-survival-medical = Medical Survival Box +loadout-group-survival-security = Security Survival Box +loadout-group-survival-syndicate = Github is forcing me to write text that is literally twice-impossible for the player to ever see, send help # Command loadout-group-captain-head = Captain head diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml b/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml index 7e89af3320c..c2b0660dc07 100644 --- a/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml +++ b/Resources/Prototypes/Catalog/Fills/Boxes/emergency.yml @@ -1,122 +1,225 @@ - type: entity - name: survival box parent: BoxCardboard id: BoxSurvival + name: survival box description: It's a box with basic internals inside. + suffix: Standard components: - type: StorageFill contents: - - id: ClothingMaskBreath - - id: EmergencyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick - - id: DrinkWaterBottleFull + - id: ClothingMaskBreath + - id: EmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull - type: Sprite layers: - - state: internals - - state: emergencytank + - state: internals + - state: emergencytank + +- type: entity + parent: BoxSurvival + id: BoxSurvivalNitrogen + suffix: Standard N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + # Intentionally wrong picture on the box. NT did not care enough to change it. + - type: Label + currentLabel: reagent-name-nitrogen - type: entity - name: extended-capacity survival box parent: BoxCardboard id: BoxSurvivalEngineering + name: extended-capacity survival box description: It's a box with basic internals inside. This one is labelled to contain an extended-capacity tank. - suffix: Engineering + suffix: Extended components: - type: StorageFill contents: - - id: ClothingMaskBreath - - id: ExtendedEmergencyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick - - id: DrinkWaterBottleFull + - id: ClothingMaskBreath + - id: ExtendedEmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull - type: Sprite layers: - - state: internals - - state: extendedtank + - state: internals + - state: extendedtank - type: entity - name: survival box + parent: BoxSurvivalEngineering + id: BoxSurvivalEngineeringNitrogen + suffix: Extended N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: ExtendedEmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + # Intentionally wrong picture on the box. NT did not care enough to change it. + - type: Label + currentLabel: reagent-name-nitrogen + +- type: entity + parent: BoxCardboard id: BoxSurvivalSecurity + name: survival box description: It's a box with basic internals inside. suffix: Security components: - type: StorageFill contents: - - id: ClothingMaskGasSecurity - - id: EmergencyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick - - id: DrinkWaterBottleFull + - id: ClothingMaskGasSecurity + - id: EmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull - type: Sprite layers: - - state: internals - - state: emergencytank + - state: internals + - state: emergencytank - type: entity - name: survival box + parent: BoxSurvivalSecurity + id: BoxSurvivalSecurityNitrogen + suffix: Security N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskGasSecurity + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + # Intentionally wrong picture on the box. NT did not care enough to change it. + - type: Label + currentLabel: reagent-name-nitrogen + +- type: entity + parent: BoxCardboard id: BoxSurvivalMedical + name: survival box description: It's a box with basic internals inside. suffix: Medical components: - type: StorageFill contents: - - id: ClothingMaskBreathMedical - - id: EmergencyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick - - id: DrinkWaterBottleFull + - id: ClothingMaskBreathMedical + - id: EmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull - type: Sprite layers: - - state: internals - - state: emergencytank + - state: internals + - state: emergencytank + +- type: entity + parent: BoxSurvivalMedical + id: BoxSurvivalMedicalNitrogen + suffix: Medical N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreathMedical + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + # Intentionally wrong picture on the box. NT did not care enough to change it. + - type: Label + currentLabel: reagent-name-nitrogen - type: entity - name: box of hugs parent: BoxCardboard id: BoxHug + name: box of hugs description: A special box for sensitive people. suffix: Emergency components: - type: Sprite layers: - - state: box_hug - - state: heart + - state: box_hug + - state: heart - type: Item heldPrefix: hug - type: StorageFill contents: - - id: ClothingMaskBreath - - id: EmergencyFunnyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick - - id: DrinkWaterBottleFull + - id: ClothingMaskBreath + - id: EmergencyFunnyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull - type: Tag tags: - - BoxHug + - BoxHug + +- type: entity + parent: BoxHug + id: BoxHugNitrogen + suffix: Emergency N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskBreath + - id: EmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + - id: DrinkWaterBottleFull + - type: Label + currentLabel: reagent-name-nitrogen - type: entity - name: extended-capacity survival box parent: BoxCardboard id: BoxSurvivalSyndicate + name: extended-capacity survival box description: It's a box with basic internals inside. This one is labelled to contain an extended-capacity tank. suffix: Syndicate components: - type: StorageFill contents: - - id: ClothingMaskGasSyndicate - - id: ExtendedEmergencyOxygenTankFilled - - id: EmergencyMedipen - - id: Flare - - id: FoodSnackNutribrick + - id: ClothingMaskGasSyndicate + - id: ExtendedEmergencyOxygenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick - type: Sprite layers: - - state: internals - - state: extendedtank + - state: internals + - state: extendedtank + +- type: entity + parent: BoxSurvivalSyndicate + id: BoxSurvivalSyndicateNitrogen + suffix: Syndicate N2 + components: + - type: StorageFill + contents: + - id: ClothingMaskGasSyndicate + - id: ExtendedEmergencyNitrogenTankFilled + - id: EmergencyMedipen + - id: Flare + - id: FoodSnackNutribrick + # Intentionally wrong picture on the box to mimic the NT one + - type: Label + currentLabel: reagent-name-nitrogen diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/survival.yml b/Resources/Prototypes/Loadouts/Miscellaneous/survival.yml new file mode 100644 index 00000000000..5a9f6a0d07a --- /dev/null +++ b/Resources/Prototypes/Loadouts/Miscellaneous/survival.yml @@ -0,0 +1,182 @@ +# Species +- type: loadoutEffectGroup + id: NitrogenBreather + effects: + - !type:SpeciesLoadoutEffect + species: + - SlimePerson + - Vox + +- type: loadoutEffectGroup + id: OxygenBreather + effects: + - !type:SpeciesLoadoutEffect + species: + - Arachnid + - Diona + - Dwarf + - Human + - Moth + - Reptilian + +# Basic +- type: loadout + id: EmergencyOxygen + equipment: EmergencyOxygen + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygen + storage: + back: + - BoxSurvival + +- type: loadout + id: EmergencyNitrogen + equipment: EmergencyNitrogen + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogen + storage: + back: + - BoxSurvivalNitrogen + +# Clown +- type: loadout + id: EmergencyOxygenClown + equipment: EmergencyOxygenClown + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygenClown + storage: + back: + - BoxHug + +- type: loadout + id: EmergencyNitrogenClown + equipment: EmergencyNitrogenClown + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogenClown + storage: + back: + - BoxHugNitrogen + +# Engineering / Extended +- type: loadout + id: EmergencyOxygenExtended + equipment: EmergencyOxygenExtended + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygenExtended + storage: + back: + - BoxSurvivalEngineering + +- type: loadout + id: EmergencyNitrogenExtended + equipment: EmergencyNitrogenExtended + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogenExtended + storage: + back: + - BoxSurvivalEngineeringNitrogen + +# Medical +- type: loadout + id: EmergencyOxygenMedical + equipment: EmergencyOxygenMedical + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygenMedical + storage: + back: + - BoxSurvivalMedical + +- type: loadout + id: EmergencyNitrogenMedical + equipment: EmergencyNitrogenMedical + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogenMedical + storage: + back: + - BoxSurvivalMedicalNitrogen + +# Security +- type: loadout + id: EmergencyOxygenSecurity + equipment: EmergencyOxygenSecurity + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygenSecurity + storage: + back: + - BoxSurvivalSecurity + +- type: loadout + id: EmergencyNitrogenSecurity + equipment: EmergencyNitrogenSecurity + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogenSecurity + storage: + back: + - BoxSurvivalSecurityNitrogen + +# Syndicate +- type: loadout + id: EmergencyOxygenSyndicate + equipment: EmergencyOxygenSyndicate + effects: + - !type:GroupLoadoutEffect + proto: OxygenBreather + +- type: startingGear + id: EmergencyOxygenSyndicate + storage: + back: + - BoxSurvivalSyndicate + +- type: loadout + id: EmergencyNitrogenSyndicate + equipment: EmergencyNitrogenSyndicate + effects: + - !type:GroupLoadoutEffect + proto: NitrogenBreather + +- type: startingGear + id: EmergencyNitrogenSyndicate + storage: + back: + - BoxSurvivalSyndicateNitrogen diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml index 3d4583a063e..fb25e87f161 100644 --- a/Resources/Prototypes/Loadouts/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/loadout_groups.yml @@ -35,6 +35,16 @@ - GlassesJamjar - GlassesJensen +- type: loadoutGroup + id: Survival + name: loadout-group-survival-basic + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogen + - EmergencyOxygen + # Command - type: loadoutGroup id: CaptainHead @@ -390,6 +400,16 @@ - ClownShoes - JesterShoes +- type: loadoutGroup + id: SurvivalClown + name: loadout-group-survival-clown + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogenClown + - EmergencyOxygenClown + - type: loadoutGroup id: MimeHead name: loadout-group-mime-head @@ -703,6 +723,16 @@ - WhiteShoes - EngineeringWinterBoots +- type: loadoutGroup + id: SurvivalExtended + name: loadout-group-survival-extended + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogenExtended + - EmergencyOxygenExtended + # Science - type: loadoutGroup id: ResearchDirectorHead @@ -973,6 +1003,16 @@ - RedJumpsuit - RedJumpskirt +- type: loadoutGroup + id: SurvivalSecurity + name: loadout-group-survival-security + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogenSecurity + - EmergencyOxygenSecurity + # Medical - type: loadoutGroup id: ChiefMedicalOfficerHead @@ -1145,6 +1185,16 @@ - BlueShoes - MedicalWinterBoots +- type: loadoutGroup + id: SurvivalMedical + name: loadout-group-survival-medical + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogenMedical + - EmergencyOxygenMedical + # Wildcards - type: loadoutGroup id: ReporterJumpsuit @@ -1168,3 +1218,14 @@ - BlueBoxingGloves - GreenBoxingGloves - YellowBoxingGloves + +# Other +- type: loadoutGroup + id: SurvivalSyndicate + name: loadout-group-survival-syndicate + minLimit: 2 + maxLimit: 2 + hidden: true + loadouts: + - EmergencyNitrogenSyndicate + - EmergencyOxygenSyndicate diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml index 5f0ec2b274f..29b81664391 100644 --- a/Resources/Prototypes/Loadouts/role_loadouts.yml +++ b/Resources/Prototypes/Loadouts/role_loadouts.yml @@ -8,6 +8,7 @@ - CaptainBackpack - CaptainOuterClothing - Trinkets + - Survival - type: roleLoadout id: JobHeadOfPersonnel @@ -19,6 +20,7 @@ - HoPOuterClothing - Glasses - Trinkets + - Survival # Civilian - type: roleLoadout @@ -32,6 +34,7 @@ - PassengerShoes - Glasses - Trinkets + - Survival - type: roleLoadout id: JobBartender @@ -42,6 +45,7 @@ - BartenderOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobServiceWorker @@ -50,6 +54,7 @@ - CommonBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobChef @@ -61,6 +66,7 @@ - ChefOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobLibrarian @@ -69,6 +75,7 @@ - CommonBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobLawyer @@ -78,6 +85,7 @@ - CommonBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobChaplain @@ -90,6 +98,7 @@ - ChaplainOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobJanitor @@ -101,6 +110,7 @@ - JanitorOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobBotanist @@ -111,6 +121,7 @@ - BotanistOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobClown @@ -122,6 +133,7 @@ - ClownShoes - Glasses - Trinkets + - SurvivalClown - type: roleLoadout id: JobMime @@ -133,6 +145,7 @@ - MimeOuterClothing - Glasses - Trinkets + - Survival - type: roleLoadout id: JobMusician @@ -143,6 +156,7 @@ - Glasses - Trinkets - Instruments + - Survival # Cargo - type: roleLoadout @@ -156,6 +170,7 @@ - QuartermasterShoes - Glasses - Trinkets + - Survival - type: roleLoadout id: JobCargoTechnician @@ -167,6 +182,7 @@ - CargoTechnicianShoes - Glasses - Trinkets + - Survival - type: roleLoadout id: JobSalvageSpecialist @@ -176,6 +192,7 @@ - SalvageSpecialistShoes - Glasses - Trinkets + - Survival # Engineering - type: roleLoadout @@ -188,6 +205,7 @@ - ChiefEngineerOuterClothing - ChiefEngineerShoes - Trinkets + - SurvivalExtended - type: roleLoadout id: JobTechnicalAssistant @@ -195,6 +213,7 @@ - TechnicalAssistantJumpsuit - StationEngineerBackpack - Trinkets + - SurvivalExtended - type: roleLoadout id: JobStationEngineer @@ -206,6 +225,7 @@ - StationEngineerShoes - StationEngineerID - Trinkets + - SurvivalExtended - type: roleLoadout id: JobAtmosphericTechnician @@ -215,6 +235,7 @@ - AtmosphericTechnicianOuterClothing - AtmosphericTechnicianShoes - Trinkets + - SurvivalExtended # Science - type: roleLoadout @@ -229,6 +250,7 @@ - ResearchDirectorShoes - Glasses - Trinkets + - Survival - type: roleLoadout id: JobScientist @@ -243,6 +265,7 @@ - ScientistPDA - Glasses - Trinkets + - Survival - type: roleLoadout id: JobResearchAssistant @@ -251,6 +274,7 @@ - ScientistBackpack - Glasses - Trinkets + - Survival # Security - type: roleLoadout @@ -264,6 +288,7 @@ - HeadofSecurityOuterClothing - SecurityShoes - Trinkets + - SurvivalSecurity - type: roleLoadout id: JobWarden @@ -275,6 +300,7 @@ - WardenOuterClothing - SecurityShoes - Trinkets + - SurvivalSecurity - type: roleLoadout id: JobSecurityOfficer @@ -287,6 +313,7 @@ - SecurityPDA - SecurityBelt - Trinkets + - SurvivalSecurity - type: roleLoadout id: JobDetective @@ -298,6 +325,7 @@ - DetectiveOuterClothing - SecurityShoes - Trinkets + - SurvivalSecurity - type: roleLoadout id: JobSecurityCadet @@ -305,6 +333,7 @@ - SecurityCadetJumpsuit - SecurityBackpack - Trinkets + - SurvivalSecurity # Medical - type: roleLoadout @@ -320,6 +349,7 @@ - ChiefMedicalOfficerShoes - Glasses - Trinkets + - SurvivalMedical - type: roleLoadout id: JobMedicalDoctor @@ -334,6 +364,7 @@ - MedicalDoctorPDA - Glasses - Trinkets + - SurvivalMedical - type: roleLoadout id: JobMedicalIntern @@ -342,6 +373,7 @@ - MedicalBackpack - Glasses - Trinkets + - SurvivalMedical - type: roleLoadout id: JobChemist @@ -353,6 +385,7 @@ - ChemistOuterClothing - MedicalShoes - Trinkets + - SurvivalMedical - type: roleLoadout id: JobParamedic @@ -366,6 +399,7 @@ - ParamedicShoes - Glasses - Trinkets + - SurvivalMedical # Wildcards - type: roleLoadout @@ -374,6 +408,7 @@ - CommonBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobReporter @@ -382,6 +417,7 @@ - CommonBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobPsychologist @@ -389,6 +425,7 @@ - MedicalBackpack - Glasses - Trinkets + - Survival - type: roleLoadout id: JobBoxer @@ -398,3 +435,36 @@ - CommonBackpack - Glasses - Trinkets + - Survival + +# These loadouts will be used without player configuration, thus they must be designed to work without manual selection + +- type: roleLoadout + id: LoadoutSurvivalStandard + groups: + - Survival + +- type: roleLoadout + id: LoadoutSurvivalClown + groups: + - SurvivalClown + +- type: roleLoadout + id: LoadoutSurvivalExtended + groups: + - SurvivalExtended + +- type: roleLoadout + id: LoadoutSurvivalMedical + groups: + - SurvivalMedical + +- type: roleLoadout + id: LoadoutSurvivalSecurity + groups: + - SurvivalSecurity + +- type: roleLoadout + id: LoadoutSurvivalSyndicate + groups: + - SurvivalSyndicate diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml b/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml index 78ab1847a45..655e103bca2 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml @@ -18,6 +18,6 @@ id: CargoPDA ears: ClothingHeadsetCargo pocket1: AppraisalTool - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml index e39c327e903..515e2f8425a 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml @@ -44,5 +44,4 @@ pocket1: AppraisalTool storage: back: - - BoxSurvival - - Flash \ No newline at end of file + - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml index 4dd20f51fb0..679b39f05bf 100644 --- a/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml +++ b/Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml @@ -24,6 +24,6 @@ jumpsuit: ClothingUniformJumpsuitSalvageSpecialist id: SalvagePDA ears: ClothingHeadsetCargo - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml b/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml index 06b66448cb8..5155c8ca00e 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml @@ -14,6 +14,6 @@ equipment: id: PassengerPDA ears: ClothingHeadsetGrey - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml index 6bceebc65ea..2d035ee3b45 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml @@ -24,6 +24,6 @@ shoes: ClothingShoesColorBlack id: BartenderPDA ears: ClothingHeadsetService - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml index e8cda189145..dd25a2abe2e 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml @@ -21,6 +21,6 @@ id: BotanistPDA ears: ClothingHeadsetService belt: ClothingBeltPlantFilled - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml index 0ae49f853c6..1ab77dbae58 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml @@ -22,6 +22,5 @@ ears: ClothingHeadsetService storage: back: - - BoxSurvival - Bible - RubberStampChaplain diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml index 0d8d1911e56..5abb1d4c8ed 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/chef.yml @@ -25,6 +25,6 @@ id: ChefPDA ears: ClothingHeadsetService belt: ClothingBeltChefFilled - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml index 627469f6c55..29232d9dbb5 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/clown.yml @@ -35,6 +35,5 @@ ears: ClothingHeadsetService storage: back: - - BoxHug - RubberStampClown - - CrayonRainbow \ No newline at end of file + - CrayonRainbow diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml index 83fd9ae5d2e..d361348a6e0 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml @@ -22,9 +22,9 @@ id: JanitorPDA ears: ClothingHeadsetService belt: ClothingBeltJanitorFilled - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff - type: startingGear id: JanitorMaidGear diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml index 3ec0eba51ca..58a8ebece11 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml @@ -26,5 +26,4 @@ - BriefcaseBrownFilled storage: back: - - BoxSurvival - RubberStampLawyer diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml b/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml index f5826b76bfb..d865d57cab3 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml @@ -20,5 +20,4 @@ pocket2: HandLabeler # for making named bestsellers storage: back: - - BoxSurvival - BookRandom diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml index b7e932acb4f..bfdf92b5dd5 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml @@ -30,7 +30,6 @@ ears: ClothingHeadsetService storage: back: - - BoxSurvival - RubberStampMime - type: entity diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml b/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml index 5d01cd4938c..b33525d2054 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/musician.yml @@ -21,6 +21,6 @@ shoes: ClothingShoesBootsLaceup id: MusicianPDA ears: ClothingHeadsetService - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml index efda20712fb..e883f4cc669 100644 --- a/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml +++ b/Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml @@ -21,6 +21,6 @@ shoes: ClothingShoesColorBlack id: ServiceWorkerPDA ears: ClothingHeadsetService - storage: - back: - - BoxSurvival + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index 1e93ff73cce..79634aa5d9f 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -42,6 +42,5 @@ ears: ClothingHeadsetAltCommand storage: back: - - BoxSurvival - Flash - # - StationCharter \ No newline at end of file + # - StationCharter diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml index 72a862f2648..d5521f767f1 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -66,5 +66,4 @@ belt: BoxFolderClipboard storage: back: - - BoxSurvival - - Flash \ No newline at end of file + - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml index 49a89786337..9810b485713 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml @@ -23,6 +23,6 @@ id: AtmosPDA belt: ClothingBeltUtilityEngineering ears: ClothingHeadsetEngineering - storage: - back: - - BoxSurvivalEngineering \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml index 8cc934d634a..0ee0b6736ca 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml @@ -46,5 +46,4 @@ belt: ClothingBeltUtilityEngineering storage: back: - - BoxSurvivalEngineering - - Flash \ No newline at end of file + - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml index 1bff9257bba..0bd81e65e02 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml @@ -23,6 +23,6 @@ eyes: ClothingEyesGlassesMeson belt: ClothingBeltUtilityEngineering ears: ClothingHeadsetEngineering - storage: - back: - - BoxSurvivalEngineering \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml index 8bbaf2a3903..d0d3cfe7861 100644 --- a/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml @@ -27,6 +27,6 @@ belt: ClothingBeltUtilityEngineering ears: ClothingHeadsetEngineering pocket2: BookEngineersHandbook - storage: - back: - - BoxSurvivalEngineering \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Fun/emergencyresponseteam.yml b/Resources/Prototypes/Roles/Jobs/Fun/emergencyresponseteam.yml index 337b7e1de91..011ac8812ba 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/emergencyresponseteam.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/emergencyresponseteam.yml @@ -380,7 +380,7 @@ pocket1: Flare storage: back: - - BoxSurvivalMedical + - BoxSurvivalMedical # Is this actually supposed to be Medical? All other ERT has Extended - Hypospray - MedkitAdvancedFilled - CrowbarRed @@ -405,7 +405,7 @@ pocket1: Flare storage: back: - - BoxSurvivalMedical + - BoxSurvivalMedical # Is this actually supposed to be Medical? All other ERT has Extended - Hypospray - MedkitAdvancedFilled - CrowbarRed @@ -474,4 +474,4 @@ - BoxLightMixed - Soap - CrowbarRed - - AdvMopItem \ No newline at end of file + - AdvMopItem diff --git a/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml index d053d8da1cc..a4e5e3c3ab8 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml @@ -32,4 +32,4 @@ id: WizardHardsuitGear parent: WizardVioletGear equipment: - outerClothing: ClothingOuterHardsuitWizard \ No newline at end of file + outerClothing: ClothingOuterHardsuitWizard diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml index fefd0110acc..dec2b6273eb 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chemist.yml @@ -23,6 +23,6 @@ belt: ChemBag pocket1: HandLabeler eyes: ClothingEyesGlassesChemical - storage: - back: - - BoxSurvivalMedical \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml index 3f6bd5e4529..25871134155 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml @@ -46,5 +46,4 @@ belt: ClothingBeltMedicalFilled storage: back: - - BoxSurvivalMedical - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml index be058518a68..11a8d101888 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml @@ -21,6 +21,6 @@ equipment: ears: ClothingHeadsetMedical belt: ClothingBeltMedicalFilled - storage: - back: - - BoxSurvivalMedical \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml index 3fe26e6056b..41808558fdf 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml @@ -24,6 +24,6 @@ ears: ClothingHeadsetMedical belt: ClothingBeltMedicalFilled pocket2: BookMedicalReferenceBook - storage: - back: - - BoxSurvivalMedical \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml index 242fac8d0c0..166f9ac42b7 100644 --- a/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml +++ b/Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml @@ -26,5 +26,4 @@ belt: ClothingBeltMedicalEMTFilled storage: back: - - BoxSurvivalMedical - - EmergencyRollerBedSpawnFolded \ No newline at end of file + - EmergencyRollerBedSpawnFolded diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml index aec2dd6fff3..c421c09b8ab 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml @@ -23,6 +23,6 @@ id: ResearchAssistantPDA ears: ClothingHeadsetScience pocket2: BookScientistsGuidebook - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml index fd6e00a8e3f..b54ba54b1a4 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/research_director.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/research_director.yml @@ -36,5 +36,4 @@ ears: ClothingHeadsetRD storage: back: - - BoxSurvival - - Flash \ No newline at end of file + - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml index b2e63fead8a..8bb99eab3c0 100644 --- a/Resources/Prototypes/Roles/Jobs/Science/scientist.yml +++ b/Resources/Prototypes/Roles/Jobs/Science/scientist.yml @@ -18,6 +18,6 @@ id: ScientistGear equipment: ears: ClothingHeadsetScience - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Security/detective.yml b/Resources/Prototypes/Roles/Jobs/Security/detective.yml index 47258fa00bf..22ef115d7cd 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/detective.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/detective.yml @@ -30,7 +30,6 @@ belt: ClothingBeltHolsterFilled storage: back: - - BoxSurvivalSecurity - Flash - ForensicPad - - ForensicScanner \ No newline at end of file + - ForensicScanner diff --git a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml index aac7c7b3508..044df7f69e2 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml @@ -49,6 +49,5 @@ pocket1: WeaponPistolMk58 storage: back: - - BoxSurvivalSecurity - Flash - - MagazinePistol \ No newline at end of file + - MagazinePistol diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml index b68336c9530..4561d1adb0e 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml @@ -37,6 +37,5 @@ pocket2: BookSecurity storage: back: - - BoxSurvivalSecurity - Flash - - MagazinePistol \ No newline at end of file + - MagazinePistol diff --git a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml index c773396601d..dd2f82d6669 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/security_officer.yml @@ -30,6 +30,5 @@ pocket1: WeaponPistolMk58 storage: back: - - BoxSurvivalSecurity - Flash - - MagazinePistol \ No newline at end of file + - MagazinePistol diff --git a/Resources/Prototypes/Roles/Jobs/Security/warden.yml b/Resources/Prototypes/Roles/Jobs/Security/warden.yml index 7f157525767..6d79d748049 100644 --- a/Resources/Prototypes/Roles/Jobs/Security/warden.yml +++ b/Resources/Prototypes/Roles/Jobs/Security/warden.yml @@ -33,6 +33,5 @@ pocket1: WeaponPistolMk58 storage: back: - - BoxSurvivalSecurity - Flash - - MagazinePistol \ No newline at end of file + - MagazinePistol diff --git a/Resources/Prototypes/Roles/Jobs/Wildcards/boxer.yml b/Resources/Prototypes/Roles/Jobs/Wildcards/boxer.yml index aceb7a10f6a..e2f8de96093 100644 --- a/Resources/Prototypes/Roles/Jobs/Wildcards/boxer.yml +++ b/Resources/Prototypes/Roles/Jobs/Wildcards/boxer.yml @@ -17,6 +17,6 @@ ears: ClothingHeadsetService shoes: ClothingShoesColorRed belt: ClothingBeltChampion - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml b/Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml index 17bed0ee7f6..5414430bf54 100644 --- a/Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml +++ b/Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml @@ -19,6 +19,6 @@ shoes: ClothingShoesLeather id: PsychologistPDA ears: ClothingHeadsetMedical - storage: - back: - - BoxSurvivalMedical \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Wildcards/reporter.yml b/Resources/Prototypes/Roles/Jobs/Wildcards/reporter.yml index b379a0ec235..d0a35990d70 100644 --- a/Resources/Prototypes/Roles/Jobs/Wildcards/reporter.yml +++ b/Resources/Prototypes/Roles/Jobs/Wildcards/reporter.yml @@ -16,6 +16,6 @@ shoes: ClothingShoesColorWhite id: ReporterPDA ears: ClothingHeadsetService - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml b/Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml index 815ead412c7..32c98ed4871 100644 --- a/Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml +++ b/Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml @@ -18,6 +18,6 @@ shoes: ClothingShoesColorWhite id: ZookeeperPDA ears: ClothingHeadsetService - storage: - back: - - BoxSurvival \ No newline at end of file + #storage: + #back: + #- Stuff diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json index f30d9c9ae72..6963a9cdad0 100644 --- a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json +++ b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef.", + "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef, nitrogentank modified from extendedtank by Errant", "size": { "x": 32, "y": 32 @@ -25,6 +25,9 @@ { "name": "emergencytank" }, + { + "name": "nitrogentank" + }, { "name": "heart" }, diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/nitrogentank.png b/Resources/Textures/Objects/Storage/boxes.rsi/nitrogentank.png new file mode 100644 index 0000000000000000000000000000000000000000..e179be543c2742e60684c74a5870df0e809cf14d GIT binary patch literal 214 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?3oArNM~bhqvgP_V<( z#W5tpJvl*wb+Mv>6cZYuY|A2{$;%M0zy800v046Yw@sBQ*$k1>UL>$?7dc|TUuWJy zgDl0v(t3<+L9z#(vN#%a=4Y(nyI%JCA$Qwe!)%E@23>Xb32z#HviBL@`}vJqmBm3! t{$6>TprzG`!ZR!8T@;YuZJa2{#E@PiXME1@Pan`t44$rjF6*2UngBzCK>q*$ literal 0 HcmV?d00001 From cf374ac905149dc10c15ecc2167031fcfa1163a5 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Thu, 20 Jun 2024 16:05:40 +0200 Subject: [PATCH 059/109] You can now pry multiple tiles at once (#29231) * You can now pry multiple tiles at once * More advanced do after duplicate checking. Instead of just saying "lol tile prying can raise duplicates", we now have a system so tile prying can properly distinguish events on 2 different tiles. This is achieved with a virtual function on DoAfterEvent. --- Content.Shared/DoAfter/DoAfterEvent.cs | 8 ++++++++ Content.Shared/DoAfter/SharedDoAfterSystem.cs | 2 +- .../Components/ToolTileCompatibleComponent.cs | 15 ++++++++++++--- .../Tools/Systems/SharedToolSystem.Tile.cs | 14 +++++++------- Content.Shared/Tools/Systems/SharedToolSystem.cs | 5 +++++ 5 files changed, 33 insertions(+), 11 deletions(-) diff --git a/Content.Shared/DoAfter/DoAfterEvent.cs b/Content.Shared/DoAfter/DoAfterEvent.cs index bc9abdab87b..5affbe74850 100644 --- a/Content.Shared/DoAfter/DoAfterEvent.cs +++ b/Content.Shared/DoAfter/DoAfterEvent.cs @@ -34,6 +34,14 @@ public abstract partial class DoAfterEvent : HandledEntityEventArgs public EntityUid? Used => DoAfter.Args.Used; public DoAfterArgs Args => DoAfter.Args; #endregion + + /// + /// Check whether this event is "the same" as another event for duplicate checking. + /// + public virtual bool IsDuplicate(DoAfterEvent other) + { + return GetType() == other.GetType(); + } } /// diff --git a/Content.Shared/DoAfter/SharedDoAfterSystem.cs b/Content.Shared/DoAfter/SharedDoAfterSystem.cs index 9ad649683dd..77b47415333 100644 --- a/Content.Shared/DoAfter/SharedDoAfterSystem.cs +++ b/Content.Shared/DoAfter/SharedDoAfterSystem.cs @@ -310,7 +310,7 @@ private bool IsDuplicate(DoAfterArgs args, DoAfterArgs otherArgs, DuplicateCondi } if ((conditions & DuplicateConditions.SameEvent) != 0 - && args.Event.GetType() != otherArgs.Event.GetType()) + && !args.Event.IsDuplicate(otherArgs.Event)) { return false; } diff --git a/Content.Shared/Tools/Components/ToolTileCompatibleComponent.cs b/Content.Shared/Tools/Components/ToolTileCompatibleComponent.cs index caac41a3def..57058a5781a 100644 --- a/Content.Shared/Tools/Components/ToolTileCompatibleComponent.cs +++ b/Content.Shared/Tools/Components/ToolTileCompatibleComponent.cs @@ -30,15 +30,24 @@ public sealed partial class ToolTileCompatibleComponent : Component [Serializable, NetSerializable] public sealed partial class TileToolDoAfterEvent : DoAfterEvent { - public NetCoordinates Coordinates; + public NetEntity Grid; + public Vector2i GridTile; - public TileToolDoAfterEvent(NetCoordinates coordinates) + public TileToolDoAfterEvent(NetEntity grid, Vector2i gridTile) { - Coordinates = coordinates; + Grid = grid; + GridTile = gridTile; } public override DoAfterEvent Clone() { return this; } + + public override bool IsDuplicate(DoAfterEvent other) + { + return other is TileToolDoAfterEvent otherTile + && Grid == otherTile.Grid + && GridTile == otherTile.GridTile; + } } diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs b/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs index 4ccdb2ef494..f392b317406 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs @@ -37,24 +37,24 @@ private void OnToolTileComplete(Entity ent, ref Til if (!TryComp(ent, out var tool)) return; - var coordinates = GetCoordinates(args.Coordinates); - var gridUid = coordinates.GetGridUid(EntityManager); + var gridUid = GetEntity(args.Grid); if (!TryComp(gridUid, out var grid)) { Log.Error("Attempted use tool on a non-existent grid?"); return; } - var tileRef = _maps.GetTileRef(gridUid.Value, grid, coordinates); - if (comp.RequiresUnobstructed && _turfs.IsTileBlocked(gridUid.Value, tileRef.GridIndices, CollisionGroup.MobMask)) + var tileRef = _maps.GetTileRef(gridUid, grid, args.GridTile); + var coords = _maps.ToCoordinates(tileRef, grid); + if (comp.RequiresUnobstructed && _turfs.IsTileBlocked(gridUid, tileRef.GridIndices, CollisionGroup.MobMask)) return; if (!TryDeconstructWithToolQualities(tileRef, tool.Qualities)) return; AdminLogger.Add(LogType.LatticeCut, LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {args.Coordinates}"); + $"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {coords}"); args.Handled = true; } @@ -66,7 +66,7 @@ private bool UseToolOnTile(Entity var comp = ent.Comp1!; var tool = ent.Comp2!; - if (!_mapManager.TryFindGridAt(clickLocation.ToMap(EntityManager, _transformSystem), out var gridUid, out var mapGrid)) + if (!_mapManager.TryFindGridAt(_transformSystem.ToMapCoordinates(clickLocation), out var gridUid, out var mapGrid)) return false; var tileRef = _maps.GetTileRef(gridUid, mapGrid, clickLocation); @@ -85,7 +85,7 @@ private bool UseToolOnTile(Entity if (!InteractionSystem.InRangeUnobstructed(user, coordinates, popup: false)) return false; - var args = new TileToolDoAfterEvent(GetNetCoordinates(coordinates)); + var args = new TileToolDoAfterEvent(GetNetEntity(gridUid), tileRef.GridIndices); UseTool(ent, user, ent, comp.Delay, tool.Qualities, args, out _, toolComponent: tool); return true; } diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.cs b/Content.Shared/Tools/Systems/SharedToolSystem.cs index 72e984fd82a..56ce81413fb 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.cs @@ -271,6 +271,11 @@ public override DoAfterEvent Clone() return new ToolDoAfterEvent(Fuel, evClone, OriginalTarget); } + + public override bool IsDuplicate(DoAfterEvent other) + { + return other is ToolDoAfterEvent toolDoAfter && WrappedEvent.IsDuplicate(toolDoAfter.WrappedEvent); + } } [Serializable, NetSerializable] From c91789a78de1923f29886b5af6169c16c6eee5f4 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 14:06:47 +0000 Subject: [PATCH 060/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index a9c24ddfeb3..272f93e89d7 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,19 +1,4 @@ Entries: -- author: Simyon - changes: - - message: The hands of cyborgs are now explosion proof. Your beakers wont ever - break again! - type: Fix - id: 6292 - time: '2024-04-02T05:18:31.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26515 -- author: lzk228 - changes: - - message: Typing indicator now can be shaded by shadows. - type: Tweak - id: 6293 - time: '2024-04-03T02:12:47.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26678 - author: EmoGarbage404 changes: - message: Increased time between pulses for various anomalies. @@ -3842,3 +3827,18 @@ id: 6791 time: '2024-06-20T12:34:46.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/27826 +- author: Errant + changes: + - message: Slimepeople crewmembers now spawn with survival boxes that contain emergency + nitrogen tanks. + type: Tweak + id: 6792 + time: '2024-06-20T14:05:01.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29131 +- author: PJB3005 + changes: + - message: You can now pry multiple tiles at once. + type: Tweak + id: 6793 + time: '2024-06-20T14:05:40.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29231 From 50146d4b98e6a14eba3a4b2df0a436ed31ae199b Mon Sep 17 00:00:00 2001 From: MilenVolf <63782763+MilenVolf@users.noreply.github.com> Date: Thu, 20 Jun 2024 17:19:22 +0300 Subject: [PATCH 061/109] Fix prying speed & log (#29210) * cleanup prototypes with `PryingComponent` & fix jaws of life prying speed * Minor cleanup for tools and prying systems Remove some obsolete methods. * Fix doafter continues when not held & log * Modifiy delays for floor prying * Fix test fail --- .../Prying/Components/PryingComponent.cs | 14 +++++++------- Content.Shared/Prying/Systems/PryingSystem.cs | 15 +++++++-------- .../Tools/Components/ToolForcePoweredComponent.cs | 10 ---------- .../Systems/SharedToolSystem.MultipleTool.cs | 11 ++--------- .../Tools/Systems/SharedToolSystem.Tile.cs | 6 ++++-- .../Entities/Debugging/spanisharmyknife.yml | 2 -- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 13 +++---------- Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml | 11 +++-------- .../Entities/Objects/Tools/cowtools.yml | 5 ++--- .../Entities/Objects/Tools/jaws_of_life.yml | 8 +++----- .../Entities/Objects/Weapons/Melee/armblade.yml | 3 --- .../Entities/Objects/Weapons/Melee/mining.yml | 3 --- 12 files changed, 31 insertions(+), 70 deletions(-) delete mode 100644 Content.Shared/Tools/Components/ToolForcePoweredComponent.cs diff --git a/Content.Shared/Prying/Components/PryingComponent.cs b/Content.Shared/Prying/Components/PryingComponent.cs index 7a7f304d8f8..6651e2b5744 100644 --- a/Content.Shared/Prying/Components/PryingComponent.cs +++ b/Content.Shared/Prying/Components/PryingComponent.cs @@ -9,32 +9,32 @@ public sealed partial class PryingComponent : Component /// /// Whether the entity can pry open powered doors /// - [DataField("pryPowered")] - public bool PryPowered = false; + [DataField] + public bool PryPowered; /// /// Whether the tool can bypass certain restrictions when prying. /// For example door bolts. /// - [DataField("force")] - public bool Force = false; + [DataField] + public bool Force; /// /// Modifier on the prying time. /// Lower values result in more time. /// - [DataField("speedModifier")] + [DataField] public float SpeedModifier = 1.0f; /// /// What sound to play when prying is finished. /// - [DataField("useSound")] + [DataField] public SoundSpecifier UseSound = new SoundPathSpecifier("/Audio/Items/crowbar.ogg"); /// /// Whether the entity can currently pry things. /// - [DataField("enabled")] + [DataField] public bool Enabled = true; } diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index 372c89c9ae0..52459c4f744 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -21,7 +21,7 @@ public sealed class PryingSystem : EntitySystem [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; - [Dependency] private readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; public override void Initialize() { @@ -46,7 +46,7 @@ private void OnDoorAltVerb(EntityUid uid, DoorComponent component, GetVerbsEvent if (!args.CanInteract || !args.CanAccess) return; - if (!TryComp(args.User, out var tool)) + if (!TryComp(args.User, out _)) return; args.Verbs.Add(new AlternativeVerb() @@ -74,7 +74,7 @@ public bool TryPry(EntityUid target, EntityUid user, out DoAfterId? id, EntityUi if (!CanPry(target, user, out var message, comp)) { if (!string.IsNullOrWhiteSpace(message)) - Popup.PopupClient(Loc.GetString(message), target, user); + _popup.PopupClient(Loc.GetString(message), target, user); // If we have reached this point we want the event that caused this // to be marked as handled. return true; @@ -138,9 +138,10 @@ private bool StartPry(EntityUid target, EntityUid user, EntityUid? tool, float t { BreakOnDamage = true, BreakOnMove = true, + NeedHand = tool != user, }; - if (tool != null) + if (tool != user && tool != null) { _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user)} is using {ToPrettyString(tool.Value)} to pry {ToPrettyString(target)}"); } @@ -163,7 +164,7 @@ private void OnDoAfter(EntityUid uid, DoorComponent door, DoorPryDoAfterEvent ar if (!CanPry(uid, args.User, out var message, comp)) { if (!string.IsNullOrWhiteSpace(message)) - Popup.PopupClient(Loc.GetString(message), uid, args.User); + _popup.PopupClient(Loc.GetString(message), uid, args.User); return; } @@ -178,6 +179,4 @@ private void OnDoAfter(EntityUid uid, DoorComponent door, DoorPryDoAfterEvent ar } [Serializable, NetSerializable] -public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent -{ -} +public sealed partial class DoorPryDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Tools/Components/ToolForcePoweredComponent.cs b/Content.Shared/Tools/Components/ToolForcePoweredComponent.cs deleted file mode 100644 index d38034c7540..00000000000 --- a/Content.Shared/Tools/Components/ToolForcePoweredComponent.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Content.Shared.Tools.Components -{ - /// - /// Tag component to let a tool ignore restrictions on whether devices are powered - /// or not to work. - /// - [RegisterComponent] - public sealed partial class ToolForcePoweredComponent : Component - {} -} diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs index d69f01d762f..595ef8a9252 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs @@ -70,16 +70,9 @@ public virtual void SetMultipleTool(EntityUid uid, tool.Qualities = current.Behavior; // TODO: Replace this with a better solution later - if (TryComp(uid, out var pcomp)) + if (TryComp(uid, out var pryComp)) { - if (current.Behavior.Contains("Prying")) - { - pcomp.Enabled = true; - } - else - { - pcomp.Enabled = false; - } + pryComp.Enabled = current.Behavior.Contains("Prying"); } if (playSound && current.ChangeSound != null) diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs b/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs index f392b317406..6fa4fac4b3a 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.Tile.cs @@ -53,8 +53,10 @@ private void OnToolTileComplete(Entity ent, ref Til if (!TryDeconstructWithToolQualities(tileRef, tool.Qualities)) return; - AdminLogger.Add(LogType.LatticeCut, LogImpact.Medium, - $"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {coords}"); + AdminLogger.Add( + LogType.LatticeCut, + LogImpact.Medium, + $"{ToPrettyString(args.User):player} used {ToPrettyString(ent)} to edit the tile at {coords}"); args.Handled = true; } diff --git a/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml b/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml index b7fb1188ccd..023ba8c08a8 100644 --- a/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml +++ b/Resources/Prototypes/Entities/Debugging/spanisharmyknife.yml @@ -22,8 +22,6 @@ Slash: 10 - type: ToolTileCompatible - type: Tool - qualities: - - Prying - type: Prying - type: MultipleTool statusShowBehavior: true diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 93d24be5af8..fc9e5151e91 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2265,17 +2265,10 @@ tags: - DoorBumpOpener - FootstepSound - - type: Tool # Open door from xeno.yml. + - type: Prying # Open door from xeno.yml. + pryPowered: true + force: true speedModifier: 1.5 - qualities: - - Prying - useSound: - path: /Audio/Items/crowbar.ogg - - type: Prying - pryPowered: !type:Bool - true - force: !type:Bool - true useSound: path: /Audio/Items/crowbar.ogg - type: PassiveDamage # Slight passive regen. Assuming one damage type, comes out to about 4 damage a minute from base.yml. diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml index 8c262d23da6..9fb0be02aec 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/xeno.yml @@ -21,15 +21,10 @@ true NavSmash: !type:Bool true - - type: Tool - speedModifier: 1.5 - qualities: - - Prying - type: Prying - pryPowered: !type:Bool - true - force: !type:Bool - true + pryPowered: true + force: true + speedModifier: 1.5 useSound: path: /Audio/Items/crowbar.ogg - type: Reactive diff --git a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml index c9b37b8b1ae..2d584bd233c 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/cowtools.yml @@ -91,11 +91,10 @@ - type: Tool qualities: - Prying - useSound: - path: /Audio/Items/crowbar.ogg - speedModifier: 0.05 - type: ToolTileCompatible + delay: 10 - type: Prying + speedModifier: 0.05 - type: entity name: mooltitool diff --git a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml index c80e53870e5..bd4c7aa7b5d 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/jaws_of_life.yml @@ -18,15 +18,15 @@ slots: - Belt - type: ToolTileCompatible + delay: 0.5 - type: Tool qualities: - Prying - speedModifier: 1.5 useSound: /Audio/Items/jaws_pry.ogg - type: Prying + speedModifier: 1.5 pryPowered: true useSound: /Audio/Items/jaws_pry.ogg - - type: ToolForcePowered - type: MultipleTool statusShowBehavior: true entries: @@ -66,9 +66,7 @@ right: - state: syn_inhand-right size: Normal - - type: Tool - qualities: - - Prying + - type: Prying speedModifier: 3.0 - type: MultipleTool entries: diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index 9e1134195d5..49b22c000d5 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -18,7 +18,4 @@ - type: Item size: Normal sprite: Objects/Weapons/Melee/armblade.rsi - - type: Tool - qualities: - - Prying - type: Prying diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml index ccf45bf59aa..4eec90584c8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/mining.yml @@ -62,9 +62,6 @@ - type: Item size: Ginormous - type: DisarmMalus - - type: Tool - qualities: - - Prying - type: Prying - type: entity From 0a3b221766635e469a7ad2030c2073a0c7ebf5b7 Mon Sep 17 00:00:00 2001 From: PJBot Date: Thu, 20 Jun 2024 14:20:29 +0000 Subject: [PATCH 062/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 272f93e89d7..9a5de7a56f9 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: EmoGarbage404 - changes: - - message: Increased time between pulses for various anomalies. - type: Tweak - id: 6294 - time: '2024-04-03T03:15:57.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26677 - author: Plykiya changes: - message: Dropped items are no longer rotated to world north. @@ -3842,3 +3835,13 @@ id: 6793 time: '2024-06-20T14:05:40.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29231 +- author: MilenVolf + changes: + - message: Jaws of life, xeno and spiders now use proper prying speed that will + lead into a faster prying. + type: Fix + - message: Jaws of life now pries tiles much faster. + type: Tweak + id: 6794 + time: '2024-06-20T14:19:22.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29210 From b90c6acacd25df65abffbda8edaa14ab4892f93e Mon Sep 17 00:00:00 2001 From: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com> Date: Thu, 20 Jun 2024 10:24:36 -0400 Subject: [PATCH 063/109] golden plunger (#29043) * golden plunger * Add wood material (the handle is still wood) * 52 hours --------- Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> --- .../en-US/preferences/loadout-groups.ftl | 1 + .../Objects/Specific/Janitorial/janitor.yml | 30 ++++++++++++++++++ .../Loadouts/Jobs/Civilian/janitor.yml | 23 ++++++++++++++ .../Prototypes/Loadouts/loadout_groups.yml | 7 ++++ .../Prototypes/Loadouts/role_loadouts.yml | 1 + .../golden_plunger.rsi/equipped-HELMET.png | Bin 0 -> 223 bytes .../Janitorial/golden_plunger.rsi/meta.json | 26 +++++++++++++++ .../plunger-inhand-left.png | Bin 0 -> 265 bytes .../plunger-inhand-right.png | Bin 0 -> 264 bytes .../Janitorial/golden_plunger.rsi/plunger.png | Bin 0 -> 299 bytes 10 files changed, 88 insertions(+) create mode 100644 Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/equipped-HELMET.png create mode 100644 Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/meta.json create mode 100644 Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-left.png create mode 100644 Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-right.png create mode 100644 Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger.png diff --git a/Resources/Locale/en-US/preferences/loadout-groups.ftl b/Resources/Locale/en-US/preferences/loadout-groups.ftl index 793a11d690f..1c28509b2d4 100644 --- a/Resources/Locale/en-US/preferences/loadout-groups.ftl +++ b/Resources/Locale/en-US/preferences/loadout-groups.ftl @@ -54,6 +54,7 @@ loadout-group-janitor-head = Janitor head loadout-group-janitor-jumpsuit = Janitor jumpsuit loadout-group-janitor-gloves = Janitor gloves loadout-group-janitor-outerclothing = Janitor outer clothing +loadout-group-janitor-plunger = Janitor plunger loadout-group-botanist-head = Botanist head loadout-group-botanist-jumpsuit = Botanist jumpsuit diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml index cb5f875204b..f4cc09430a2 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/janitor.yml @@ -248,6 +248,36 @@ Blunt: 3 - type: Plunger +- type: entity + name: golden plunger + id: GoldenPlunger + parent: Plunger + description: A plunger with a plastic suction cup coated in a thin layer of gold given as a token of appreciation for years of service. Still used to unclog drains. + components: + - type: Sprite + sprite: Objects/Specific/Janitorial/golden_plunger.rsi + state: plunger + - type: Item + sprite: Objects/Specific/Janitorial/golden_plunger.rsi + heldPrefix: plunger + - type: Clothing + sprite: Objects/Specific/Janitorial/golden_plunger.rsi + slots: + - HEAD + clothingVisuals: + head: + - state: equipped-HELMET + offset: "0, 0.15625" + - type: PointLight + radius: 1.5 + energy: 0.75 + color: "#ffc20e" + - type: PhysicalComposition + materialComposition: + Plastic: 50 + Gold: 10 + Wood: 25 + - type: weightedRandomEntity id: PlungerLoot weights: diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml index 1964acbb02c..44570421837 100644 --- a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml +++ b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml @@ -1,3 +1,12 @@ +- type: loadoutEffectGroup + id: SeniorJanitorial + effects: + - !type:JobRequirementLoadoutEffect + requirement: + !type:RoleTimeRequirement + role: JobJanitor + time: 187200 #52 hrs (1 hour per week for 1 year) + # Head - type: loadout id: JanitorHead @@ -64,3 +73,17 @@ id: JanitorWintercoat equipment: outerClothing: ClothingOuterWinterJani + +# Misc +- type: loadout + id: JanitorGoldenPlunger + equipment: JanitorGoldenPlunger + effects: + - !type:GroupLoadoutEffect + proto: SeniorJanitorial + +- type: startingGear + id: JanitorGoldenPlunger + storage: + back: + - GoldenPlunger \ No newline at end of file diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml index fb25e87f161..530022424ec 100644 --- a/Resources/Prototypes/Loadouts/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/loadout_groups.yml @@ -331,6 +331,13 @@ loadouts: - JanitorWintercoat +- type: loadoutGroup + id: JanitorPlunger + name: loadout-group-janitor-plunger + minLimit: 0 + loadouts: + - JanitorGoldenPlunger + - type: loadoutGroup id: BotanistHead name: loadout-group-botanist-head diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml index 29b81664391..f2f76485218 100644 --- a/Resources/Prototypes/Loadouts/role_loadouts.yml +++ b/Resources/Prototypes/Loadouts/role_loadouts.yml @@ -110,6 +110,7 @@ - JanitorOuterClothing - Glasses - Trinkets + - JanitorPlunger - Survival - type: roleLoadout diff --git a/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/equipped-HELMET.png b/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/equipped-HELMET.png new file mode 100644 index 0000000000000000000000000000000000000000..97a8bf5997c9365b9eb0b65d90f01328197c7f24 GIT binary patch literal 223 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|W_r3fhE&XX zdutz+}c3myJBwPIz6>Ii{d#>>y?ktF>zLRN;@+d;jkDb2AC|YX1H?F8bW| zR}<@B2R=E)qp;h-jQND#gxnvDJ{Oxy=kMRY;N?fo`;+I&S232(&yL)7*=YX-WBYHM zI?K0WH*VEO{Wt~$(69DB1QV;+D literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/meta.json b/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/meta.json new file mode 100644 index 00000000000..22ee7db3a21 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Plunger sprites taken from tgstation https://github.com/tgstation/tgstation/blob/master/icons/obj/watercloset.dmi, Helmet sprite made for ss14 by VasilisThePikachu on github, golden plunger modified from original by Ghagliiarghii on github", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "plunger" + }, + { + "name": "plunger-inhand-left", + "directions": 4 + }, + { + "name": "plunger-inhand-right", + "directions": 4 + }, + { + "name": "equipped-HELMET", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-left.png b/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-left.png new file mode 100644 index 0000000000000000000000000000000000000000..b335a9d673bd4a49eb3efebe3bb6376818728a5b GIT binary patch literal 265 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|4tu&dhE&XX zdrML1kONQa!zq$Evf++JY9?PZ(pK$cYJVa6VZ}U#M&2gJEeX?nZ`^$LKP@Y4Pu}G` zUnM36hK3nZs}*FUZ0Ffs=dYfaR_?#={PsQXCe8aCZuM=Iv-Qq3pVoY{*#3!Y+n3Yg zAw7M2_uY?qxmTpvHh*tY2X7fpP^@H@?6zRC1dgTe~DWM4f DMBrz_ literal 0 HcmV?d00001 diff --git a/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-right.png b/Resources/Textures/Objects/Specific/Janitorial/golden_plunger.rsi/plunger-inhand-right.png new file mode 100644 index 0000000000000000000000000000000000000000..9dd24a2f757477db30171cc2c73a62ef0818ab27 GIT binary patch literal 264 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|4tcsbhE&XX zd&`io$w8p?;lU%*&aIB9;_y#eyY-q{bhYHVg#4}Q8#dI2x%Du{C@ES0zs`8`$=L~a z(~k21wcCmM%7;v~lDU6B-#qQ;t6Mwra((Xwov=SJBC$jwPx#x%wnN4>PWqusQh#Ccc8RD{f4%hz=}9 zr#c{n7(-HO;$XjodrkFiwZc9q481*C`sq}7WBzJUs;fC{L9 zSYR?UzZH-o5|CC80HYy6lqdBYAPd{I(_xBvcxLa%4xl`tZejqqWDfv9SvYJ7ehab? zuu=WJaP9C`umswpZ$vt{*1!uYq>;~hqVZ0kss1gw1eU;Rjo#$0HP{{RECJSqDYu3; x`~H3}7rbd;PeB^_ Date: Fri, 21 Jun 2024 00:13:02 +0200 Subject: [PATCH 064/109] Rate limit ahelps (#29219) * Make chat rate limits a general-purpose system. Intending to use this with ahelps next. * Rate limt ahelps Fixes #28762 * Review comments --- .../Administration/Systems/BwoinkSystem.cs | 23 ++ .../Chat/Managers/ChatManager.RateLimit.cs | 85 ++---- Content.Server/Chat/Managers/ChatManager.cs | 10 +- Content.Server/Chat/Managers/IChatManager.cs | 4 +- Content.Server/Chat/Systems/ChatSystem.cs | 5 +- Content.Server/Entry/EntryPoint.cs | 3 + Content.Server/IoC/ServerContentIoC.cs | 3 + .../RateLimiting/PlayerRateLimitManager.cs | 254 ++++++++++++++++++ Content.Shared.Database/LogType.cs | 10 +- Content.Shared/CCVar/CCVars.cs | 19 ++ .../Locale/en-US/administration/bwoink.ftl | 2 + 11 files changed, 344 insertions(+), 74 deletions(-) create mode 100644 Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs diff --git a/Content.Server/Administration/Systems/BwoinkSystem.cs b/Content.Server/Administration/Systems/BwoinkSystem.cs index a07115544bf..0a797aa02a8 100644 --- a/Content.Server/Administration/Systems/BwoinkSystem.cs +++ b/Content.Server/Administration/Systems/BwoinkSystem.cs @@ -9,6 +9,7 @@ using Content.Server.Afk; using Content.Server.Discord; using Content.Server.GameTicking; +using Content.Server.Players.RateLimiting; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Mind; @@ -27,6 +28,8 @@ namespace Content.Server.Administration.Systems [UsedImplicitly] public sealed partial class BwoinkSystem : SharedBwoinkSystem { + private const string RateLimitKey = "AdminHelp"; + [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IConfigurationManager _config = default!; @@ -35,6 +38,7 @@ public sealed partial class BwoinkSystem : SharedBwoinkSystem [Dependency] private readonly GameTicker _gameTicker = default!; [Dependency] private readonly SharedMindSystem _minds = default!; [Dependency] private readonly IAfkManager _afkManager = default!; + [Dependency] private readonly PlayerRateLimitManager _rateLimit = default!; [GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")] private static partial Regex DiscordRegex(); @@ -80,6 +84,22 @@ public override void Initialize() SubscribeLocalEvent(OnGameRunLevelChanged); SubscribeNetworkEvent(OnClientTypingUpdated); + + _rateLimit.Register( + RateLimitKey, + new RateLimitRegistration + { + CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod, + CVarLimitCount = CCVars.AhelpRateLimitCount, + PlayerLimitedAction = PlayerRateLimitedAction + }); + } + + private void PlayerRateLimitedAction(ICommonSession obj) + { + RaiseNetworkEvent( + new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false), + obj.Channel); } private void OnOverrideChanged(string obj) @@ -395,6 +415,9 @@ protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySes return; } + if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed) + return; + var escapedText = FormattedMessage.EscapeText(message.Text); string bwoinkText; diff --git a/Content.Server/Chat/Managers/ChatManager.RateLimit.cs b/Content.Server/Chat/Managers/ChatManager.RateLimit.cs index cf87ab6322d..45e7d2e20d0 100644 --- a/Content.Server/Chat/Managers/ChatManager.RateLimit.cs +++ b/Content.Server/Chat/Managers/ChatManager.RateLimit.cs @@ -1,84 +1,41 @@ -using System.Runtime.InteropServices; +using Content.Server.Players.RateLimiting; using Content.Shared.CCVar; using Content.Shared.Database; -using Robust.Shared.Enums; using Robust.Shared.Player; -using Robust.Shared.Timing; namespace Content.Server.Chat.Managers; internal sealed partial class ChatManager { - private readonly Dictionary _rateLimitData = new(); + private const string RateLimitKey = "Chat"; - public bool HandleRateLimit(ICommonSession player) + private void RegisterRateLimits() { - ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(_rateLimitData, player, out _); - var time = _gameTiming.RealTime; - if (datum.CountExpires < time) - { - // Period expired, reset it. - var periodLength = _configurationManager.GetCVar(CCVars.ChatRateLimitPeriod); - datum.CountExpires = time + TimeSpan.FromSeconds(periodLength); - datum.Count = 0; - datum.Announced = false; - } - - var maxCount = _configurationManager.GetCVar(CCVars.ChatRateLimitCount); - datum.Count += 1; - - if (datum.Count <= maxCount) - return true; - - // Breached rate limits, inform admins if configured. - if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins)) - { - if (datum.NextAdminAnnounce < time) + _rateLimitManager.Register(RateLimitKey, + new RateLimitRegistration { - SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name))); - var delay = _configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdminsDelay); - datum.NextAdminAnnounce = time + TimeSpan.FromSeconds(delay); - } - } - - if (!datum.Announced) - { - DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true); - _adminLogger.Add(LogType.ChatRateLimited, LogImpact.Medium, $"Player {player} breached chat rate limits"); - - datum.Announced = true; - } - - return false; + CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod, + CVarLimitCount = CCVars.ChatRateLimitCount, + CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay, + PlayerLimitedAction = RateLimitPlayerLimited, + AdminAnnounceAction = RateLimitAlertAdmins, + AdminLogType = LogType.ChatRateLimited, + }); } - private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e) + private void RateLimitPlayerLimited(ICommonSession player) { - if (e.NewStatus == SessionStatus.Disconnected) - _rateLimitData.Remove(e.Session); + DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true); } - private struct RateLimitDatum + private void RateLimitAlertAdmins(ICommonSession player) { - /// - /// Time stamp (relative to ) this rate limit period will expire at. - /// - public TimeSpan CountExpires; - - /// - /// How many messages have been sent in the current rate limit period. - /// - public int Count; - - /// - /// Have we announced to the player that they've been blocked in this rate limit period? - /// - public bool Announced; + if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins)) + SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name))); + } - /// - /// Time stamp (relative to ) of the - /// next time we can send an announcement to admins about rate limit breach. - /// - public TimeSpan NextAdminAnnounce; + public RateLimitStatus HandleRateLimit(ICommonSession player) + { + return _rateLimitManager.CountAction(player, RateLimitKey); } } diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 79683db6411..6bb552d9769 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -5,18 +5,17 @@ using Content.Server.Administration.Managers; using Content.Server.Administration.Systems; using Content.Server.MoMMI; +using Content.Server.Players.RateLimiting; using Content.Server.Preferences.Managers; using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Mind; -using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Replays; -using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Chat.Managers @@ -43,8 +42,7 @@ internal sealed partial class ChatManager : IChatManager [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly INetConfigurationManager _netConfigManager = default!; [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IGameTiming _gameTiming = default!; - [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!; /// /// The maximum length a player-sent message can be sent @@ -64,7 +62,7 @@ public void Initialize() _configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true); - _playerManager.PlayerStatusChanged += PlayerStatusChanged; + RegisterRateLimits(); } private void OnOocEnabledChanged(bool val) @@ -206,7 +204,7 @@ public void SendHookOOC(string sender, string message) /// The type of message. public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type) { - if (!HandleRateLimit(player)) + if (HandleRateLimit(player) != RateLimitStatus.Allowed) return; // Check if message exceeds the character limit diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index c8c057a1ad7..15d1288ee23 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -1,4 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using Content.Server.Players; +using Content.Server.Players.RateLimiting; using Content.Shared.Administration; using Content.Shared.Chat; using Robust.Shared.Network; @@ -50,6 +52,6 @@ void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessag /// /// The player sending a chat message. /// False if the player has violated rate limits and should be blocked from sending further messages. - bool HandleRateLimit(ICommonSession player); + RateLimitStatus HandleRateLimit(ICommonSession player); } } diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 65977c74f51..55beaf1f7f5 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -6,6 +6,7 @@ using Content.Server.Chat.Managers; using Content.Server.Examine; using Content.Server.GameTicking; +using Content.Server.Players.RateLimiting; using Content.Server.Speech.Components; using Content.Server.Speech.EntitySystems; using Content.Server.Station.Components; @@ -183,7 +184,7 @@ public void TrySendInGameICMessage( return; } - if (player != null && !_chatManager.HandleRateLimit(player)) + if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed) return; // Sus @@ -272,7 +273,7 @@ public void TrySendInGameOOCMessage( if (!CanSendInGame(message, shell, player)) return; - if (player != null && !_chatManager.HandleRateLimit(player)) + if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed) return; // It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index e5b2338c5b7..3a9d07126e5 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -14,8 +14,10 @@ using Content.Server.IoC; using Content.Server.Maps; using Content.Server.NodeContainer.NodeGroups; +using Content.Server.Players; using Content.Server.Players.JobWhitelist; using Content.Server.Players.PlayTimeTracking; +using Content.Server.Players.RateLimiting; using Content.Server.Preferences.Managers; using Content.Server.ServerInfo; using Content.Server.ServerUpdates; @@ -108,6 +110,7 @@ public override void Init() _updateManager.Initialize(); _playTimeTracking.Initialize(); IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); } } diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index c6dfcadd382..858ad2fe264 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -13,8 +13,10 @@ using Content.Server.Maps; using Content.Server.MoMMI; using Content.Server.NodeContainer.NodeGroups; +using Content.Server.Players; using Content.Server.Players.JobWhitelist; using Content.Server.Players.PlayTimeTracking; +using Content.Server.Players.RateLimiting; using Content.Server.Preferences.Managers; using Content.Server.ServerInfo; using Content.Server.ServerUpdates; @@ -63,6 +65,7 @@ public static void Register() IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs b/Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs new file mode 100644 index 00000000000..59f086f9c31 --- /dev/null +++ b/Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs @@ -0,0 +1,254 @@ +using System.Runtime.InteropServices; +using Content.Server.Administration.Logs; +using Content.Shared.Database; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Player; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.Players.RateLimiting; + +/// +/// General-purpose system to rate limit actions taken by clients, such as chat messages. +/// +/// +/// +/// Different categories of rate limits must be registered ahead of time by calling . +/// Once registered, you can simply call to count a rate-limited action for a player. +/// +/// +/// This system is intended for rate limiting player actions over short periods, +/// to ward against spam that can cause technical issues such as admin client load. +/// It should not be used for in-game actions or similar. +/// +/// +/// Rate limits are reset when a client reconnects. +/// This should not be an issue for the reasonably short rate limit periods this system is intended for. +/// +/// +/// +public sealed class PlayerRateLimitManager +{ + [Dependency] private readonly IAdminLogManager _adminLog = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + private readonly Dictionary _registrations = new(); + private readonly Dictionary> _rateLimitData = new(); + + /// + /// Count and validate an action performed by a player against rate limits. + /// + /// The player performing the action. + /// The key string that was previously used to register a rate limit category. + /// Whether the action counted should be blocked due to surpassing rate limits or not. + /// + /// is not a connected player + /// OR is not a registered rate limit category. + /// + /// + public RateLimitStatus CountAction(ICommonSession player, string key) + { + if (player.Status == SessionStatus.Disconnected) + throw new ArgumentException("Player is not connected"); + if (!_registrations.TryGetValue(key, out var registration)) + throw new ArgumentException($"Unregistered key: {key}"); + + var playerData = _rateLimitData.GetOrNew(player); + ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(playerData, key, out _); + var time = _gameTiming.RealTime; + if (datum.CountExpires < time) + { + // Period expired, reset it. + datum.CountExpires = time + registration.LimitPeriod; + datum.Count = 0; + datum.Announced = false; + } + + datum.Count += 1; + + if (datum.Count <= registration.LimitCount) + return RateLimitStatus.Allowed; + + // Breached rate limits, inform admins if configured. + if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay) + { + if (datum.NextAdminAnnounce < time) + { + registration.Registration.AdminAnnounceAction!(player); + datum.NextAdminAnnounce = time + cvarAnnounceDelay; + } + } + + if (!datum.Announced) + { + registration.Registration.PlayerLimitedAction(player); + _adminLog.Add( + registration.Registration.AdminLogType, + LogImpact.Medium, + $"Player {player} breached '{key}' rate limit "); + + datum.Announced = true; + } + + return RateLimitStatus.Blocked; + } + + /// + /// Register a new rate limit category. + /// + /// + /// The key string that will be referred to later with . + /// Must be unique and should probably just be a constant somewhere. + /// + /// The data specifying the rate limit's parameters. + /// has already been registered. + /// is invalid. + public void Register(string key, RateLimitRegistration registration) + { + if (_registrations.ContainsKey(key)) + throw new InvalidOperationException($"Key already registered: {key}"); + + var data = new RegistrationData + { + Registration = registration, + }; + + if ((registration.AdminAnnounceAction == null) != (registration.CVarAdminAnnounceDelay == null)) + { + throw new ArgumentException( + $"Must set either both {nameof(registration.AdminAnnounceAction)} and {nameof(registration.CVarAdminAnnounceDelay)} or neither"); + } + + _cfg.OnValueChanged( + registration.CVarLimitCount, + i => data.LimitCount = i, + invokeImmediately: true); + _cfg.OnValueChanged( + registration.CVarLimitPeriodLength, + i => data.LimitPeriod = TimeSpan.FromSeconds(i), + invokeImmediately: true); + + if (registration.CVarAdminAnnounceDelay != null) + { + _cfg.OnValueChanged( + registration.CVarLimitCount, + i => data.AdminAnnounceDelay = TimeSpan.FromSeconds(i), + invokeImmediately: true); + } + + _registrations.Add(key, data); + } + + /// + /// Initialize the manager's functionality at game startup. + /// + public void Initialize() + { + _playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged; + } + + private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e) + { + if (e.NewStatus == SessionStatus.Disconnected) + _rateLimitData.Remove(e.Session); + } + + private sealed class RegistrationData + { + public required RateLimitRegistration Registration { get; init; } + public TimeSpan LimitPeriod { get; set; } + public int LimitCount { get; set; } + public TimeSpan? AdminAnnounceDelay { get; set; } + } + + private struct RateLimitDatum + { + /// + /// Time stamp (relative to ) this rate limit period will expire at. + /// + public TimeSpan CountExpires; + + /// + /// How many actions have been done in the current rate limit period. + /// + public int Count; + + /// + /// Have we announced to the player that they've been blocked in this rate limit period? + /// + public bool Announced; + + /// + /// Time stamp (relative to ) of the + /// next time we can send an announcement to admins about rate limit breach. + /// + public TimeSpan NextAdminAnnounce; + } +} + +/// +/// Contains all data necessary to register a rate limit with . +/// +public sealed class RateLimitRegistration +{ + /// + /// CVar that controls the period over which the rate limit is counted, measured in seconds. + /// + public required CVarDef CVarLimitPeriodLength { get; init; } + + /// + /// CVar that controls how many actions are allowed in a single rate limit period. + /// + public required CVarDef CVarLimitCount { get; init; } + + /// + /// An action that gets invoked when this rate limit has been breached by a player. + /// + /// + /// This can be used for informing players or taking administrative action. + /// + public required Action PlayerLimitedAction { get; init; } + + /// + /// CVar that controls the minimum delay between admin notifications, measured in seconds. + /// This can be omitted to have no admin notification system. + /// + /// + /// If set, must be set too. + /// + public CVarDef? CVarAdminAnnounceDelay { get; init; } + + /// + /// An action that gets invoked when a rate limit was breached and admins should be notified. + /// + /// + /// If set, must be set too. + /// + public Action? AdminAnnounceAction { get; init; } + + /// + /// Log type used to log rate limit violations to the admin logs system. + /// + public LogType AdminLogType { get; init; } = LogType.RateLimited; +} + +/// +/// Result of a rate-limited operation. +/// +/// +public enum RateLimitStatus : byte +{ + /// + /// The action was not blocked by the rate limit. + /// + Allowed, + + /// + /// The action was blocked by the rate limit. + /// + Blocked, +} diff --git a/Content.Shared.Database/LogType.cs b/Content.Shared.Database/LogType.cs index f486a7416c7..33a5d30c6a9 100644 --- a/Content.Shared.Database/LogType.cs +++ b/Content.Shared.Database/LogType.cs @@ -96,5 +96,13 @@ public enum LogType ChatRateLimited = 87, AtmosTemperatureChanged = 88, DeviceNetwork = 89, - StoreRefund = 90 + StoreRefund = 90, + + /// + /// User was rate-limited for some spam action. + /// + /// + /// This is a default value used by PlayerRateLimitManager, though users can use different log types. + /// + RateLimited = 91, } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index f20ea21491d..1a1a7f02262 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -883,6 +883,25 @@ public static readonly CVarDef public static readonly CVarDef AdminBypassMaxPlayers = CVarDef.Create("admin.bypass_max_players", true, CVar.SERVERONLY); + /* + * AHELP + */ + + /// + /// Ahelp rate limit values are accounted in periods of this size (seconds). + /// After the period has passed, the count resets. + /// + /// + public static readonly CVarDef AhelpRateLimitPeriod = + CVarDef.Create("ahelp.rate_limit_period", 2, CVar.SERVERONLY); + + /// + /// How many ahelp messages are allowed in a single rate limit period. + /// + /// + public static readonly CVarDef AhelpRateLimitCount = + CVarDef.Create("ahelp.rate_limit_count", 10, CVar.SERVERONLY); + /* * Explosions */ diff --git a/Resources/Locale/en-US/administration/bwoink.ftl b/Resources/Locale/en-US/administration/bwoink.ftl index 474af89c268..3a92f58ad18 100644 --- a/Resources/Locale/en-US/administration/bwoink.ftl +++ b/Resources/Locale/en-US/administration/bwoink.ftl @@ -14,3 +14,5 @@ bwoink-system-typing-indicator = {$players} {$count -> admin-bwoink-play-sound = Bwoink? bwoink-title-none-selected = None selected + +bwoink-system-rate-limited = System: you are sending messages too quickly. From a3ffe22e66f5049a8b5efa3e7eb5502636aa08c7 Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Thu, 20 Jun 2024 22:25:35 +0000 Subject: [PATCH 065/109] return empty string for invalid identity (#29274) Co-authored-by: deltanedas <@deltanedas:kde.org> --- Content.Shared/IdentityManagement/Identity.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Content.Shared/IdentityManagement/Identity.cs b/Content.Shared/IdentityManagement/Identity.cs index 2fb53e3d5a9..cdb3c81adac 100644 --- a/Content.Shared/IdentityManagement/Identity.cs +++ b/Content.Shared/IdentityManagement/Identity.cs @@ -15,6 +15,9 @@ public static class Identity /// public static string Name(EntityUid uid, IEntityManager ent, EntityUid? viewer=null) { + if (!uid.IsValid()) + return string.Empty; + var meta = ent.GetComponent(uid); if (meta.EntityLifeStage <= EntityLifeStage.Initializing) return meta.EntityName; // Identity component and such will not yet have initialized and may throw NREs From 831d962e5a34f53e3405b710ede55870a43852f4 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 20 Jun 2024 20:15:37 -0400 Subject: [PATCH 066/109] Make Loadout MinLimit not count failed attempts (#29264) Loadout MinLimit doesn't count failed attempts --- Content.Shared/Preferences/Loadouts/RoleLoadout.cs | 10 ++++++++-- Resources/Prototypes/Loadouts/loadout_groups.yml | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Content.Shared/Preferences/Loadouts/RoleLoadout.cs b/Content.Shared/Preferences/Loadouts/RoleLoadout.cs index d02929cd965..ca6d841616d 100644 --- a/Content.Shared/Preferences/Loadouts/RoleLoadout.cs +++ b/Content.Shared/Preferences/Loadouts/RoleLoadout.cs @@ -193,9 +193,14 @@ public void SetDefault(HumanoidCharacterProfile? profile, ICommonSession? sessio if (groupProto.MinLimit > 0) { // Apply any loadouts we can. - for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++) + var addedCount = 0; + foreach (var protoId in groupProto.Loadouts) { - if (!protoManager.TryIndex(groupProto.Loadouts[j], out var loadoutProto)) + // Reached the limit, time to stop + if (addedCount >= groupProto.MinLimit) + break; + + if (!protoManager.TryIndex(protoId, out var loadoutProto)) continue; var defaultLoadout = new Loadout() @@ -209,6 +214,7 @@ public void SetDefault(HumanoidCharacterProfile? profile, ICommonSession? sessio loadouts.Add(defaultLoadout); Apply(loadoutProto); + addedCount++; } } } diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml index 530022424ec..e3da71b4c16 100644 --- a/Resources/Prototypes/Loadouts/loadout_groups.yml +++ b/Resources/Prototypes/Loadouts/loadout_groups.yml @@ -38,8 +38,8 @@ - type: loadoutGroup id: Survival name: loadout-group-survival-basic - minLimit: 2 - maxLimit: 2 + minLimit: 1 + maxLimit: 1 hidden: true loadouts: - EmergencyNitrogen From 0b3459268ae445eefeaec11b81baf152b5d3dc6e Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 20 Jun 2024 20:16:43 -0400 Subject: [PATCH 067/109] Add logging to SharedStorageSystem prototype indexing failure (#29273) --- .../Storage/EntitySystems/SharedStorageSystem.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index f20e2f9ab1f..2b434befe53 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -1384,7 +1384,12 @@ public ItemSizePrototype GetMaxItemSize(Entity uid) // If we specify a max item size, use that if (uid.Comp.MaxItemSize != null) - return _prototype.Index(uid.Comp.MaxItemSize.Value); + { + if (_prototype.TryIndex(uid.Comp.MaxItemSize.Value, out var proto)) + return proto; + + Log.Error($"{ToPrettyString(uid.Owner)} tried to get invalid item size prototype: {uid.Comp.MaxItemSize.Value}. Stack trace:\\n{Environment.StackTrace}"); + } if (!_itemQuery.TryGetComponent(uid, out var item)) return _defaultStorageMaxItemSize; From afc800274caaecf3647db2f04cd0635285eb675b Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 20 Jun 2024 20:17:16 -0400 Subject: [PATCH 068/109] Fix null exceptions in SurveillanceCameraMonitorSystem (#29275) * Add IsNullOrEmpty checks before indexing KnownSubnets * actor --- .../Systems/SurveillanceCameraMonitorSystem.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs index ca0f59cd14d..f61c57a4a6b 100644 --- a/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs +++ b/Content.Server/SurveillanceCamera/Systems/SurveillanceCameraMonitorSystem.cs @@ -90,7 +90,7 @@ private void OnComponentStartup(EntityUid uid, SurveillanceCameraMonitorComponen private void OnSubnetRequest(EntityUid uid, SurveillanceCameraMonitorComponent component, SurveillanceCameraMonitorSubnetRequestMessage args) { - if (args.Actor != null) + if (args.Actor is { Valid: true } actor && !Deleted(actor)) { SetActiveSubnet(uid, args.Subnet, component); } @@ -146,6 +146,7 @@ private void OnPacketReceived(EntityUid uid, SurveillanceCameraMonitorComponent break; case SurveillanceCameraSystem.CameraSubnetData: if (args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnet) + && !string.IsNullOrEmpty(subnet) && !component.KnownSubnets.ContainsKey(subnet)) { component.KnownSubnets.Add(subnet, args.SenderAddress); @@ -217,6 +218,7 @@ private void SendHeartbeat(EntityUid uid, SurveillanceCameraMonitorComponent? mo { if (!Resolve(uid, ref monitor) || monitor.LastHeartbeatSent < _heartbeatDelay + || string.IsNullOrEmpty(monitor.ActiveSubnet) || !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress)) { return; @@ -278,6 +280,7 @@ private void SetActiveSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null) { if (!Resolve(uid, ref monitor) + || string.IsNullOrEmpty(subnet) || !monitor.KnownSubnets.ContainsKey(subnet)) { return; @@ -295,6 +298,7 @@ private void SetActiveSubnet(EntityUid uid, string subnet, private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null) { if (!Resolve(uid, ref monitor) + || string.IsNullOrEmpty(monitor.ActiveSubnet) || !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var address)) { return; @@ -310,6 +314,7 @@ private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorCom private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null) { if (!Resolve(uid, ref monitor) + || string.IsNullOrEmpty(subnet) || !monitor.KnownSubnets.TryGetValue(subnet, out var address)) { return; @@ -327,6 +332,7 @@ private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMon private void DisconnectFromSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null) { if (!Resolve(uid, ref monitor) + || string.IsNullOrEmpty(subnet) || !monitor.KnownSubnets.TryGetValue(subnet, out var address)) { return; @@ -415,6 +421,7 @@ private void TrySwitchCameraByAddress(EntityUid uid, string address, SurveillanceCameraMonitorComponent? monitor = null) { if (!Resolve(uid, ref monitor) + || string.IsNullOrEmpty(monitor.ActiveSubnet) || !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress)) { return; From 9cd2705d6be910ff0de65ddfbc8679464a15d677 Mon Sep 17 00:00:00 2001 From: MilenVolf <63782763+MilenVolf@users.noreply.github.com> Date: Fri, 21 Jun 2024 06:02:23 +0300 Subject: [PATCH 069/109] Make stasis bed power toggleable (#29268) Stasis bed is now power toggleable --- Resources/Prototypes/Entities/Structures/Machines/stasisbed.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Structures/Machines/stasisbed.yml b/Resources/Prototypes/Entities/Structures/Machines/stasisbed.yml index ba9d7be88b0..4dd1197593c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/stasisbed.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/stasisbed.yml @@ -59,3 +59,4 @@ - type: GuideHelp guides: - Medical Doctor + - type: PowerSwitch From f94d66345293c58ee5b8519de3f5a61515894464 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 21 Jun 2024 03:03:30 +0000 Subject: [PATCH 070/109] 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 9a5de7a56f9..ea68fd3804c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Plykiya - changes: - - message: Dropped items are no longer rotated to world north. - type: Fix - id: 6295 - time: '2024-04-03T05:31:57.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26662 - author: deltanedas changes: - message: Disabled scooping foam due to a reagent duplication bug. @@ -3845,3 +3838,10 @@ id: 6794 time: '2024-06-20T14:19:22.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29210 +- author: MilenVolf + changes: + - message: Stasis bed can be toggled on or off! + type: Tweak + id: 6795 + time: '2024-06-21T03:02:23.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29268 From 880fef507d66b3654f6859b11424a0a8e00b0292 Mon Sep 17 00:00:00 2001 From: Tayrtahn Date: Thu, 20 Jun 2024 23:04:02 -0400 Subject: [PATCH 071/109] Replace BlockSolutionAccessComponent with an attempt event (#26988) * BlockSolutionAccessComponent now only blocks one specified solution. * Significant overhaul Separated spilling when worn functionality into its own component/system. Removed BlockSolutionAccessComponent. Added an event for solution access. --- .../EntitySystems/PuddleSystem.Spillable.cs | 29 ---------- .../BlockSolutionAccessComponent.cs | 11 ---- .../SharedSolutionContainerSystem.cs | 32 ++++++++--- .../Components/SpillWhenWornComponent.cs | 24 ++++++++ .../Fluids/Components/SpillableComponent.cs | 7 --- .../EntitySystems/SpillWhenWornSystem.cs | 55 +++++++++++++++++++ .../Entities/Objects/Tools/bucket.yml | 2 + 7 files changed, 104 insertions(+), 56 deletions(-) delete mode 100644 Content.Shared/Chemistry/Components/BlockSolutionAccessComponent.cs create mode 100644 Content.Shared/Fluids/Components/SpillWhenWornComponent.cs create mode 100644 Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs diff --git a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs index d02dd44e81f..2eaf19170b7 100644 --- a/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs +++ b/Content.Server/Fluids/EntitySystems/PuddleSystem.Spillable.cs @@ -27,8 +27,6 @@ protected override void InitializeSpillable() SubscribeLocalEvent(SpillOnLand); // Openable handles the event if it's closed SubscribeLocalEvent(SplashOnMeleeHit, after: [typeof(OpenableSystem)]); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); SubscribeLocalEvent(OnOverflow); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnAttemptPacifiedThrow); @@ -97,33 +95,6 @@ private void SplashOnMeleeHit(Entity entity, ref MeleeHitEve } } - private void OnGotEquipped(Entity entity, ref ClothingGotEquippedEvent args) - { - if (!entity.Comp.SpillWorn) - return; - - if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) - return; - - // block access to the solution while worn - AddComp(entity); - - if (solution.Volume == 0) - return; - - // spill all solution on the player - var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume); - TrySplashSpillAt(entity.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _); - } - - private void OnGotUnequipped(Entity entity, ref ClothingGotUnequippedEvent args) - { - if (!entity.Comp.SpillWorn) - return; - - RemCompDeferred(entity); - } - private void SpillOnLand(Entity entity, ref LandEvent args) { if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution)) diff --git a/Content.Shared/Chemistry/Components/BlockSolutionAccessComponent.cs b/Content.Shared/Chemistry/Components/BlockSolutionAccessComponent.cs deleted file mode 100644 index 182f92d7d33..00000000000 --- a/Content.Shared/Chemistry/Components/BlockSolutionAccessComponent.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Chemistry.Components; - -/// -/// Blocks all attempts to access solutions contained by this entity. -/// -[RegisterComponent, NetworkedComponent] -public sealed partial class BlockSolutionAccessComponent : Component -{ -} diff --git a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs index 5e58a0944a4..7e00157b6ee 100644 --- a/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs +++ b/Content.Shared/Chemistry/EntitySystems/SharedSolutionContainerSystem.cs @@ -48,6 +48,12 @@ public partial record struct SolutionOverflowEvent(Entity Sol public bool Handled = false; } +[ByRefEvent] +public partial record struct SolutionAccessAttemptEvent(string SolutionName) +{ + public bool Cancelled; +} + /// /// Part of Chemistry system deal with SolutionContainers /// @@ -156,12 +162,6 @@ public bool TryGetSolution( [NotNullWhen(true)] out Entity? entity, bool errorOnMissing = false) { - if (TryComp(container, out BlockSolutionAccessComponent? blocker)) - { - entity = null; - return false; - } - EntityUid uid; if (name is null) uid = container; @@ -170,7 +170,18 @@ public bool TryGetSolution( solutionContainer is ContainerSlot solutionSlot && solutionSlot.ContainedEntity is { } containedSolution ) + { + var attemptEv = new SolutionAccessAttemptEvent(name); + RaiseLocalEvent(container, ref attemptEv); + + if (attemptEv.Cancelled) + { + entity = null; + return false; + } + uid = containedSolution; + } else { entity = null; @@ -218,11 +229,14 @@ public bool TryGetSolution(SolutionContainerManagerComponent container, if (!Resolve(container, ref container.Comp, logMissing: false)) yield break; - if (HasComp(container)) - yield break; - foreach (var name in container.Comp.Containers) { + var attemptEv = new SolutionAccessAttemptEvent(name); + RaiseLocalEvent(container, ref attemptEv); + + if (attemptEv.Cancelled) + continue; + if (ContainerSystem.GetContainer(container, $"solution@{name}") is ContainerSlot slot && slot.ContainedEntity is { } solutionId) yield return (name, (solutionId, Comp(solutionId))); } diff --git a/Content.Shared/Fluids/Components/SpillWhenWornComponent.cs b/Content.Shared/Fluids/Components/SpillWhenWornComponent.cs new file mode 100644 index 00000000000..d456d181b4a --- /dev/null +++ b/Content.Shared/Fluids/Components/SpillWhenWornComponent.cs @@ -0,0 +1,24 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Fluids.Components; + +/// +/// This entity will spill its contained solution onto the wearer when worn, and its +/// (empty) contents will be inaccessible while still worn. +/// +[RegisterComponent] +[NetworkedComponent, AutoGenerateComponentState] +public sealed partial class SpillWhenWornComponent : Component +{ + /// + /// Name of the solution to spill. + /// + [DataField] + public string Solution = "default"; + + /// + /// Tracks if this item is currently being worn. + /// + [DataField, AutoNetworkedField] + public bool IsWorn; +} diff --git a/Content.Shared/Fluids/Components/SpillableComponent.cs b/Content.Shared/Fluids/Components/SpillableComponent.cs index 51bb73a11fe..cc57dbf2b60 100644 --- a/Content.Shared/Fluids/Components/SpillableComponent.cs +++ b/Content.Shared/Fluids/Components/SpillableComponent.cs @@ -14,13 +14,6 @@ public sealed partial class SpillableComponent : Component [DataField("solution")] public string SolutionName = "puddle"; - /// - /// Should this item be spilled when worn as clothing? - /// Doesn't count for pockets or hands. - /// - [DataField] - public bool SpillWorn = true; - [DataField] public float? SpillDelay; diff --git a/Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs b/Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs new file mode 100644 index 00000000000..210dbfd4de7 --- /dev/null +++ b/Content.Shared/Fluids/EntitySystems/SpillWhenWornSystem.cs @@ -0,0 +1,55 @@ +using Content.Shared.Chemistry.EntitySystems; +using Content.Shared.Clothing; +using Content.Shared.Fluids.Components; + +namespace Content.Shared.Fluids.EntitySystems; + +/// +public sealed class SpillWhenWornSystem : EntitySystem +{ + [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; + [Dependency] private readonly SharedPuddleSystem _puddle = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); + SubscribeLocalEvent(OnSolutionAccessAttempt); + } + + private void OnGotEquipped(Entity ent, ref ClothingGotEquippedEvent args) + { + if (_solutionContainer.TryGetSolution(ent.Owner, ent.Comp.Solution, out var soln, out var solution) + && solution.Volume > 0) + { + // Spill all solution on the player + var drainedSolution = _solutionContainer.Drain(ent.Owner, soln.Value, solution.Volume); + _puddle.TrySplashSpillAt(ent.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _); + } + + // Flag as worn after draining, otherwise we'll block ourself from accessing! + ent.Comp.IsWorn = true; + Dirty(ent); + } + + private void OnGotUnequipped(Entity ent, ref ClothingGotUnequippedEvent args) + { + ent.Comp.IsWorn = false; + Dirty(ent); + } + + private void OnSolutionAccessAttempt(Entity ent, ref SolutionAccessAttemptEvent args) + { + // If we're not being worn right now, we don't care + if (!ent.Comp.IsWorn) + return; + + // Make sure it's the right solution + if (ent.Comp.Solution != args.SolutionName) + return; + + args.Cancelled = true; + } +} diff --git a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml index 77c5e548978..58c8dae2b05 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/bucket.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/bucket.yml @@ -45,6 +45,8 @@ Blunt: 0 - type: Spillable solution: bucket + - type: SpillWhenWorn + solution: bucket - type: DrawableSolution solution: bucket - type: RefillableSolution From d62dddcfcb7d031f8525b5248040af260fe372f6 Mon Sep 17 00:00:00 2001 From: slarticodefast <161409025+slarticodefast@users.noreply.github.com> Date: Fri, 21 Jun 2024 07:42:17 +0200 Subject: [PATCH 072/109] fix initial infected icons and add a briefing to the character menu (#29259) --- .../Systems/AdminVerbSystem.Antags.cs | 17 +++++++++++++++++ .../GameTicking/Rules/ZombieRuleSystem.cs | 19 +++++++++++++++++++ .../Locale/en-US/administration/antag.ftl | 4 +++- Resources/Prototypes/GameRules/events.yml | 3 ++- Resources/Prototypes/GameRules/roundstart.yml | 1 + 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 4103b8a8aa7..28918e960e5 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -21,6 +21,9 @@ public sealed partial class AdminVerbSystem [ValidatePrototypeId] private const string DefaultTraitorRule = "Traitor"; + [ValidatePrototypeId] + private const string DefaultInitialInfectedRule = "Zombie"; + [ValidatePrototypeId] private const string DefaultNukeOpRule = "LoneOpsSpawn"; @@ -63,6 +66,20 @@ private void AddAntagVerbs(GetVerbsEvent args) }; args.Verbs.Add(traitor); + Verb initialInfected = new() + { + Text = Loc.GetString("admin-verb-text-make-initial-infected"), + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"), + Act = () => + { + _antag.ForceMakeAntag(targetPlayer, DefaultInitialInfectedRule); + }, + Impact = LogImpact.High, + Message = Loc.GetString("admin-verb-make-initial-infected"), + }; + args.Verbs.Add(initialInfected); + Verb zombie = new() { Text = Loc.GetString("admin-verb-text-make-zombie"), diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index 95d1f791ef2..e91931300e2 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules.Components; using Content.Server.Popups; +using Content.Server.Roles; using Content.Server.RoundEnd; using Content.Server.Station.Components; using Content.Server.Station.Systems; @@ -35,9 +36,27 @@ public override void Initialize() { base.Initialize(); + SubscribeLocalEvent(OnGetBriefing); + SubscribeLocalEvent(OnGetBriefing); SubscribeLocalEvent(OnZombifySelf); } + private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args) + { + if (!TryComp(uid, out var mind) || mind.OwnedEntity == null) + return; + if (HasComp(uid)) // don't show both briefings + return; + args.Append(Loc.GetString("zombie-patientzero-role-greeting")); + } + + private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args) + { + if (!TryComp(uid, out var mind) || mind.OwnedEntity == null) + return; + args.Append(Loc.GetString("zombie-infection-greeting")); + } + protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule, ref RoundEndTextAppendEvent args) { diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl index 535659f27e5..587c038557c 100644 --- a/Resources/Locale/en-US/administration/antag.ftl +++ b/Resources/Locale/en-US/administration/antag.ftl @@ -1,5 +1,6 @@ verb-categories-antag = Antag ctrl admin-verb-make-traitor = Make the target into a traitor. +admin-verb-make-initial-infected = Make the target into an Initial Infected. admin-verb-make-zombie = Zombifies the target immediately. admin-verb-make-nuclear-operative = Make target into a lone Nuclear Operative. admin-verb-make-pirate = Make the target into a pirate. Note this doesn't configure the game rule. @@ -7,8 +8,9 @@ admin-verb-make-head-rev = Make the target into a Head Revolutionary. admin-verb-make-thief = Make the target into a thief. admin-verb-text-make-traitor = Make Traitor +admin-verb-text-make-initial-infected = Make Initial Infected admin-verb-text-make-zombie = Make Zombie admin-verb-text-make-nuclear-operative = Make Nuclear Operative admin-verb-text-make-pirate = Make Pirate admin-verb-text-make-head-rev = Make Head Rev -admin-verb-text-make-thief = Make Thief \ No newline at end of file +admin-verb-text-make-thief = Make Thief diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index f8f2ef49e70..3e63ad0283a 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -277,7 +277,7 @@ duration: 60 maxDuration: 120 - type: PowerGridCheckRule - + - type: entity parent: BaseGameRule id: SolarFlare @@ -404,6 +404,7 @@ maxInitialInfectedGrace: 450 - type: ZombifyOnDeath - type: IncurableZombie + - type: InitialInfected mindComponents: - type: InitialInfectedRole prototype: InitialInfected diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 923a0114606..a7b749a35f9 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -244,6 +244,7 @@ - type: PendingZombie - type: ZombifyOnDeath - type: IncurableZombie + - type: InitialInfected mindComponents: - type: InitialInfectedRole prototype: InitialInfected From 7ef1002101c64a7785e9bff131bd9a46b3200662 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 21 Jun 2024 05:43:23 +0000 Subject: [PATCH 073/109] Automatic changelog update --- Resources/Changelog/Admin.yml | 7 +++++++ Resources/Changelog/Changelog.yml | 17 ++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index 0ef47a775b6..cfa58d80355 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -313,5 +313,12 @@ Entries: id: 38 time: '2024-06-15T11:25:42.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/28899 +- author: slarticodefast + changes: + - message: Added the "Make Inititial Infected" verb to the antag control. + type: Add + id: 39 + time: '2024-06-21T05:42:17.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29259 Name: Admin Order: 1 diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ea68fd3804c..c3ed77303b4 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: deltanedas - changes: - - message: Disabled scooping foam due to a reagent duplication bug. - type: Fix - id: 6296 - time: '2024-04-03T13:41:23.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26686 - author: Aserovich changes: - message: New lobby art! @@ -3845,3 +3838,13 @@ id: 6795 time: '2024-06-21T03:02:23.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29268 +- author: slarticodefast + changes: + - message: Fixed initial infected status icons not showing up. + type: Fix + - message: Added an antagonist message to the character menu for initial infected + and zombies. + type: Add + id: 6796 + time: '2024-06-21T05:42:17.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29259 From 711acedfe5a3fc3e35d6f0cbe7bb02bee67c70f7 Mon Sep 17 00:00:00 2001 From: Alex Pavlenko Date: Fri, 21 Jun 2024 09:43:15 +0300 Subject: [PATCH 074/109] =?UTF-8?q?feat:=20update=20cyborg=20parts=20namin?= =?UTF-8?q?g=20for=20them=20to=20be=20ordered=20consistently,=E2=80=A6=20(?= =?UTF-8?q?#29272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: update cyborg parts naming for them to be ordered consistently, closes #29270 --- Resources/Prototypes/Body/Parts/silicon.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Resources/Prototypes/Body/Parts/silicon.yml b/Resources/Prototypes/Body/Parts/silicon.yml index 24d88276ccb..2f5a621ab0e 100644 --- a/Resources/Prototypes/Body/Parts/silicon.yml +++ b/Resources/Prototypes/Body/Parts/silicon.yml @@ -29,7 +29,7 @@ - type: entity id: BaseBorgArmLeft parent: PartSilicon - name: left cyborg arm + name: cyborg left arm abstract: true components: - type: BodyPart @@ -43,7 +43,7 @@ - type: entity id: BaseBorgArmRight parent: PartSilicon - name: right cyborg arm + name: cyborg right arm abstract: true components: - type: BodyPart @@ -57,7 +57,7 @@ - type: entity id: BaseBorgLegLeft parent: PartSilicon - name: left cyborg leg + name: cyborg left leg abstract: true components: - type: BodyPart @@ -71,7 +71,7 @@ - type: entity id: BaseBorgLegRight parent: PartSilicon - name: right cyborg leg + name: cyborg right leg abstract: true components: - type: BodyPart From 949a837720b744817d239f3505f44c2320ba5355 Mon Sep 17 00:00:00 2001 From: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com> Date: Fri, 21 Jun 2024 11:41:54 +0200 Subject: [PATCH 075/109] Ghostrole rule updates (#29249) * First batch of ghostrole rule updates * Second pass * Re-word free agent * Apply review comments Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> * You heard it here first folks God rules are weird to write. Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> * Honkbot -> Free Agent Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> * Mimebot -> Free Agent Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> * Jonkbot -> Free Agent Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> * Softer blue --------- Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com> --- .../ToggleableGhostRoleComponent.cs | 3 + .../Ghost/Roles/ToggleableGhostRoleSystem.cs | 3 +- .../ghost/roles/ghost-role-component.ftl | 113 +++++++----------- .../Entities/Markers/Spawners/ghost_roles.yml | 10 +- .../Prototypes/Entities/Mobs/NPCs/animals.yml | 8 ++ .../Entities/Mobs/NPCs/behonker.yml | 1 + .../Prototypes/Entities/Mobs/NPCs/carp.yml | 1 + .../Entities/Mobs/NPCs/elemental.yml | 1 + .../Entities/Mobs/NPCs/hellspawn.yml | 1 + .../Prototypes/Entities/Mobs/NPCs/pets.yml | 5 + .../Entities/Mobs/NPCs/regalrat.yml | 2 +- .../Entities/Mobs/NPCs/revenant.yml | 2 +- .../Prototypes/Entities/Mobs/NPCs/silicon.yml | 4 + .../Prototypes/Entities/Mobs/NPCs/slimes.yml | 4 + .../Entities/Mobs/Player/dragon.yml | 3 +- .../Entities/Mobs/Player/familiars.yml | 4 +- .../Entities/Mobs/Player/guardian.yml | 4 + .../Entities/Mobs/Player/humanoid.yml | 30 ++++- .../Entities/Mobs/Player/skeleton.yml | 3 + .../Objects/Consumable/Food/Baked/bread.yml | 2 +- .../Objects/Consumable/Food/Baked/cake.yml | 2 +- .../reinforcement_teleporter.yml | 6 +- .../Prototypes/Entities/Objects/Fun/pai.yml | 3 + .../Objects/Specific/Robotics/mmi.yml | 1 + .../XenoArch/Effects/utility_effects.yml | 1 + 25 files changed, 125 insertions(+), 92 deletions(-) diff --git a/Content.Server/Ghost/Roles/Components/ToggleableGhostRoleComponent.cs b/Content.Server/Ghost/Roles/Components/ToggleableGhostRoleComponent.cs index fc10d55069e..ef1b1dbed99 100644 --- a/Content.Server/Ghost/Roles/Components/ToggleableGhostRoleComponent.cs +++ b/Content.Server/Ghost/Roles/Components/ToggleableGhostRoleComponent.cs @@ -24,6 +24,9 @@ public sealed partial class ToggleableGhostRoleComponent : Component [DataField("roleDescription")] public string RoleDescription = string.Empty; + [DataField("roleRules")] + public string RoleRules = string.Empty; + [DataField("wipeVerbText")] public string WipeVerbText = string.Empty; diff --git a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs index 9bab229c001..8354a24f90d 100644 --- a/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs +++ b/Content.Server/Ghost/Roles/ToggleableGhostRoleSystem.cs @@ -53,13 +53,14 @@ private void OnUseInHand(EntityUid uid, ToggleableGhostRoleComponent component, EnsureComp(uid); ghostRole.RoleName = Loc.GetString(component.RoleName); ghostRole.RoleDescription = Loc.GetString(component.RoleDescription); + ghostRole.RoleRules = Loc.GetString(component.RoleRules); } private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args) { if (!args.IsInDetailsRange) return; - + if (TryComp(uid, out var mind) && mind.HasMind) { args.PushMarkup(Loc.GetString(component.ExamineTextMindPresent)); diff --git a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl index 4e6ec1f1886..98f31e0ab07 100644 --- a/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl +++ b/Resources/Locale/en-US/ghost/roles/ghost-role-component.ftl @@ -3,6 +3,26 @@ ghost-role-component-default-rules = All normal rules apply unless an administra You don't remember any of your previous life, and you don't remember anything you learned as a ghost. You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. +ghost-role-information-nonantagonist-rules = You are a [color=green][bold]Non-antagonist[/bold][/color]. You should generally not seek to harm the station and its crew. + You don't remember any of your previous life, and you don't remember anything you learned as a ghost. + You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. + You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. +ghost-role-information-freeagent-rules = You are a [color=yellow][bold]Free Agent[/bold][/color]. You are free to act as either an antagonist or a non-antagonist. + You don't remember any of your previous life, and you don't remember anything you learned as a ghost. + You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. + You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. +ghost-role-information-antagonist-rules = You are a [color=red][bold]Solo Antagonist[/bold][/color]. Your intentions are clear, and harmful to the station and its crew. + You don't remember any of your previous life, and you don't remember anything you learned as a ghost. + You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. + You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. +ghost-role-information-familiar-rules = You are a [color=#6495ed][bold]Familiar[/bold][/color]. Serve the interests of your master, whatever those may be. + You don't remember any of your previous life, and you don't remember anything you learned as a ghost. + You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. + You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. +ghost-role-information-silicon-rules = You are a [color=#6495ed][bold]Silicon[/bold][/color]. Obey your laws. You are a Free Agent if you are not currently bound by any laws. + You don't remember any of your previous life, and you don't remember anything you learned as a ghost. + You are allowed to remember knowledge about the game in general, such as how to cook, how to use objects, etc. + You are absolutely [color=red]NOT[/color] allowed to remember, say, the name, appearance, etc. of your previous character. ghost-role-information-mouse-name = Mouse ghost-role-information-mouse-description = A hungry and mischievous mouse. @@ -12,6 +32,7 @@ ghost-role-information-mothroach-description = A cute but mischievous mothroach. ghost-role-information-giant-spider-name = Giant spider ghost-role-information-giant-spider-description = This station's inhabitants look mighty tasty, and your sticky web is perfect to catch them! +ghost-role-information-giant-spider-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all other giant spiders. ghost-role-information-cognizine-description = Made conscious with the magic of cognizine. @@ -26,6 +47,7 @@ ghost-role-information-slimes-description = An ordinary slime with no special ne ghost-role-information-angry-slimes-name = Slime ghost-role-information-angry-slimes-description = Everyone around you irritates your instincts, destroy them! +ghost-role-information-angry-slimes-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all other angry slimes. ghost-role-information-smile-name = Smile the Slime ghost-role-information-smile-description = The sweetest creature in the world. Smile Slime! @@ -35,11 +57,10 @@ ghost-role-information-punpun-description = An honorable member of the monkey so ghost-role-information-xeno-name = Xeno ghost-role-information-xeno-description = You are a xeno, co-operate with your hive to kill all crewmembers! -ghost-role-information-xeno-rules = You are an antagonist, smack, slash, and wack! +ghost-role-information-xeno-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all other xenos. ghost-role-information-revenant-name = Revenant ghost-role-information-revenant-description = You are a Revenant. Use your powers to harvest souls and unleash chaos upon the crew. Unlock new abilities with the essence you harvest. -ghost-role-information-revenant-rules = You are an antagonist, harvest, defile, and drive the crew insane. ghost-role-information-kangaroo-name = Kangaroo ghost-role-information-kangaroo-description = You're a kangaroo! Do whatever kangaroos do. @@ -51,31 +72,17 @@ ghost-role-information-kobold-name = Kobold ghost-role-information-kobold-description = Be the little gremlin you are, yell at people and beg for meat! ghost-role-information-rat-king-name = Rat King -ghost-role-information-rat-king-description = You are the Rat King, scavenge food in order to produce rat minions to do your bidding. -ghost-role-information-rat-king-rules = You are an antagonist, scavenge, attack, and grow your hoard! +ghost-role-information-rat-king-description = You are the Rat King, your interests are food, food, and more food. Cooperate with or fight against the station for food. Did I say food interests you? ghost-role-information-rat-servant-name = Rat Servant ghost-role-information-rat-servant-description = You are a Rat Servant. You must follow your king's orders. -ghost-role-information-rat-servant-rules = You are an antagonist, scavenge, attack, and serve your king! - -ghost-role-information-salvage-carp-name = Space carp on salvage wreck -ghost-role-information-salvage-carp-description = Defend the loot inside the salvage wreck! ghost-role-information-sentient-carp-name = Sentient Carp ghost-role-information-sentient-carp-description = Help the dragon flood the station with carps! -ghost-role-information-salvage-shark-name = Space sharkminnow on salvage wreck -ghost-role-information-salvage-shark-description = Help the younger fellow carp protect their prey. Smell the blood! - ghost-role-information-willow-name = Willow the kangaroo ghost-role-information-willow-description = You're a kangaroo named willow! Willow likes to box. -ghost-role-information-space-tick-name = Space tick -ghost-role-information-space-tick-description = Wreak havoc on the station! - -ghost-role-information-salvage-tick-name = Space tick on salvage wreck -ghost-role-information-salvage-tick-description = Defend the loot inside the salvage wreck! - ghost-role-information-honkbot-name = Honkbot ghost-role-information-honkbot-description = An artificial being of pure evil. @@ -85,39 +92,13 @@ ghost-role-information-jonkbot-description = An artificial being of pure evil. ghost-role-information-mimebot-name = Mimebot ghost-role-information-mimebot-description = A Mimebot, act like a mime but don't act like a greytider. -ghost-role-information-taxibot-name = TaxiBot -ghost-role-information-taxibot-description = Drive the station crew to their destination. - ghost-role-information-supplybot-name = SupplyBot ghost-role-information-supplybot-description = Deliver goods around the station. ghost-role-information-space-bear-name = Space bear ghost-role-information-space-bear-description = Your tummy rumbles, and these people look really yummy... What a feast! -ghost-role-information-salvage-bear-name = Space bear on salvage wreck -ghost-role-information-salvage-bear-description = Defend the loot inside the salvage wreck! - -ghost-role-information-space-kangaroo-name = Space kangaroo -ghost-role-information-space-kangaroo-description = Give the crew a taste of your sharp claws! - -ghost-role-information-salvage-kangaroo-name = Space kangaroo on salvage wreck -ghost-role-information-salvage-kangaroo-description = Defend the loot inside the salvage wreck! - -ghost-role-information-space-spider-name = Space spider -ghost-role-information-space-spider-description = Space spiders are just as aggressive as regular spiders, feed. - -ghost-role-information-salvage-spider-name = Space spider on salvage wreck -ghost-role-information-salvage-spider-description = Space spiders are just as aggressive as regular spiders, feed. - -ghost-role-information-space-cobra-name = Space cobra -ghost-role-information-space-cobra-description = Space cobras really don't like guests, and will always snack on a visitor. - -ghost-role-information-salvage-cobra-name = Space cobra on salvage wreck -ghost-role-information-salvage-cobra-description = Space cobras really don't like guests, and will always snack on a visitor. - -ghost-role-information-salvage-flesh-name = Aberrant flesh on salvage wreck -ghost-role-information-salvage-flesh-description = Defend the loot inside the salvage wreck! - +# Still exists as a commented out reference for Tropico. Keeping it around. -TsjipTsjip, 2024-06-20 ghost-role-information-tropico-name = Tropico ghost-role-information-tropico-description = The noble companion of Atmosia, and its most stalwart defender. Viva! @@ -135,7 +116,11 @@ ghost-role-information-ifrit-description = Listen to your owner. Don't tank dama ghost-role-information-space-dragon-name = Space dragon ghost-role-information-space-dragon-description = Call in 3 carp rifts and take over this quadrant! You have only 5 minutes in between each rift before you will disappear. +ghost-role-information-space-dragon-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all your summoned carp. +ghost-role-information-space-dragon-summoned-carp-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with your dragon and its allies. + ghost-role-information-space-dragon-dungeon-description = Defend the expedition dungeon with your fishy comrades! +ghost-role-information-space-dragon-dungeon-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all dungeon mobs. ghost-role-information-cluwne-name = Cluwne ghost-role-information-cluwne-description = Become a pitiful cluwne, your only goal in life is to find a sweet release from your suffering (usually by being beaten to death). A cluwne is not an antagonist but may defend itself. Crewmembers may murder cluwnes freely. @@ -147,19 +132,13 @@ ghost-role-information-skeleton-biker-name = Skeleton Biker ghost-role-information-skeleton-biker-description = Ride around on your sweet ride. ghost-role-information-closet-skeleton-name = Closet Skeleton -ghost-role-information-closet-skeleton-description = Wreak havoc! You are a primordial force with no allegiance. Live happily with the crew or wage sweet skeletal war. - -ghost-role-information-onestar-mecha-name = Onestar Mecha -ghost-role-information-onestar-mecha-description = You are an experimental mecha created by who-knows-what, all you know is that you have weapons and you detect fleshy moving targets nearby... -ghost-role-information-onestar-mecha-rules = Use your weapons to cause havoc. You are an antagonist. +ghost-role-information-closet-skeleton-description = You are arguably one of the oldest members of the station! Get your old job back, or cause chaos! The world is yours to shape. ghost-role-information-remilia-name = Remilia, the chaplain's familiar -ghost-role-information-remilia-description = Obey your master. Eat fruit. -ghost-role-information-remilia-rules = You are an intelligent fruit bat. Follow the chaplain around. Don't cause any trouble unless the chaplain tells you to. +ghost-role-information-remilia-description = Follow and obey the chaplain. Eat fruit. Screech loudly into people's ears and write it off as echolocation. ghost-role-information-cerberus-name = Cerberus, Evil Familiar ghost-role-information-cerberus-description = Obey your master. Spread chaos. -ghost-role-information-cerberus-rules = You are an intelligent, demonic dog. Try to help the chaplain and any of his flock. As an antagonist, you're otherwise unrestrained. ghost-role-information-ert-leader-name = ERT Leader ghost-role-information-ert-leader-description = Lead a team of specialists to resolve the station's issues. @@ -185,70 +164,64 @@ ghost-role-information-cburn-agent-description = A highly trained CentCom agent, ghost-role-information-centcom-official-name = CentComm official ghost-role-information-centcom-official-description = Perform CentComm related duties such as inspect the station, jotting down performance reviews for heads of staff, and managing the fax machine. -ghost-role-information-nukeop-rules = You are a syndicate operative tasked with the destruction of the station. As an antagonist, do whatever is required to complete this task. +ghost-role-information-nukeop-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all other nuclear operatives. Covert syndicate agents are not guaranteed to help you. ghost-role-information-loneop-name = Lone Operative ghost-role-information-loneop-description = You are a lone nuclear operative. Destroy the station! -ghost-role-information-loneop-rules = You are a syndicate operative tasked with the destruction of the station. As an antagonist, do whatever is required to complete this task. +ghost-role-information-loneop-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with all other nuclear operatives. Covert syndicate agents are not guaranteed to help you. ghost-role-information-behonker-name = Behonker ghost-role-information-behonker-description = You are an antagonist, bring death and honks to those who do not follow the honkmother. ghost-role-information-hellspawn-name = Hellspawn -ghost-role-information-hellspawn-description = You are an antagonist, bring death to those who do not follow the great god Nar'Sie. +ghost-role-information-hellspawn-description = Bring death to those who do not follow the great god Nar'Sie. ghost-role-information-Death-Squad-name = Death Squad Operative ghost-role-information-Death-Squad-description = One of Nanotrasen's top internal affairs agents. Await orders from CentComm or an official. +ghost-role-information-Death-Squad-rules = You are required to obey orders given by your superior, you are effectively their [color=#6495ed][bold]Familiar[/bold][/color]. ghost-role-information-SyndiCat-name = SyndiCat ghost-role-information-SyndiCat-description = You're the faithful trained pet of nuclear operatives with a microbomb. Serve your master to the death! -ghost-role-information-SyndiCat-rules = You're the faithful trained pet of nuclear operatives with a microbomb. Serve your master to the death! +ghost-role-information-SyndiCat-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. ghost-role-information-Cak-name = Cak ghost-role-information-Cak-description = You are the chef's favorite child. You're a living cake cat. -ghost-role-information-Cak-rules = You are a living edible sweet cat. Your task is to find your place in this world where everything wants to eat you. ghost-role-information-BreadDog-name = BreadDog ghost-role-information-BreadDog-description = You are the chef's favorite child. You're a living bread dog. -ghost-role-information-BreadDog-rules = You're an edible dog made of bread. Your task is to find your place in this world where everything wants to eat you. ghost-role-information-space-ninja-name = Space Ninja ghost-role-information-space-ninja-description = Use stealth and deception to sabotage the station. -ghost-role-information-space-ninja-rules = You are an elite mercenary of the Spider Clan. You aren't required to follow your objectives, yet your NINJA HONOR demands you try. ghost-role-information-syndicate-reinforcement-name = Syndicate Agent ghost-role-information-syndicate-reinforcement-description = Someone needs reinforcements. You, the first person the syndicate could find, will help them. -ghost-role-information-syndicate-reinforcement-rules = Normal syndicate antagonist rules apply. Work with whoever called you in, and don't harm them. +ghost-role-information-syndicate-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. ghost-role-information-syndicate-monkey-reinforcement-name = Syndicate Monkey Agent ghost-role-information-syndicate-monkey-reinforcement-description = Someone needs reinforcements. You, a trained monkey, will help them. -ghost-role-information-syndicate-monkey-reinforcement-rules = Normal syndicate antagonist rules apply. Work with whoever called you in, and don't harm them. +ghost-role-information-syndicate-monkey-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. ghost-role-information-lost-cargo-technical-name = Lost Cargo Technician ghost-role-information-lost-cargo-technical-description = Something went wrong and your cargo shuttle with the goods was beamed into the sector to another station. -ghost-role-information-lost-cargo-technical-rules = You're a regular cargo technician from another station. Do what regular cargo do. ghost-role-information-clown-troupe-name = Space Clown ghost-role-information-clown-troupe-description = You and your troupe have come to cheer up this station with your best jokes. Honk! -ghost-role-information-clown-troupe-rules = Normal station crew rules apply. ghost-role-information-traveling-chef-name = Traveling Chef ghost-role-information-traveling-chef-description = You are a chef on a traveling shuttle of exotic cuisine. Delight the station with delicious food! -ghost-role-information-traveling-chef-rules = Normal station crew rules apply. ghost-role-information-disaster-victim-name = Disaster Victim ghost-role-information-disaster-victim-description = You were rescued in an escape pod from another station that suffered a terrible fate. Perhaps you will be found and rescued. -ghost-role-information-disaster-victim-rules = Normal station crew rules apply. ghost-role-information-syndie-disaster-victim-name = Syndie Disaster Victim ghost-role-information-syndie-disaster-victim-description = You're a regular passenger from a syndicate station. Unfortunately, an evacuation pod has thrown you into an enemy sector..... -ghost-role-information-syndie-disaster-victim-rules = Normal station crew rules apply. You are NOT an antagonist! ghost-role-information-syndicate-kobold-reinforcement-name = Syndicate Kobold Agent ghost-role-information-syndicate-kobold-reinforcement-description = Someone needs reinforcements. You, a trained kobold, will help them. -ghost-role-information-syndicate-kobold-reinforcement-rules = Normal syndicate antagonist rules apply. Work with whoever called you in, and don't harm them. +ghost-role-information-syndicate-kobold-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you. ghost-role-information-artifact-name = Sentient Artifact -ghost-role-information-artifact-description = - Enact your eldritch whims. - Forcibly activate your nodes for good or for evil. +ghost-role-information-artifact-description = Enact your eldritch whims. Forcibly activate your nodes for good or for evil. + +ghost-role-information-syndie-assaultborg-name = Syndicate Assault Borg +ghost-role-information-syndie-assaultborg-description = Nuclear operatives needs reinforcements. You, a cold silicon killing machine, will help them. More dakka! diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index b21792579f4..bdefd14a452 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -18,7 +18,7 @@ - type: GhostRole name: ghost-role-information-rat-king-name description: ghost-role-information-rat-king-description - rules: ghost-role-information-rat-king-rules + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostRoleMobSpawner @@ -39,7 +39,7 @@ - type: GhostRole name: ghost-role-information-remilia-name description: ghost-role-information-remilia-description - rules: ghost-role-information-remilia-rules + rules: ghost-role-information-familiar-rules raffle: settings: short - type: GhostRoleMobSpawner @@ -60,7 +60,7 @@ - type: GhostRole name: ghost-role-information-cerberus-name description: ghost-role-information-cerberus-description - rules: ghost-role-information-cerberus-rules + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostRoleMobSpawner @@ -143,7 +143,7 @@ - type: GhostRole name: ghost-role-information-space-dragon-name description: ghost-role-information-space-dragon-description - rules: ghost-role-component-default-rules + rules: ghost-role-component-space-dragon-rules - type: Sprite layers: - state: green @@ -159,7 +159,7 @@ - type: GhostRole name: ghost-role-information-space-ninja-name description: ghost-role-information-space-ninja-description - rules: ghost-role-information-space-ninja-rules + rules: ghost-role-information-antagonist-rules raffle: settings: default - type: GhostRoleMobSpawner diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index fc9e5151e91..3bc5779b52f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -421,6 +421,7 @@ allowMovement: true name: ghost-role-information-mothroach-name description: ghost-role-information-mothroach-description + rules: ghost-role-information-freeagent-rules - type: Fixtures fixtures: fix1: @@ -1104,6 +1105,7 @@ prob: 0.25 name: ghost-role-information-kangaroo-name description: ghost-role-information-kangaroo-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Vocal sounds: @@ -1278,6 +1280,7 @@ makeSentient: true name: ghost-role-information-monkey-name description: ghost-role-information-monkey-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Clumsy clumsyDamage: @@ -1312,6 +1315,7 @@ makeSentient: true name: ghost-role-information-monkey-name description: ghost-role-information-monkey-description + rules: ghost-role-information-syndicate-monkey-reinforcement-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -1443,6 +1447,7 @@ makeSentient: true name: ghost-role-information-kobold-name description: ghost-role-information-kobold-description + rules: ghost-role-information-nonantagonist-rules - type: entity name: kobold @@ -1516,6 +1521,7 @@ allowMovement: true name: ghost-role-information-mouse-name description: ghost-role-information-mouse-description + rules: ghost-role-information-freeagent-rules - type: GhostTakeoverAvailable - type: Speech speechSounds: Squeak @@ -2300,6 +2306,7 @@ makeSentient: true name: ghost-role-information-giant-spider-name description: ghost-role-information-giant-spider-description + rules: ghost-role-information-giant-spider-rules raffle: settings: short - type: GhostTakeoverAvailable @@ -3079,6 +3086,7 @@ allowMovement: true name: ghost-role-information-hamster-name description: ghost-role-information-hamster-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Speech speechVerb: SmallMob diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml index 07ee6b1536a..6f9935d351d 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/behonker.yml @@ -10,6 +10,7 @@ makeSentient: true name: ghost-role-information-behonker-name description: ghost-role-information-behonker-description + rules: ghost-role-information-antagonist-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml index 10bc7861fac..e345ec477b1 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/carp.yml @@ -163,6 +163,7 @@ makeSentient: true name: ghost-role-information-sentient-carp-name description: ghost-role-information-sentient-carp-description + rules: ghost-role-information-space-dragon-summoned-carp-rules raffle: settings: short - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml index a6641efe8cc..ba21ca4da23 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/elemental.yml @@ -232,6 +232,7 @@ - type: GhostRole prob: 0 description: ghost-role-information-angry-slimes-description + rules: ghost-role-information-angry-slimes-rules raffle: settings: short - type: NpcFactionMember diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml b/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml index 74658f0a2db..e0a3a764561 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/hellspawn.yml @@ -12,6 +12,7 @@ makeSentient: true name: ghost-role-information-hellspawn-name description: ghost-role-information-hellspawn-description + rules: ghost-role-information-antagonist-rules raffle: settings: default - type: RotationVisuals diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml index 510cae3be33..32c71408d5c 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/pets.yml @@ -572,6 +572,7 @@ allowMovement: true name: ghost-role-information-hamlet-name description: ghost-role-information-hamlet-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: InteractionPopup successChance: 1 @@ -686,6 +687,7 @@ prob: 0.25 name: ghost-role-information-willow-name description: ghost-role-information-willow-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Loadout prototypes: [ BoxingKangarooGear ] @@ -756,6 +758,7 @@ - type: GhostRole name: ghost-role-information-smile-name description: ghost-role-information-smile-description + rules: ghost-role-information-nonantagonist-rules - type: Grammar attributes: proper: true @@ -774,6 +777,7 @@ allowMovement: true name: ghost-role-information-punpun-name description: ghost-role-information-punpun-description + rules: ghost-role-information-nonantagonist-rules - type: GhostTakeoverAvailable - type: Butcherable butcheringType: Spike @@ -807,6 +811,7 @@ # allowMovement: true # name: ghost-role-information-tropico-name # description: ghost-role-information-tropico-description +# rules: ghost-role-information-nonantagonist-rules # - type: GhostTakeoverAvailable - type: Tag tags: diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml index 38debe2a9a7..c615b8721e0 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml @@ -89,7 +89,7 @@ makeSentient: true name: ghost-role-information-rat-king-name description: ghost-role-information-rat-king-description - rules: ghost-role-information-rat-king-rules + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml index 8a56a399163..45171fff676 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/revenant.yml @@ -55,7 +55,7 @@ makeSentient: true name: ghost-role-information-revenant-name description: ghost-role-information-revenant-description - rules: ghost-role-information-revenant-rules + rules: ghost-role-information-antagonist-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml index 96dfd53cec1..b3bb5d4d07f 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml @@ -156,6 +156,7 @@ makeSentient: true name: ghost-role-information-honkbot-name description: ghost-role-information-honkbot-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -183,6 +184,7 @@ makeSentient: true name: ghost-role-information-jonkbot-name description: ghost-role-information-jonkbot-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: InteractionPopup @@ -315,6 +317,7 @@ makeSentient: true name: ghost-role-information-mimebot-name description: ghost-role-information-mimebot-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -338,6 +341,7 @@ makeSentient: true name: ghost-role-information-supplybot-name description: ghost-role-information-supplybot-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml index ffca699bfcc..d68415992ad 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml @@ -127,6 +127,7 @@ makeSentient: true name: ghost-role-information-slimes-name description: ghost-role-information-slimes-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Speech @@ -200,6 +201,7 @@ - SimpleHostile - type: GhostRole description: ghost-role-information-angry-slimes-description + rules: ghost-role-information-angry-slimes-rules raffle: settings: short @@ -237,6 +239,7 @@ - SimpleHostile - type: GhostRole description: ghost-role-information-angry-slimes-description + rules: ghost-role-information-angry-slimes-rules raffle: settings: short @@ -274,5 +277,6 @@ - SimpleHostile - type: GhostRole description: ghost-role-information-angry-slimes-description + rules: ghost-role-information-angry-slimes-rules raffle: settings: short diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index 869fb880841..8bf9bfab41e 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -14,6 +14,7 @@ makeSentient: true name: ghost-role-information-space-dragon-name description: ghost-role-information-space-dragon-description + rules: ghost-role-information-space-dragon-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -142,7 +143,6 @@ - type: entity parent: BaseMobDragon id: MobDragon - suffix: No role or objectives components: - type: Dragon spawnRiftAction: ActionSpawnRift @@ -180,6 +180,7 @@ components: - type: GhostRole description: ghost-role-information-space-dragon-dungeon-description + rules: ghost-role-information-space-dragon-dungeon-rules raffle: settings: default - type: SlowOnDamage diff --git a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml index b7fb7eb66c0..272436236c5 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/familiars.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/familiars.yml @@ -10,7 +10,7 @@ allowSpeech: true name: ghost-role-information-remilia-name description: ghost-role-information-remilia-description - rules: ghost-role-information-remilia-rules + rules: ghost-role-information-familiar-rules - type: GhostTakeoverAvailable - type: Grammar attributes: @@ -43,7 +43,7 @@ allowSpeech: true name: ghost-role-information-cerberus-name description: ghost-role-information-cerberus-description - rules: ghost-role-information-cerberus-rules + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml index 9ccfdf4e507..6a11c966860 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml @@ -13,6 +13,7 @@ makeSentient: true name: ghost-role-information-guardian-name description: ghost-role-information-guardian-description + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -122,6 +123,7 @@ makeSentient: true name: ghost-role-information-holoparasite-name description: ghost-role-information-holoparasite-description + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -154,6 +156,7 @@ makeSentient: true name: ghost-role-information-ifrit-name description: ghost-role-information-ifrit-description + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -182,6 +185,7 @@ makeSentient: true name: ghost-role-information-holoclown-name description: ghost-role-information-holoclown-description + rules: ghost-role-information-familiar-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml index 6067c95fd3c..5bcc33b5f6d 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/humanoid.yml @@ -35,10 +35,11 @@ id: DeathSquad parent: EventHumanoidMindShielded randomizeName: false - components: + components: - type: GhostRole name: ghost-role-information-Death-Squad-name description: ghost-role-information-Death-Squad-description + rules: ghost-role-information-Death-Squad-rules raffle: settings: short - type: Loadout @@ -74,6 +75,7 @@ - type: GhostRole name: ghost-role-information-ert-leader-name description: ghost-role-information-ert-leader-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -102,6 +104,7 @@ - type: GhostRole name: ghost-role-information-ert-leader-name description: ghost-role-information-ert-leader-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -122,6 +125,7 @@ - type: GhostRole name: ghost-role-information-ert-leader-name description: ghost-role-information-ert-leader-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -152,6 +156,7 @@ - type: GhostRole name: ghost-role-information-ert-chaplain-name description: ghost-role-information-ert-chaplain-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -180,6 +185,7 @@ - type: GhostRole name: ghost-role-information-ert-chaplain-name description: ghost-role-information-ert-chaplain-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -210,6 +216,7 @@ - type: GhostRole name: ghost-role-information-ert-janitor-name description: ghost-role-information-ert-janitor-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -238,6 +245,7 @@ - type: GhostRole name: ghost-role-information-ert-janitor-name description: ghost-role-information-ert-janitor-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -268,6 +276,7 @@ - type: GhostRole name: ghost-role-information-ert-engineer-name description: ghost-role-information-ert-engineer-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -296,6 +305,7 @@ - type: GhostRole name: ghost-role-information-ert-engineer-name description: ghost-role-information-ert-engineer-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -326,6 +336,7 @@ - type: GhostRole name: ghost-role-information-ert-security-name description: ghost-role-information-ert-security-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -354,6 +365,7 @@ - type: GhostRole name: ghost-role-information-ert-security-name description: ghost-role-information-ert-security-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -374,6 +386,7 @@ - type: GhostRole name: ghost-role-information-ert-security-name description: ghost-role-information-ert-security-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -404,6 +417,7 @@ - type: GhostRole name: ghost-role-information-ert-medical-name description: ghost-role-information-ert-medical-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -432,6 +446,7 @@ - type: GhostRole name: ghost-role-information-ert-medical-name description: ghost-role-information-ert-medical-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -459,6 +474,7 @@ - type: GhostRole name: ghost-role-information-cburn-agent-name description: ghost-role-information-cburn-agent-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: RandomMetadata @@ -485,6 +501,7 @@ - type: GhostRole name: ghost-role-information-centcom-official-name description: ghost-role-information-centcom-official-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: default - type: Loadout @@ -552,6 +569,7 @@ - type: GhostRole name: ghost-role-information-cluwne-name description: ghost-role-information-cluwne-description + rules: ghost-role-information-nonantagonist-rules raffle: settings: default - type: Cluwne @@ -593,7 +611,7 @@ - type: GhostRole name: ghost-role-information-lost-cargo-technical-name description: ghost-role-information-lost-cargo-technical-description - rules: ghost-role-information-lost-cargo-technical-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -644,7 +662,7 @@ - type: GhostRole name: ghost-role-information-clown-troupe-name description: ghost-role-information-clown-troupe-description - rules: ghost-role-information-clown-troupe-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -694,7 +712,7 @@ - type: GhostRole name: ghost-role-information-traveling-chef-name description: ghost-role-information-traveling-chef-description - rules: ghost-role-information-traveling-chef-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout @@ -754,7 +772,7 @@ - type: GhostRole name: ghost-role-information-disaster-victim-name description: ghost-role-information-disaster-victim-description - rules: ghost-role-information-disaster-victim-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: default @@ -815,7 +833,7 @@ - type: GhostRole name: ghost-role-information-syndie-disaster-victim-name description: ghost-role-information-syndie-disaster-victim-description - rules: ghost-role-information-syndie-disaster-victim-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: Loadout diff --git a/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml b/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml index 76727181b4e..e10e2928ab2 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/skeleton.yml @@ -17,6 +17,7 @@ - type: GhostRole name: ghost-role-information-skeleton-pirate-name description: ghost-role-information-skeleton-pirate-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -33,6 +34,7 @@ - type: GhostRole name: ghost-role-information-skeleton-biker-name description: ghost-role-information-skeleton-biker-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable @@ -48,6 +50,7 @@ - type: GhostRole name: ghost-role-information-closet-skeleton-name description: ghost-role-information-closet-skeleton-description + rules: ghost-role-information-freeagent-rules raffle: settings: default - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml index 12a6e2a246b..a92dc9b25df 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml @@ -818,7 +818,7 @@ name: ghost-role-information-BreadDog-name allowMovement: true description: ghost-role-information-BreadDog-description - rules: ghost-role-information-BreadDog-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml index 922d4938885..b9a5cc42e45 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml @@ -699,7 +699,7 @@ name: ghost-role-information-Cak-name allowMovement: true description: ghost-role-information-Cak-description - rules: ghost-role-information-Cak-rules + rules: ghost-role-information-nonantagonist-rules raffle: settings: short - type: GhostTakeoverAvailable diff --git a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml index 2085422c9b5..d49c79f48b3 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/Syndicate_Gadgets/reinforcement_teleporter.yml @@ -79,9 +79,9 @@ suffix: NukeOps components: - type: GhostRole - name: Syndicate Assault Cyborg - description: Nuclear operatives needs reinforcements. You, a cold silicon killing machine, will help them. - rules: Normal syndicate antagonist rules apply. Work with whoever called you in, and don't harm them. + name: ghost-role-information-syndie-assaultborg-name + description: ghost-role-information-syndie-assaultborg-description + rules: ghost-role-information-silicon-rules raffle: settings: default - type: GhostRoleMobSpawner diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml index 02bbce2843b..c12f8a8d008 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml @@ -37,6 +37,7 @@ beginSearchingText: pai-system-searching roleName: pai-system-role-name roleDescription: pai-system-role-description + roleRules: ghost-role-information-familiar-rules wipeVerbText: pai-system-wipe-device-verb-text wipeVerbPopup: pai-system-wiped-device stopSearchVerbText: pai-system-stop-searching-verb-text @@ -89,6 +90,7 @@ - type: ToggleableGhostRole roleName: pai-system-role-name-syndicate roleDescription: pai-system-role-description-syndicate + roleRules: ghost-role-information-familiar-rules - type: IntrinsicRadioTransmitter channels: - Syndicate @@ -120,6 +122,7 @@ - type: ToggleableGhostRole roleName: pai-system-role-name-potato roleDescription: pai-system-role-description-potato + roleRules: ghost-role-information-familiar-rules - type: Appearance - type: GenericVisualizer visuals: diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml index 308c013da46..44f95e7a9be 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/mmi.yml @@ -84,6 +84,7 @@ beginSearchingText: positronic-brain-searching roleName: positronic-brain-role-name roleDescription: positronic-brain-role-description + roleRules: ghost-role-information-silicon-rules wipeVerbText: positronic-brain-wipe-device-verb-text wipeVerbPopup: positronic-brain-wiped-device stopSearchVerbText: positronic-brain-stop-searching-verb-text diff --git a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml index a8967080579..bfaff8fe44a 100644 --- a/Resources/Prototypes/XenoArch/Effects/utility_effects.yml +++ b/Resources/Prototypes/XenoArch/Effects/utility_effects.yml @@ -199,6 +199,7 @@ makeSentient: true name: ghost-role-information-artifact-name description: ghost-role-information-artifact-description + rules: ghost-role-information-freeagent-rules - type: GhostTakeoverAvailable - type: MovementSpeedModifier baseWalkSpeed: 0.25 From 38439996e95e80bb8169be392407241842204abc Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 21 Jun 2024 09:43:00 +0000 Subject: [PATCH 076/109] 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 c3ed77303b4..8530f48397f 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Aserovich - changes: - - message: New lobby art! - type: Add - id: 6297 - time: '2024-04-04T05:28:30.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26505 - author: Beck Thompson changes: - message: Items thrown at disposals now have to be insertable to display the miss @@ -3848,3 +3841,11 @@ id: 6796 time: '2024-06-21T05:42:17.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29259 +- author: Chief_Engineer and TsjipTsjip + changes: + - message: All ghostroles now have their role type, as defined in the rules, defined + in their ghostrole request box. + type: Tweak + id: 6797 + time: '2024-06-21T09:41:54.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29249 From 70a40b62cb9a79ded02061c3aadb087e54564ef1 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Fri, 21 Jun 2024 22:50:52 +1200 Subject: [PATCH 077/109] Fix some buckle interactions (#29293) --- .../Buckle/SharedBuckleSystem.Interaction.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs index 8c2d0b8ee18..463e0d55962 100644 --- a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs +++ b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs @@ -13,10 +13,11 @@ public abstract partial class SharedBuckleSystem private void InitializeInteraction() { SubscribeLocalEvent>(AddStrapVerbs); - SubscribeLocalEvent(OnStrapInteractHand); + SubscribeLocalEvent(OnStrapInteractHand, after: [typeof(InteractionPopupSystem)]); SubscribeLocalEvent(OnStrapDragDropTarget); SubscribeLocalEvent(OnCanDropTarget); + SubscribeLocalEvent(OnBuckleInteractHand, after: [typeof(InteractionPopupSystem)]); SubscribeLocalEvent>(AddUnbuckleVerb); } @@ -58,6 +59,9 @@ private void OnStrapInteractHand(EntityUid uid, StrapComponent component, Intera if (args.Handled) return; + if (!component.Enabled) + return; + if (!TryComp(args.User, out BuckleComponent? buckle)) return; @@ -68,7 +72,20 @@ private void OnStrapInteractHand(EntityUid uid, StrapComponent component, Intera else return; - args.Handled = true; // This generate popups on failure. + // TODO BUCKLE add out bool for whether a pop-up was generated or not. + args.Handled = true; + } + + private void OnBuckleInteractHand(Entity ent, ref InteractHandEvent args) + { + if (args.Handled) + return; + + if (ent.Comp.BuckledTo != null) + TryUnbuckle(ent!, args.User, popup: true); + + // TODO BUCKLE add out bool for whether a pop-up was generated or not. + args.Handled = true; } private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent args) From 6b13f5f0cbeb60c04dc2a4aa6d99fb32814bc11d Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 21 Jun 2024 10:51:58 +0000 Subject: [PATCH 078/109] 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 8530f48397f..68ccd7a6d4c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,12 +1,4 @@ Entries: -- author: Beck Thompson - changes: - - message: Items thrown at disposals now have to be insertable to display the miss - message. - type: Fix - id: 6298 - time: '2024-04-04T06:25:47.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26716 - author: Tayrtahn changes: - message: Some devices may have broken wiring at the start of each round. @@ -3849,3 +3841,12 @@ id: 6797 time: '2024-06-21T09:41:54.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29249 +- author: ElectroJr + changes: + - message: Fixed not being able to pick up folded rollerbeds. + type: Fix + - message: Fixed not being able to unbuckle entities by clicking on them. + type: Fix + id: 6798 + time: '2024-06-21T10:50:53.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29293 From 76a65c8e4345bb55d8ac48cab4214e2145a604c7 Mon Sep 17 00:00:00 2001 From: nikthechampiongr <32041239+nikthechampiongr@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:06:07 +0000 Subject: [PATCH 079/109] Implement BlacklistedRange exempt flag (#29258) * Implement a new kind of ip range ban that only applies to new players * Put determining whether a player record exists to its own function * Make BlacklistedRange bans get bypassed by any ban exemption * Stop trying to get another DbGuard while already having one This does break with convention on the functions in that area but considering the use of this function it's probably fine? I could alternatively just move the place it's called from. Also I was suppossed to wait for tests to finish locally just to be sure, but nah. I am pushing this now --- Content.Server.Database/Model.cs | 8 ++++++++ Content.Server/Database/ServerDbBase.cs | 5 +++++ Content.Server/Database/ServerDbPostgres.cs | 16 ++++++++++++---- Content.Server/Database/ServerDbSqlite.cs | 20 ++++++++++++++++---- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 3510a5422e3..164cf421bd2 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -693,6 +693,14 @@ public enum ServerBanExemptFlags /// Intended use is for users with shared connections. This should not be used as an alternative to . /// IP = 1 << 1, + + /// + /// Ban is an IP range that is only applied for first time joins. + /// + /// + /// Intended for use with residential IP ranges that are often used maliciously. + /// + BlacklistedRange = 1 << 2, // @formatter:on } diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index cd03af7087b..d91ef391981 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -625,6 +625,11 @@ public async Task UpdatePlayerRecord( return record == null ? null : MakePlayerRecord(record); } + protected async Task PlayerRecordExists(DbGuard db, NetUserId userId) + { + return await db.DbContext.Player.AnyAsync(p => p.UserId == userId); + } + [return: NotNullIfNotNull(nameof(player))] protected PlayerRecord? MakePlayerRecord(Player? player) { diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index fd4699fff4e..f8eef1a554e 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -78,7 +78,8 @@ public ServerDbPostgres( await using var db = await GetDbImpl(); var exempt = await GetBanExemptionCore(db, userId); - var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt) + var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); + var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer) .OrderByDescending(b => b.BanTime); var ban = await query.FirstOrDefaultAsync(); @@ -98,7 +99,8 @@ public override async Task> GetServerBansAsync(IPAddress? add await using var db = await GetDbImpl(); var exempt = await GetBanExemptionCore(db, userId); - var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt); + var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId); + var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer); var queryBans = await query.ToArrayAsync(); var bans = new List(queryBans.Length); @@ -122,7 +124,8 @@ private static IQueryable MakeBanLookupQuery( ImmutableArray? hwId, DbGuardImpl db, bool includeUnbanned, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + bool newPlayer) { DebugTools.Assert(!(address == null && userId == null && hwId == null)); @@ -141,7 +144,9 @@ private static IQueryable MakeBanLookupQuery( { var newQ = db.PgDbContext.Ban .Include(p => p.Unban) - .Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address)); + .Where(b => b.Address != null + && EF.Functions.ContainsOrEqual(b.Address.Value, address) + && !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer)); query = query == null ? newQ : query.Union(newQ); } @@ -167,6 +172,9 @@ private static IQueryable MakeBanLookupQuery( if (exemptFlags is { } exempt) { + if (exempt != ServerBanExemptFlags.None) + exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange + query = query.Where(b => (b.ExemptFlags & exempt) == 0); } diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index ffec90bb43d..ce6f97a1171 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -86,11 +86,13 @@ public ServerDbSqlite( var exempt = await GetBanExemptionCore(db, userId); + var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value); + // SQLite can't do the net masking stuff we need to match IP address ranges. // So just pull down the whole list into memory. var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt); - return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt)) is { } foundBan + return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) is { } foundBan ? ConvertBan(foundBan) : null; } @@ -103,12 +105,14 @@ public override async Task> GetServerBansAsync(IPAddress? add var exempt = await GetBanExemptionCore(db, userId); + var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId); + // SQLite can't do the net masking stuff we need to match IP address ranges. // So just pull down the whole list into memory. var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt); return queryBans - .Where(b => BanMatches(b, address, userId, hwId, exempt)) + .Where(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) .Select(ConvertBan) .ToList()!; } @@ -137,10 +141,18 @@ private static bool BanMatches(ServerBan ban, IPAddress? address, NetUserId? userId, ImmutableArray? hwId, - ServerBanExemptFlags? exemptFlags) + ServerBanExemptFlags? exemptFlags, + bool newPlayer) { + // Any flag to bypass BlacklistedRange bans. + var exemptFromBlacklistedRange = exemptFlags != null && exemptFlags.Value != ServerBanExemptFlags.None; + if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP) - && address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value)) + && address != null + && ban.Address is not null + && address.IsInSubnet(ban.Address.ToTuple().Value) + && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || + newPlayer && !exemptFromBlacklistedRange)) { return true; } From 730a4d289dfdae5e573c0e5c32227c563f8b95e5 Mon Sep 17 00:00:00 2001 From: PJBot Date: Fri, 21 Jun 2024 12:07:14 +0000 Subject: [PATCH 080/109] Automatic changelog update --- Resources/Changelog/Admin.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index cfa58d80355..84f31adef45 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -320,5 +320,15 @@ Entries: id: 39 time: '2024-06-21T05:42:17.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29259 +- author: nikthechampiongr + changes: + - message: It is now possible to issue BlacklistedRanges bans(This requires you + to edit the database manually at the moment.) Bans marked as BlacklistedRange + will lead to players joining for the first time who match an ip range to be + denied. Players that have joined the server before will be unaffected. + type: Add + id: 40 + time: '2024-06-21T12:06:07.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29258 Name: Admin Order: 1 From f041f58a6dc172992e75569ca07ca7c9877590e7 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Fri, 21 Jun 2024 15:29:10 +0200 Subject: [PATCH 081/109] Add time index to connection log (#29281) * Add time index to connection log Queries go nyoom. * Don't let me code shit at 5 AM --- ...1120713_ConnectionLogTimeIndex.Designer.cs | 1915 +++++++++++++++++ .../20240621120713_ConnectionLogTimeIndex.cs | 27 + .../PostgresServerDbContextModelSnapshot.cs | 2 + ...1120705_ConnectionLogTimeIndex.Designer.cs | 1840 ++++++++++++++++ .../20240621120705_ConnectionLogTimeIndex.cs | 27 + .../SqliteServerDbContextModelSnapshot.cs | 2 + Content.Server.Database/Model.cs | 3 + 7 files changed, 3816 insertions(+) create mode 100644 Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs diff --git a/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs new file mode 100644 index 00000000000..8bd837236af --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.Designer.cs @@ -0,0 +1,1915 @@ +// +using System; +using System.Net; +using System.Text.Json; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using NpgsqlTypes; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresServerDbContext))] + [Migration("20240621120713_ConnectionLogTimeIndex")] + partial class ConnectionLogTimeIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminId") + .HasColumnType("uuid") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("boolean") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("integer") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("smallint") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Message") + .HasAnnotation("Npgsql:TsVectorConfig", "english"); + + NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("integer") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_messages_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("boolean") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("boolean") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_notes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("boolean") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_rank_flag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminRankId") + .HasColumnType("integer") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("admin_watchlists_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("boolean") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("uuid") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .IsRequired() + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("character varying(4096)") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("antag_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("assigned_user_id_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("connection_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("smallint") + .HasColumnName("denied"); + + b.Property("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("timestamp with time zone") + .HasColumnName("time"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("job_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("play_time_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("interval") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("player_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FirstSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("inet") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenHWId") + .HasColumnType("bytea") + .HasColumnName("last_seen_hwid"); + + b.Property("LastSeenTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", null, t => + { + t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Age") + .HasColumnType("integer") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("text") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("integer") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("integer") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("text") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_loadout_group_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("profile_role_loadout_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("text") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("round_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ServerId") + .HasColumnType("integer") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("integer") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("integer") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_ban_hit_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("integer") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("server_role_ban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .HasColumnType("inet") + .HasColumnName("address"); + + b.Property("BanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("uuid") + .HasColumnName("banning_admin"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("expiration_time"); + + b.Property("HWId") + .HasColumnType("bytea") + .HasColumnName("hwid"); + + b.Property("Hidden") + .HasColumnType("boolean") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("uuid") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("interval") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_id"); + + b.Property("RoundId") + .HasColumnType("integer") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("integer") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"); + + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("role_unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("unban_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BanId") + .HasColumnType("integer") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("uuid") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("trait_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("uploaded_resource_log_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Data") + .IsRequired() + .HasColumnType("bytea") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("timestamp with time zone") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("text") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("integer") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("integer") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs new file mode 100644 index 00000000000..736e9e37a22 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20240621120713_ConnectionLogTimeIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class ConnectionLogTimeIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_connection_log_time", + table: "connection_log", + column: "time"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_connection_log_time", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index 32a655f7f65..ea5d0066928 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -559,6 +559,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", null, t => diff --git a/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs new file mode 100644 index 00000000000..b3e512548ad --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.Designer.cs @@ -0,0 +1,1840 @@ +// +using System; +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqliteServerDbContext))] + [Migration("20240621120705_ConnectionLogTimeIndex")] + partial class ConnectionLogTimeIndex + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("UserId") + .HasName("PK_admin"); + + b.HasIndex("AdminRankId") + .HasDatabaseName("IX_admin_admin_rank_id"); + + b.ToTable("admin", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_flag_id"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .HasColumnName("negative"); + + b.HasKey("Id") + .HasName("PK_admin_flag"); + + b.HasIndex("AdminId") + .HasDatabaseName("IX_admin_flag_admin_id"); + + b.HasIndex("Flag", "AdminId") + .IsUnique(); + + b.ToTable("admin_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Id") + .HasColumnType("INTEGER") + .HasColumnName("admin_log_id"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .HasColumnName("impact"); + + b.Property("Json") + .IsRequired() + .HasColumnType("jsonb") + .HasColumnName("json"); + + b.Property("Message") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("INTEGER") + .HasColumnName("type"); + + b.HasKey("RoundId", "Id") + .HasName("PK_admin_log"); + + b.HasIndex("Date"); + + b.HasIndex("Type") + .HasDatabaseName("IX_admin_log_type"); + + b.ToTable("admin_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("LogId") + .HasColumnType("INTEGER") + .HasColumnName("log_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.HasKey("RoundId", "LogId", "PlayerUserId") + .HasName("PK_admin_log_player"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_log_player_player_user_id"); + + b.ToTable("admin_log_player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_messages_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .HasColumnName("seen"); + + b.HasKey("Id") + .HasName("PK_admin_messages"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_messages_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_messages_round_id"); + + b.ToTable("admin_messages", null, t => + { + t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_notes_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .HasColumnName("secret"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_admin_notes"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_notes_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_notes_round_id"); + + b.ToTable("admin_notes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_admin_rank"); + + b.ToTable("admin_rank", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_flag_id"); + + b.Property("AdminRankId") + .HasColumnType("INTEGER") + .HasColumnName("admin_rank_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.HasKey("Id") + .HasName("PK_admin_rank_flag"); + + b.HasIndex("AdminRankId"); + + b.HasIndex("Flag", "AdminRankId") + .IsUnique(); + + b.ToTable("admin_rank_flag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("admin_watchlists_id"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.HasKey("Id") + .HasName("PK_admin_watchlists"); + + b.HasIndex("CreatedById"); + + b.HasIndex("DeletedById"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_admin_watchlists_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_admin_watchlists_round_id"); + + b.ToTable("admin_watchlists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("antag_id"); + + b.Property("AntagName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("antag_name"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_antag"); + + b.HasIndex("ProfileId", "AntagName") + .IsUnique(); + + b.ToTable("antag", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.AssignedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("assigned_user_id_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_assigned_user_id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.HasIndex("UserName") + .IsUnique(); + + b.ToTable("assigned_user_id", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("connection_log_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("UserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("user_name"); + + b.HasKey("Id") + .HasName("PK_connection_log"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_connection_log_server_id"); + + b.HasIndex("Time"); + + b.HasIndex("UserId"); + + b.ToTable("connection_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_id"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("job_name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.HasKey("Id") + .HasName("PK_job"); + + b.HasIndex("ProfileId"); + + b.HasIndex("ProfileId", "JobName") + .IsUnique(); + + b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority") + .IsUnique() + .HasFilter("priority = 3"); + + b.ToTable("job", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PlayTime", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("play_time_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .HasColumnName("time_spent"); + + b.Property("Tracker") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tracker"); + + b.HasKey("Id") + .HasName("PK_play_time"); + + b.HasIndex("PlayerId", "Tracker") + .IsUnique(); + + b.ToTable("play_time", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenHWId") + .HasColumnType("BLOB") + .HasColumnName("last_seen_hwid"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_preference"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("preference", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("eye_color"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_color"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("facial_hair_name"); + + b.Property("FlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flavor_text"); + + b.Property("Gender") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("gender"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("sex"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("skin_color"); + + b.Property("Slot") + .HasColumnType("INTEGER") + .HasColumnName("slot"); + + b.Property("SpawnPriority") + .HasColumnType("INTEGER") + .HasColumnName("spawn_priority"); + + b.Property("Species") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("species"); + + b.HasKey("Id") + .HasName("PK_profile"); + + b.HasIndex("PreferenceId") + .HasDatabaseName("IX_profile_preference_id"); + + b.HasIndex("Slot", "PreferenceId") + .IsUnique(); + + b.ToTable("profile", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_id"); + + b.Property("LoadoutName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("loadout_name"); + + b.Property("ProfileLoadoutGroupId") + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout"); + + b.HasIndex("ProfileLoadoutGroupId"); + + b.ToTable("profile_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_loadout_group_id"); + + b.Property("GroupName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("group_name"); + + b.Property("ProfileRoleLoadoutId") + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.HasKey("Id") + .HasName("PK_profile_loadout_group"); + + b.HasIndex("ProfileRoleLoadoutId"); + + b.ToTable("profile_loadout_group", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("profile_role_loadout_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_profile_role_loadout"); + + b.HasIndex("ProfileId"); + + b.ToTable("profile_role_loadout", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("RoleId") + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.HasKey("PlayerUserId", "RoleId") + .HasName("PK_role_whitelists"); + + b.ToTable("role_whitelists", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .HasColumnName("start_date"); + + b.HasKey("Id") + .HasName("PK_round"); + + b.HasIndex("ServerId") + .HasDatabaseName("IX_round_server_id"); + + b.HasIndex("StartDate"); + + b.ToTable("round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("PK_server"); + + b.ToTable("server", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_id"); + + b.Property("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_ban_round_id"); + + b.ToTable("server_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.Property("Flags") + .HasColumnType("INTEGER") + .HasColumnName("flags"); + + b.HasKey("UserId") + .HasName("PK_server_ban_exemption"); + + b.ToTable("server_ban_exemption", null, t => + { + t.HasCheckConstraint("FlagsNotZero", "flags != 0"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_ban_hit_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("ConnectionId") + .HasColumnType("INTEGER") + .HasColumnName("connection_id"); + + b.HasKey("Id") + .HasName("PK_server_ban_hit"); + + b.HasIndex("BanId") + .HasDatabaseName("IX_server_ban_hit_ban_id"); + + b.HasIndex("ConnectionId") + .HasDatabaseName("IX_server_ban_hit_connection_id"); + + b.ToTable("server_ban_hit", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("server_role_ban_id"); + + b.Property("Address") + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("HWId") + .HasColumnType("BLOB") + .HasColumnName("hwid"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_id"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.HasKey("Id") + .HasName("PK_server_role_ban"); + + b.HasIndex("Address"); + + b.HasIndex("BanningAdmin"); + + b.HasIndex("LastEditedById"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_server_role_ban_player_user_id"); + + b.HasIndex("RoundId") + .HasDatabaseName("IX_server_role_ban_round_id"); + + b.ToTable("server_role_ban", null, t => + { + t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"); + }); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("role_unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_role_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_role_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("unban_id"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("unbanning_admin"); + + b.HasKey("Id") + .HasName("PK_server_unban"); + + b.HasIndex("BanId") + .IsUnique(); + + b.ToTable("server_unban", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("trait_id"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("TraitName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("trait_name"); + + b.HasKey("Id") + .HasName("PK_trait"); + + b.HasIndex("ProfileId", "TraitName") + .IsUnique(); + + b.ToTable("trait", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("uploaded_resource_log_id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_uploaded_resource_log"); + + b.ToTable("uploaded_resource_log", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Whitelist", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("UserId") + .HasName("PK_whitelist"); + + b.ToTable("whitelist", (string)null); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.Property("PlayersId") + .HasColumnType("INTEGER") + .HasColumnName("players_id"); + + b.Property("RoundsId") + .HasColumnType("INTEGER") + .HasColumnName("rounds_id"); + + b.HasKey("PlayersId", "RoundsId") + .HasName("PK_player_round"); + + b.HasIndex("RoundsId") + .HasDatabaseName("IX_player_round_rounds_id"); + + b.ToTable("player_round", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.HasOne("Content.Server.Database.AdminRank", "AdminRank") + .WithMany("Admins") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_admin_rank_admin_rank_id"); + + b.Navigation("AdminRank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminFlag", b => + { + b.HasOne("Content.Server.Database.Admin", "Admin") + .WithMany("Flags") + .HasForeignKey("AdminId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_flag_admin_admin_id"); + + b.Navigation("Admin"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany("AdminLogs") + .HasForeignKey("RoundId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_round_round_id"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminLogs") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_player_player_user_id"); + + b.HasOne("Content.Server.Database.AdminLog", "Log") + .WithMany("Players") + .HasForeignKey("RoundId", "LogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id"); + + b.Navigation("Log"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminMessage", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminMessagesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminMessagesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminMessagesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_messages_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminMessagesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_messages_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_messages_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminNote", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminNotesCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminNotesDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminNotesLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_notes_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminNotesReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_notes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_notes_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b => + { + b.HasOne("Content.Server.Database.AdminRank", "Rank") + .WithMany("Flags") + .HasForeignKey("AdminRankId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id"); + + b.Navigation("Rank"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminWatchlistsCreated") + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_created_by_id"); + + b.HasOne("Content.Server.Database.Player", "DeletedBy") + .WithMany("AdminWatchlistsDeleted") + .HasForeignKey("DeletedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_deleted_by_id"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminWatchlistsLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("AdminWatchlistsReceived") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("FK_admin_watchlists_player_player_user_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_admin_watchlists_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("DeletedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Player"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.Antag", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Antags") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_antag_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("ConnectionLogs") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.SetNull) + .IsRequired() + .HasConstraintName("FK_connection_log_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.HasOne("Content.Server.Database.Preference", "Preference") + .WithMany("Profiles") + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_preference_preference_id"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b => + { + b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup") + .WithMany("Loadouts") + .HasForeignKey("ProfileLoadoutGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id"); + + b.Navigation("ProfileLoadoutGroup"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout") + .WithMany("Groups") + .HasForeignKey("ProfileRoleLoadoutId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id"); + + b.Navigation("ProfileRoleLoadout"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Loadouts") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_profile_role_loadout_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("JobWhitelists") + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_role_whitelists_player_player_user_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.HasOne("Content.Server.Database.Server", "Server") + .WithMany("Rounds") + .HasForeignKey("ServerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_round_server_server_id"); + + b.Navigation("Server"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBanHit", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithMany("BanHits") + .HasForeignKey("BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_server_ban_ban_id"); + + b.HasOne("Content.Server.Database.ConnectionLog", "Connection") + .WithMany("BanHits") + .HasForeignKey("ConnectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_ban_hit_connection_log_connection_id"); + + b.Navigation("Ban"); + + b.Navigation("Connection"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany("AdminServerRoleBansCreated") + .HasForeignKey("BanningAdmin") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_banning_admin"); + + b.HasOne("Content.Server.Database.Player", "LastEditedBy") + .WithMany("AdminServerRoleBansLastEdited") + .HasForeignKey("LastEditedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_server_role_ban_player_last_edited_by_id"); + + b.HasOne("Content.Server.Database.Round", "Round") + .WithMany() + .HasForeignKey("RoundId") + .HasConstraintName("FK_server_role_ban_round_round_id"); + + b.Navigation("CreatedBy"); + + b.Navigation("LastEditedBy"); + + b.Navigation("Round"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b => + { + b.HasOne("Content.Server.Database.ServerRoleBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerUnban", b => + { + b.HasOne("Content.Server.Database.ServerBan", "Ban") + .WithOne("Unban") + .HasForeignKey("Content.Server.Database.ServerUnban", "BanId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_server_unban_server_ban_ban_id"); + + b.Navigation("Ban"); + }); + + modelBuilder.Entity("Content.Server.Database.Trait", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Traits") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_trait_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("PlayerRound", b => + { + b.HasOne("Content.Server.Database.Player", null) + .WithMany() + .HasForeignKey("PlayersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_player_players_id"); + + b.HasOne("Content.Server.Database.Round", null) + .WithMany() + .HasForeignKey("RoundsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_player_round_round_rounds_id"); + }); + + modelBuilder.Entity("Content.Server.Database.Admin", b => + { + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminLog", b => + { + b.Navigation("Players"); + }); + + modelBuilder.Entity("Content.Server.Database.AdminRank", b => + { + b.Navigation("Admins"); + + b.Navigation("Flags"); + }); + + modelBuilder.Entity("Content.Server.Database.ConnectionLog", b => + { + b.Navigation("BanHits"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.Navigation("AdminLogs"); + + b.Navigation("AdminMessagesCreated"); + + b.Navigation("AdminMessagesDeleted"); + + b.Navigation("AdminMessagesLastEdited"); + + b.Navigation("AdminMessagesReceived"); + + b.Navigation("AdminNotesCreated"); + + b.Navigation("AdminNotesDeleted"); + + b.Navigation("AdminNotesLastEdited"); + + b.Navigation("AdminNotesReceived"); + + b.Navigation("AdminServerBansCreated"); + + b.Navigation("AdminServerBansLastEdited"); + + b.Navigation("AdminServerRoleBansCreated"); + + b.Navigation("AdminServerRoleBansLastEdited"); + + b.Navigation("AdminWatchlistsCreated"); + + b.Navigation("AdminWatchlistsDeleted"); + + b.Navigation("AdminWatchlistsLastEdited"); + + b.Navigation("AdminWatchlistsReceived"); + + b.Navigation("JobWhitelists"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("Antags"); + + b.Navigation("Jobs"); + + b.Navigation("Loadouts"); + + b.Navigation("Traits"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b => + { + b.Navigation("Loadouts"); + }); + + modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b => + { + b.Navigation("Groups"); + }); + + modelBuilder.Entity("Content.Server.Database.Round", b => + { + b.Navigation("AdminLogs"); + }); + + modelBuilder.Entity("Content.Server.Database.Server", b => + { + b.Navigation("ConnectionLogs"); + + b.Navigation("Rounds"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerBan", b => + { + b.Navigation("BanHits"); + + b.Navigation("Unban"); + }); + + modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b => + { + b.Navigation("Unban"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs new file mode 100644 index 00000000000..f97c62f6ccf --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20240621120705_ConnectionLogTimeIndex.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class ConnectionLogTimeIndex : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_connection_log_time", + table: "connection_log", + column: "time"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_connection_log_time", + table: "connection_log"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 2c444c83eb3..17f87cf2e39 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -528,6 +528,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasIndex("ServerId") .HasDatabaseName("IX_connection_log_server_id"); + b.HasIndex("Time"); + b.HasIndex("UserId"); b.ToTable("connection_log", (string)null); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index 164cf421bd2..d57b8299053 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -187,6 +187,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.Entity() .HasIndex(p => p.UserId); + modelBuilder.Entity() + .HasIndex(p => p.Time); + modelBuilder.Entity() .Property(p => p.ServerId) .HasDefaultValue(0); From b7c088fbf36304c41a8c8944bf9f561877487afc Mon Sep 17 00:00:00 2001 From: Alex Evgrashin Date: Sat, 22 Jun 2024 03:11:40 +0200 Subject: [PATCH 082/109] Fix SSD indicator for scaled humanoids (#29310) Fix ssd --- Resources/Textures/Effects/ssd.rsi/default0.png | Bin 200 -> 524 bytes Resources/Textures/Effects/ssd.rsi/meta.json | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Textures/Effects/ssd.rsi/default0.png b/Resources/Textures/Effects/ssd.rsi/default0.png index bfd8c84da99a874861d8ef47b8c7f4891adf1901..962a7298cf390486a7cce55807e8f7f37876500c 100644 GIT binary patch literal 524 zcmV+n0`vWeP)EX>4Tx04R}tkv&MmKpe$iQ?()$2ZM+>1gS3CMMWG-6^me@v=v%)FuC+YXws0h zxHt-~1qVMCs}3&Cx;nTDg5U>;vxAeOiqYdvy6CZAq~&*bq^n3@4`IG``n+SSIt=r@JYlGW|$S?b>f-L ziotoGSY$<6B|ayfFzJHCk6c$=e&bwlSm2qWnNH3Ti^Ni)jg>ZLMN=c5CJw2ZPWeK{ zWtH<5XRTCa&3p0}26Osyit99o5yK+lNI-&uDmG9;1z}osQY<8CKkDHha{LK$DdgG! zBgZ_-&>%a0@IUz7t(Bjebd!Qnp!3DHKSqJTF3_mi_V=-EH%AO zrG<`wzHQ**x}_<5z~v4w_+-eY>_~o^LOu_?pV2pEf&N>dXVvYkxsTHaAWdB@-2exN zz*vE@*F4_c-QL^3XPW)}0QMVl+o@b*A z>^!DBKmq|yP!a%h<49Aq65s$tU$QTJ<}4w~JlQVuEQ>bs_seLnM@=j)VGN}B{OCvk O0000sCu*ji*FV&-O}tMesBGd6M!n}jZ6ZublGDZCDc)yD?mBLf-*DNVMd9WF+a~{y ooB{uYtC&5K8W`Cm5}<^1V+enV!Z diff --git a/Resources/Textures/Effects/ssd.rsi/meta.json b/Resources/Textures/Effects/ssd.rsi/meta.json index 6c5e7b24e33..6e9db0d8ded 100644 --- a/Resources/Textures/Effects/ssd.rsi/meta.json +++ b/Resources/Textures/Effects/ssd.rsi/meta.json @@ -1,10 +1,10 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Take from https://github.com/Skyrat-SS13/Skyrat-tg/blob/92377cd6203bc4d93a6e289d7b857e19bb6cf338/modular_skyrat/modules/indicators/icons/ssd_indicator.dmi", + "copyright": "Take from https://github.com/Skyrat-SS13/Skyrat-tg/blob/92377cd6203bc4d93a6e289d7b857e19bb6cf338/modular_skyrat/modules/indicators/icons/ssd_indicator.dmi and changed by Macoron", "size": { - "x": 32, - "y": 32 + "x": 8, + "y": 8 }, "states": [ { From 0ff144af73ffc9e58ddb56c1460da72c7a1f9cb1 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 22 Jun 2024 01:12:48 +0000 Subject: [PATCH 083/109] 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 68ccd7a6d4c..a209fdd1dad 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,11 +1,4 @@ Entries: -- author: Tayrtahn - changes: - - message: Some devices may have broken wiring at the start of each round. - type: Add - id: 6299 - time: '2024-04-04T06:28:09.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26695 - author: lzk228 changes: - message: Turning off thrusters and gyroscopes now stop consume power. @@ -3850,3 +3843,10 @@ id: 6798 time: '2024-06-21T10:50:53.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29293 +- author: Macoron + changes: + - message: Fixed SSD indicator for dwarfs. + type: Fix + id: 6799 + time: '2024-06-22T01:11:40.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29310 From 6aa7e2edce0d983d0e927896ac4ee41d7045c888 Mon Sep 17 00:00:00 2001 From: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com> Date: Sat, 22 Jun 2024 04:45:33 +0300 Subject: [PATCH 084/109] Fix noticeboard drawdepth (#29262) darwdepth added --- .../Prototypes/Entities/Structures/Wallmounts/noticeboard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml index f2315583e3c..2da4fbc3c4c 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/noticeboard.yml @@ -7,6 +7,7 @@ components: - type: WallMount - type: Sprite + drawdepth: WallMountedItems sprite: Structures/Wallmounts/noticeboard.rsi layers: - state: noticeboard From e0a6604d062a5de3e20be3cbd5d845bc0cd675f2 Mon Sep 17 00:00:00 2001 From: Doomsdrayk Date: Fri, 21 Jun 2024 19:46:51 -0600 Subject: [PATCH 085/109] Fix pistols not displaying ammo count in-hand (#29289) * Make Drozd and C-20r not unwield on use * Fix wielding mispredict * add AmmoCounter to pistols --- .../Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml index f3a8215e3d7..ae309661fee 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Pistols/pistols.yml @@ -58,6 +58,7 @@ containers: gun_magazine: !type:ContainerSlot gun_chamber: !type:ContainerSlot + - type: AmmoCounter - type: MagazineVisuals magState: mag steps: 1 From 07fe1a6b5a0724a266e99781f697f423fe2badd5 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Sat, 22 Jun 2024 06:11:14 +0200 Subject: [PATCH 086/109] Rewrite the options menu (#28389) * Basic attempt at rewriting how the options menu works, move accessibility settings into their own tab. * Audio tab uses the new options system. * Rewrite Misc tab * Clean up heading styling * Rewrite options tab and other minor cleanup all over the place. * Documentation comments and minor cleanup. --------- Co-authored-by: AJCM Co-authored-by: metalgearsloth --- Content.Client/Options/UI/OptionDropDown.xaml | 6 + .../Options/UI/OptionDropDown.xaml.cs | 21 + Content.Client/Options/UI/OptionSlider.xaml | 7 + .../Options/UI/OptionSlider.xaml.cs | 22 + Content.Client/Options/UI/OptionsMenu.xaml | 1 + Content.Client/Options/UI/OptionsMenu.xaml.cs | 9 +- .../Options/UI/OptionsTabControlRow.xaml | 18 + .../Options/UI/OptionsTabControlRow.xaml.cs | 684 ++++++++++++++++++ .../Options/UI/Tabs/AccessibilityTab.xaml | 16 + .../Options/UI/Tabs/AccessibilityTab.xaml.cs | 24 + Content.Client/Options/UI/Tabs/AudioTab.xaml | 124 +--- .../Options/UI/Tabs/AudioTab.xaml.cs | 248 ++----- .../Options/UI/Tabs/GraphicsTab.xaml | 79 +- .../Options/UI/Tabs/GraphicsTab.xaml.cs | 372 +++++----- Content.Client/Options/UI/Tabs/MiscTab.xaml | 54 +- .../Options/UI/Tabs/MiscTab.xaml.cs | 213 +----- .../en-US/escape-menu/ui/options-menu.ftl | 36 +- 17 files changed, 1137 insertions(+), 797 deletions(-) create mode 100644 Content.Client/Options/UI/OptionDropDown.xaml create mode 100644 Content.Client/Options/UI/OptionDropDown.xaml.cs create mode 100644 Content.Client/Options/UI/OptionSlider.xaml create mode 100644 Content.Client/Options/UI/OptionSlider.xaml.cs create mode 100644 Content.Client/Options/UI/OptionsTabControlRow.xaml create mode 100644 Content.Client/Options/UI/OptionsTabControlRow.xaml.cs create mode 100644 Content.Client/Options/UI/Tabs/AccessibilityTab.xaml create mode 100644 Content.Client/Options/UI/Tabs/AccessibilityTab.xaml.cs diff --git a/Content.Client/Options/UI/OptionDropDown.xaml b/Content.Client/Options/UI/OptionDropDown.xaml new file mode 100644 index 00000000000..58dcdca6c8b --- /dev/null +++ b/Content.Client/Options/UI/OptionDropDown.xaml @@ -0,0 +1,6 @@ + + + + diff --git a/Content.Client/Options/UI/OptionDropDown.xaml.cs b/Content.Client/Options/UI/OptionDropDown.xaml.cs new file mode 100644 index 00000000000..506e241a06e --- /dev/null +++ b/Content.Client/Options/UI/OptionDropDown.xaml.cs @@ -0,0 +1,21 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; + +namespace Content.Client.Options.UI; + +/// +/// Standard UI control used for drop-downs in the options menu. Intended for use with . +/// +/// +[GenerateTypedNameReferences] +public sealed partial class OptionDropDown : Control +{ + /// + /// The text describing what this drop-down controls. + /// + public string? Title + { + get => NameLabel.Text; + set => NameLabel.Text = value; + } +} diff --git a/Content.Client/Options/UI/OptionSlider.xaml b/Content.Client/Options/UI/OptionSlider.xaml new file mode 100644 index 00000000000..fa2d78c67ff --- /dev/null +++ b/Content.Client/Options/UI/OptionSlider.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/Content.Client/Options/UI/OptionSlider.xaml.cs b/Content.Client/Options/UI/OptionSlider.xaml.cs new file mode 100644 index 00000000000..6a377f7ee19 --- /dev/null +++ b/Content.Client/Options/UI/OptionSlider.xaml.cs @@ -0,0 +1,22 @@ +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface; + +namespace Content.Client.Options.UI; + +/// +/// Standard UI control used for sliders in the options menu. Intended for use with . +/// +/// +/// +[GenerateTypedNameReferences] +public sealed partial class OptionSlider : Control +{ + /// + /// The text describing what this slider controls. + /// + public string? Title + { + get => NameLabel.Text; + set => NameLabel.Text = value; + } +} diff --git a/Content.Client/Options/UI/OptionsMenu.xaml b/Content.Client/Options/UI/OptionsMenu.xaml index 4f624c1bb69..90486a196ad 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml +++ b/Content.Client/Options/UI/OptionsMenu.xaml @@ -7,5 +7,6 @@ + diff --git a/Content.Client/Options/UI/OptionsMenu.xaml.cs b/Content.Client/Options/UI/OptionsMenu.xaml.cs index 35a3f751bbf..61037f4e4af 100644 --- a/Content.Client/Options/UI/OptionsMenu.xaml.cs +++ b/Content.Client/Options/UI/OptionsMenu.xaml.cs @@ -1,9 +1,6 @@ using Robust.Client.AutoGenerated; using Robust.Client.UserInterface.CustomControls; using Robust.Client.UserInterface.XAML; -using Robust.Shared.IoC; -using Content.Client.Options.UI.Tabs; - namespace Content.Client.Options.UI { @@ -19,13 +16,17 @@ public OptionsMenu() Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-graphics")); Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls")); Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio")); + Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-accessibility")); UpdateTabs(); } public void UpdateTabs() { - GraphicsTab.UpdateProperties(); + GraphicsTab.Control.ReloadValues(); + MiscTab.Control.ReloadValues(); + AccessibilityTab.Control.ReloadValues(); + AudioTab.Control.ReloadValues(); } } } diff --git a/Content.Client/Options/UI/OptionsTabControlRow.xaml b/Content.Client/Options/UI/OptionsTabControlRow.xaml new file mode 100644 index 00000000000..fafdee4df76 --- /dev/null +++ b/Content.Client/Options/UI/OptionsTabControlRow.xaml @@ -0,0 +1,18 @@ + + + + + diff --git a/Content.Client/Administration/UI/Tabs/AtmosTab/AddAtmosWindow.xaml b/Content.Client/Administration/UI/Tabs/AtmosTab/AddAtmosWindow.xaml index 4a1719cbf8a..a7c4c35494d 100644 --- a/Content.Client/Administration/UI/Tabs/AtmosTab/AddAtmosWindow.xaml +++ b/Content.Client/Administration/UI/Tabs/AtmosTab/AddAtmosWindow.xaml @@ -1,11 +1,11 @@  + xmlns="https://spacestation14.io" Title="{Loc admin-ui-atmos-add}"> - - [ByRefEvent] -public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force) +public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force, bool StrongPry) { public readonly EntityUid User = User; + /// + /// Whether prying should be allowed even if whatever is being pried is powered. + /// public readonly bool PryPowered = PryPowered; + /// + /// Whether prying should be allowed to go through under most circumstances. (E.g. airlock is bolted). + /// Systems may still wish to ignore this occasionally. + /// public readonly bool Force = Force; + /// + /// Whether anything other than bare hands were used. This should only be false if prying is being performed without a prying comp. + /// + public readonly bool StrongPry = StrongPry; + public string? Message; public bool Cancelled; diff --git a/Content.Shared/Prying/Systems/PryingSystem.cs b/Content.Shared/Prying/Systems/PryingSystem.cs index 52459c4f744..10c80cfab59 100644 --- a/Content.Shared/Prying/Systems/PryingSystem.cs +++ b/Content.Shared/Prying/Systems/PryingSystem.cs @@ -109,7 +109,7 @@ private bool CanPry(EntityUid target, EntityUid user, out string? message, Pryin if (comp != null || Resolve(user, ref comp, false)) { - canev = new BeforePryEvent(user, comp.PryPowered, comp.Force); + canev = new BeforePryEvent(user, comp.PryPowered, comp.Force, true); } else { @@ -119,7 +119,7 @@ private bool CanPry(EntityUid target, EntityUid user, out string? message, Pryin return false; } - canev = new BeforePryEvent(user, false, false); + canev = new BeforePryEvent(user, false, false, false); } RaiseLocalEvent(target, ref canev); From 640115d646eadbf673622dcc0e3378deb548f285 Mon Sep 17 00:00:00 2001 From: PJBot Date: Sat, 22 Jun 2024 17:50:56 +0000 Subject: [PATCH 109/109] Automatic changelog update --- Resources/Changelog/Changelog.yml | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index ec80a583865..5dd0c08f283 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,13 +1,4 @@ Entries: -- author: Daemon - changes: - - message: Shooting targets can now have their popup type changed with a left click - to show total damage, damage of a single hit, both, or just a notice that it - was hit. - type: Tweak - id: 6307 - time: '2024-04-05T07:19:41.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/26734 - author: Vermidia changes: - message: Baseball bats now require a knife to craft @@ -3844,3 +3835,10 @@ id: 6806 time: '2024-06-22T15:47:02.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/29084 +- author: nikthechampiongr + changes: + - message: Locked firelocks can no longer be pried by hand when powered. + type: Tweak + id: 6807 + time: '2024-06-22T17:49:50.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/29221