Compare commits
429 Commits
ed-06-10-2
...
LocalHelpe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20f2ba6a3 | ||
|
|
c00de7fd09 | ||
|
|
ef6f23e4b8 | ||
|
|
06308961ed | ||
|
|
7a0fb9fc15 | ||
|
|
5bcc378bbe | ||
|
|
df4900e148 | ||
|
|
b106554cfe | ||
|
|
0196f6158a | ||
|
|
ae4fa34b90 | ||
|
|
45dc4739fd | ||
|
|
caee7c1f97 | ||
|
|
6f0bb473d2 | ||
|
|
84e07c1ded | ||
|
|
c8ab937cf0 | ||
|
|
6147740f3e | ||
|
|
2f91247fa2 | ||
|
|
8013b883c1 | ||
|
|
682b0f7b78 | ||
|
|
c7b230857f | ||
|
|
e062d386fc | ||
|
|
87dc3f9fa5 | ||
|
|
12a838ac8e | ||
|
|
ab7296ff4a | ||
|
|
ebac4a2eec | ||
|
|
7e4fb90e02 | ||
|
|
0f091b4cdf | ||
|
|
955145a9a5 | ||
|
|
ae27cf7e08 | ||
|
|
1579da881a | ||
|
|
24e6b9f42c | ||
|
|
8ddc5435a6 | ||
|
|
a9bdab0705 | ||
|
|
d18dba982b | ||
|
|
bcfacec287 | ||
|
|
1ded8e7f03 | ||
|
|
2382162d72 | ||
|
|
da19abdc76 | ||
|
|
564606a532 | ||
|
|
cb59826dcb | ||
|
|
b5df675187 | ||
|
|
c3d4303270 | ||
|
|
120386bb2d | ||
|
|
aef7dd514b | ||
|
|
8f87bad83c | ||
|
|
26cf6dc01a | ||
|
|
54c7a40967 | ||
|
|
01ac967640 | ||
|
|
a3f10ccfbb | ||
|
|
c21dd891f3 | ||
|
|
4ba8e351fe | ||
|
|
5b4f4237a3 | ||
|
|
75dd790aa6 | ||
|
|
ad78b30ca3 | ||
|
|
f5fac7c5f2 | ||
|
|
5d9acb7643 | ||
|
|
b8b958dfdb | ||
|
|
83f7d3f3df | ||
|
|
9a5152832a | ||
|
|
cc3a19c212 | ||
|
|
16a0b99daf | ||
|
|
98854c01d7 | ||
|
|
53df2848c4 | ||
|
|
a0ef431255 | ||
|
|
912ac2c069 | ||
|
|
d2a487dc9e | ||
|
|
d993582d00 | ||
|
|
4a39341e46 | ||
|
|
0ba3350c7e | ||
|
|
6f1eeba191 | ||
|
|
65462d8ca5 | ||
|
|
a3ce9b0db4 | ||
|
|
8c1281adf7 | ||
|
|
db4b2e0a6f | ||
|
|
e7ca4b8f2f | ||
|
|
51f4ec84d5 | ||
|
|
7614c2fba7 | ||
|
|
11f0dc420f | ||
|
|
131e492e6f | ||
|
|
9520e829de | ||
|
|
d1ab60f7bb | ||
|
|
4f68315584 | ||
|
|
2a6314bf90 | ||
|
|
1c2fd6a11b | ||
|
|
fcbf515203 | ||
|
|
b000a3e387 | ||
|
|
7276fff9c1 | ||
|
|
05ae40400c | ||
|
|
51b8101dc2 | ||
|
|
1c8eed8b45 | ||
|
|
2537bff7ba | ||
|
|
957b8de89b | ||
|
|
26194e2f41 | ||
|
|
146ae8a6a6 | ||
|
|
3382743086 | ||
|
|
2b9d949eea | ||
|
|
3b520ac69c | ||
|
|
7124eb5335 | ||
|
|
ac732c6180 | ||
|
|
844a92026f | ||
|
|
f9fea9e6ed | ||
|
|
b8a98de784 | ||
|
|
d7a1753c7d | ||
|
|
c9cd778133 | ||
|
|
c8259c6c0f | ||
|
|
e17d152838 | ||
|
|
b1a8eee52f | ||
|
|
6ce80d914d | ||
|
|
25b3898b28 | ||
|
|
d708a150e1 | ||
|
|
973aeb1aaa | ||
|
|
06da4fcc60 | ||
|
|
b3190b8935 | ||
|
|
59f1287aa3 | ||
|
|
835d0b4d4a | ||
|
|
cf0d6c482e | ||
|
|
45aa782ec4 | ||
|
|
56d62311b1 | ||
|
|
2282e3c352 | ||
|
|
d2baf18759 | ||
|
|
4626904fa8 | ||
|
|
1c2a96590b | ||
|
|
5c4b5d1572 | ||
|
|
d446a3e8e9 | ||
|
|
bca8d95191 | ||
|
|
a08da9d31f | ||
|
|
872adb5c93 | ||
|
|
55861b4fcf | ||
|
|
a875bf3c64 | ||
|
|
4261698371 | ||
|
|
f5e5646400 | ||
|
|
826bd1ab45 | ||
|
|
a3dc0eb75a | ||
|
|
fdd713a0f0 | ||
|
|
4b467685b2 | ||
|
|
827d00eb18 | ||
|
|
6834bc1fbd | ||
|
|
0f1e11c356 | ||
|
|
010638d0e9 | ||
|
|
d4db338ec6 | ||
|
|
0aa46f6a2a | ||
|
|
3d70cdf23c | ||
|
|
440da3c640 | ||
|
|
907a9cef10 | ||
|
|
16e23098db | ||
|
|
4e2af32ebe | ||
|
|
60f8c2c56f | ||
|
|
df53f9a01f | ||
|
|
2f57f11771 | ||
|
|
e6bbc3900f | ||
|
|
f5503352b9 | ||
|
|
817c53d98f | ||
|
|
cc80518d2f | ||
|
|
ae3cbd6092 | ||
|
|
a8c512c769 | ||
|
|
94e686ca9c | ||
|
|
a5a5840ee0 | ||
|
|
63f8aba359 | ||
|
|
6236d1abb3 | ||
|
|
dc3a2f6d28 | ||
|
|
7ac2d9a8bd | ||
|
|
dbef6cbecc | ||
|
|
e900ad7742 | ||
|
|
93315d4695 | ||
|
|
30017bc2d8 | ||
|
|
1ef5b2226a | ||
|
|
67332502c1 | ||
|
|
f86798792b | ||
|
|
0468c0f6bb | ||
|
|
24f79c3ecc | ||
|
|
a4717556e1 | ||
|
|
ee445c4938 | ||
|
|
08d0077719 | ||
|
|
b14c75390c | ||
|
|
7e526da521 | ||
|
|
4fbe50ab28 | ||
|
|
3e43d0695f | ||
|
|
2850665e33 | ||
|
|
c2404d6c0c | ||
|
|
a1c36dcb79 | ||
|
|
4252fdffb9 | ||
|
|
6486cdf183 | ||
|
|
546e1d8fa1 | ||
|
|
9d6e6257bd | ||
|
|
427817e237 | ||
|
|
a95c8baf28 | ||
|
|
08a4c4340d | ||
|
|
dcb615d678 | ||
|
|
97ef4637fd | ||
|
|
5a10bf0954 | ||
|
|
988b62074f | ||
|
|
66432c5cd8 | ||
|
|
6998fa83a4 | ||
|
|
e3f442e790 | ||
|
|
9b0ae98836 | ||
|
|
ae1c5572fc | ||
|
|
c28665c7d3 | ||
|
|
1307733c4b | ||
|
|
5ff5a72a7c | ||
|
|
c31f1ad130 | ||
|
|
0f6dd2905d | ||
|
|
f2b8713dc2 | ||
|
|
4e3c70a9d1 | ||
|
|
d4da9923ea | ||
|
|
79c35e0a41 | ||
|
|
5a86b88f8f | ||
|
|
92c49afcf2 | ||
|
|
0652c3f0b4 | ||
|
|
1d43bad57f | ||
|
|
1ebe48978e | ||
|
|
05a00515ed | ||
|
|
d2216835d8 | ||
|
|
3b0d8e63a1 | ||
|
|
c1d5e6fc4d | ||
|
|
445cad4955 | ||
|
|
ee8dedea9c | ||
|
|
2b02545f97 | ||
|
|
8f52a3448e | ||
|
|
5d6ec18b2a | ||
|
|
5e637aa7bf | ||
|
|
9873efd514 | ||
|
|
fc2bb79ef3 | ||
|
|
62f5a31c4a | ||
|
|
94bbf7262c | ||
|
|
5f1b848c08 | ||
|
|
8142ac007f | ||
|
|
7d91bcba3e | ||
|
|
a227c3bb42 | ||
|
|
dacac22663 | ||
|
|
afd4c73bec | ||
|
|
8a5d9a3321 | ||
|
|
41d3ccab8b | ||
|
|
04e422bd3f | ||
|
|
b5687e4c73 | ||
|
|
1d2ad3c335 | ||
|
|
0b8d6a1bd5 | ||
|
|
8c06677ff4 | ||
|
|
85463d380f | ||
|
|
c221ef06b9 | ||
|
|
eec533cb77 | ||
|
|
4c3fd3130d | ||
|
|
8cf5f93b9f | ||
|
|
628f51bb3d | ||
|
|
b7bd7c1d68 | ||
|
|
088ec569fe | ||
|
|
69849bfb30 | ||
|
|
a62ddf2f99 | ||
|
|
928877f0ef | ||
|
|
a4750b3a9e | ||
|
|
0a105213b4 | ||
|
|
77a2907535 | ||
|
|
73a82d5615 | ||
|
|
109e0bcf96 | ||
|
|
7e0e6416a7 | ||
|
|
c4233cc0a4 | ||
|
|
78e94b1623 | ||
|
|
bf14b3cdaa | ||
|
|
22938a9a56 | ||
|
|
091e4c1cb0 | ||
|
|
b137b0caa2 | ||
|
|
758c9e464e | ||
|
|
fca95ef250 | ||
|
|
cf1b3b0913 | ||
|
|
e04e3a6250 | ||
|
|
304d1d5934 | ||
|
|
15290486dc | ||
|
|
7fc27a18f6 | ||
|
|
23e4f81b30 | ||
|
|
f14177bf86 | ||
|
|
338781c243 | ||
|
|
3da9e93b3d | ||
|
|
c606f7144c | ||
|
|
103c1dc221 | ||
|
|
c78b5ae83d | ||
|
|
adb7aee831 | ||
|
|
ab86745c4e | ||
|
|
ea19a159f8 | ||
|
|
0bdc57dce8 | ||
|
|
22749db3fe | ||
|
|
aac3d73456 | ||
|
|
73b2b36243 | ||
|
|
81c654ddce | ||
|
|
ac120f7ce9 | ||
|
|
1518dc94a3 | ||
|
|
48f8aac732 | ||
|
|
3b3a7a6a73 | ||
|
|
876c44cd66 | ||
|
|
ea96e8a1cf | ||
|
|
a739b21b04 | ||
|
|
9a8d30022a | ||
|
|
b3c61530bf | ||
|
|
ddec2fff70 | ||
|
|
7039f87bd4 | ||
|
|
9889f8db9d | ||
|
|
79a23d3e0b | ||
|
|
fae5c89ef7 | ||
|
|
e891838d4b | ||
|
|
dccd00999c | ||
|
|
18c8a803ff | ||
|
|
8b74244ae9 | ||
|
|
1c49c638d8 | ||
|
|
0bee22e7e2 | ||
|
|
c027919618 | ||
|
|
dec9ff69ec | ||
|
|
05ac74dfcd | ||
|
|
cc17acbda8 | ||
|
|
8aac87a2fe | ||
|
|
8cafd7261d | ||
|
|
f387f66387 | ||
|
|
ebca84d28f | ||
|
|
2a22964e51 | ||
|
|
17319c7fc0 | ||
|
|
81a8646c4d | ||
|
|
87981f3886 | ||
|
|
5b4a7f6edc | ||
|
|
a83d6f0d87 | ||
|
|
5d89812abc | ||
|
|
31d39b36ae | ||
|
|
dc47bb2283 | ||
|
|
d259191d3d | ||
|
|
d871313899 | ||
|
|
7e2c6ea3ea | ||
|
|
7532d6f26e | ||
|
|
a7741fe9e2 | ||
|
|
6654e00411 | ||
|
|
8c17624896 | ||
|
|
519a6b2474 | ||
|
|
4425b77c90 | ||
|
|
f5ab4f5cb6 | ||
|
|
573f490896 | ||
|
|
5be82d2a7f | ||
|
|
a7339a5bf9 | ||
|
|
906913563e | ||
|
|
9b71757c07 | ||
|
|
97f6097dad | ||
|
|
30ada26315 | ||
|
|
dd6433c44b | ||
|
|
71c9894903 | ||
|
|
d79b34de9a | ||
|
|
59490d8259 | ||
|
|
4a5178a538 | ||
|
|
9ceb971535 | ||
|
|
2d60a4684c | ||
|
|
4e0018697f | ||
|
|
870eb439f3 | ||
|
|
0bec8af824 | ||
|
|
076c692878 | ||
|
|
796764d755 | ||
|
|
c7b0d5a27c | ||
|
|
56ba3fdf09 | ||
|
|
e5ad32fe93 | ||
|
|
30effd5ccd | ||
|
|
648505dc15 | ||
|
|
0e3f3536ff | ||
|
|
9bf7c44585 | ||
|
|
7af913ad13 | ||
|
|
8093a49943 | ||
|
|
52937a2e64 | ||
|
|
2e3db0e2c5 | ||
|
|
af72f2e17c | ||
|
|
7c6ff536eb | ||
|
|
1f6bab7957 | ||
|
|
561d55e7a4 | ||
|
|
7f42c0a531 | ||
|
|
40959cf422 | ||
|
|
214a20da8e | ||
|
|
b999752e88 | ||
|
|
561297495f | ||
|
|
4704309b4e | ||
|
|
d6d8c55d57 | ||
|
|
a6fc5e6ace | ||
|
|
70b7747fdd | ||
|
|
a9ecf805e5 | ||
|
|
729de28844 | ||
|
|
7a4cefa8ac | ||
|
|
e98af125dc | ||
|
|
bc64676747 | ||
|
|
e7b7e2270d | ||
|
|
740288eb96 | ||
|
|
93c7bdc134 | ||
|
|
3e078ab3e0 | ||
|
|
ac16a05fef | ||
|
|
327466a6e2 | ||
|
|
6d99597349 | ||
|
|
844b4d4616 | ||
|
|
fc1c709d44 | ||
|
|
5a41cc81b3 | ||
|
|
d450b613d6 | ||
|
|
b657aba2e1 | ||
|
|
ddaa0e83c6 | ||
|
|
1dbbf315c7 | ||
|
|
b9df2a495a | ||
|
|
357e998cbb | ||
|
|
384ff7e8f6 | ||
|
|
1ca7456363 | ||
|
|
dc2899c274 | ||
|
|
6f9b4f4226 | ||
|
|
28f576dae8 | ||
|
|
1366f6b405 | ||
|
|
0e0887bd49 | ||
|
|
1f04117edf | ||
|
|
922dd0bce6 | ||
|
|
33042b00d0 | ||
|
|
38fd54a1bf | ||
|
|
34df781668 | ||
|
|
f22f9e39c5 | ||
|
|
eecbfb63a0 | ||
|
|
6b10e00da4 | ||
|
|
a6c468b697 | ||
|
|
46a2eb545e | ||
|
|
35fc1b4037 | ||
|
|
867efe8b5b | ||
|
|
126c1786de | ||
|
|
386e59b462 | ||
|
|
eb4e422cb0 | ||
|
|
2287df13bd | ||
|
|
db64ad9c4d | ||
|
|
644d7ab682 | ||
|
|
ec4acdebbf | ||
|
|
94c8018ff3 | ||
|
|
fbb6f17add | ||
|
|
88f060d51a | ||
|
|
8b65186bfa | ||
|
|
9e054c1f9d | ||
|
|
423d394af1 | ||
|
|
295e63193c | ||
|
|
570c166517 | ||
|
|
7edd1c6d52 | ||
|
|
c0d67429ab |
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -19,7 +19,7 @@
|
||||
/Resources/engineCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
/Resources/clientCommandPerms.yml @moonheart08 @Chief-Engineer
|
||||
|
||||
/Resources/Prototypes/Maps/ @Emisse
|
||||
/Resources/Prototypes/Maps/** @Emisse
|
||||
|
||||
/Resources/Prototypes/Body/ @DrSmugleaf # suffering
|
||||
/Resources/Prototypes/Entities/Mobs/Player/ @DrSmugleaf
|
||||
|
||||
4
.github/workflows/publish.yml
vendored
4
.github/workflows/publish.yml
vendored
@@ -5,8 +5,8 @@ concurrency:
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 10 * * *'
|
||||
# schedule:
|
||||
# - cron: '0 10 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -23,6 +23,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
{
|
||||
private readonly IEntityManager _entManager;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
private readonly SharedNavMapSystem _navMapSystem;
|
||||
|
||||
private EntityUid? _owner;
|
||||
private NetEntity? _trackedEntity;
|
||||
@@ -42,19 +43,32 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
|
||||
private const float SilencingDuration = 2.5f;
|
||||
|
||||
// Colors
|
||||
private Color _wallColor = new Color(64, 64, 64);
|
||||
private Color _tileColor = new Color(28, 28, 28);
|
||||
private Color _monitorBlipColor = Color.Cyan;
|
||||
private Color _untrackedEntColor = Color.DimGray;
|
||||
private Color _regionBaseColor = new Color(154, 154, 154);
|
||||
private Color _inactiveColor = StyleNano.DisabledFore;
|
||||
private Color _statusTextColor = StyleNano.GoodGreenFore;
|
||||
private Color _goodColor = Color.LimeGreen;
|
||||
private Color _warningColor = new Color(255, 182, 72);
|
||||
private Color _dangerColor = new Color(255, 67, 67);
|
||||
|
||||
public AtmosAlertsComputerWindow(AtmosAlertsComputerBoundUserInterface userInterface, EntityUid? owner)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_entManager = IoCManager.Resolve<IEntityManager>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
_navMapSystem = _entManager.System<SharedNavMapSystem>();
|
||||
|
||||
// Pass the owner to nav map
|
||||
_owner = owner;
|
||||
NavMap.Owner = _owner;
|
||||
|
||||
// Set nav map colors
|
||||
NavMap.WallColor = new Color(64, 64, 64);
|
||||
NavMap.TileColor = Color.DimGray * NavMap.WallColor;
|
||||
NavMap.WallColor = _wallColor;
|
||||
NavMap.TileColor = _tileColor;
|
||||
|
||||
// Set nav map grid uid
|
||||
var stationName = Loc.GetString("atmos-alerts-window-unknown-location");
|
||||
@@ -179,6 +193,9 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
// Add tracked entities to the nav map
|
||||
foreach (var device in console.AtmosDevices)
|
||||
{
|
||||
if (!device.NetEntity.Valid)
|
||||
continue;
|
||||
|
||||
if (!NavMap.Visible)
|
||||
continue;
|
||||
|
||||
@@ -209,7 +226,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
if (consoleCoords != null && consoleUid != null)
|
||||
{
|
||||
var texture = _spriteSystem.Frame0(new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")));
|
||||
var blip = new NavMapBlip(consoleCoords.Value, texture, Color.Cyan, true, false);
|
||||
var blip = new NavMapBlip(consoleCoords.Value, texture, _monitorBlipColor, true, false);
|
||||
NavMap.TrackedEntities[consoleUid.Value] = blip;
|
||||
}
|
||||
|
||||
@@ -258,7 +275,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", StyleNano.GoodGreenFore.ToHexNoAlpha())));
|
||||
label.SetMarkup(Loc.GetString("atmos-alerts-window-no-active-alerts", ("color", _statusTextColor.ToHexNoAlpha())));
|
||||
|
||||
AlertsTable.AddChild(label);
|
||||
}
|
||||
@@ -270,6 +287,34 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
else
|
||||
MasterTabContainer.SetTabTitle(0, Loc.GetString("atmos-alerts-window-tab-alerts", ("value", activeAlarmCount)));
|
||||
|
||||
// Update sensor regions
|
||||
NavMap.RegionOverlays.Clear();
|
||||
var prioritizedRegionOverlays = new Dictionary<NavMapRegionOverlay, int>();
|
||||
|
||||
if (_owner != null &&
|
||||
_entManager.TryGetComponent<TransformComponent>(_owner, out var xform) &&
|
||||
_entManager.TryGetComponent<NavMapComponent>(xform.GridUid, out var navMap))
|
||||
{
|
||||
var regionOverlays = _navMapSystem.GetNavMapRegionOverlays(_owner.Value, navMap, AtmosAlertsComputerUiKey.Key);
|
||||
|
||||
foreach (var (regionOwner, regionOverlay) in regionOverlays)
|
||||
{
|
||||
var alarmState = GetAlarmState(regionOwner);
|
||||
|
||||
if (!TryGetSensorRegionColor(regionOwner, alarmState, out var regionColor))
|
||||
continue;
|
||||
|
||||
regionOverlay.Color = regionColor;
|
||||
|
||||
var priority = (_trackedEntity == regionOwner) ? 999 : (int)alarmState;
|
||||
prioritizedRegionOverlays.Add(regionOverlay, priority);
|
||||
}
|
||||
|
||||
// Sort overlays according to their priority
|
||||
var sortedOverlays = prioritizedRegionOverlays.OrderBy(x => x.Value).Select(x => x.Key).ToList();
|
||||
NavMap.RegionOverlays = sortedOverlays;
|
||||
}
|
||||
|
||||
// Auto-scroll re-enable
|
||||
if (_autoScrollAwaitsUpdate)
|
||||
{
|
||||
@@ -290,7 +335,7 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
var coords = _entManager.GetCoordinates(metaData.NetCoordinates);
|
||||
|
||||
if (_trackedEntity != null && _trackedEntity != metaData.NetEntity)
|
||||
color *= Color.DimGray;
|
||||
color *= _untrackedEntColor;
|
||||
|
||||
var selectable = true;
|
||||
var blip = new NavMapBlip(coords, _spriteSystem.Frame0(texture), color, _trackedEntity == metaData.NetEntity, selectable);
|
||||
@@ -298,6 +343,24 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
NavMap.TrackedEntities[metaData.NetEntity] = blip;
|
||||
}
|
||||
|
||||
private bool TryGetSensorRegionColor(NetEntity regionOwner, AtmosAlarmType alarmState, out Color color)
|
||||
{
|
||||
color = Color.White;
|
||||
|
||||
var blip = GetBlipTexture(alarmState);
|
||||
|
||||
if (blip == null)
|
||||
return false;
|
||||
|
||||
// Color the region based on alarm state and entity tracking
|
||||
color = blip.Value.Item2 * _regionBaseColor;
|
||||
|
||||
if (_trackedEntity != null && _trackedEntity != regionOwner)
|
||||
color *= _untrackedEntColor;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateUIEntry(AtmosAlertsComputerEntry entry, int index, Control table, AtmosAlertsComputerComponent console, AtmosAlertsFocusDeviceData? focusData = null)
|
||||
{
|
||||
// Make new UI entry if required
|
||||
@@ -534,13 +597,13 @@ public sealed partial class AtmosAlertsComputerWindow : FancyWindow
|
||||
switch (alarmState)
|
||||
{
|
||||
case AtmosAlarmType.Invalid:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), StyleNano.DisabledFore); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _inactiveColor); break;
|
||||
case AtmosAlarmType.Normal:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), Color.LimeGreen); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_circle.png")), _goodColor); break;
|
||||
case AtmosAlarmType.Warning:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), new Color(255, 182, 72)); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_triangle.png")), _warningColor); break;
|
||||
case AtmosAlarmType.Danger:
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), new Color(255, 67, 67)); break;
|
||||
output = (new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/NavMap/beveled_square.png")), _dangerColor); break;
|
||||
}
|
||||
|
||||
return output;
|
||||
|
||||
@@ -43,6 +43,8 @@ public sealed partial class ContentAudioSystem
|
||||
|
||||
private void CP14UpdateAmbientLoops()
|
||||
{
|
||||
return; //DISABLED UNTIL CLIENT ERROR SPAM FIXED
|
||||
|
||||
if (_timing.CurTime <= _nextUpdateTime)
|
||||
return;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Overlays;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Commands;
|
||||
@@ -34,7 +36,7 @@ public sealed class ShowHealthBarsCommand : LocalizedCommands
|
||||
{
|
||||
var showHealthBarsComponent = new ShowHealthBarsComponent
|
||||
{
|
||||
DamageContainers = args.ToList(),
|
||||
DamageContainers = args.Select(arg => new ProtoId<DamageContainerPrototype>(arg)).ToList(),
|
||||
HealthStatusIcon = null,
|
||||
NetSyncEnabled = false
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Client.Chat.Managers;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Input;
|
||||
@@ -71,6 +72,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
|
||||
public override void Init()
|
||||
{
|
||||
@@ -140,6 +142,12 @@ namespace Content.Client.Entry
|
||||
_configManager.SetCVar("interface.resolutionAutoScaleMinimum", 0.5f);
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
base.Shutdown();
|
||||
_titleWindowManager.Shutdown();
|
||||
}
|
||||
|
||||
public override void PostInit()
|
||||
{
|
||||
base.PostInit();
|
||||
@@ -160,6 +168,7 @@ namespace Content.Client.Entry
|
||||
_userInterfaceManager.SetDefaultTheme(_configManager.GetCVar(CCVars.UIDefaultInterfaceTheme));
|
||||
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
|
||||
_documentParsingManager.Initialize();
|
||||
_titleWindowManager.Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
|
||||
62
Content.Client/GameTicking/Managers/TitleWindowManager.cs
Normal file
62
Content.Client/GameTicking/Managers/TitleWindowManager.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client.GameTicking.Managers;
|
||||
|
||||
public sealed class TitleWindowManager
|
||||
{
|
||||
[Dependency] private readonly IBaseClient _client = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IGameController _gameController = default!;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_cfg.OnValueChanged(CVars.GameHostName, OnHostnameChange, true);
|
||||
_cfg.OnValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange, true);
|
||||
|
||||
_client.RunLevelChanged += OnRunLevelChangedChange;
|
||||
}
|
||||
|
||||
public void Shutdown()
|
||||
{
|
||||
_cfg.UnsubValueChanged(CVars.GameHostName, OnHostnameChange);
|
||||
_cfg.UnsubValueChanged(CCVars.GameHostnameInTitlebar, OnHostnameTitleChange);
|
||||
}
|
||||
|
||||
private void OnHostnameChange(string hostname)
|
||||
{
|
||||
var defaultWindowTitle = _gameController.GameTitle();
|
||||
|
||||
// Since the game assumes the server name is MyServer and that GameHostnameInTitlebar CCVar is true by default
|
||||
// Lets just... not show anything. This also is used to revert back to just the game title on disconnect.
|
||||
if (_client.RunLevel == ClientRunLevel.Initialize)
|
||||
{
|
||||
_clyde.SetWindowTitle(defaultWindowTitle);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cfg.GetCVar(CCVars.GameHostnameInTitlebar))
|
||||
// If you really dislike the dash I guess change it here
|
||||
_clyde.SetWindowTitle(hostname + " - " + defaultWindowTitle);
|
||||
else
|
||||
_clyde.SetWindowTitle(defaultWindowTitle);
|
||||
}
|
||||
|
||||
// Clients by default assume game.hostname_in_titlebar is true
|
||||
// but we need to clear it as soon as we join and actually receive the servers preference on this.
|
||||
// This will ensure we rerun OnHostnameChange and set the correct title bar name.
|
||||
private void OnHostnameTitleChange(bool colonthree)
|
||||
{
|
||||
OnHostnameChange(_cfg.GetCVar(CVars.GameHostName));
|
||||
}
|
||||
|
||||
// This is just used we can rerun the hostname change function when we disconnect to revert back to just the games title.
|
||||
private void OnRunLevelChangedChange(object? sender, RunLevelChangedEventArgs runLevelChangedEventArgs)
|
||||
{
|
||||
OnHostnameChange(_cfg.GetCVar(CVars.GameHostName));
|
||||
}
|
||||
}
|
||||
@@ -130,9 +130,9 @@ namespace Content.Client.Hands.Systems
|
||||
OnPlayerHandsAdded?.Invoke(hands);
|
||||
}
|
||||
|
||||
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null)
|
||||
public override void DoDrop(EntityUid uid, Hand hand, bool doDropInteraction = true, HandsComponent? hands = null, bool log = true)
|
||||
{
|
||||
base.DoDrop(uid, hand, doDropInteraction, hands);
|
||||
base.DoDrop(uid, hand, doDropInteraction, hands, log);
|
||||
|
||||
if (TryComp(hand.HeldEntity, out SpriteComponent? sprite))
|
||||
sprite.RenderOrder = EntityManager.CurrentTick.Value;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -16,19 +17,10 @@ public sealed partial class PlaytimeStatsEntry : ContainerButton
|
||||
|
||||
RoleLabel.Text = role;
|
||||
Playtime = playtime; // store the TimeSpan value directly
|
||||
PlaytimeLabel.Text = ConvertTimeSpanToHoursMinutes(playtime); // convert to string for display
|
||||
PlaytimeLabel.Text = ContentLocalizationManager.FormatPlaytime(playtime); // convert to string for display
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
}
|
||||
|
||||
private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
|
||||
{
|
||||
var hours = (int)timeSpan.TotalHours;
|
||||
var minutes = timeSpan.Minutes;
|
||||
|
||||
var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
|
||||
return formattedTimeLoc;
|
||||
}
|
||||
|
||||
public void UpdateShading(StyleBoxFlat styleBox)
|
||||
{
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
|
||||
@@ -104,8 +104,7 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow
|
||||
{
|
||||
var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime();
|
||||
|
||||
var formattedPlaytime = ConvertTimeSpanToHoursMinutes(overallPlaytime);
|
||||
OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", formattedPlaytime));
|
||||
OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", overallPlaytime));
|
||||
|
||||
var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles();
|
||||
|
||||
@@ -134,13 +133,4 @@ public sealed partial class PlaytimeStatsWindow : FancyWindow
|
||||
_sawmill.Error($"The provided playtime string '{playtimeString}' is not in the correct format.");
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
|
||||
{
|
||||
var hours = (int) timeSpan.TotalHours;
|
||||
var minutes = timeSpan.Minutes;
|
||||
|
||||
var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
|
||||
return formattedTimeLoc;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Client.Clickable;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
using Content.Client.Guidebook;
|
||||
using Content.Client.Launcher;
|
||||
@@ -57,6 +58,7 @@ namespace Content.Client.IoC
|
||||
collection.Register<DebugMonitorManager>();
|
||||
collection.Register<PlayerRateLimitManager>();
|
||||
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
||||
collection.Register<TitleWindowManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Buckle;
|
||||
using Content.Client.Gravity;
|
||||
using Content.Shared.ActionBlocker;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
|
||||
namespace Content.Client.Movement.Systems;
|
||||
|
||||
public sealed class WaddleAnimationSystem : SharedWaddleAnimationSystem
|
||||
{
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly GravitySystem _gravity = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly BuckleSystem _buckle = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeAllEvent<StartedWaddlingEvent>(OnStartWaddling);
|
||||
SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
|
||||
SubscribeAllEvent<StoppedWaddlingEvent>(OnStopWaddling);
|
||||
}
|
||||
|
||||
private void OnStartWaddling(StartedWaddlingEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
|
||||
StartWaddling((GetEntity(msg.Entity), comp));
|
||||
}
|
||||
|
||||
private void OnStopWaddling(StoppedWaddlingEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
if (TryComp<WaddleAnimationComponent>(GetEntity(msg.Entity), out var comp))
|
||||
StopWaddling((GetEntity(msg.Entity), comp));
|
||||
}
|
||||
|
||||
private void StartWaddling(Entity<WaddleAnimationComponent> entity)
|
||||
{
|
||||
if (_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
|
||||
return;
|
||||
|
||||
if (!TryComp<InputMoverComponent>(entity.Owner, out var mover))
|
||||
return;
|
||||
|
||||
if (_gravity.IsWeightless(entity.Owner))
|
||||
return;
|
||||
|
||||
if (!_actionBlocker.CanMove(entity.Owner, mover))
|
||||
return;
|
||||
|
||||
// Do nothing if buckled in
|
||||
if (_buckle.IsBuckled(entity.Owner))
|
||||
return;
|
||||
|
||||
// Do nothing if crit or dead (for obvious reasons)
|
||||
if (_mobState.IsIncapacitated(entity.Owner))
|
||||
return;
|
||||
|
||||
PlayWaddleAnimationUsing(
|
||||
(entity.Owner, entity.Comp),
|
||||
CalculateAnimationLength(entity.Comp, mover),
|
||||
CalculateTumbleIntensity(entity.Comp)
|
||||
);
|
||||
}
|
||||
|
||||
private static float CalculateTumbleIntensity(WaddleAnimationComponent component)
|
||||
{
|
||||
return component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
|
||||
}
|
||||
|
||||
private static float CalculateAnimationLength(WaddleAnimationComponent component, InputMoverComponent mover)
|
||||
{
|
||||
return mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
|
||||
}
|
||||
|
||||
private void OnAnimationCompleted(Entity<WaddleAnimationComponent> entity, ref AnimationCompletedEvent args)
|
||||
{
|
||||
if (args.Key != entity.Comp.KeyName)
|
||||
return;
|
||||
|
||||
if (!TryComp<InputMoverComponent>(entity.Owner, out var mover))
|
||||
return;
|
||||
|
||||
PlayWaddleAnimationUsing(
|
||||
(entity.Owner, entity.Comp),
|
||||
CalculateAnimationLength(entity.Comp, mover),
|
||||
CalculateTumbleIntensity(entity.Comp)
|
||||
);
|
||||
}
|
||||
|
||||
private void StopWaddling(Entity<WaddleAnimationComponent> entity)
|
||||
{
|
||||
if (!_animation.HasRunningAnimation(entity.Owner, entity.Comp.KeyName))
|
||||
return;
|
||||
|
||||
_animation.Stop(entity.Owner, entity.Comp.KeyName);
|
||||
|
||||
if (!TryComp<SpriteComponent>(entity.Owner, out var sprite))
|
||||
return;
|
||||
|
||||
sprite.Offset = new Vector2();
|
||||
sprite.Rotation = Angle.FromDegrees(0);
|
||||
}
|
||||
|
||||
private void PlayWaddleAnimationUsing(Entity<WaddleAnimationComponent> entity, float len, float tumbleIntensity)
|
||||
{
|
||||
entity.Comp.LastStep = !entity.Comp.LastStep;
|
||||
|
||||
var anim = new Animation()
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(len),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackComponentProperty()
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Rotation),
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(tumbleIntensity), len/2),
|
||||
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), len/2),
|
||||
}
|
||||
},
|
||||
new AnimationTrackComponentProperty()
|
||||
{
|
||||
ComponentType = typeof(SpriteComponent),
|
||||
Property = nameof(SpriteComponent.Offset),
|
||||
InterpolationMode = AnimationInterpolationMode.Linear,
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackProperty.KeyFrame(new Vector2(), 0),
|
||||
new AnimationTrackProperty.KeyFrame(entity.Comp.HopIntensity, len/2),
|
||||
new AnimationTrackProperty.KeyFrame(new Vector2(), len/2),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_animation.Play(entity.Owner, anim, entity.Comp.KeyName);
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,6 @@ public sealed class ShowThirstIconsSystem : EquipmentHudSystem<ShowThirstIconsCo
|
||||
return;
|
||||
|
||||
if (_thirst.TryGetStatusIconPrototype(component, out var iconPrototype))
|
||||
ev.StatusIcons.Add(iconPrototype!);
|
||||
ev.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,16 +76,16 @@ public sealed partial class StencilOverlay : Overlay
|
||||
|
||||
|
||||
//CP14 Overlays
|
||||
if (_entManager.TryGetComponent<CP14WorldEdgeComponent>(mapUid, out var worldEdge))
|
||||
if (_entManager.TryGetComponent<CP14CloudShadowsComponent>(mapUid, out var shadows))
|
||||
{
|
||||
DrawWorldEdge(args, worldEdge, invMatrix);
|
||||
DrawCloudShadows(args, shadows, invMatrix);
|
||||
}
|
||||
//CP14 Overlays end
|
||||
|
||||
//CP14 Overlays
|
||||
if (_entManager.TryGetComponent<CP14CloudShadowsComponent>(mapUid, out var shadows))
|
||||
if (_entManager.TryGetComponent<CP14WorldEdgeComponent>(mapUid, out var worldEdge))
|
||||
{
|
||||
DrawCloudShadows(args, shadows, invMatrix);
|
||||
DrawWorldEdge(args, worldEdge, invMatrix);
|
||||
}
|
||||
//CP14 Overlays end
|
||||
|
||||
|
||||
303
Content.Client/Pinpointer/NavMapSystem.Regions.cs
Normal file
303
Content.Client/Pinpointer/NavMapSystem.Regions.cs
Normal file
@@ -0,0 +1,303 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Pinpointer;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Pinpointer;
|
||||
|
||||
public sealed partial class NavMapSystem
|
||||
{
|
||||
private (AtmosDirection, Vector2i, AtmosDirection)[] _regionPropagationTable =
|
||||
{
|
||||
(AtmosDirection.East, new Vector2i(1, 0), AtmosDirection.West),
|
||||
(AtmosDirection.West, new Vector2i(-1, 0), AtmosDirection.East),
|
||||
(AtmosDirection.North, new Vector2i(0, 1), AtmosDirection.South),
|
||||
(AtmosDirection.South, new Vector2i(0, -1), AtmosDirection.North),
|
||||
};
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
// To prevent compute spikes, only one region is flood filled per frame
|
||||
var query = AllEntityQuery<NavMapComponent>();
|
||||
|
||||
while (query.MoveNext(out var ent, out var entNavMapRegions))
|
||||
FloodFillNextEnqueuedRegion(ent, entNavMapRegions);
|
||||
}
|
||||
|
||||
private void FloodFillNextEnqueuedRegion(EntityUid uid, NavMapComponent component)
|
||||
{
|
||||
if (!component.QueuedRegionsToFlood.Any())
|
||||
return;
|
||||
|
||||
var regionOwner = component.QueuedRegionsToFlood.Dequeue();
|
||||
|
||||
// If the region is no longer valid, flood the next one in the queue
|
||||
if (!component.RegionProperties.TryGetValue(regionOwner, out var regionProperties) ||
|
||||
!regionProperties.Seeds.Any())
|
||||
{
|
||||
FloodFillNextEnqueuedRegion(uid, component);
|
||||
return;
|
||||
}
|
||||
|
||||
// Flood fill the region, using the region seeds as starting points
|
||||
var (floodedTiles, floodedChunks) = FloodFillRegion(uid, component, regionProperties);
|
||||
|
||||
// Combine the flooded tiles into larger rectangles
|
||||
var gridCoords = GetMergedRegionTiles(floodedTiles);
|
||||
|
||||
// Create and assign the new region overlay
|
||||
var regionOverlay = new NavMapRegionOverlay(regionProperties.UiKey, gridCoords)
|
||||
{
|
||||
Color = regionProperties.Color
|
||||
};
|
||||
|
||||
component.RegionOverlays[regionOwner] = regionOverlay;
|
||||
|
||||
// To reduce unnecessary future flood fills, we will track which chunks have been flooded by a region owner
|
||||
|
||||
// First remove an old assignments
|
||||
if (component.RegionOwnerToChunkTable.TryGetValue(regionOwner, out var oldChunks))
|
||||
{
|
||||
foreach (var chunk in oldChunks)
|
||||
{
|
||||
if (component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var oldOwners))
|
||||
{
|
||||
oldOwners.Remove(regionOwner);
|
||||
component.ChunkToRegionOwnerTable[chunk] = oldOwners;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now update with the new assignments
|
||||
component.RegionOwnerToChunkTable[regionOwner] = floodedChunks;
|
||||
|
||||
foreach (var chunk in floodedChunks)
|
||||
{
|
||||
if (!component.ChunkToRegionOwnerTable.TryGetValue(chunk, out var owners))
|
||||
owners = new();
|
||||
|
||||
owners.Add(regionOwner);
|
||||
component.ChunkToRegionOwnerTable[chunk] = owners;
|
||||
}
|
||||
}
|
||||
|
||||
private (HashSet<Vector2i>, HashSet<Vector2i>) FloodFillRegion(EntityUid uid, NavMapComponent component, NavMapRegionProperties regionProperties)
|
||||
{
|
||||
if (!regionProperties.Seeds.Any())
|
||||
return (new(), new());
|
||||
|
||||
var visitedChunks = new HashSet<Vector2i>();
|
||||
var visitedTiles = new HashSet<Vector2i>();
|
||||
var tilesToVisit = new Stack<Vector2i>();
|
||||
|
||||
foreach (var regionSeed in regionProperties.Seeds)
|
||||
{
|
||||
tilesToVisit.Push(regionSeed);
|
||||
|
||||
while (tilesToVisit.Count > 0)
|
||||
{
|
||||
// If the max region area is hit, exit
|
||||
if (visitedTiles.Count > regionProperties.MaxArea)
|
||||
return (new(), new());
|
||||
|
||||
// Pop the top tile from the stack
|
||||
var current = tilesToVisit.Pop();
|
||||
|
||||
// If the current tile position has already been visited,
|
||||
// or is too far away from the seed, continue
|
||||
if ((regionSeed - current).Length > regionProperties.MaxRadius)
|
||||
continue;
|
||||
|
||||
if (visitedTiles.Contains(current))
|
||||
continue;
|
||||
|
||||
// Determine the tile's chunk index
|
||||
var chunkOrigin = SharedMapSystem.GetChunkIndices(current, ChunkSize);
|
||||
var relative = SharedMapSystem.GetChunkRelative(current, ChunkSize);
|
||||
var idx = GetTileIndex(relative);
|
||||
|
||||
// Extract the tile data
|
||||
if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
|
||||
continue;
|
||||
|
||||
var flag = chunk.TileData[idx];
|
||||
|
||||
// If the current tile is entirely occupied, continue
|
||||
if ((FloorMask & flag) == 0)
|
||||
continue;
|
||||
|
||||
if ((WallMask & flag) == WallMask)
|
||||
continue;
|
||||
|
||||
if ((AirlockMask & flag) == AirlockMask)
|
||||
continue;
|
||||
|
||||
// Otherwise the tile can be added to this region
|
||||
visitedTiles.Add(current);
|
||||
visitedChunks.Add(chunkOrigin);
|
||||
|
||||
// Determine if we can propagate the region into its cardinally adjacent neighbors
|
||||
// To propagate to a neighbor, movement into the neighbors closest edge must not be
|
||||
// blocked, and vice versa
|
||||
|
||||
foreach (var (direction, tileOffset, reverseDirection) in _regionPropagationTable)
|
||||
{
|
||||
if (!RegionCanPropagateInDirection(chunk, current, direction))
|
||||
continue;
|
||||
|
||||
var neighbor = current + tileOffset;
|
||||
var neighborOrigin = SharedMapSystem.GetChunkIndices(neighbor, ChunkSize);
|
||||
|
||||
if (!component.Chunks.TryGetValue(neighborOrigin, out var neighborChunk))
|
||||
continue;
|
||||
|
||||
visitedChunks.Add(neighborOrigin);
|
||||
|
||||
if (!RegionCanPropagateInDirection(neighborChunk, neighbor, reverseDirection))
|
||||
continue;
|
||||
|
||||
tilesToVisit.Push(neighbor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (visitedTiles, visitedChunks);
|
||||
}
|
||||
|
||||
private bool RegionCanPropagateInDirection(NavMapChunk chunk, Vector2i tile, AtmosDirection direction)
|
||||
{
|
||||
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
|
||||
var idx = GetTileIndex(relative);
|
||||
var flag = chunk.TileData[idx];
|
||||
|
||||
if ((FloorMask & flag) == 0)
|
||||
return false;
|
||||
|
||||
var directionMask = 1 << (int)direction;
|
||||
var wallMask = (int)direction << (int)NavMapChunkType.Wall;
|
||||
var airlockMask = (int)direction << (int)NavMapChunkType.Airlock;
|
||||
|
||||
if ((wallMask & flag) > 0)
|
||||
return false;
|
||||
|
||||
if ((airlockMask & flag) > 0)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<(Vector2i, Vector2i)> GetMergedRegionTiles(HashSet<Vector2i> tiles)
|
||||
{
|
||||
if (!tiles.Any())
|
||||
return new();
|
||||
|
||||
var x = tiles.Select(t => t.X);
|
||||
var minX = x.Min();
|
||||
var maxX = x.Max();
|
||||
|
||||
var y = tiles.Select(t => t.Y);
|
||||
var minY = y.Min();
|
||||
var maxY = y.Max();
|
||||
|
||||
var matrix = new int[maxX - minX + 1, maxY - minY + 1];
|
||||
|
||||
foreach (var tile in tiles)
|
||||
{
|
||||
var a = tile.X - minX;
|
||||
var b = tile.Y - minY;
|
||||
|
||||
matrix[a, b] = 1;
|
||||
}
|
||||
|
||||
return GetMergedRegionTiles(matrix, new Vector2i(minX, minY));
|
||||
}
|
||||
|
||||
private List<(Vector2i, Vector2i)> GetMergedRegionTiles(int[,] matrix, Vector2i offset)
|
||||
{
|
||||
var output = new List<(Vector2i, Vector2i)>();
|
||||
|
||||
var rows = matrix.GetLength(0);
|
||||
var cols = matrix.GetLength(1);
|
||||
|
||||
var dp = new int[rows, cols];
|
||||
var coords = (new Vector2i(), new Vector2i());
|
||||
var maxArea = 0;
|
||||
|
||||
var count = 0;
|
||||
|
||||
while (!IsArrayEmpty(matrix))
|
||||
{
|
||||
count++;
|
||||
|
||||
if (count > rows * cols)
|
||||
break;
|
||||
|
||||
// Clear old values
|
||||
dp = new int[rows, cols];
|
||||
coords = (new Vector2i(), new Vector2i());
|
||||
maxArea = 0;
|
||||
|
||||
// Initialize the first row of dp
|
||||
for (int j = 0; j < cols; j++)
|
||||
{
|
||||
dp[0, j] = matrix[0, j];
|
||||
}
|
||||
|
||||
// Calculate dp values for remaining rows
|
||||
for (int i = 1; i < rows; i++)
|
||||
{
|
||||
for (int j = 0; j < cols; j++)
|
||||
dp[i, j] = matrix[i, j] == 1 ? dp[i - 1, j] + 1 : 0;
|
||||
}
|
||||
|
||||
// Find the largest rectangular area seeded for each position in the matrix
|
||||
for (int i = 0; i < rows; i++)
|
||||
{
|
||||
for (int j = 0; j < cols; j++)
|
||||
{
|
||||
int minWidth = dp[i, j];
|
||||
|
||||
for (int k = j; k >= 0; k--)
|
||||
{
|
||||
if (dp[i, k] <= 0)
|
||||
break;
|
||||
|
||||
minWidth = Math.Min(minWidth, dp[i, k]);
|
||||
var currArea = Math.Max(maxArea, minWidth * (j - k + 1));
|
||||
|
||||
if (currArea > maxArea)
|
||||
{
|
||||
maxArea = currArea;
|
||||
coords = (new Vector2i(i - minWidth + 1, k), new Vector2i(i, j));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save the recorded rectangle vertices
|
||||
output.Add((coords.Item1 + offset, coords.Item2 + offset));
|
||||
|
||||
// Removed the tiles covered by the rectangle from matrix
|
||||
for (int i = coords.Item1.X; i <= coords.Item2.X; i++)
|
||||
{
|
||||
for (int j = coords.Item1.Y; j <= coords.Item2.Y; j++)
|
||||
matrix[i, j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private bool IsArrayEmpty(int[,] matrix)
|
||||
{
|
||||
for (int i = 0; i < matrix.GetLength(0); i++)
|
||||
{
|
||||
for (int j = 0; j < matrix.GetLength(1); j++)
|
||||
{
|
||||
if (matrix[i, j] == 1)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
@@ -16,6 +17,7 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
Dictionary<Vector2i, int[]> modifiedChunks;
|
||||
Dictionary<NetEntity, NavMapBeacon> beacons;
|
||||
Dictionary<NetEntity, NavMapRegionProperties> regions;
|
||||
|
||||
switch (args.Current)
|
||||
{
|
||||
@@ -23,6 +25,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
modifiedChunks = delta.ModifiedChunks;
|
||||
beacons = delta.Beacons;
|
||||
regions = delta.Regions;
|
||||
|
||||
foreach (var index in component.Chunks.Keys)
|
||||
{
|
||||
if (!delta.AllChunks!.Contains(index))
|
||||
@@ -35,6 +39,8 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
{
|
||||
modifiedChunks = state.Chunks;
|
||||
beacons = state.Beacons;
|
||||
regions = state.Regions;
|
||||
|
||||
foreach (var index in component.Chunks.Keys)
|
||||
{
|
||||
if (!state.Chunks.ContainsKey(index))
|
||||
@@ -47,13 +53,54 @@ public sealed partial class NavMapSystem : SharedNavMapSystem
|
||||
return;
|
||||
}
|
||||
|
||||
// Update region data and queue new regions for flooding
|
||||
var prevRegionOwners = component.RegionProperties.Keys.ToList();
|
||||
var validRegionOwners = new List<NetEntity>();
|
||||
|
||||
component.RegionProperties.Clear();
|
||||
|
||||
foreach (var (regionOwner, regionData) in regions)
|
||||
{
|
||||
if (!regionData.Seeds.Any())
|
||||
continue;
|
||||
|
||||
component.RegionProperties[regionOwner] = regionData;
|
||||
validRegionOwners.Add(regionOwner);
|
||||
|
||||
if (component.RegionOverlays.ContainsKey(regionOwner))
|
||||
continue;
|
||||
|
||||
if (component.QueuedRegionsToFlood.Contains(regionOwner))
|
||||
continue;
|
||||
|
||||
component.QueuedRegionsToFlood.Enqueue(regionOwner);
|
||||
}
|
||||
|
||||
// Remove stale region owners
|
||||
var regionOwnersToRemove = prevRegionOwners.Except(validRegionOwners);
|
||||
|
||||
foreach (var regionOwnerRemoved in regionOwnersToRemove)
|
||||
RemoveNavMapRegion(uid, component, regionOwnerRemoved);
|
||||
|
||||
// Modify chunks
|
||||
foreach (var (origin, chunk) in modifiedChunks)
|
||||
{
|
||||
var newChunk = new NavMapChunk(origin);
|
||||
Array.Copy(chunk, newChunk.TileData, chunk.Length);
|
||||
component.Chunks[origin] = newChunk;
|
||||
|
||||
// If the affected chunk intersects one or more regions, re-flood them
|
||||
if (!component.ChunkToRegionOwnerTable.TryGetValue(origin, out var affectedOwners))
|
||||
continue;
|
||||
|
||||
foreach (var affectedOwner in affectedOwners)
|
||||
{
|
||||
if (!component.QueuedRegionsToFlood.Contains(affectedOwner))
|
||||
component.QueuedRegionsToFlood.Enqueue(affectedOwner);
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh beacons
|
||||
component.Beacons.Clear();
|
||||
foreach (var (nuid, beacon) in beacons)
|
||||
{
|
||||
|
||||
@@ -48,6 +48,7 @@ public partial class NavMapControl : MapGridControl
|
||||
public List<(Vector2, Vector2)> TileLines = new();
|
||||
public List<(Vector2, Vector2)> TileRects = new();
|
||||
public List<(Vector2[], Color)> TilePolygons = new();
|
||||
public List<NavMapRegionOverlay> RegionOverlays = new();
|
||||
|
||||
// Default colors
|
||||
public Color WallColor = new(102, 217, 102);
|
||||
@@ -228,7 +229,7 @@ public partial class NavMapControl : MapGridControl
|
||||
{
|
||||
if (!blip.Selectable)
|
||||
continue;
|
||||
|
||||
|
||||
var currentDistance = (_transformSystem.ToMapCoordinates(blip.Coordinates).Position - worldPosition).Length();
|
||||
|
||||
if (closestDistance < currentDistance || currentDistance * MinimapScale > MaxSelectableDistance)
|
||||
@@ -319,6 +320,22 @@ public partial class NavMapControl : MapGridControl
|
||||
}
|
||||
}
|
||||
|
||||
// Draw region overlays
|
||||
if (_grid != null)
|
||||
{
|
||||
foreach (var regionOverlay in RegionOverlays)
|
||||
{
|
||||
foreach (var gridCoords in regionOverlay.GridCoords)
|
||||
{
|
||||
var positionTopLeft = ScalePosition(new Vector2(gridCoords.Item1.X, -gridCoords.Item1.Y) - new Vector2(offset.X, -offset.Y));
|
||||
var positionBottomRight = ScalePosition(new Vector2(gridCoords.Item2.X + _grid.TileSize, -gridCoords.Item2.Y - _grid.TileSize) - new Vector2(offset.X, -offset.Y));
|
||||
|
||||
var box = new UIBox2(positionTopLeft, positionBottomRight);
|
||||
handle.DrawRect(box, regionOverlay.Color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw map lines
|
||||
if (TileLines.Any())
|
||||
{
|
||||
|
||||
@@ -51,6 +51,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
{
|
||||
// Reset on disconnect, just in case.
|
||||
_roles.Clear();
|
||||
_jobWhitelists.Clear();
|
||||
_roleBans.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +60,6 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
{
|
||||
_sawmill.Debug($"Received roleban info containing {message.Bans.Count} entries.");
|
||||
|
||||
if (_roleBans.Equals(message.Bans))
|
||||
return;
|
||||
|
||||
_roleBans.Clear();
|
||||
_roleBans.AddRange(message.Bans);
|
||||
Updated?.Invoke();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using Content.Client.Power.APC.UI;
|
||||
using Content.Client.Power.APC.UI;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.APC;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Client.Power.APC
|
||||
{
|
||||
@@ -22,6 +23,14 @@ namespace Content.Client.Power.APC
|
||||
_menu = this.CreateWindow<ApcMenu>();
|
||||
_menu.SetEntity(Owner);
|
||||
_menu.OnBreaker += BreakerPressed;
|
||||
|
||||
var hasAccess = false;
|
||||
if (PlayerManager.LocalEntity != null)
|
||||
{
|
||||
var accessReader = EntMan.System<AccessReaderSystem>();
|
||||
hasAccess = accessReader.IsAllowed((EntityUid)PlayerManager.LocalEntity, Owner);
|
||||
}
|
||||
_menu?.SetAccessEnabled(hasAccess);
|
||||
}
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -36,19 +36,9 @@ namespace Content.Client.Power.APC.UI
|
||||
{
|
||||
var castState = (ApcBoundInterfaceState) state;
|
||||
|
||||
if (BreakerButton != null)
|
||||
if (!BreakerButton.Disabled)
|
||||
{
|
||||
if(castState.HasAccess == false)
|
||||
{
|
||||
BreakerButton.Disabled = true;
|
||||
BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access");
|
||||
}
|
||||
else
|
||||
{
|
||||
BreakerButton.Disabled = false;
|
||||
BreakerButton.ToolTip = null;
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
}
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
}
|
||||
|
||||
if (PowerLabel != null)
|
||||
@@ -86,6 +76,20 @@ namespace Content.Client.Power.APC.UI
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAccessEnabled(bool hasAccess)
|
||||
{
|
||||
if(hasAccess)
|
||||
{
|
||||
BreakerButton.Disabled = false;
|
||||
BreakerButton.ToolTip = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
BreakerButton.Disabled = true;
|
||||
BreakerButton.ToolTip = Loc.GetString("apc-component-insufficient-access");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateChargeBarColor(float charge)
|
||||
{
|
||||
if (ChargeBar == null)
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Content.Client.Stylesheets
|
||||
public static readonly Color ButtonColorDefaultRed = Color.FromHex("#D43B3B");
|
||||
public static readonly Color ButtonColorHovered = Color.FromHex("#7f6357");
|
||||
public static readonly Color ButtonColorHoveredRed = Color.FromHex("#DF6B6B");
|
||||
public static readonly Color ButtonColorPressed = Color.FromHex("#6c4a3e");
|
||||
public static readonly Color ButtonColorPressed = Color.FromHex("#4a332b");
|
||||
public static readonly Color ButtonColorDisabled = Color.FromHex("#3c3330");
|
||||
|
||||
public static readonly Color ButtonColorCautionDefault = Color.FromHex("#ab3232");
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
Margin="8 0 8 0">
|
||||
<BoxContainer Name="Buttons"
|
||||
Orientation="Vertical"
|
||||
SeparationOverride="5">
|
||||
<!-- Buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -10,20 +10,17 @@ using Robust.Shared.Utility;
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostRolesEntry : BoxContainer
|
||||
public sealed partial class GhostRoleButtonsBox : BoxContainer
|
||||
{
|
||||
private SpriteSystem _spriteSystem;
|
||||
public event Action<GhostRoleInfo>? OnRoleSelected;
|
||||
public event Action<GhostRoleInfo>? OnRoleFollow;
|
||||
|
||||
public GhostRolesEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
public GhostRoleButtonsBox(bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_spriteSystem = spriteSystem;
|
||||
|
||||
Title.Text = name;
|
||||
Description.SetMessage(description);
|
||||
|
||||
foreach (var role in roles)
|
||||
{
|
||||
var button = new GhostRoleEntryButtons(role);
|
||||
@@ -1,15 +1,15 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Horizontal">
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Stretch">
|
||||
<Button Name="RequestButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'ghost-roles-window-request-role-button'}"
|
||||
StyleClasses="OpenRight"
|
||||
HorizontalAlignment="Left"
|
||||
SetWidth="300"/>
|
||||
HorizontalExpand="True"
|
||||
SizeFlagsStretchRatio="3"/>
|
||||
<Button Name="FollowButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'ghost-roles-window-follow-role-button'}"
|
||||
StyleClasses="OpenLeft"
|
||||
HorizontalAlignment="Right"
|
||||
SetWidth="150"/>
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical">
|
||||
<Label Name="Title"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<RichTextLabel Name="Description"
|
||||
Margin="0 4"/>
|
||||
</BoxContainer>
|
||||
@@ -0,0 +1,18 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GhostRoleInfoBox : BoxContainer
|
||||
{
|
||||
public GhostRoleInfoBox(string name, string description)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Title.Text = name;
|
||||
Description.SetMessage(description);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 8 8">
|
||||
<Label Name="Title"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
<PanelContainer StyleClasses="HighDivider" />
|
||||
<RichTextLabel Name="Description"
|
||||
Margin="0 4"/>
|
||||
<BoxContainer Name="Buttons"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Vertical"
|
||||
SeparationOverride="5">
|
||||
<!-- Buttons are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.Eui;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
{
|
||||
@@ -77,6 +76,13 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
|
||||
if (state is not GhostRolesEuiState ghostState)
|
||||
return;
|
||||
|
||||
// We must save BodyVisible state, so all Collapsible boxes will not close
|
||||
// on adding new ghost role.
|
||||
// Save the current state of each Collapsible box being visible or not
|
||||
_window.SaveCollapsibleBoxesStates();
|
||||
|
||||
// Clearing the container before adding new roles
|
||||
_window.ClearEntries();
|
||||
|
||||
var entityManager = IoCManager.Resolve<IEntityManager>();
|
||||
@@ -84,28 +90,32 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
var spriteSystem = sysManager.GetEntitySystem<SpriteSystem>();
|
||||
var requirementsManager = IoCManager.Resolve<JobRequirementsManager>();
|
||||
|
||||
// TODO: role.Requirements value doesn't work at all as an equality key, this must be fixed
|
||||
// Grouping roles
|
||||
var groupedRoles = ghostState.GhostRoles.GroupBy(
|
||||
role => (role.Name, role.Description, role.Requirements));
|
||||
|
||||
// Add a new entry for each role group
|
||||
foreach (var group in groupedRoles)
|
||||
{
|
||||
var name = group.Key.Name;
|
||||
var description = group.Key.Description;
|
||||
bool hasAccess = true;
|
||||
FormattedMessage? reason;
|
||||
|
||||
if (!requirementsManager.CheckRoleRequirements(group.Key.Requirements, null, out reason))
|
||||
{
|
||||
hasAccess = false;
|
||||
}
|
||||
var hasAccess = requirementsManager.CheckRoleRequirements(
|
||||
group.Key.Requirements,
|
||||
null,
|
||||
out var reason);
|
||||
|
||||
// Adding a new role
|
||||
_window.AddEntry(name, description, hasAccess, reason, group, spriteSystem);
|
||||
}
|
||||
|
||||
// Restore the Collapsible box state if it is saved
|
||||
_window.RestoreCollapsibleBoxesStates();
|
||||
|
||||
// Close the rules window if it is no longer needed
|
||||
var closeRulesWindow = ghostState.GhostRoles.All(role => role.Identifier != _windowRulesId);
|
||||
if (closeRulesWindow)
|
||||
{
|
||||
_windowRules?.Close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Ghost.Roles;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
@@ -12,20 +15,86 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
|
||||
public event Action<GhostRoleInfo>? OnRoleRequestButtonClicked;
|
||||
public event Action<GhostRoleInfo>? OnRoleFollow;
|
||||
|
||||
private Dictionary<(string name, string description), Collapsible> _collapsibleBoxes = new();
|
||||
private HashSet<(string name, string description)> _uncollapsedStates = new();
|
||||
|
||||
public GhostRolesWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void ClearEntries()
|
||||
{
|
||||
NoRolesMessage.Visible = true;
|
||||
EntryContainer.DisposeAllChildren();
|
||||
_collapsibleBoxes.Clear();
|
||||
}
|
||||
|
||||
public void SaveCollapsibleBoxesStates()
|
||||
{
|
||||
_uncollapsedStates.Clear();
|
||||
foreach (var (key, collapsible) in _collapsibleBoxes)
|
||||
{
|
||||
if (collapsible.BodyVisible)
|
||||
{
|
||||
_uncollapsedStates.Add(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RestoreCollapsibleBoxesStates()
|
||||
{
|
||||
foreach (var (key, collapsible) in _collapsibleBoxes)
|
||||
{
|
||||
collapsible.BodyVisible = _uncollapsedStates.Contains(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddEntry(string name, string description, bool hasAccess, FormattedMessage? reason, IEnumerable<GhostRoleInfo> roles, SpriteSystem spriteSystem)
|
||||
{
|
||||
NoRolesMessage.Visible = false;
|
||||
|
||||
var entry = new GhostRolesEntry(name, description, hasAccess, reason, roles, spriteSystem);
|
||||
entry.OnRoleSelected += OnRoleRequestButtonClicked;
|
||||
entry.OnRoleFollow += OnRoleFollow;
|
||||
EntryContainer.AddChild(entry);
|
||||
var ghostRoleInfos = roles.ToList();
|
||||
var rolesCount = ghostRoleInfos.Count;
|
||||
|
||||
var info = new GhostRoleInfoBox(name, description);
|
||||
var buttons = new GhostRoleButtonsBox(hasAccess, reason, ghostRoleInfos, spriteSystem);
|
||||
buttons.OnRoleSelected += OnRoleRequestButtonClicked;
|
||||
buttons.OnRoleFollow += OnRoleFollow;
|
||||
|
||||
EntryContainer.AddChild(info);
|
||||
|
||||
if (rolesCount > 1)
|
||||
{
|
||||
var buttonHeading = new CollapsibleHeading(Loc.GetString("ghost-roles-window-available-button", ("rolesCount", rolesCount)));
|
||||
|
||||
buttonHeading.AddStyleClass(ContainerButton.StyleClassButton);
|
||||
buttonHeading.Label.HorizontalAlignment = HAlignment.Center;
|
||||
buttonHeading.Label.HorizontalExpand = true;
|
||||
|
||||
var body = new CollapsibleBody
|
||||
{
|
||||
Margin = new Thickness(0, 5, 0, 0),
|
||||
};
|
||||
|
||||
// TODO: Add Requirements to this key when it'll be fixed and work as an equality key in GhostRolesEui
|
||||
var key = (name, description);
|
||||
|
||||
var collapsible = new Collapsible(buttonHeading, body)
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Vertical,
|
||||
Margin = new Thickness(0, 0, 0, 8),
|
||||
};
|
||||
|
||||
body.AddChild(buttons);
|
||||
|
||||
EntryContainer.AddChild(collapsible);
|
||||
_collapsibleBoxes.Add(key, collapsible);
|
||||
}
|
||||
else
|
||||
{
|
||||
EntryContainer.AddChild(buttons);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinHeight="210">
|
||||
<BoxContainer Name="MainContainer" Orientation="Vertical">
|
||||
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'vending-machine-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
|
||||
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
@@ -8,6 +9,7 @@ using Content.Shared.Voting;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.State;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -28,6 +30,7 @@ namespace Content.Client.Voting.UI
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IEntityNetworkManager _entNetManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IStateManager _state = default!;
|
||||
|
||||
private VotingSystem _votingSystem;
|
||||
|
||||
@@ -62,7 +65,7 @@ namespace Content.Client.Voting.UI
|
||||
|
||||
Stylesheet = IoCManager.Resolve<IStylesheetManager>().SheetSpace;
|
||||
CloseButton.OnPressed += _ => Close();
|
||||
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime) / 60));
|
||||
VoteNotTrustedLabel.Text = Loc.GetString("ui-vote-trusted-users-notice", ("timeReq", _cfg.GetCVar(CCVars.VotekickEligibleVoterDeathtime)));
|
||||
|
||||
foreach (StandardVoteType voteType in Enum.GetValues<StandardVoteType>())
|
||||
{
|
||||
@@ -70,6 +73,7 @@ namespace Content.Client.Voting.UI
|
||||
VoteTypeButton.AddItem(Loc.GetString(option.Name), (int)voteType);
|
||||
}
|
||||
|
||||
_state.OnStateChanged += OnStateChanged;
|
||||
VoteTypeButton.OnItemSelected += VoteTypeSelected;
|
||||
CreateButton.OnPressed += CreatePressed;
|
||||
FollowButton.OnPressed += FollowSelected;
|
||||
@@ -101,6 +105,14 @@ namespace Content.Client.Voting.UI
|
||||
UpdateVoteTimeout();
|
||||
}
|
||||
|
||||
private void OnStateChanged(StateChangedEventArgs obj)
|
||||
{
|
||||
if (obj.NewState is not GameplayState)
|
||||
return;
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
private void CanCallVoteChanged(bool obj)
|
||||
{
|
||||
if (!obj)
|
||||
|
||||
@@ -19,11 +19,6 @@ public sealed class VotingSystem : EntitySystem
|
||||
|
||||
private void OnVotePlayerListResponseEvent(VotePlayerListResponseEvent msg)
|
||||
{
|
||||
if (!_ghostSystem.IsGhost)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
VotePlayerListResponse?.Invoke(msg);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
|
||||
[Dependency] private readonly InputSystem _inputSystem = default!;
|
||||
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
|
||||
[Dependency] private readonly MapSystem _map = default!;
|
||||
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
@@ -109,11 +110,11 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
|
||||
if (MapManager.TryFindGridAt(mousePos, out var gridUid, out _))
|
||||
{
|
||||
coordinates = EntityCoordinates.FromMap(gridUid, mousePos, TransformSystem, EntityManager);
|
||||
coordinates = TransformSystem.ToCoordinates(gridUid, mousePos);
|
||||
}
|
||||
else
|
||||
{
|
||||
coordinates = EntityCoordinates.FromMap(MapManager.GetMapEntityId(mousePos.MapId), mousePos, TransformSystem, EntityManager);
|
||||
coordinates = TransformSystem.ToCoordinates(_map.GetMap(mousePos.MapId), mousePos);
|
||||
}
|
||||
|
||||
// Heavy attack.
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
namespace Content.Client._CP14.Localization;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the visual of the sprite, depending on the localization. Useful for drawn lettering
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class CP14LocalizationVisualsComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// map(map,(lang, state))
|
||||
/// in yml:
|
||||
///
|
||||
/// - type: Sprite
|
||||
/// layers:
|
||||
/// - state: stateName0
|
||||
/// map: ["map1"]
|
||||
/// - state: stateName0
|
||||
/// map: ["map2"]
|
||||
/// - type: CP14LocalizationVisuals
|
||||
/// mapStates:
|
||||
/// map1:
|
||||
/// ru-RU: stateName1
|
||||
/// en-US: stateName2
|
||||
/// map2:
|
||||
/// ru-RU: stateName3
|
||||
/// en-US: stateName4
|
||||
///
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<string, Dictionary<string, string>> MapStates;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client._CP14.Localization;
|
||||
|
||||
public sealed class CP14LocalizationVisualsSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CP14LocalizationVisualsComponent, ComponentInit>(OnCompInit);
|
||||
}
|
||||
|
||||
private void OnCompInit(Entity<CP14LocalizationVisualsComponent> visuals, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(visuals, out var sprite))
|
||||
return;
|
||||
|
||||
foreach (var (map, pDictionary) in visuals.Comp.MapStates)
|
||||
{
|
||||
if (!pDictionary.TryGetValue(ContentLocalizationManager.Culture, out var state))
|
||||
return;
|
||||
|
||||
if (sprite.LayerMapTryGet(map, out _))
|
||||
sprite.LayerSetState(map, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Content.Client/_CP14/MagicSpell/CP14ClientMagicSystem.cs
Normal file
7
Content.Client/_CP14/MagicSpell/CP14ClientMagicSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Content.Shared._CP14.MagicSpell;
|
||||
|
||||
namespace Content.Client._CP14.MagicSpell;
|
||||
|
||||
public sealed partial class CP14ClientMagicSystem : CP14SharedMagicSystem
|
||||
{
|
||||
}
|
||||
17
Content.Client/_CP14/Shitcode/CP14EdSystem.cs
Normal file
17
Content.Client/_CP14/Shitcode/CP14EdSystem.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Client._CP14.Shitcode;
|
||||
|
||||
/// <summary>
|
||||
/// Эта система - сборник разного мелкого барахла, который слишком мелкий чтобы иметь свои собственные системы. В идеале в будущем разнести по отдельным файлам.
|
||||
/// </summary>
|
||||
public sealed class CP14EdSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_cfg.SetCVar(CVars.EntitiesCategoryFilter, "ForkFiltered");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<GridContainer Columns="6">
|
||||
|
||||
<TextureRect Name="GoldView"
|
||||
MinSize="10 10"
|
||||
Stretch="KeepAspectCentered"/>
|
||||
<Label Name="GoldText"
|
||||
Margin="0,0,5,0"/>
|
||||
|
||||
<TextureRect Name="SilverView"
|
||||
MinSize="10 10"
|
||||
Stretch="KeepAspectCentered"/>
|
||||
<Label Name="SilverText"
|
||||
Margin="0,0,5,0"/>
|
||||
|
||||
<TextureRect Name="CopperView"
|
||||
MinSize="10 10"
|
||||
Stretch="KeepAspectCentered"/>
|
||||
<Label Name="CopperText"
|
||||
Margin="0,0,5,0"/>
|
||||
</GridContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,46 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._CP14.TravelingStoreShip;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CP14PriceControl : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public CP14PriceControl(int price)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entity.System<SpriteSystem>();
|
||||
|
||||
var rsiPath = new ResPath("_CP14/Interface/Misc/coins.rsi");
|
||||
|
||||
var total = price;
|
||||
|
||||
var gp = total / 100;
|
||||
total %= 100;
|
||||
|
||||
var sp = total / 10;
|
||||
total %= 10;
|
||||
|
||||
var cp = total;
|
||||
|
||||
CopperView.Texture = _sprite.Frame0(new SpriteSpecifier.Rsi(rsiPath, "c"));
|
||||
CopperText.Text = cp.ToString();
|
||||
|
||||
SilverView.Texture = _sprite.Frame0(new SpriteSpecifier.Rsi(rsiPath, "s"));
|
||||
SilverText.Text = sp.ToString();
|
||||
|
||||
GoldView.Texture = _sprite.Frame0(new SpriteSpecifier.Rsi(rsiPath, "g"));
|
||||
GoldText.Text = gp.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Content.Shared._CP14.Cargo;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client._CP14.TravelingStoreShip;
|
||||
|
||||
public sealed class CP14StoreBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
private CP14StoreWindow? _window;
|
||||
|
||||
public CP14StoreBoundUserInterface(EntityUid owner, [NotNull] Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<CP14StoreWindow>();
|
||||
}
|
||||
|
||||
|
||||
protected override void UpdateState(BoundUserInterfaceState state)
|
||||
{
|
||||
base.UpdateState(state);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case CP14StoreUiState storeState:
|
||||
_window?.UpdateUI(storeState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Control xmlns="https://spacestation14.io">
|
||||
<Button Name="ProductButton" Access="Public">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<TextureRect Name="View"
|
||||
MinSize="48 48"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Top"
|
||||
Stretch="KeepAspectCentered"/>
|
||||
<RichTextLabel Name="ProductName" VerticalAlignment="Center" Access="Public"/>
|
||||
<BoxContainer Name="PriceHolder" VerticalAlignment="Center" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</Button>
|
||||
</Control>
|
||||
@@ -0,0 +1,44 @@
|
||||
using Content.Shared._CP14.Cargo;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client._CP14.TravelingStoreShip;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CP14StoreProductControl : Control
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
|
||||
private readonly SpriteSystem _sprite;
|
||||
|
||||
public CP14StoreProductControl(CP14StoreUiProductEntry entry)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_sprite = _entity.System<SpriteSystem>();
|
||||
|
||||
UpdateName(entry.Name);
|
||||
UpdateView(entry.Icon);
|
||||
UpdatePrice(entry.Price);
|
||||
}
|
||||
|
||||
private void UpdatePrice(int price)
|
||||
{
|
||||
PriceHolder.RemoveAllChildren();
|
||||
PriceHolder.AddChild(new CP14PriceControl(price));
|
||||
}
|
||||
|
||||
private void UpdateName(string name)
|
||||
{
|
||||
ProductName.Text = $"[bold]{name}[/bold]";
|
||||
}
|
||||
|
||||
private void UpdateView(SpriteSpecifier spriteSpecifier)
|
||||
{
|
||||
View.Texture = _sprite.Frame0(spriteSpecifier);
|
||||
}
|
||||
}
|
||||
31
Content.Client/_CP14/TravelingStoreShip/CP14StoreWindow.xaml
Normal file
31
Content.Client/_CP14/TravelingStoreShip/CP14StoreWindow.xaml
Normal file
@@ -0,0 +1,31 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'cp14-store-ui-title'}"
|
||||
MinSize="800 600"
|
||||
SetSize="800 600">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal">
|
||||
<!-- Product list (left side UI) -->
|
||||
<TabContainer SizeFlagsStretchRatio="0.5" Name="Tabs" HorizontalExpand="True" VerticalExpand="True" MinSize="0 200">
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" MinSize="0 200">
|
||||
<BoxContainer Name="BuyProductsContainer" Orientation="Vertical" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" MinSize="0 200">
|
||||
<BoxContainer Name="SellProductsContainer" Orientation="Vertical" HorizontalExpand="True"/>
|
||||
</ScrollContainer>
|
||||
</TabContainer>
|
||||
<!-- Station trading data (right side UI) -->
|
||||
<BoxContainer SizeFlagsStretchRatio="0.5" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 10 0">
|
||||
<controls:StripeBack>
|
||||
<PanelContainer>
|
||||
<Label Text="{Loc 'cp14-store-ui-order'}" Align="Center" Margin="0 5 0 3"/>
|
||||
</PanelContainer>
|
||||
</controls:StripeBack>
|
||||
<Label Name="TravelTimeLabel" Text="00:00" HorizontalAlignment="Center" HorizontalExpand="True" Margin="0 15 0 0"/>
|
||||
<controls:HLine Color="#404040" Thickness="2" Margin="0 5"/>
|
||||
<RichTextLabel Name="SelectedName" HorizontalExpand="True" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="5"/>
|
||||
<RichTextLabel Name="SelectedDesc" HorizontalExpand="True" VerticalExpand="True" VerticalAlignment="Top" Margin="5"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
@@ -0,0 +1,79 @@
|
||||
using Content.Shared._CP14.Cargo;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client._CP14.TravelingStoreShip;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CP14StoreWindow : DefaultWindow
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
private TimeSpan? _nextTravelTime;
|
||||
private bool _onStation;
|
||||
|
||||
public CP14StoreWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
Tabs.SetTabTitle(0, Loc.GetString("cp14-store-ui-tab-buy"));
|
||||
Tabs.SetTabTitle(1, Loc.GetString("cp14-store-ui-tab-sell"));
|
||||
}
|
||||
|
||||
public void UpdateUI(CP14StoreUiState state)
|
||||
{
|
||||
UpdateProducts(state);
|
||||
|
||||
_nextTravelTime = state.NextTravelTime;
|
||||
_onStation = state.OnStation;
|
||||
}
|
||||
|
||||
protected override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
//Updating time
|
||||
if (_nextTravelTime is not null)
|
||||
{
|
||||
var time = _nextTravelTime.Value - _timing.CurTime;
|
||||
|
||||
TravelTimeLabel.Text =
|
||||
$"{Loc.GetString(_onStation ? "cp14-store-ui-next-travel-out" : "cp14-store-ui-next-travel-in")} {Math.Max(time.Minutes, 0):00}:{Math.Max(time.Seconds, 0):00}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateProducts(CP14StoreUiState state)
|
||||
{
|
||||
BuyProductsContainer.RemoveAllChildren();
|
||||
SellProductsContainer.RemoveAllChildren();
|
||||
|
||||
foreach (var product in state.ProductsBuy)
|
||||
{
|
||||
var control = new CP14StoreProductControl(product);
|
||||
control.ProductButton.OnPressed += _ =>
|
||||
{
|
||||
SelectProduct(product);
|
||||
};
|
||||
BuyProductsContainer.AddChild(control);
|
||||
}
|
||||
|
||||
foreach (var product in state.ProductsSell)
|
||||
{
|
||||
var control = new CP14StoreProductControl(product);
|
||||
control.ProductButton.OnPressed += _ =>
|
||||
{
|
||||
SelectProduct(product);
|
||||
};
|
||||
SellProductsContainer.AddChild(control);
|
||||
}
|
||||
}
|
||||
|
||||
private void SelectProduct(CP14StoreUiProductEntry? entry)
|
||||
{
|
||||
SelectedName.Text = entry is null ? string.Empty : $"[bold]{entry.Value.Name}[/bold]";
|
||||
SelectedDesc.Text = entry is null ? string.Empty : entry.Value.Desc;
|
||||
}
|
||||
}
|
||||
@@ -107,13 +107,41 @@ public sealed partial class TestPair
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent<T>(
|
||||
public List<(EntityPrototype, T)> GetPrototypesWithComponent<T>(
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
where T : IComponent
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(T));
|
||||
var list = new List<(EntityPrototype, T)>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (ignored != null && ignored.Contains(proto.ID))
|
||||
continue;
|
||||
|
||||
if (ignoreAbstract && proto.Abstract)
|
||||
continue;
|
||||
|
||||
if (ignoreTestPrototypes && IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (proto.Components.TryGetComponent(id, out var cmp))
|
||||
list.Add((proto, (T)cmp));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieve all entity prototypes that have some component.
|
||||
/// </summary>
|
||||
public List<EntityPrototype> GetPrototypesWithComponent(Type type,
|
||||
HashSet<string>? ignored = null,
|
||||
bool ignoreAbstract = true,
|
||||
bool ignoreTestPrototypes = true)
|
||||
{
|
||||
var id = Server.ResolveDependency<IComponentFactory>().GetComponentName(type);
|
||||
var list = new List<EntityPrototype>();
|
||||
foreach (var proto in Server.ProtoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
@@ -127,7 +155,7 @@ public sealed partial class TestPair
|
||||
continue;
|
||||
|
||||
if (proto.Components.ContainsKey(id))
|
||||
list.Add(proto);
|
||||
list.Add((proto));
|
||||
}
|
||||
|
||||
return list;
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.GameRules;
|
||||
// Lets not let that happen again.
|
||||
[TestFixture]
|
||||
public sealed class AntagPreferenceTest
|
||||
{
|
||||
{/* CP14 disabled
|
||||
[Test]
|
||||
public async Task TestLobbyPlayersValid()
|
||||
{
|
||||
@@ -72,5 +72,5 @@ public sealed class AntagPreferenceTest
|
||||
|
||||
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.GameTicking;
|
||||
@@ -120,8 +121,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind));
|
||||
Assert.That(factionSys.IsMember(player, "Syndicate"), Is.True);
|
||||
Assert.That(factionSys.IsMember(player, "NanoTrasen"), Is.False);
|
||||
var roles = roleSys.MindGetAllRoles(mind);
|
||||
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander" && x.Component is NukeopsRoleComponent);
|
||||
var roles = roleSys.MindGetAllRoleInfo(mind);
|
||||
var cmdRoles = roles.Where(x => x.Prototype == "NukeopsCommander");
|
||||
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
|
||||
|
||||
// The second dummy player should be a medic
|
||||
@@ -131,8 +132,8 @@ public sealed class NukeOpsTest
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(dummyMind));
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], "Syndicate"), Is.True);
|
||||
Assert.That(factionSys.IsMember(dummyEnts[1], "NanoTrasen"), Is.False);
|
||||
roles = roleSys.MindGetAllRoles(dummyMind);
|
||||
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic" && x.Component is NukeopsRoleComponent);
|
||||
roles = roleSys.MindGetAllRoleInfo(dummyMind);
|
||||
cmdRoles = roles.Where(x => x.Prototype == "NukeopsMedic");
|
||||
Assert.That(cmdRoles.Count(), Is.EqualTo(1));
|
||||
|
||||
// The other two players should have just spawned in as normal.
|
||||
@@ -141,13 +142,14 @@ public sealed class NukeOpsTest
|
||||
void CheckDummy(int i)
|
||||
{
|
||||
var ent = dummyEnts[i];
|
||||
var mind = mindSys.GetMind(ent)!.Value;
|
||||
var mindCrew = mindSys.GetMind(ent)!.Value;
|
||||
Assert.That(entMan.HasComponent<NukeOperativeComponent>(ent), Is.False);
|
||||
Assert.That(roleSys.MindIsAntagonist(mind), Is.False);
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mind), Is.False);
|
||||
Assert.That(roleSys.MindIsAntagonist(mindCrew), Is.False);
|
||||
Assert.That(roleSys.MindHasRole<NukeopsRoleComponent>(mindCrew), Is.False);
|
||||
Assert.That(factionSys.IsMember(ent, "Syndicate"), Is.False);
|
||||
Assert.That(factionSys.IsMember(ent, "NanoTrasen"), Is.True);
|
||||
Assert.That(roleSys.MindGetAllRoles(mind).Any(x => x.Component is NukeopsRoleComponent), Is.False);
|
||||
var nukeroles = new List<string>() { "Nukeops", "NukeopsMedic", "NukeopsCommander" };
|
||||
Assert.That(roleSys.MindGetAllRoleInfo(mindCrew).Any(x => nukeroles.Contains(x.Prototype)), Is.False);
|
||||
}
|
||||
|
||||
// The game rule exists, and all the stations/shuttles/maps are properly initialized
|
||||
@@ -238,7 +240,8 @@ public sealed class NukeOpsTest
|
||||
for (var i = 0; i < nukies.Length - 1; i++)
|
||||
{
|
||||
entMan.DeleteEntity(nukies[i]);
|
||||
Assert.That(roundEndSys.IsRoundEndRequested, Is.False,
|
||||
Assert.That(roundEndSys.IsRoundEndRequested,
|
||||
Is.False,
|
||||
$"The round ended, but {nukies.Length - i - 1} nukies are still alive!");
|
||||
}
|
||||
// Delete the last nukie and make sure the round ends.
|
||||
|
||||
@@ -2,7 +2,6 @@ using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Internals;
|
||||
|
||||
@@ -25,10 +24,7 @@ public sealed class AutoInternalsTests
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var profile = new HumanoidCharacterProfile();
|
||||
var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, new JobComponent()
|
||||
{
|
||||
Prototype = "TestInternalsDummy"
|
||||
}, profile, station: null);
|
||||
var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, "TestInternalsDummy", profile, station: null);
|
||||
|
||||
Assert.That(atmos.HasAtmosphere(testMap.Grid), Is.False, "Test map has atmosphere - test needs adjustment!");
|
||||
Assert.That(internals.AreInternalsWorking(dummy), "Internals did not automatically connect!");
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
#nullable enable
|
||||
using System.Linq;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Ghost.Roles;
|
||||
using Content.Server.Ghost.Roles.Components;
|
||||
using Content.Server.Mind.Commands;
|
||||
using Content.Server.Players;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
@@ -18,7 +16,6 @@ using Robust.Server.Console;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -287,27 +284,27 @@ public sealed partial class MindTests
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
|
||||
});
|
||||
|
||||
var traitorRole = new TraitorRoleComponent();
|
||||
var traitorRole = "MindRoleTraitor";
|
||||
|
||||
roleSystem.MindAddRole(mindId, traitorRole);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
|
||||
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
|
||||
});
|
||||
|
||||
var jobRole = new JobComponent();
|
||||
var jobRole = "";
|
||||
|
||||
roleSystem.MindAddRole(mindId, jobRole);
|
||||
roleSystem.MindAddJobRole(mindId, jobPrototype:jobRole);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId));
|
||||
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
|
||||
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
|
||||
});
|
||||
|
||||
roleSystem.MindRemoveRole<TraitorRoleComponent>(mindId);
|
||||
@@ -315,15 +312,15 @@ public sealed partial class MindTests
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId));
|
||||
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId));
|
||||
});
|
||||
|
||||
roleSystem.MindRemoveRole<JobComponent>(mindId);
|
||||
roleSystem.MindRemoveRole<JobRoleComponent>(mindId);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(roleSystem.MindHasRole<TraitorRoleComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobComponent>(mindId), Is.False);
|
||||
Assert.That(roleSystem.MindHasRole<JobRoleComponent>(mindId), Is.False);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
95
Content.IntegrationTests/Tests/Minds/RoleTests.cs
Normal file
95
Content.IntegrationTests/Tests/Minds/RoleTests.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Reflection;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Minds;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class RoleTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Check that any prototype with a <see cref="MindRoleComponent"/> is properly configured
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateRolePrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var jobComp = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(JobRoleComponent));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<MindRoleComponent>())
|
||||
{
|
||||
Assert.That(comp.AntagPrototype == null || comp.JobPrototype == null, $"Role {proto.ID} has both a job and antag prototype.");
|
||||
Assert.That(!comp.ExclusiveAntag || comp.Antag, $"Role {proto.ID} is marked as an exclusive antag, despite not being an antag.");
|
||||
Assert.That(comp.Antag || comp.AntagPrototype == null, $"Role {proto.ID} has an antag prototype, despite not being an antag.");
|
||||
|
||||
if (comp.JobPrototype != null)
|
||||
Assert.That(proto.Components.ContainsKey(jobComp), $"Role {proto.ID} is a job, despite not having a job prototype.");
|
||||
|
||||
// It is possible that this is meant to be supported? Though I would assume that it would be for
|
||||
// admin / prototype uploads, and that pre-defined roles should still check this.
|
||||
Assert.That(!comp.Antag || comp.AntagPrototype != null , $"Role {proto.ID} is an antag, despite not having a antag prototype.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that any prototype with a <see cref="JobRoleComponent"/> also has a properly configured
|
||||
/// <see cref="MindRoleComponent"/>
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateJobPrototypes()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var (proto, comp) in pair.GetPrototypesWithComponent<JobRoleComponent>())
|
||||
{
|
||||
if (proto.Components.TryGetComponent(mindCompId, out var mindComp))
|
||||
Assert.That(((MindRoleComponent)mindComp).JobPrototype, Is.Not.Null);
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that any prototype with a component that inherits from <see cref="BaseMindRoleComponent"/> also has a
|
||||
/// <see cref="MindRoleComponent"/>
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task ValidateRolesHaveMindRoleComp()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var refMan = pair.Server.ResolveDependency<IReflectionManager>();
|
||||
var mindCompId = pair.Server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(MindRoleComponent));
|
||||
|
||||
var compTypes = refMan.GetAllChildren(typeof(BaseMindRoleComponent))
|
||||
.Append(typeof(RoleBriefingComponent))
|
||||
.Where(x => !x.IsAbstract);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
foreach (var comp in compTypes)
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent(comp))
|
||||
{
|
||||
Assert.That(proto.Components.ContainsKey(mindCompId), $"Role {proto.ID} does not have a {nameof(MindRoleComponent)} despite having a {comp.Name}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -50,9 +50,8 @@ namespace Content.IntegrationTests.Tests
|
||||
"MeteorArena",
|
||||
|
||||
//CrystallPunk maps
|
||||
"AlchemyTest",
|
||||
"BattleRoyale",
|
||||
"ExpeditionTest",
|
||||
"Village",
|
||||
"Island",
|
||||
//CrystallPunk Map replacement end
|
||||
};
|
||||
|
||||
@@ -236,7 +235,6 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
// Test all availableJobs have spawnPoints
|
||||
// This is done inside gamemap test because loading the map takes ages and we already have it.
|
||||
/*
|
||||
var comp = entManager.GetComponent<StationJobsComponent>(station);
|
||||
var jobs = new HashSet<ProtoId<JobPrototype>>(comp.SetupAvailableJobs.Keys);
|
||||
|
||||
@@ -253,7 +251,6 @@ namespace Content.IntegrationTests.Tests
|
||||
jobs.ExceptWith(spawnPoints);
|
||||
|
||||
Assert.That(jobs, Is.Empty, $"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
|
||||
*/ //CP14 disable job spawners test
|
||||
}
|
||||
|
||||
try
|
||||
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
|
||||
@@ -68,10 +67,7 @@ public sealed class LoadoutTests
|
||||
|
||||
profile.SetLoadout(new RoleLoadout("LoadoutTester"));
|
||||
|
||||
var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
|
||||
{
|
||||
Prototype = "LoadoutTester"
|
||||
}, profile, station: null);
|
||||
var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: "LoadoutTester", profile, station: null);
|
||||
|
||||
var slotQuery = inventorySystem.GetSlotEnumerator(tester);
|
||||
var checkedCount = 0;
|
||||
|
||||
@@ -35,15 +35,16 @@ public sealed class StartingGearPrototypeStorageTest
|
||||
{
|
||||
foreach (var gearProto in protos)
|
||||
{
|
||||
var backpackProto = ((IEquipmentLoadout) gearProto).GetGear("back");
|
||||
if (backpackProto == string.Empty)
|
||||
continue;
|
||||
|
||||
var bag = server.EntMan.SpawnEntity(backpackProto, coords);
|
||||
var ents = new ValueList<EntityUid>();
|
||||
|
||||
foreach (var (slot, entProtos) in gearProto.Storage)
|
||||
{
|
||||
ents.Clear();
|
||||
var storageProto = ((IEquipmentLoadout)gearProto).GetGear(slot);
|
||||
if (storageProto == string.Empty)
|
||||
continue;
|
||||
|
||||
var bag = server.EntMan.SpawnEntity(storageProto, coords);
|
||||
if (entProtos.Count == 0)
|
||||
continue;
|
||||
|
||||
@@ -59,9 +60,8 @@ public sealed class StartingGearPrototypeStorageTest
|
||||
|
||||
server.EntMan.DeleteEntity(ent);
|
||||
}
|
||||
server.EntMan.DeleteEntity(bag);
|
||||
}
|
||||
|
||||
server.EntMan.DeleteEntity(bag);
|
||||
}
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
|
||||
@@ -40,7 +40,7 @@ public sealed class PrototypeSaveTest
|
||||
|
||||
await pair.Client.WaitPost(() =>
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
|
||||
foreach (var (proto, _) in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
|
||||
{
|
||||
var dummy = pair.Client.EntMan.Spawn(proto.ID);
|
||||
pair.Client.EntMan.RunMapInit(dummy, pair.Client.MetaData(dummy));
|
||||
|
||||
@@ -94,14 +94,13 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
await Assert.MultipleAsync(async () =>
|
||||
{
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<EntityStorageComponent>(compFact))
|
||||
continue;
|
||||
|
||||
StorageComponent? storage = null;
|
||||
ItemComponent? item = null;
|
||||
StorageFillComponent fill = default!;
|
||||
var size = 0;
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
@@ -112,7 +111,6 @@ namespace Content.IntegrationTests.Tests
|
||||
}
|
||||
|
||||
proto.TryGetComponent("Item", out item);
|
||||
fill = (StorageFillComponent) proto.Components[id].Component;
|
||||
size = GetFillSize(fill, false, protoMan, itemSys);
|
||||
});
|
||||
|
||||
@@ -179,7 +177,7 @@ namespace Content.IntegrationTests.Tests
|
||||
|
||||
var itemSys = entMan.System<SharedItemSystem>();
|
||||
|
||||
foreach (var proto in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
foreach (var (proto, fill) in pair.GetPrototypesWithComponent<StorageFillComponent>())
|
||||
{
|
||||
if (proto.HasComponent<StorageComponent>(compFact))
|
||||
continue;
|
||||
@@ -192,7 +190,6 @@ namespace Content.IntegrationTests.Tests
|
||||
if (entStorage == null)
|
||||
return;
|
||||
|
||||
var fill = (StorageFillComponent) proto.Components[id].Component;
|
||||
var size = GetFillSize(fill, true, protoMan, itemSys);
|
||||
Assert.That(size, Is.LessThanOrEqualTo(entStorage.Capacity),
|
||||
$"{proto.ID} storage fill is too large.");
|
||||
|
||||
37
Content.IntegrationTests/Tests/_CP14/CP14CargoTest.cs
Normal file
37
Content.IntegrationTests/Tests/_CP14/CP14CargoTest.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.Shared._CP14.Cargo.Prototype;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests._CP14;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[TestFixture]
|
||||
public sealed class CP14CargoTest
|
||||
{
|
||||
[Test]
|
||||
public async Task CheckAllBuyPositionsUniqueCode()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var compFactory = server.ResolveDependency<IComponentFactory>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
HashSet<string> existedCodes = new();
|
||||
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<CP14StoreBuyPositionPrototype>())
|
||||
{
|
||||
Assert.That(!existedCodes.Contains(proto.Code), $"Repeated purchasing code {proto.Code} of the {proto}");
|
||||
existedCodes.Add(proto.Code);
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
41
Content.IntegrationTests/Tests/_CP14/CP14EntityTest.cs
Normal file
41
Content.IntegrationTests/Tests/_CP14/CP14EntityTest.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests._CP14;
|
||||
|
||||
#nullable enable
|
||||
|
||||
[TestFixture]
|
||||
public sealed class CP14EntityTest
|
||||
{
|
||||
[Test]
|
||||
public async Task CheckAllCP14EntityHasForkFilteredCategory()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var compFactory = server.ResolveDependency<IComponentFactory>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
if (!protoManager.TryIndex<EntityCategoryPrototype>("ForkFiltered", out var indexedFilter))
|
||||
return;
|
||||
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.ID.StartsWith("CP14"))
|
||||
continue;
|
||||
|
||||
if (proto.Abstract || proto.HideSpawnMenu)
|
||||
continue;
|
||||
|
||||
Assert.That(proto.Categories.Contains(indexedFilter), $"CP14 fork proto: {proto} does not marked abstract, or have a HideSpawnMenu or ForkFiltered category");
|
||||
}
|
||||
});
|
||||
});
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ public sealed class CP14RitualTest
|
||||
|
||||
foreach (var edge in phase.Edges)
|
||||
{
|
||||
Assert.That(edge.Triggers.Count > 0, $"{{proto}} is ritual node, but edge to {edge.Target} has no triggers and cannot be activated.");
|
||||
Assert.That(edge.Triggers.Count > 0, $"{proto} is ritual node, but edge to {edge.Target} has no triggers and cannot be activated.");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Content.Server.Access.Systems
|
||||
if (!TryComp<IdCardComponent>(uid, out var idCard))
|
||||
return;
|
||||
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon);
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.LocalizedJobTitle ?? "", idCard.JobIcon);
|
||||
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem
|
||||
PrivilegedIdIsAuthorized(uid, component),
|
||||
true,
|
||||
targetIdComponent.FullName,
|
||||
targetIdComponent.JobTitle,
|
||||
targetIdComponent.LocalizedJobTitle,
|
||||
targetAccessComponent.Tags.ToList(),
|
||||
possibleAccess,
|
||||
jobProto,
|
||||
|
||||
@@ -14,13 +14,13 @@ using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Roles;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -45,14 +45,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
public const string SawmillId = "admin.bans";
|
||||
public const string JobPrefix = "Job:";
|
||||
|
||||
private readonly Dictionary<NetUserId, HashSet<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
// Cached ban exemption flags are used to handle
|
||||
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
_netManager.RegisterNetMessage<MsgRoleBans>();
|
||||
|
||||
_db.SubscribeToNotifications(OnDatabaseNotification);
|
||||
@@ -63,12 +61,23 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
|
||||
{
|
||||
// Yeah so role ban loading code isn't integrated with exempt flag loading code.
|
||||
// Have you seen how garbage role ban code code is? I don't feel like refactoring it right now.
|
||||
|
||||
var flags = await _db.GetBanExemption(player.UserId, cancel);
|
||||
|
||||
var netChannel = player.Channel;
|
||||
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
|
||||
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, false);
|
||||
|
||||
var userRoleBans = new List<ServerRoleBanDef>();
|
||||
foreach (var ban in roleBans)
|
||||
{
|
||||
userRoleBans.Add(ban);
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
_cachedBanExemptions[player] = flags;
|
||||
_cachedRoleBans[player] = userRoleBans;
|
||||
|
||||
SendRoleBans(player);
|
||||
}
|
||||
|
||||
private void ClearPlayerData(ICommonSession player)
|
||||
@@ -76,25 +85,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
_cachedBanExemptions.Remove(player);
|
||||
}
|
||||
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.Connected || _cachedRoleBans.ContainsKey(e.Session.UserId))
|
||||
return;
|
||||
|
||||
var netChannel = e.Session.Channel;
|
||||
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
|
||||
await CacheDbRoleBans(e.Session.UserId, netChannel.RemoteEndPoint.Address, hwId);
|
||||
|
||||
SendRoleBans(e.Session);
|
||||
}
|
||||
|
||||
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
|
||||
{
|
||||
banDef = await _db.AddServerRoleBanAsync(banDef);
|
||||
|
||||
if (banDef.UserId != null)
|
||||
if (banDef.UserId != null
|
||||
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
{
|
||||
_cachedRoleBans.GetOrNew(banDef.UserId.Value).Add(banDef);
|
||||
cachedBans.Add(banDef);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -102,31 +101,21 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
return _cachedRoleBans.TryGetValue(playerUserId, out var roleBans)
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return _cachedRoleBans.TryGetValue(session, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
private async Task CacheDbRoleBans(NetUserId userId, IPAddress? address = null, ImmutableArray<byte>? hwId = null)
|
||||
{
|
||||
var roleBans = await _db.GetServerRoleBansAsync(address, userId, hwId, false);
|
||||
|
||||
var userRoleBans = new HashSet<ServerRoleBanDef>();
|
||||
foreach (var ban in roleBans)
|
||||
{
|
||||
userRoleBans.Add(ban);
|
||||
}
|
||||
|
||||
_cachedRoleBans[userId] = userRoleBans;
|
||||
}
|
||||
|
||||
public void Restart()
|
||||
{
|
||||
// Clear out players that have disconnected.
|
||||
var toRemove = new List<NetUserId>();
|
||||
var toRemove = new ValueList<ICommonSession>();
|
||||
foreach (var player in _cachedRoleBans.Keys)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(player, out _))
|
||||
if (player.Status == SessionStatus.Disconnected)
|
||||
toRemove.Add(player);
|
||||
}
|
||||
|
||||
@@ -138,7 +127,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
// Check for expired bans
|
||||
foreach (var roleBans in _cachedRoleBans.Values)
|
||||
{
|
||||
roleBans.RemoveWhere(ban => DateTimeOffset.Now > ban.ExpirationTime);
|
||||
roleBans.RemoveAll(ban => DateTimeOffset.Now > ban.ExpirationTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,9 +270,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
|
||||
|
||||
if (target != null)
|
||||
if (target != null && _playerManager.TryGetSessionById(target.Value, out var session))
|
||||
{
|
||||
SendRoleBans(target.Value);
|
||||
SendRoleBans(session);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,10 +300,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
|
||||
|
||||
if (ban.UserId is { } player && _cachedRoleBans.TryGetValue(player, out var roleBans))
|
||||
if (ban.UserId is { } player
|
||||
&& _playerManager.TryGetSessionById(player, out var session)
|
||||
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
{
|
||||
roleBans.RemoveWhere(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(player);
|
||||
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
return $"Pardoned ban with id {banId}";
|
||||
@@ -322,8 +313,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_cachedRoleBans.TryGetValue(playerUserId, out var roleBans))
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
if (!_cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
return null;
|
||||
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(JobPrefix, StringComparison.Ordinal))
|
||||
.Select(ban => new ProtoId<JobPrototype>(ban.Role[JobPrefix.Length..]))
|
||||
@@ -331,19 +326,9 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
}
|
||||
#endregion
|
||||
|
||||
public void SendRoleBans(NetUserId userId)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(userId, out var player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SendRoleBans(player);
|
||||
}
|
||||
|
||||
public void SendRoleBans(ICommonSession pSession)
|
||||
{
|
||||
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession.UserId) ?? new HashSet<ServerRoleBanDef>();
|
||||
var roleBans = _cachedRoleBans.GetValueOrDefault(pSession) ?? new List<ServerRoleBanDef>();
|
||||
var bans = new MsgRoleBans()
|
||||
{
|
||||
Bans = roleBans.Select(o => o.Role).ToList()
|
||||
|
||||
@@ -47,12 +47,6 @@ public interface IBanManager
|
||||
/// <param name="unbanTime">The time at which this role ban was pardoned.</param>
|
||||
public Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime);
|
||||
|
||||
/// <summary>
|
||||
/// Sends role bans to the target
|
||||
/// </summary>
|
||||
/// <param name="pSession">Player's user ID</param>
|
||||
public void SendRoleBans(NetUserId userId);
|
||||
|
||||
/// <summary>
|
||||
/// Sends role bans to the target
|
||||
/// </summary>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server._CP14.Roles;
|
||||
using Content.Server.Administration.Commands;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
@@ -52,6 +53,23 @@ public sealed partial class AdminVerbSystem
|
||||
|
||||
var targetPlayer = targetActor.PlayerSession;
|
||||
|
||||
|
||||
Verb CP14Sociopath = new()
|
||||
{
|
||||
Text = Loc.GetString("cp14-admin-verb-text-make-sociopath"),
|
||||
Category = VerbCategory.Antag,
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/Color/black.rsi"),
|
||||
"icon"), //TODO
|
||||
Act = () =>
|
||||
{
|
||||
//_antag.ForceMakeAntag<CP14BanditRoleComponent>(targetPlayer, "CP14Bandit"); //TODO
|
||||
},
|
||||
Impact = LogImpact.High,
|
||||
Message = Loc.GetString("cp14-admin-verb-make-sociopath"),
|
||||
};
|
||||
args.Verbs.Add(CP14Sociopath);
|
||||
|
||||
/* CP14 disable default antags
|
||||
Verb traitor = new()
|
||||
{
|
||||
Text = Loc.GetString("admin-verb-text-make-traitor"),
|
||||
@@ -151,5 +169,6 @@ public sealed partial class AdminVerbSystem
|
||||
Message = Loc.GetString("admin-verb-make-thief"),
|
||||
};
|
||||
args.Verbs.Add(thief);
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +95,10 @@ public sealed partial class AdminVerbSystem
|
||||
if (HasComp<MapComponent>(args.Target) || HasComp<MapGridComponent>(args.Target))
|
||||
return;
|
||||
|
||||
var explodeName = Loc.GetString("admin-smite-explode-name").ToLowerInvariant();
|
||||
Verb explode = new()
|
||||
{
|
||||
Text = "admin-smite-explode-name",
|
||||
Text = explodeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/smite.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -111,13 +112,14 @@ public sealed partial class AdminVerbSystem
|
||||
_bodySystem.GibBody(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-explode-description")
|
||||
Message = string.Join(": ", explodeName, Loc.GetString("admin-smite-explode-description")) // we do this so the description tells admins the Text to run it via console.
|
||||
};
|
||||
args.Verbs.Add(explode);
|
||||
|
||||
var chessName = Loc.GetString("admin-smite-chess-dimension-name").ToLowerInvariant();
|
||||
Verb chess = new()
|
||||
{
|
||||
Text = "admin-smite-chess-dimension-name",
|
||||
Text = chessName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/Tabletop/chessboard.rsi"), "chessboard"),
|
||||
Act = () =>
|
||||
@@ -137,12 +139,13 @@ public sealed partial class AdminVerbSystem
|
||||
xform.WorldRotation = Angle.Zero;
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-chess-dimension-description")
|
||||
Message = string.Join(": ", chessName, Loc.GetString("admin-smite-chess-dimension-description"))
|
||||
};
|
||||
args.Verbs.Add(chess);
|
||||
|
||||
if (TryComp<FlammableComponent>(args.Target, out var flammable))
|
||||
{
|
||||
var flamesName = Loc.GetString("admin-smite-set-alight-name").ToLowerInvariant();
|
||||
Verb flames = new()
|
||||
{
|
||||
Text = "admin-smite-set-alight-name",
|
||||
@@ -160,14 +163,15 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-set-alight-description")
|
||||
Message = string.Join(": ", flamesName, Loc.GetString("admin-smite-set-alight-description"))
|
||||
};
|
||||
args.Verbs.Add(flames);
|
||||
}
|
||||
|
||||
var monkeyName = Loc.GetString("admin-smite-monkeyify-name").ToLowerInvariant();
|
||||
Verb monkey = new()
|
||||
{
|
||||
Text = "admin-smite-monkeyify-name",
|
||||
Text = monkeyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Animals/monkey.rsi"), "monkey"),
|
||||
Act = () =>
|
||||
@@ -175,13 +179,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMonkeySmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-monkeyify-description")
|
||||
Message = string.Join(": ", monkeyName, Loc.GetString("admin-smite-monkeyify-description"))
|
||||
};
|
||||
args.Verbs.Add(monkey);
|
||||
|
||||
var disposalBinName = Loc.GetString("admin-smite-garbage-can-name").ToLowerInvariant();
|
||||
Verb disposalBin = new()
|
||||
{
|
||||
Text = "admin-smite-electrocute-name",
|
||||
Text = disposalBinName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Piping/disposal.rsi"), "disposal"),
|
||||
Act = () =>
|
||||
@@ -189,16 +194,17 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminDisposalsSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-garbage-can-description")
|
||||
Message = string.Join(": ", disposalBinName, Loc.GetString("admin-smite-garbage-can-description"))
|
||||
};
|
||||
args.Verbs.Add(disposalBin);
|
||||
|
||||
if (TryComp<DamageableComponent>(args.Target, out var damageable) &&
|
||||
HasComp<MobStateComponent>(args.Target))
|
||||
{
|
||||
var hardElectrocuteName = Loc.GetString("admin-smite-electrocute-name").ToLowerInvariant();
|
||||
Verb hardElectrocute = new()
|
||||
{
|
||||
Text = "admin-smite-creampie-name",
|
||||
Text = hardElectrocuteName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Hands/Gloves/Color/yellow.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -234,16 +240,17 @@ public sealed partial class AdminVerbSystem
|
||||
TimeSpan.FromSeconds(30), refresh: true, ignoreInsulation: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-electrocute-description")
|
||||
Message = string.Join(": ", hardElectrocuteName, Loc.GetString("admin-smite-electrocute-description"))
|
||||
};
|
||||
args.Verbs.Add(hardElectrocute);
|
||||
}
|
||||
|
||||
if (TryComp<CreamPiedComponent>(args.Target, out var creamPied))
|
||||
{
|
||||
var creamPieName = Loc.GetString("admin-smite-creampie-name").ToLowerInvariant();
|
||||
Verb creamPie = new()
|
||||
{
|
||||
Text = "admin-smite-remove-blood-name",
|
||||
Text = creamPieName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Consumable/Food/Baked/pie.rsi"), "plain-slice"),
|
||||
Act = () =>
|
||||
@@ -251,16 +258,17 @@ public sealed partial class AdminVerbSystem
|
||||
_creamPieSystem.SetCreamPied(args.Target, creamPied, true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-creampie-description")
|
||||
Message = string.Join(": ", creamPieName, Loc.GetString("admin-smite-creampie-description"))
|
||||
};
|
||||
args.Verbs.Add(creamPie);
|
||||
}
|
||||
|
||||
if (TryComp<BloodstreamComponent>(args.Target, out var bloodstream))
|
||||
{
|
||||
var bloodRemovalName = Loc.GetString("admin-smite-remove-blood-name").ToLowerInvariant();
|
||||
Verb bloodRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-vomit-organs-name",
|
||||
Text = bloodRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Fluids/tomato_splat.rsi"), "puddle-1"),
|
||||
Act = () =>
|
||||
@@ -273,7 +281,7 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-blood-description")
|
||||
Message = string.Join(": ", bloodRemovalName, Loc.GetString("admin-smite-remove-blood-description"))
|
||||
};
|
||||
args.Verbs.Add(bloodRemoval);
|
||||
}
|
||||
@@ -281,9 +289,10 @@ public sealed partial class AdminVerbSystem
|
||||
// bobby...
|
||||
if (TryComp<BodyComponent>(args.Target, out var body))
|
||||
{
|
||||
var vomitOrgansName = Loc.GetString("admin-smite-vomit-organs-name").ToLowerInvariant();
|
||||
Verb vomitOrgans = new()
|
||||
{
|
||||
Text = "admin-smite-remove-hands-name",
|
||||
Text = vomitOrgansName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Fluids/vomit_toxin.rsi"), "vomit_toxin-1"),
|
||||
Act = () =>
|
||||
@@ -305,13 +314,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.MediumCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-vomit-organs-description")
|
||||
Message = string.Join(": ", vomitOrgansName, Loc.GetString("admin-smite-vomit-organs-description"))
|
||||
};
|
||||
args.Verbs.Add(vomitOrgans);
|
||||
|
||||
var handsRemovalName = Loc.GetString("admin-smite-remove-hands-name").ToLowerInvariant();
|
||||
Verb handsRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-remove-hand-name",
|
||||
Text = handsRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hands.png")),
|
||||
Act = () =>
|
||||
@@ -327,13 +337,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.Medium);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-hands-description")
|
||||
Message = string.Join(": ", handsRemovalName, Loc.GetString("admin-smite-remove-hands-description"))
|
||||
};
|
||||
args.Verbs.Add(handsRemoval);
|
||||
|
||||
var handRemovalName = Loc.GetString("admin-smite-remove-hand-name").ToLowerInvariant();
|
||||
Verb handRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-pinball-name",
|
||||
Text = handRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/remove-hand.png")),
|
||||
Act = () =>
|
||||
@@ -350,13 +361,14 @@ public sealed partial class AdminVerbSystem
|
||||
Filter.PvsExcept(args.Target), true, PopupType.Medium);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-hand-description")
|
||||
Message = string.Join(": ", handRemovalName, Loc.GetString("admin-smite-remove-hand-description"))
|
||||
};
|
||||
args.Verbs.Add(handRemoval);
|
||||
|
||||
var stomachRemovalName = Loc.GetString("admin-smite-stomach-removal-name").ToLowerInvariant();
|
||||
Verb stomachRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-yeet-name",
|
||||
Text = stomachRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "stomach"),
|
||||
Act = () =>
|
||||
@@ -370,13 +382,14 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-stomach-removal-description"),
|
||||
Message = string.Join(": ", stomachRemovalName, Loc.GetString("admin-smite-stomach-removal-description"))
|
||||
};
|
||||
args.Verbs.Add(stomachRemoval);
|
||||
|
||||
var lungRemovalName = Loc.GetString("admin-smite-lung-removal-name").ToLowerInvariant();
|
||||
Verb lungRemoval = new()
|
||||
{
|
||||
Text = "admin-smite-become-bread-name",
|
||||
Text = lungRemovalName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Species/Human/organs.rsi"), "lung-r"),
|
||||
Act = () =>
|
||||
@@ -390,16 +403,17 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-lung-removal-description"),
|
||||
Message = string.Join(": ", lungRemovalName, Loc.GetString("admin-smite-lung-removal-description"))
|
||||
};
|
||||
args.Verbs.Add(lungRemoval);
|
||||
}
|
||||
|
||||
if (TryComp<PhysicsComponent>(args.Target, out var physics))
|
||||
{
|
||||
var pinballName = Loc.GetString("admin-smite-pinball-name").ToLowerInvariant();
|
||||
Verb pinball = new()
|
||||
{
|
||||
Text = "admin-smite-ghostkick-name",
|
||||
Text = pinballName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "basketball"),
|
||||
Act = () =>
|
||||
@@ -427,13 +441,14 @@ public sealed partial class AdminVerbSystem
|
||||
_physics.SetAngularDamping(args.Target, physics, 0f);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-pinball-description")
|
||||
Message = string.Join(": ", pinballName, Loc.GetString("admin-smite-pinball-description"))
|
||||
};
|
||||
args.Verbs.Add(pinball);
|
||||
|
||||
var yeetName = Loc.GetString("admin-smite-yeet-name").ToLowerInvariant();
|
||||
Verb yeet = new()
|
||||
{
|
||||
Text = "admin-smite-nyanify-name",
|
||||
Text = yeetName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -457,11 +472,12 @@ public sealed partial class AdminVerbSystem
|
||||
_physics.SetAngularDamping(args.Target, physics, 0f);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-yeet-description")
|
||||
Message = string.Join(": ", yeetName, Loc.GetString("admin-smite-yeet-description"))
|
||||
};
|
||||
args.Verbs.Add(yeet);
|
||||
}
|
||||
|
||||
var breadName = Loc.GetString("admin-smite-become-bread-name").ToLowerInvariant(); // Will I get cancelled for breadName-ing you?
|
||||
Verb bread = new()
|
||||
{
|
||||
Text = "admin-smite-kill-sign-name",
|
||||
@@ -472,10 +488,11 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminBreadSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-bread-description")
|
||||
Message = string.Join(": ", breadName, Loc.GetString("admin-smite-become-bread-description"))
|
||||
};
|
||||
args.Verbs.Add(bread);
|
||||
|
||||
var mouseName = Loc.GetString("admin-smite-become-mouse-name").ToLowerInvariant();
|
||||
Verb mouse = new()
|
||||
{
|
||||
Text = "admin-smite-cluwne-name",
|
||||
@@ -486,15 +503,16 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminMouseSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-mouse-description")
|
||||
Message = string.Join(": ", mouseName, Loc.GetString("admin-smite-become-mouse-description"))
|
||||
};
|
||||
args.Verbs.Add(mouse);
|
||||
|
||||
if (TryComp<ActorComponent>(args.Target, out var actorComponent))
|
||||
{
|
||||
var ghostKickName = Loc.GetString("admin-smite-ghostkick-name").ToLowerInvariant();
|
||||
Verb ghostKick = new()
|
||||
{
|
||||
Text = "admin-smite-anger-pointing-arrows-name",
|
||||
Text = ghostKickName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/gavel.svg.192dpi.png")),
|
||||
Act = () =>
|
||||
@@ -502,15 +520,18 @@ public sealed partial class AdminVerbSystem
|
||||
_ghostKickManager.DoDisconnect(actorComponent.PlayerSession.Channel, "Smitten.");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-ghostkick-description")
|
||||
Message = string.Join(": ", ghostKickName, Loc.GetString("admin-smite-ghostkick-description"))
|
||||
|
||||
};
|
||||
args.Verbs.Add(ghostKick);
|
||||
}
|
||||
|
||||
if (TryComp<InventoryComponent>(args.Target, out var inventory)) {
|
||||
if (TryComp<InventoryComponent>(args.Target, out var inventory))
|
||||
{
|
||||
var nyanifyName = Loc.GetString("admin-smite-nyanify-name").ToLowerInvariant();
|
||||
Verb nyanify = new()
|
||||
{
|
||||
Text = "admin-smite-dust-name",
|
||||
Text = nyanifyName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Head/Hats/catears.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -521,13 +542,14 @@ public sealed partial class AdminVerbSystem
|
||||
_inventorySystem.TryEquip(args.Target, ears, "head", true, true, false, inventory);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-nyanify-description")
|
||||
Message = string.Join(": ", nyanifyName, Loc.GetString("admin-smite-nyanify-description"))
|
||||
};
|
||||
args.Verbs.Add(nyanify);
|
||||
|
||||
var killSignName = Loc.GetString("admin-smite-kill-sign-name").ToLowerInvariant();
|
||||
Verb killSign = new()
|
||||
{
|
||||
Text = "admin-smite-buffering-name",
|
||||
Text = killSignName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Misc/killsign.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -535,13 +557,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<KillSignComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-kill-sign-description")
|
||||
Message = string.Join(": ", killSignName, Loc.GetString("admin-smite-kill-sign-description"))
|
||||
};
|
||||
args.Verbs.Add(killSign);
|
||||
|
||||
var cluwneName = Loc.GetString("admin-smite-cluwne-name").ToLowerInvariant();
|
||||
Verb cluwne = new()
|
||||
{
|
||||
Text = "admin-smite-become-instrument-name",
|
||||
Text = cluwneName,
|
||||
Category = VerbCategory.Smite,
|
||||
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Mask/cluwne.rsi"), "icon"),
|
||||
@@ -551,13 +574,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<CluwneComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-cluwne-description")
|
||||
Message = string.Join(": ", cluwneName, Loc.GetString("admin-smite-cluwne-description"))
|
||||
};
|
||||
args.Verbs.Add(cluwne);
|
||||
|
||||
var maidenName = Loc.GetString("admin-smite-maid-name").ToLowerInvariant();
|
||||
Verb maiden = new()
|
||||
{
|
||||
Text = "admin-smite-remove-gravity-name",
|
||||
Text = maidenName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Clothing/Uniforms/Jumpskirt/janimaid.rsi"), "icon"),
|
||||
Act = () =>
|
||||
@@ -570,14 +594,15 @@ public sealed partial class AdminVerbSystem
|
||||
});
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-maid-description")
|
||||
Message = string.Join(": ", maidenName, Loc.GetString("admin-smite-maid-description"))
|
||||
};
|
||||
args.Verbs.Add(maiden);
|
||||
}
|
||||
|
||||
var angerPointingArrowsName = Loc.GetString("admin-smite-anger-pointing-arrows-name").ToLowerInvariant();
|
||||
Verb angerPointingArrows = new()
|
||||
{
|
||||
Text = "admin-smite-reptilian-species-swap-name",
|
||||
Text = angerPointingArrowsName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Interface/Misc/pointing.rsi"), "pointing"),
|
||||
Act = () =>
|
||||
@@ -585,13 +610,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<PointingArrowAngeringComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-anger-pointing-arrows-description")
|
||||
Message = string.Join(": ", angerPointingArrowsName, Loc.GetString("admin-smite-anger-pointing-arrows-description"))
|
||||
};
|
||||
args.Verbs.Add(angerPointingArrows);
|
||||
|
||||
var dustName = Loc.GetString("admin-smite-dust-name").ToLowerInvariant();
|
||||
Verb dust = new()
|
||||
{
|
||||
Text = "admin-smite-locker-stuff-name",
|
||||
Text = dustName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Materials/materials.rsi"), "ash"),
|
||||
Act = () =>
|
||||
@@ -601,13 +627,14 @@ public sealed partial class AdminVerbSystem
|
||||
_popupSystem.PopupEntity(Loc.GetString("admin-smite-turned-ash-other", ("name", args.Target)), args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-dust-description"),
|
||||
Message = string.Join(": ", dustName, Loc.GetString("admin-smite-dust-description"))
|
||||
};
|
||||
args.Verbs.Add(dust);
|
||||
|
||||
var youtubeVideoSimulationName = Loc.GetString("admin-smite-buffering-name").ToLowerInvariant();
|
||||
Verb youtubeVideoSimulation = new()
|
||||
{
|
||||
Text = "admin-smite-headstand-name",
|
||||
Text = youtubeVideoSimulationName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Misc/buffering_smite_icon.png")),
|
||||
Act = () =>
|
||||
@@ -615,10 +642,11 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<BufferingComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-buffering-description"),
|
||||
Message = string.Join(": ", youtubeVideoSimulationName, Loc.GetString("admin-smite-buffering-description"))
|
||||
};
|
||||
args.Verbs.Add(youtubeVideoSimulation);
|
||||
|
||||
var instrumentationName = Loc.GetString("admin-smite-become-instrument-name").ToLowerInvariant();
|
||||
Verb instrumentation = new()
|
||||
{
|
||||
Text = "admin-smite-become-mouse-name",
|
||||
@@ -629,13 +657,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminInstrumentSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-become-instrument-description"),
|
||||
Message = string.Join(": ", instrumentationName, Loc.GetString("admin-smite-become-instrument-description"))
|
||||
};
|
||||
args.Verbs.Add(instrumentation);
|
||||
|
||||
var noGravityName = Loc.GetString("admin-smite-remove-gravity-name").ToLowerInvariant();
|
||||
Verb noGravity = new()
|
||||
{
|
||||
Text = "admin-smite-maid-name",
|
||||
Text = noGravityName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
|
||||
Act = () =>
|
||||
@@ -646,13 +675,14 @@ public sealed partial class AdminVerbSystem
|
||||
Dirty(args.Target, grav);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-remove-gravity-description"),
|
||||
Message = string.Join(": ", noGravityName, Loc.GetString("admin-smite-remove-gravity-description"))
|
||||
};
|
||||
args.Verbs.Add(noGravity);
|
||||
|
||||
var reptilianName = Loc.GetString("admin-smite-reptilian-species-swap-name").ToLowerInvariant();
|
||||
Verb reptilian = new()
|
||||
{
|
||||
Text = "admin-smite-zoom-in-name",
|
||||
Text = reptilianName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Objects/Fun/toys.rsi"), "plushie_lizard"),
|
||||
Act = () =>
|
||||
@@ -660,13 +690,14 @@ public sealed partial class AdminVerbSystem
|
||||
_polymorphSystem.PolymorphEntity(args.Target, "AdminLizardSmite");
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-reptilian-species-swap-description"),
|
||||
Message = string.Join(": ", reptilianName, Loc.GetString("admin-smite-reptilian-species-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(reptilian);
|
||||
|
||||
var lockerName = Loc.GetString("admin-smite-locker-stuff-name").ToLowerInvariant();
|
||||
Verb locker = new()
|
||||
{
|
||||
Text = "admin-smite-flip-eye-name",
|
||||
Text = lockerName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Storage/closet.rsi"), "generic"),
|
||||
Act = () =>
|
||||
@@ -682,10 +713,11 @@ public sealed partial class AdminVerbSystem
|
||||
_weldableSystem.SetWeldedState(locker, true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-locker-stuff-description"),
|
||||
Message = string.Join(": ", lockerName, Loc.GetString("admin-smite-locker-stuff-description"))
|
||||
};
|
||||
args.Verbs.Add(locker);
|
||||
|
||||
var headstandName = Loc.GetString("admin-smite-headstand-name").ToLowerInvariant();
|
||||
Verb headstand = new()
|
||||
{
|
||||
Text = "admin-smite-run-walk-swap-name",
|
||||
@@ -696,13 +728,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<HeadstandComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-headstand-description"),
|
||||
Message = string.Join(": ", headstandName, Loc.GetString("admin-smite-headstand-description"))
|
||||
};
|
||||
args.Verbs.Add(headstand);
|
||||
|
||||
var zoomInName = Loc.GetString("admin-smite-zoom-in-name").ToLowerInvariant();
|
||||
Verb zoomIn = new()
|
||||
{
|
||||
Text = "admin-smite-super-speed-name",
|
||||
Text = zoomInName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/zoom.png")),
|
||||
Act = () =>
|
||||
@@ -711,13 +744,14 @@ public sealed partial class AdminVerbSystem
|
||||
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * 0.2f, ignoreLimits: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-zoom-in-description"),
|
||||
Message = string.Join(": ", zoomInName, Loc.GetString("admin-smite-zoom-in-description"))
|
||||
};
|
||||
args.Verbs.Add(zoomIn);
|
||||
|
||||
var flipEyeName = Loc.GetString("admin-smite-flip-eye-name").ToLowerInvariant();
|
||||
Verb flipEye = new()
|
||||
{
|
||||
Text = "admin-smite-stomach-removal-name",
|
||||
Text = flipEyeName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/flip.png")),
|
||||
Act = () =>
|
||||
@@ -726,13 +760,14 @@ public sealed partial class AdminVerbSystem
|
||||
_eyeSystem.SetZoom(args.Target, eye.TargetZoom * -1, ignoreLimits: true);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-flip-eye-description"),
|
||||
Message = string.Join(": ", flipEyeName, Loc.GetString("admin-smite-flip-eye-description"))
|
||||
};
|
||||
args.Verbs.Add(flipEye);
|
||||
|
||||
var runWalkSwapName = Loc.GetString("admin-smite-run-walk-swap-name").ToLowerInvariant();
|
||||
Verb runWalkSwap = new()
|
||||
{
|
||||
Text = "admin-smite-speak-backwards-name",
|
||||
Text = runWalkSwapName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/run-walk-swap.png")),
|
||||
Act = () =>
|
||||
@@ -746,13 +781,14 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-run-walk-swap-description"),
|
||||
Message = string.Join(": ", runWalkSwapName, Loc.GetString("admin-smite-run-walk-swap-description"))
|
||||
};
|
||||
args.Verbs.Add(runWalkSwap);
|
||||
|
||||
var backwardsAccentName = Loc.GetString("admin-smite-speak-backwards-name").ToLowerInvariant();
|
||||
Verb backwardsAccent = new()
|
||||
{
|
||||
Text = "admin-smite-lung-removal-name",
|
||||
Text = backwardsAccentName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/AdminActions/help-backwards.png")),
|
||||
Act = () =>
|
||||
@@ -760,13 +796,14 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<BackwardsAccentComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-speak-backwards-description"),
|
||||
Message = string.Join(": ", backwardsAccentName, Loc.GetString("admin-smite-speak-backwards-description"))
|
||||
};
|
||||
args.Verbs.Add(backwardsAccent);
|
||||
|
||||
var disarmProneName = Loc.GetString("admin-smite-disarm-prone-name").ToLowerInvariant();
|
||||
Verb disarmProne = new()
|
||||
{
|
||||
Text = "admin-smite-disarm-prone-name",
|
||||
Text = disarmProneName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/disarm.png")),
|
||||
Act = () =>
|
||||
@@ -774,10 +811,11 @@ public sealed partial class AdminVerbSystem
|
||||
EnsureComp<DisarmProneComponent>(args.Target);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-disarm-prone-description"),
|
||||
Message = string.Join(": ", disarmProneName, Loc.GetString("admin-smite-disarm-prone-description"))
|
||||
};
|
||||
args.Verbs.Add(disarmProne);
|
||||
|
||||
var superSpeedName = Loc.GetString("admin-smite-super-speed-name").ToLowerInvariant();
|
||||
Verb superSpeed = new()
|
||||
{
|
||||
Text = "admin-smite-garbage-can-name",
|
||||
@@ -792,41 +830,45 @@ public sealed partial class AdminVerbSystem
|
||||
args.Target, PopupType.LargeCaution);
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-super-speed-description"),
|
||||
Message = string.Join(": ", superSpeedName, Loc.GetString("admin-smite-super-speed-description"))
|
||||
};
|
||||
args.Verbs.Add(superSpeed);
|
||||
|
||||
//Bonk
|
||||
var superBonkLiteName = Loc.GetString("admin-smite-super-bonk-lite-name").ToLowerInvariant();
|
||||
Verb superBonkLite = new()
|
||||
{
|
||||
Text = "admin-smite-super-bonk-name",
|
||||
Text = superBonkLiteName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/glass.rsi"), "full"),
|
||||
Act = () =>
|
||||
{
|
||||
_superBonkSystem.StartSuperBonk(args.Target, stopWhenDead: true);
|
||||
},
|
||||
Message = Loc.GetString("admin-smite-super-bonk-lite-description"),
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", superBonkLiteName, Loc.GetString("admin-smite-super-bonk-lite-description"))
|
||||
};
|
||||
args.Verbs.Add(superBonkLite);
|
||||
|
||||
var superBonkName = Loc.GetString("admin-smite-super-bonk-name").ToLowerInvariant();
|
||||
Verb superBonk= new()
|
||||
{
|
||||
Text = "admin-smite-super-bonk-lite-name",
|
||||
Text = superBonkName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Structures/Furniture/Tables/generic.rsi"), "full"),
|
||||
Act = () =>
|
||||
{
|
||||
_superBonkSystem.StartSuperBonk(args.Target);
|
||||
},
|
||||
Message = Loc.GetString("admin-smite-super-bonk-description"),
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = string.Join(": ", superBonkName, Loc.GetString("admin-smite-super-bonk-description"))
|
||||
};
|
||||
args.Verbs.Add(superBonk);
|
||||
|
||||
var superslipName = Loc.GetString("admin-smite-super-slip-name").ToLowerInvariant();
|
||||
Verb superslip = new()
|
||||
{
|
||||
Text = "admin-smite-super-slip-name",
|
||||
Text = superslipName,
|
||||
Category = VerbCategory.Smite,
|
||||
Icon = new SpriteSpecifier.Rsi(new("Objects/Specific/Janitorial/soap.rsi"), "omega-4"),
|
||||
Act = () =>
|
||||
@@ -846,7 +888,7 @@ public sealed partial class AdminVerbSystem
|
||||
}
|
||||
},
|
||||
Impact = LogImpact.Extreme,
|
||||
Message = Loc.GetString("admin-smite-super-slip-description")
|
||||
Message = string.Join(": ", superslipName, Loc.GetString("admin-smite-super-slip-description"))
|
||||
};
|
||||
args.Verbs.Add(superslip);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,10 @@ using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using Content.Server.Silicons.Laws;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Robust.Server.Player;
|
||||
using Content.Shared.Silicons.StationAi;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using static Content.Shared.Configurable.ConfigurationComponent;
|
||||
|
||||
@@ -345,7 +347,30 @@ namespace Content.Server.Administration.Systems
|
||||
Impact = LogImpact.Low
|
||||
});
|
||||
|
||||
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
|
||||
// This logic is needed to be able to modify the AI's laws through its core and eye.
|
||||
EntityUid? target = null;
|
||||
SiliconLawBoundComponent? lawBoundComponent = null;
|
||||
|
||||
if (TryComp(args.Target, out lawBoundComponent))
|
||||
{
|
||||
target = args.Target;
|
||||
}
|
||||
// When inspecting the core we can find the entity with its laws by looking at the AiHolderComponent.
|
||||
else if (TryComp<StationAiHolderComponent>(args.Target, out var holder) && holder.Slot.Item != null
|
||||
&& TryComp(holder.Slot.Item, out lawBoundComponent))
|
||||
{
|
||||
target = holder.Slot.Item.Value;
|
||||
// For the eye we can find the entity with its laws as the source of the movement relay since the eye
|
||||
// is just a proxy for it to move around and look around the station.
|
||||
}
|
||||
else if (TryComp<MovementRelayTargetComponent>(args.Target, out var relay)
|
||||
&& TryComp(relay.Source, out lawBoundComponent))
|
||||
{
|
||||
target = relay.Source;
|
||||
|
||||
}
|
||||
|
||||
if (lawBoundComponent != null && target != null)
|
||||
{
|
||||
args.Verbs.Add(new Verb()
|
||||
{
|
||||
@@ -359,7 +384,7 @@ namespace Content.Server.Administration.Systems
|
||||
return;
|
||||
}
|
||||
_euiManager.OpenEui(ui, session);
|
||||
ui.UpdateLaws(lawBoundComponent, args.Target);
|
||||
ui.UpdateLaws(lawBoundComponent, target.Value);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
|
||||
});
|
||||
|
||||
@@ -47,20 +47,23 @@ namespace Content.Server.Administration.Systems
|
||||
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
|
||||
private static partial Regex DiscordRegex();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private string _webhookUrl = string.Empty;
|
||||
private WebhookData? _webhookData;
|
||||
|
||||
private string _onCallUrl = string.Empty;
|
||||
private WebhookData? _onCallData;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly HttpClient _httpClient = new();
|
||||
|
||||
private string _footerIconUrl = string.Empty;
|
||||
private string _avatarUrl = string.Empty;
|
||||
private string _serverName = string.Empty;
|
||||
|
||||
private readonly
|
||||
Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel
|
||||
lastRunLevel)> _relayMessages = new();
|
||||
private readonly Dictionary<NetUserId, DiscordRelayInteraction> _relayMessages = new();
|
||||
|
||||
private Dictionary<NetUserId, string> _oldMessageIds = new();
|
||||
private readonly Dictionary<NetUserId, Queue<string>> _messageQueues = new();
|
||||
private readonly Dictionary<NetUserId, Queue<DiscordRelayedData>> _messageQueues = new();
|
||||
private readonly HashSet<NetUserId> _processingChannels = new();
|
||||
private readonly Dictionary<NetUserId, (TimeSpan Timestamp, bool Typing)> _typingUpdateTimestamps = new();
|
||||
private string _overrideClientName = string.Empty;
|
||||
@@ -82,12 +85,16 @@ namespace Content.Server.Administration.Systems
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
Subs.CVar(_config, CCVars.DiscordOnCallWebhook, OnCallChanged, true);
|
||||
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpWebhook, OnWebhookChanged, true);
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpFooterIcon, OnFooterIconChanged, true);
|
||||
Subs.CVar(_config, CCVars.DiscordAHelpAvatar, OnAvatarChanged, true);
|
||||
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
|
||||
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
|
||||
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
|
||||
|
||||
var defaultParams = new AHelpMessageParams(
|
||||
string.Empty,
|
||||
string.Empty,
|
||||
@@ -96,7 +103,7 @@ namespace Content.Server.Administration.Systems
|
||||
_gameTicker.RunLevel,
|
||||
playedSound: false
|
||||
);
|
||||
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Length;
|
||||
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Message.Length;
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
|
||||
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
|
||||
@@ -111,6 +118,33 @@ namespace Content.Server.Administration.Systems
|
||||
);
|
||||
}
|
||||
|
||||
private async void OnCallChanged(string url)
|
||||
{
|
||||
_onCallUrl = url;
|
||||
|
||||
if (url == string.Empty)
|
||||
return;
|
||||
|
||||
var match = DiscordRegex().Match(url);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
Log.Error("On call URL does not appear to be valid.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.Groups.Count <= 2)
|
||||
{
|
||||
Log.Error("Could not get webhook ID or token for on call URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
var webhookId = match.Groups[1].Value;
|
||||
var webhookToken = match.Groups[2].Value;
|
||||
|
||||
_onCallData = await GetWebhookData(webhookId, webhookToken);
|
||||
}
|
||||
|
||||
private void PlayerRateLimitedAction(ICommonSession obj)
|
||||
{
|
||||
RaiseNetworkEvent(
|
||||
@@ -259,13 +293,13 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
// Store the Discord message IDs of the previous round
|
||||
_oldMessageIds = new Dictionary<NetUserId, string>();
|
||||
foreach (var message in _relayMessages)
|
||||
foreach (var (user, interaction) in _relayMessages)
|
||||
{
|
||||
var id = message.Value.id;
|
||||
var id = interaction.Id;
|
||||
if (id == null)
|
||||
return;
|
||||
|
||||
_oldMessageIds[message.Key] = id;
|
||||
_oldMessageIds[user] = id;
|
||||
}
|
||||
|
||||
_relayMessages.Clear();
|
||||
@@ -330,10 +364,10 @@ namespace Content.Server.Administration.Systems
|
||||
var webhookToken = match.Groups[2].Value;
|
||||
|
||||
// Fire and forget
|
||||
await SetWebhookData(webhookId, webhookToken);
|
||||
_webhookData = await GetWebhookData(webhookId, webhookToken);
|
||||
}
|
||||
|
||||
private async Task SetWebhookData(string id, string token)
|
||||
private async Task<WebhookData?> GetWebhookData(string id, string token)
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"https://discord.com/api/v10/webhooks/{id}/{token}");
|
||||
|
||||
@@ -342,10 +376,10 @@ namespace Content.Server.Administration.Systems
|
||||
{
|
||||
_sawmill.Log(LogLevel.Error,
|
||||
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
_webhookData = JsonSerializer.Deserialize<WebhookData>(content);
|
||||
return JsonSerializer.Deserialize<WebhookData>(content);
|
||||
}
|
||||
|
||||
private void OnFooterIconChanged(string url)
|
||||
@@ -358,14 +392,14 @@ namespace Content.Server.Administration.Systems
|
||||
_avatarUrl = url;
|
||||
}
|
||||
|
||||
private async void ProcessQueue(NetUserId userId, Queue<string> messages)
|
||||
private async void ProcessQueue(NetUserId userId, Queue<DiscordRelayedData> messages)
|
||||
{
|
||||
// Whether an embed already exists for this player
|
||||
var exists = _relayMessages.TryGetValue(userId, out var existingEmbed);
|
||||
|
||||
// Whether the message will become too long after adding these new messages
|
||||
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Length, MessageLengthCap) + "\n".Length)
|
||||
+ existingEmbed.description.Length > DescriptionMax;
|
||||
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Message.Length, MessageLengthCap) + "\n".Length)
|
||||
+ existingEmbed?.Description.Length > DescriptionMax;
|
||||
|
||||
// If there is no existing embed, or it is getting too long, we create a new embed
|
||||
if (!exists || tooLong)
|
||||
@@ -385,10 +419,10 @@ namespace Content.Server.Administration.Systems
|
||||
// If we have all the data required, we can link to the embed of the previous round or embed that was too long
|
||||
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
||||
{
|
||||
if (tooLong && existingEmbed.id != null)
|
||||
if (tooLong && existingEmbed?.Id != null)
|
||||
{
|
||||
linkToPrevious =
|
||||
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
|
||||
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**\n";
|
||||
}
|
||||
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
|
||||
{
|
||||
@@ -398,13 +432,22 @@ namespace Content.Server.Administration.Systems
|
||||
}
|
||||
|
||||
var characterName = _minds.GetCharacterName(userId);
|
||||
existingEmbed = (null, lookup.Username, linkToPrevious, characterName, _gameTicker.RunLevel);
|
||||
existingEmbed = new DiscordRelayInteraction()
|
||||
{
|
||||
Id = null,
|
||||
CharacterName = characterName,
|
||||
Description = linkToPrevious,
|
||||
Username = lookup.Username,
|
||||
LastRunLevel = _gameTicker.RunLevel,
|
||||
};
|
||||
|
||||
_relayMessages[userId] = existingEmbed;
|
||||
}
|
||||
|
||||
// Previous message was in another RunLevel, so show that in the embed
|
||||
if (existingEmbed.lastRunLevel != _gameTicker.RunLevel)
|
||||
if (existingEmbed!.LastRunLevel != _gameTicker.RunLevel)
|
||||
{
|
||||
existingEmbed.description += _gameTicker.RunLevel switch
|
||||
existingEmbed.Description += _gameTicker.RunLevel switch
|
||||
{
|
||||
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
|
||||
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
|
||||
@@ -413,26 +456,35 @@ namespace Content.Server.Administration.Systems
|
||||
$"{_gameTicker.RunLevel} was not matched."),
|
||||
};
|
||||
|
||||
existingEmbed.lastRunLevel = _gameTicker.RunLevel;
|
||||
existingEmbed.LastRunLevel = _gameTicker.RunLevel;
|
||||
}
|
||||
|
||||
// If last message of the new batch is SOS then relay it to on-call.
|
||||
// ... as long as it hasn't been relayed already.
|
||||
var discordMention = messages.Last();
|
||||
var onCallRelay = !discordMention.Receivers && !existingEmbed.OnCall;
|
||||
|
||||
// Add available messages to the embed description
|
||||
while (messages.TryDequeue(out var message))
|
||||
{
|
||||
// In case someone thinks they're funny
|
||||
if (message.Length > MessageLengthCap)
|
||||
message = message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
|
||||
string text;
|
||||
|
||||
existingEmbed.description += $"\n{message}";
|
||||
// In case someone thinks they're funny
|
||||
if (message.Message.Length > MessageLengthCap)
|
||||
text = message.Message[..(MessageLengthCap - TooLongText.Length)] + TooLongText;
|
||||
else
|
||||
text = message.Message;
|
||||
|
||||
existingEmbed.Description += $"\n{text}";
|
||||
}
|
||||
|
||||
var payload = GeneratePayload(existingEmbed.description,
|
||||
existingEmbed.username,
|
||||
existingEmbed.characterName);
|
||||
var payload = GeneratePayload(existingEmbed.Description,
|
||||
existingEmbed.Username,
|
||||
existingEmbed.CharacterName);
|
||||
|
||||
// If there is no existing embed, create a new one
|
||||
// Otherwise patch (edit) it
|
||||
if (existingEmbed.id == null)
|
||||
if (existingEmbed.Id == null)
|
||||
{
|
||||
var request = await _httpClient.PostAsync($"{_webhookUrl}?wait=true",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
@@ -455,11 +507,11 @@ namespace Content.Server.Administration.Systems
|
||||
return;
|
||||
}
|
||||
|
||||
existingEmbed.id = id.ToString();
|
||||
existingEmbed.Id = id.ToString();
|
||||
}
|
||||
else
|
||||
{
|
||||
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.id}",
|
||||
var request = await _httpClient.PatchAsync($"{_webhookUrl}/messages/{existingEmbed.Id}",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
|
||||
if (!request.IsSuccessStatusCode)
|
||||
@@ -474,6 +526,43 @@ namespace Content.Server.Administration.Systems
|
||||
|
||||
_relayMessages[userId] = existingEmbed;
|
||||
|
||||
// Actually do the on call relay last, we just need to grab it before we dequeue every message above.
|
||||
if (onCallRelay &&
|
||||
_onCallData != null)
|
||||
{
|
||||
existingEmbed.OnCall = true;
|
||||
var roleMention = _config.GetCVar(CCVars.DiscordAhelpMention);
|
||||
|
||||
if (!string.IsNullOrEmpty(roleMention))
|
||||
{
|
||||
var message = new StringBuilder();
|
||||
message.AppendLine($"<@&{roleMention}>");
|
||||
message.AppendLine("Unanswered SOS");
|
||||
|
||||
// Need webhook data to get the correct link for that channel rather than on-call data.
|
||||
if (_webhookData is { GuildId: { } guildId, ChannelId: { } channelId })
|
||||
{
|
||||
message.AppendLine(
|
||||
$"**[Go to ahelp](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.Id})**");
|
||||
}
|
||||
|
||||
payload = GeneratePayload(message.ToString(), existingEmbed.Username, existingEmbed.CharacterName);
|
||||
|
||||
var request = await _httpClient.PostAsync($"{_onCallUrl}?wait=true",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"));
|
||||
|
||||
var content = await request.Content.ReadAsStringAsync();
|
||||
if (!request.IsSuccessStatusCode)
|
||||
{
|
||||
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting relay message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
existingEmbed.OnCall = false;
|
||||
}
|
||||
|
||||
_processingChannels.Remove(userId);
|
||||
}
|
||||
|
||||
@@ -652,7 +741,7 @@ namespace Content.Server.Administration.Systems
|
||||
if (sendsWebhook)
|
||||
{
|
||||
if (!_messageQueues.ContainsKey(msg.UserId))
|
||||
_messageQueues[msg.UserId] = new Queue<string>();
|
||||
_messageQueues[msg.UserId] = new Queue<DiscordRelayedData>();
|
||||
|
||||
var str = message.Text;
|
||||
var unameLength = senderSession.Name.Length;
|
||||
@@ -701,7 +790,7 @@ namespace Content.Server.Administration.Systems
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string GenerateAHelpMessage(AHelpMessageParams parameters)
|
||||
private static DiscordRelayedData GenerateAHelpMessage(AHelpMessageParams parameters)
|
||||
{
|
||||
var stringbuilder = new StringBuilder();
|
||||
|
||||
@@ -718,13 +807,57 @@ namespace Content.Server.Administration.Systems
|
||||
stringbuilder.Append($" **{parameters.RoundTime}**");
|
||||
if (!parameters.PlayedSound)
|
||||
stringbuilder.Append(" **(S)**");
|
||||
|
||||
if (parameters.Icon == null)
|
||||
stringbuilder.Append($" **{parameters.Username}:** ");
|
||||
else
|
||||
stringbuilder.Append($" **{parameters.Username}** ");
|
||||
stringbuilder.Append(parameters.Message);
|
||||
return stringbuilder.ToString();
|
||||
|
||||
return new DiscordRelayedData()
|
||||
{
|
||||
Receivers = !parameters.NoReceivers,
|
||||
Message = stringbuilder.ToString(),
|
||||
};
|
||||
}
|
||||
|
||||
private record struct DiscordRelayedData
|
||||
{
|
||||
/// <summary>
|
||||
/// Was anyone online to receive it.
|
||||
/// </summary>
|
||||
public bool Receivers;
|
||||
|
||||
/// <summary>
|
||||
/// What's the payload to send to discord.
|
||||
/// </summary>
|
||||
public string Message;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class specifically for holding information regarding existing Discord embeds
|
||||
/// </summary>
|
||||
private sealed class DiscordRelayInteraction
|
||||
{
|
||||
public string? Id;
|
||||
|
||||
public string Username = String.Empty;
|
||||
|
||||
public string? CharacterName;
|
||||
|
||||
/// <summary>
|
||||
/// Contents for the discord message.
|
||||
/// </summary>
|
||||
public string Description = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Run level of the last interaction. If different we'll link to the last Id.
|
||||
/// </summary>
|
||||
public GameRunLevel LastRunLevel;
|
||||
|
||||
/// <summary>
|
||||
/// Did we relay this interaction to OnCall previously.
|
||||
/// </summary>
|
||||
public bool OnCall;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed partial class AmeControllerComponent : SharedAmeControllerComponen
|
||||
/// </summary>
|
||||
[DataField("injectSound")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public SoundSpecifier InjectSound = new SoundCollectionSpecifier("MetalThud");
|
||||
public SoundSpecifier InjectSound = new SoundPathSpecifier("/Audio/Machines/ame_fuelinjection.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The last time this could have injected fuel into the AME.
|
||||
|
||||
@@ -22,11 +22,17 @@ public sealed class TechAnomalySystem : EntitySystem
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TechAnomalyComponent, MapInitEvent>(OnTechMapInit);
|
||||
SubscribeLocalEvent<TechAnomalyComponent, AnomalyPulseEvent>(OnPulse);
|
||||
SubscribeLocalEvent<TechAnomalyComponent, AnomalySupercriticalEvent>(OnSupercritical);
|
||||
SubscribeLocalEvent<TechAnomalyComponent, AnomalyStabilityChangedEvent>(OnStabilityChanged);
|
||||
}
|
||||
|
||||
private void OnTechMapInit(Entity<TechAnomalyComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextTimer = _timing.CurTime;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
@@ -11,7 +11,6 @@ using Content.Server.Preferences.Managers;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Roles.Jobs;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Station.Systems;
|
||||
using Content.Shared.Antag;
|
||||
using Content.Shared.Clothing;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -20,7 +19,6 @@ using Content.Shared.Ghost;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Server.Audio;
|
||||
@@ -37,14 +35,14 @@ namespace Content.Server.Antag;
|
||||
|
||||
public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelectionComponent>
|
||||
{
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _pref = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly IChatManager _chat = default!;
|
||||
[Dependency] private readonly GhostRoleSystem _ghostRole = default!;
|
||||
[Dependency] private readonly JobSystem _jobs = default!;
|
||||
[Dependency] private readonly LoadoutSystem _loadout = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IServerPreferencesManager _pref = default!;
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly TransformSystem _transform = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
|
||||
@@ -193,6 +191,9 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// <summary>
|
||||
/// Chooses antagonists from the given selection of players
|
||||
/// </summary>
|
||||
/// <param name="ent">The antagonist rule entity</param>
|
||||
/// <param name="pool">The players to choose from</param>
|
||||
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, bool midround = false)
|
||||
{
|
||||
if (ent.Comp.SelectionsComplete)
|
||||
@@ -209,8 +210,14 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
/// <summary>
|
||||
/// Chooses antagonists from the given selection of players for the given antag definition.
|
||||
/// </summary>
|
||||
/// <param name="ent">The antagonist rule entity</param>
|
||||
/// <param name="pool">The players to choose from</param>
|
||||
/// <param name="def">The antagonist selection parameters and criteria</param>
|
||||
/// <param name="midround">Disable picking players for pre-spawn antags in the middle of a round</param>
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent, IList<ICommonSession> pool, AntagSelectionDefinition def, bool midround = false)
|
||||
public void ChooseAntags(Entity<AntagSelectionComponent> ent,
|
||||
IList<ICommonSession> pool,
|
||||
AntagSelectionDefinition def,
|
||||
bool midround = false)
|
||||
{
|
||||
var playerPool = GetPlayerPool(ent, pool, def);
|
||||
var count = GetTargetAntagCount(ent, GetTotalPlayerCount(pool), def);
|
||||
@@ -331,7 +338,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
EntityManager.AddComponents(player, def.Components);
|
||||
|
||||
// Equip the entity's RoleLoadout and LoadoutGroup
|
||||
List<ProtoId<StartingGearPrototype>>? gear = new();
|
||||
List<ProtoId<StartingGearPrototype>> gear = new();
|
||||
if (def.StartingGear is not null)
|
||||
gear.Add(def.StartingGear.Value);
|
||||
|
||||
@@ -340,8 +347,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
if (session != null)
|
||||
{
|
||||
var curMind = session.GetMind();
|
||||
|
||||
if (curMind == null ||
|
||||
|
||||
if (curMind == null ||
|
||||
!TryComp<MindComponent>(curMind.Value, out var mindComp) ||
|
||||
mindComp.OwnedEntity != antagEnt)
|
||||
{
|
||||
@@ -350,7 +357,7 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
|
||||
}
|
||||
|
||||
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
|
||||
_role.MindAddRoles(curMind.Value, def.MindComponents, null, true);
|
||||
_role.MindAddRoles(curMind.Value, def.MindRoles, null, true);
|
||||
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
|
||||
SendBriefing(session, def.Briefing);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ using Content.Shared.Antag;
|
||||
using Content.Shared.Destructible.Thresholds;
|
||||
using Content.Shared.Preferences.Loadouts;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Storage;
|
||||
using Content.Shared.Whitelist;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
@@ -145,10 +144,17 @@ public partial struct AntagSelectionDefinition()
|
||||
|
||||
/// <summary>
|
||||
/// Components added to the player's mind.
|
||||
/// Do NOT use this to add role-type components. Add those as MindRoles instead
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public ComponentRegistry MindComponents = new();
|
||||
|
||||
/// <summary>
|
||||
/// List of Mind Role Prototypes to be added to the player's mind.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<EntityPrototype>>? MindRoles;
|
||||
|
||||
/// <summary>
|
||||
/// A set of starting gear that's equipped to the player.
|
||||
/// </summary>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Atmos.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Used by FixGridAtmos. Entities with this may get magically auto-deleted on map initialization in future.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
[RegisterComponent, EntityCategory("Mapping")]
|
||||
public sealed partial class AtmosFixMarkerComponent : Component
|
||||
{
|
||||
// See FixGridAtmos for more details
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
using Content.Server.Atmos.Monitor.Components;
|
||||
using Content.Server.DeviceNetwork.Components;
|
||||
using Content.Server.DeviceNetwork.Systems;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.Consoles;
|
||||
using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Atmos.Monitor.Components;
|
||||
using Content.Shared.DeviceNetwork.Components;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
|
||||
@@ -21,6 +25,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
[Dependency] private readonly AirAlarmSystem _airAlarmSystem = default!;
|
||||
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly MapSystem _mapSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly NavMapSystem _navMapSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly DeviceListSystem _deviceListSystem = default!;
|
||||
|
||||
private const float UpdateTime = 1.0f;
|
||||
|
||||
@@ -38,6 +48,9 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
|
||||
// Grid events
|
||||
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
|
||||
|
||||
// Alarm events
|
||||
SubscribeLocalEvent<AtmosAlertsDeviceComponent, EntityTerminatingEvent>(OnDeviceTerminatingEvent);
|
||||
SubscribeLocalEvent<AtmosAlertsDeviceComponent, AnchorStateChangedEvent>(OnDeviceAnchorChanged);
|
||||
}
|
||||
|
||||
@@ -81,6 +94,16 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
}
|
||||
|
||||
private void OnDeviceAnchorChanged(EntityUid uid, AtmosAlertsDeviceComponent component, AnchorStateChangedEvent args)
|
||||
{
|
||||
OnDeviceAdditionOrRemoval(uid, component, args.Anchored);
|
||||
}
|
||||
|
||||
private void OnDeviceTerminatingEvent(EntityUid uid, AtmosAlertsDeviceComponent component, ref EntityTerminatingEvent args)
|
||||
{
|
||||
OnDeviceAdditionOrRemoval(uid, component, false);
|
||||
}
|
||||
|
||||
private void OnDeviceAdditionOrRemoval(EntityUid uid, AtmosAlertsDeviceComponent component, bool isAdding)
|
||||
{
|
||||
var xform = Transform(uid);
|
||||
var gridUid = xform.GridUid;
|
||||
@@ -88,10 +111,13 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (gridUid == null)
|
||||
return;
|
||||
|
||||
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, gridUid.Value, out var data))
|
||||
if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap))
|
||||
return;
|
||||
|
||||
var netEntity = EntityManager.GetNetEntity(uid);
|
||||
if (!TryGetAtmosDeviceNavMapData(uid, component, xform, out var data))
|
||||
return;
|
||||
|
||||
var netEntity = GetNetEntity(uid);
|
||||
|
||||
var query = AllEntityQuery<AtmosAlertsComputerComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entConsole, out var entXform))
|
||||
@@ -99,11 +125,18 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (gridUid != entXform.GridUid)
|
||||
continue;
|
||||
|
||||
if (args.Anchored)
|
||||
if (isAdding)
|
||||
{
|
||||
entConsole.AtmosDevices.Add(data.Value);
|
||||
}
|
||||
|
||||
else if (!args.Anchored)
|
||||
else
|
||||
{
|
||||
entConsole.AtmosDevices.RemoveWhere(x => x.NetEntity == netEntity);
|
||||
_navMapSystem.RemoveNavMapRegion(gridUid.Value, navMap, netEntity);
|
||||
}
|
||||
|
||||
Dirty(ent, entConsole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +242,12 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (entDevice.Group != group)
|
||||
continue;
|
||||
|
||||
if (!TryComp<MapGridComponent>(entXform.GridUid, out var mapGrid))
|
||||
continue;
|
||||
|
||||
if (!TryComp<NavMapComponent>(entXform.GridUid, out var navMap))
|
||||
continue;
|
||||
|
||||
// If emagged, change the alarm type to normal
|
||||
var alarmState = (entAtmosAlarmable.LastAlarmState == AtmosAlarmType.Emagged) ? AtmosAlarmType.Normal : entAtmosAlarmable.LastAlarmState;
|
||||
|
||||
@@ -216,14 +255,45 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
if (TryComp<ApcPowerReceiverComponent>(ent, out var entAPCPower) && !entAPCPower.Powered)
|
||||
alarmState = AtmosAlarmType.Invalid;
|
||||
|
||||
// Create entry
|
||||
var netEnt = GetNetEntity(ent);
|
||||
|
||||
var entry = new AtmosAlertsComputerEntry
|
||||
(GetNetEntity(ent),
|
||||
(netEnt,
|
||||
GetNetCoordinates(entXform.Coordinates),
|
||||
entDevice.Group,
|
||||
alarmState,
|
||||
MetaData(ent).EntityName,
|
||||
entDeviceNetwork.Address);
|
||||
|
||||
// Get the list of sensors attached to the alarm
|
||||
var sensorList = TryComp<DeviceListComponent>(ent, out var entDeviceList) ? _deviceListSystem.GetDeviceList(ent, entDeviceList) : null;
|
||||
|
||||
if (sensorList?.Any() == true)
|
||||
{
|
||||
var alarmRegionSeeds = new HashSet<Vector2i>();
|
||||
|
||||
// If valid and anchored, use the position of sensors as seeds for the region
|
||||
foreach (var (address, sensorEnt) in sensorList)
|
||||
{
|
||||
if (!sensorEnt.IsValid() || !HasComp<AtmosMonitorComponent>(sensorEnt))
|
||||
continue;
|
||||
|
||||
var sensorXform = Transform(sensorEnt);
|
||||
|
||||
if (sensorXform.Anchored && sensorXform.GridUid == entXform.GridUid)
|
||||
alarmRegionSeeds.Add(_mapSystem.CoordinatesToTile(entXform.GridUid.Value, mapGrid, _transformSystem.GetMapCoordinates(sensorEnt, sensorXform)));
|
||||
}
|
||||
|
||||
var regionProperties = new SharedNavMapSystem.NavMapRegionProperties(netEnt, AtmosAlertsComputerUiKey.Key, alarmRegionSeeds);
|
||||
_navMapSystem.AddOrUpdateNavMapRegion(gridUid, navMap, netEnt, regionProperties);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
_navMapSystem.RemoveNavMapRegion(entXform.GridUid.Value, navMap, netEnt);
|
||||
}
|
||||
|
||||
alarmStateData.Add(entry);
|
||||
}
|
||||
|
||||
@@ -306,7 +376,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
var query = AllEntityQuery<AtmosAlertsDeviceComponent, TransformComponent>();
|
||||
while (query.MoveNext(out var ent, out var entComponent, out var entXform))
|
||||
{
|
||||
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, gridUid, out var data))
|
||||
if (entXform.GridUid != gridUid)
|
||||
continue;
|
||||
|
||||
if (TryGetAtmosDeviceNavMapData(ent, entComponent, entXform, out var data))
|
||||
atmosDeviceNavMapData.Add(data.Value);
|
||||
}
|
||||
|
||||
@@ -317,14 +390,10 @@ public sealed class AtmosAlertsComputerSystem : SharedAtmosAlertsComputerSystem
|
||||
(EntityUid uid,
|
||||
AtmosAlertsDeviceComponent component,
|
||||
TransformComponent xform,
|
||||
EntityUid gridUid,
|
||||
[NotNullWhen(true)] out AtmosAlertsDeviceNavMapData? output)
|
||||
{
|
||||
output = null;
|
||||
|
||||
if (xform.GridUid != gridUid)
|
||||
return false;
|
||||
|
||||
if (!xform.Anchored)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
|
||||
Loc.GetString(
|
||||
"earlyleave-cryo-announcement",
|
||||
("character", name),
|
||||
("entity", ent.Owner),
|
||||
("entity", ent.Owner), // gender things for supporting downstreams with other languages
|
||||
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
|
||||
), Loc.GetString("earlyleave-cryo-sender"),
|
||||
playDefaultSound: false
|
||||
|
||||
@@ -472,7 +472,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume);
|
||||
var currentVolume = bloodSolution.RemoveReagent(component.BloodReagent, bloodSolution.Volume, ignoreReagentData: true);
|
||||
|
||||
component.BloodReagent = reagent;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Botany.Components;
|
||||
|
||||
@@ -23,6 +24,9 @@ public sealed partial class PlantHolderComponent : Component
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
|
||||
public TimeSpan LastCycle = TimeSpan.Zero;
|
||||
|
||||
[DataField]
|
||||
public SoundSpecifier? WateringSound;
|
||||
|
||||
[DataField]
|
||||
public bool UpdateSpriteAfterUpdate;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Botany.Components;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
@@ -18,7 +17,6 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -37,7 +35,6 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly SharedPointLightSystem _pointLight = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
|
||||
@@ -53,6 +50,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
SubscribeLocalEvent<PlantHolderComponent, ExaminedEvent>(OnExamine);
|
||||
SubscribeLocalEvent<PlantHolderComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<PlantHolderComponent, InteractHandEvent>(OnInteractHand);
|
||||
SubscribeLocalEvent<PlantHolderComponent, SolutionTransferredEvent>(OnSolutionTransferred);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
@@ -158,6 +156,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
if (!_botany.TryGetSeed(seeds, out var seed))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
var name = Loc.GetString(seed.Name);
|
||||
var noun = Loc.GetString(seed.Noun);
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-plant-success-message",
|
||||
@@ -185,6 +184,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-already-seeded-message",
|
||||
("name", Comp<MetaDataComponent>(uid).EntityName)), args.User, PopupType.Medium);
|
||||
return;
|
||||
@@ -192,6 +192,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
|
||||
if (_tagSystem.HasTag(args.Used, "Hoe"))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.WeedLevel > 0)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-weeds-message",
|
||||
@@ -211,6 +212,7 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
|
||||
if (HasComp<ShovelComponent>(args.Used))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.Seed != null)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-remove-plant-message",
|
||||
@@ -228,39 +230,9 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (_solutionContainerSystem.TryGetDrainableSolution(args.Used, out var solution, out _)
|
||||
&& _solutionContainerSystem.ResolveSolution(uid, component.SoilSolutionName, ref component.SoilSolution)
|
||||
&& TryComp(args.Used, out SprayComponent? spray))
|
||||
{
|
||||
var amount = FixedPoint2.New(1);
|
||||
|
||||
var targetEntity = uid;
|
||||
var solutionEntity = args.Used;
|
||||
|
||||
_audio.PlayPvs(spray.SpraySound, args.Used, AudioParams.Default.WithVariation(0.125f));
|
||||
|
||||
var split = _solutionContainerSystem.Drain(solutionEntity, solution.Value, amount);
|
||||
|
||||
if (split.Volume == 0)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-no-plant-message",
|
||||
("owner", args.Used)), args.User);
|
||||
return;
|
||||
}
|
||||
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-spray-message",
|
||||
("owner", uid),
|
||||
("amount", split.Volume)), args.User, PopupType.Medium);
|
||||
|
||||
_solutionContainerSystem.TryAddSolution(component.SoilSolution.Value, split);
|
||||
|
||||
ForceUpdateByExternalCause(uid, component);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (_tagSystem.HasTag(args.Used, "PlantSampleTaker"))
|
||||
{
|
||||
args.Handled = true;
|
||||
if (component.Seed == null)
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-nothing-to-sample-message"), args.User);
|
||||
@@ -316,10 +288,15 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (HasComp<SharpComponent>(args.Used))
|
||||
{
|
||||
args.Handled = true;
|
||||
DoHarvest(uid, args.User, component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<ProduceComponent>(args.Used, out var produce))
|
||||
{
|
||||
args.Handled = true;
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-compost-message",
|
||||
("owner", uid),
|
||||
("usingItem", args.Used)), args.User, PopupType.Medium);
|
||||
@@ -351,6 +328,10 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSolutionTransferred(Entity<PlantHolderComponent> ent, ref SolutionTransferredEvent args)
|
||||
{
|
||||
_audio.PlayPvs(ent.Comp.WateringSound, ent.Owner);
|
||||
}
|
||||
private void OnInteractHand(Entity<PlantHolderComponent> entity, ref InteractHandEvent args)
|
||||
{
|
||||
DoHarvest(entity, args.User, entity.Comp);
|
||||
@@ -699,7 +680,10 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
if (TryComp<HandsComponent>(user, out var hands))
|
||||
{
|
||||
if (!_botany.CanHarvest(component.Seed, hands.ActiveHandEntity))
|
||||
{
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-ligneous-cant-harvest-message"), user);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (!_botany.CanHarvest(component.Seed))
|
||||
{
|
||||
|
||||
@@ -420,6 +420,13 @@ public sealed partial class CargoSystem
|
||||
return false;
|
||||
|
||||
_nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
|
||||
var newBounty = new CargoBountyData(bounty, randomVal);
|
||||
// This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
|
||||
if (component.Bounties.Any(b => b.Id == newBounty.Id))
|
||||
{
|
||||
Log.Error("Failed to add bounty {ID} because another one with the same ID already existed!", newBounty.Id);
|
||||
return false;
|
||||
}
|
||||
component.Bounties.Add(new CargoBountyData(bounty, randomVal));
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
|
||||
component.TotalBounties++;
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
|
||||
namespace Content.Server.Chat.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes messages!
|
||||
/// It currently ony removes the shorthands for emotes (like "lol" or "^-^") from a chat message and returns the last
|
||||
/// emote in their message
|
||||
/// </summary>
|
||||
public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
|
||||
private static readonly Dictionary<string, string> SmileyToEmote = new()
|
||||
private static readonly Dictionary<string, string> ShorthandToEmote = new()
|
||||
{
|
||||
// CP14-RU-Localization-Start
|
||||
{ "лол", "chatsan-laughs" },
|
||||
@@ -60,7 +63,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ ":D", "chatsan-smiles-widely" },
|
||||
{ "D:", "chatsan-frowns-deeply" },
|
||||
{ ":O", "chatsan-surprised" },
|
||||
{ ":3", "chatsan-smiles" }, //nope
|
||||
{ ":3", "chatsan-smiles" },
|
||||
{ ":S", "chatsan-uncertain" },
|
||||
{ ":>", "chatsan-grins" },
|
||||
{ ":<", "chatsan-pouts" },
|
||||
@@ -102,7 +105,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ "kek", "chatsan-laughs" },
|
||||
{ "rofl", "chatsan-laughs" },
|
||||
{ "o7", "chatsan-salutes" },
|
||||
{ ";_;7", "chatsan-tearfully-salutes"},
|
||||
{ ";_;7", "chatsan-tearfully-salutes" },
|
||||
{ "idk", "chatsan-shrugs" },
|
||||
{ ";)", "chatsan-winks" },
|
||||
{ ";]", "chatsan-winks" },
|
||||
@@ -115,9 +118,12 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
{ "(':", "chatsan-tearfully-smiles" },
|
||||
{ "[':", "chatsan-tearfully-smiles" },
|
||||
{ "('=", "chatsan-tearfully-smiles" },
|
||||
{ "['=", "chatsan-tearfully-smiles" },
|
||||
{ "['=", "chatsan-tearfully-smiles" }
|
||||
};
|
||||
|
||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = default!;
|
||||
|
||||
private bool _doSanitize;
|
||||
|
||||
public void Initialize()
|
||||
@@ -125,29 +131,60 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
|
||||
_configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => _doSanitize = x, true);
|
||||
}
|
||||
|
||||
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote)
|
||||
/// <summary>
|
||||
/// Remove the shorthands from the message, returning the last one found as the emote
|
||||
/// </summary>
|
||||
/// <param name="message">The pre-sanitized message</param>
|
||||
/// <param name="speaker">The speaker</param>
|
||||
/// <param name="sanitized">The sanitized message with shorthands removed</param>
|
||||
/// <param name="emote">The localized emote</param>
|
||||
/// <returns>True if emote has been sanitized out</returns>
|
||||
public bool TrySanitizeEmoteShorthands(string message,
|
||||
EntityUid speaker,
|
||||
out string sanitized,
|
||||
[NotNullWhen(true)] out string? emote)
|
||||
{
|
||||
if (!_doSanitize)
|
||||
{
|
||||
sanitized = input;
|
||||
emote = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
input = input.TrimEnd();
|
||||
|
||||
foreach (var (smiley, replacement) in SmileyToEmote)
|
||||
{
|
||||
if (input.EndsWith(smiley, true, CultureInfo.InvariantCulture))
|
||||
{
|
||||
sanitized = input.Remove(input.Length - smiley.Length).TrimEnd();
|
||||
emote = Loc.GetString(replacement, ("ent", speaker));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
sanitized = input;
|
||||
emote = null;
|
||||
return false;
|
||||
sanitized = message;
|
||||
|
||||
if (!_doSanitize)
|
||||
return false;
|
||||
|
||||
// -1 is just a canary for nothing found yet
|
||||
var lastEmoteIndex = -1;
|
||||
|
||||
foreach (var (shorthand, emoteKey) in ShorthandToEmote)
|
||||
{
|
||||
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
|
||||
var escaped = Regex.Escape(shorthand);
|
||||
|
||||
// So there are 2 cases:
|
||||
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
|
||||
// Delete the word and the whitespace before
|
||||
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
|
||||
// Delete the word and the punctuation if it exists.
|
||||
var pattern =
|
||||
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
|
||||
|
||||
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
|
||||
|
||||
// We're using sanitized as the original message until the end so that we can make sure the indices of
|
||||
// the emotes are accurate.
|
||||
var lastMatch = r.Match(sanitized);
|
||||
|
||||
if (!lastMatch.Success)
|
||||
continue;
|
||||
|
||||
if (lastMatch.Index > lastEmoteIndex)
|
||||
{
|
||||
lastEmoteIndex = lastMatch.Index;
|
||||
emote = _loc.GetString(emoteKey, ("ent", speaker));
|
||||
}
|
||||
|
||||
message = r.Replace(message, string.Empty);
|
||||
}
|
||||
|
||||
sanitized = message.Trim();
|
||||
return emote is not null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,8 @@ public interface IChatSanitizationManager
|
||||
{
|
||||
public void Initialize();
|
||||
|
||||
public bool TrySanitizeOutSmilies(string input, EntityUid speaker, out string sanitized, [NotNullWhen(true)] out string? emote);
|
||||
public bool TrySanitizeEmoteShorthands(string input,
|
||||
EntityUid speaker,
|
||||
out string sanitized,
|
||||
[NotNullWhen(true)] out string? emote);
|
||||
}
|
||||
|
||||
@@ -746,8 +746,12 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
// ReSharper disable once InconsistentNaming
|
||||
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
|
||||
{
|
||||
var newMessage = message.Trim();
|
||||
newMessage = SanitizeMessageReplaceWords(newMessage);
|
||||
var newMessage = SanitizeMessageReplaceWords(message.Trim());
|
||||
|
||||
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
|
||||
|
||||
// Sanitize it first as it might change the word order
|
||||
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
|
||||
|
||||
if (capitalize)
|
||||
newMessage = SanitizeMessageCapital(newMessage);
|
||||
@@ -756,9 +760,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (punctuate)
|
||||
newMessage = SanitizeMessagePeriod(newMessage);
|
||||
|
||||
_sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
|
||||
|
||||
return newMessage;
|
||||
return prefix + newMessage;
|
||||
}
|
||||
|
||||
private string SanitizeInGameOOCMessage(string message)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
|
||||
namespace Content.Server.Chemistry.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Used for embeddable entities that should try to inject a
|
||||
/// contained solution into a target over time while they are embbeded into.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class SolutionInjectWhileEmbeddedComponent : BaseSolutionInjectOnEventComponent {
|
||||
///<summary>
|
||||
///The time at which the injection will happen.
|
||||
///</summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
|
||||
public TimeSpan NextUpdate;
|
||||
|
||||
///<summary>
|
||||
///The delay between each injection in seconds.
|
||||
///</summary>
|
||||
[DataField]
|
||||
public TimeSpan UpdateInterval = TimeSpan.FromSeconds(3);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
@@ -29,6 +30,7 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
SubscribeLocalEvent<SolutionInjectOnProjectileHitComponent, ProjectileHitEvent>(HandleProjectileHit);
|
||||
SubscribeLocalEvent<SolutionInjectOnEmbedComponent, EmbedEvent>(HandleEmbed);
|
||||
SubscribeLocalEvent<MeleeChemicalInjectorComponent, MeleeHitEvent>(HandleMeleeHit);
|
||||
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, InjectOverTimeEvent>(OnInjectOverTime);
|
||||
}
|
||||
|
||||
private void HandleProjectileHit(Entity<SolutionInjectOnProjectileHitComponent> entity, ref ProjectileHitEvent args)
|
||||
@@ -49,6 +51,11 @@ public sealed class SolutionInjectOnCollideSystem : EntitySystem
|
||||
TryInjectTargets((entity.Owner, entity.Comp), args.HitEntities, args.User);
|
||||
}
|
||||
|
||||
private void OnInjectOverTime(Entity<SolutionInjectWhileEmbeddedComponent> entity, ref InjectOverTimeEvent args)
|
||||
{
|
||||
DoInjection((entity.Owner, entity.Comp), args.EmbeddedIntoUid);
|
||||
}
|
||||
|
||||
private void DoInjection(Entity<BaseSolutionInjectOnEventComponent> injectorEntity, EntityUid target, EntityUid? source = null)
|
||||
{
|
||||
TryInjectTargets(injectorEntity, [target], source);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Events;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Weapons.Melee.Events;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Chemistry.EntitySystems;
|
||||
|
||||
/// <summary>
|
||||
/// System for handling injecting into an entity while a projectile is embedded.
|
||||
/// </summary>
|
||||
public sealed class SolutionInjectWhileEmbeddedSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
|
||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SolutionInjectWhileEmbeddedComponent, MapInitEvent>(OnMapInit);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<SolutionInjectWhileEmbeddedComponent> ent, ref MapInitEvent args)
|
||||
{
|
||||
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
|
||||
var query = EntityQueryEnumerator<SolutionInjectWhileEmbeddedComponent, EmbeddableProjectileComponent>();
|
||||
while (query.MoveNext(out var uid, out var injectComponent, out var projectileComponent))
|
||||
{
|
||||
if (_gameTiming.CurTime < injectComponent.NextUpdate)
|
||||
continue;
|
||||
|
||||
injectComponent.NextUpdate += injectComponent.UpdateInterval;
|
||||
|
||||
if(projectileComponent.EmbeddedIntoUid == null)
|
||||
continue;
|
||||
|
||||
var ev = new InjectOverTimeEvent(projectileComponent.EmbeddedIntoUid.Value);
|
||||
RaiseLocalEvent(uid, ref ev);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,7 +231,7 @@ namespace Content.Server.Cloning
|
||||
|
||||
// TODO: Ideally, components like this should be components on the mind entity so this isn't necessary.
|
||||
// Add on special job components to the mob.
|
||||
if (_jobs.MindTryGetJob(mindEnt, out _, out var prototype))
|
||||
if (_jobs.MindTryGetJob(mindEnt, out var prototype))
|
||||
{
|
||||
foreach (var special in prototype.Special)
|
||||
{
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class ExaminableDamageSystem : EntitySystem
|
||||
|
||||
var level = GetDamageLevel(uid, component);
|
||||
var msg = Loc.GetString(messages[level]);
|
||||
args.PushMarkup(msg);
|
||||
args.PushMarkup(msg,-99);
|
||||
}
|
||||
|
||||
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
|
||||
|
||||
@@ -875,10 +875,41 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
public async Task AddAdminLogs(List<AdminLog> logs)
|
||||
{
|
||||
const int maxRetryAttempts = 5;
|
||||
var initialRetryDelay = TimeSpan.FromSeconds(5);
|
||||
|
||||
DebugTools.Assert(logs.All(x => x.RoundId > 0), "Adding logs with invalid round ids.");
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.AdminLog.AddRange(logs);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
|
||||
var attempt = 0;
|
||||
var retryDelay = initialRetryDelay;
|
||||
|
||||
while (attempt < maxRetryAttempts)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
db.DbContext.AdminLog.AddRange(logs);
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
_opsLog.Debug($"Successfully saved {logs.Count} admin logs.");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
attempt += 1;
|
||||
_opsLog.Error($"Attempt {attempt} failed to save logs: {ex}");
|
||||
|
||||
if (attempt >= maxRetryAttempts)
|
||||
{
|
||||
_opsLog.Error($"Max retry attempts reached. Failed to save {logs.Count} admin logs.");
|
||||
return;
|
||||
}
|
||||
|
||||
_opsLog.Warning($"Retrying in {retryDelay.TotalSeconds} seconds...");
|
||||
await Task.Delay(retryDelay);
|
||||
|
||||
retryDelay *= 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class TimerStartBehavior : IThresholdBehavior
|
||||
{
|
||||
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
|
||||
{
|
||||
system.TriggerSystem.StartTimer(owner, cause);
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
SetBoltsDown(ent, true);
|
||||
}
|
||||
|
||||
UpdateBoltLightStatus(ent);
|
||||
ent.Comp.Powered = args.Powered;
|
||||
Dirty(ent, ent.Comp);
|
||||
UpdateBoltLightStatus(ent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,53 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.EntityEffects;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Content.Shared.Station;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.EntityEffects.EffectConditions;
|
||||
|
||||
public sealed partial class JobCondition : EntityEffectCondition
|
||||
{
|
||||
[DataField(required: true)] public List<ProtoId<JobPrototype>> Job;
|
||||
|
||||
|
||||
public override bool Condition(EntityEffectBaseArgs args)
|
||||
{
|
||||
{
|
||||
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
|
||||
if (mindContainer != null && mindContainer.Mind != null)
|
||||
|
||||
if ( mindContainer is null
|
||||
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
|
||||
return false;
|
||||
|
||||
foreach (var roleId in mind.MindRoles)
|
||||
{
|
||||
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
|
||||
if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype))
|
||||
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
|
||||
continue;
|
||||
|
||||
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))
|
||||
{
|
||||
foreach (var jobId in Job)
|
||||
{
|
||||
if (prototype.ID == jobId)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(MindRoleComponent)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (mindRole.JobPrototype == null)
|
||||
{
|
||||
Logger.Error($"Encountered job mind role entity {roleId} without a {nameof(JobPrototype)}");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Job.Contains(mindRole.JobPrototype.Value))
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public override string GuidebookExplanation(IPrototypeManager prototype)
|
||||
{
|
||||
var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
|
||||
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Explosion.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Sends a trigger when signal is received.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class TimerStartOnSignalComponent : Component
|
||||
{
|
||||
[DataField("port", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
|
||||
public string Port = "Timer";
|
||||
}
|
||||
}
|
||||
@@ -488,9 +488,12 @@ public sealed partial class ExplosionSystem
|
||||
&& physics.BodyType == BodyType.Dynamic)
|
||||
{
|
||||
var pos = _transformSystem.GetWorldPosition(xform);
|
||||
var dir = pos - epicenter.Position;
|
||||
if (dir.IsLengthZero())
|
||||
dir = _robustRandom.NextVector2().Normalized();
|
||||
_throwingSystem.TryThrow(
|
||||
uid,
|
||||
pos - epicenter.Position,
|
||||
dir,
|
||||
physics,
|
||||
xform,
|
||||
_projectileQuery,
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
{
|
||||
SubscribeLocalEvent<TriggerOnSignalComponent,SignalReceivedEvent>(OnSignalReceived);
|
||||
SubscribeLocalEvent<TriggerOnSignalComponent,ComponentInit>(OnInit);
|
||||
|
||||
SubscribeLocalEvent<TimerStartOnSignalComponent,SignalReceivedEvent>(OnTimerSignalReceived);
|
||||
SubscribeLocalEvent<TimerStartOnSignalComponent,ComponentInit>(OnTimerSignalInit);
|
||||
}
|
||||
|
||||
private void OnSignalReceived(EntityUid uid, TriggerOnSignalComponent component, ref SignalReceivedEvent args)
|
||||
@@ -24,5 +27,17 @@ namespace Content.Server.Explosion.EntitySystems
|
||||
{
|
||||
_signalSystem.EnsureSinkPorts(uid, component.Port);
|
||||
}
|
||||
|
||||
private void OnTimerSignalReceived(EntityUid uid, TimerStartOnSignalComponent component, ref SignalReceivedEvent args)
|
||||
{
|
||||
if (args.Port != component.Port)
|
||||
return;
|
||||
|
||||
StartTimer(uid, args.Trigger);
|
||||
}
|
||||
private void OnTimerSignalInit(EntityUid uid, TimerStartOnSignalComponent component, ComponentInit args)
|
||||
{
|
||||
_signalSystem.EnsureSinkPorts(uid, component.Port);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Server.Discord;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -26,6 +27,7 @@ namespace Content.Server.GameTicking
|
||||
public sealed partial class GameTicker
|
||||
{
|
||||
[Dependency] private readonly DiscordWebhook _discord = default!;
|
||||
[Dependency] private readonly RoleSystem _role = default!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = default!;
|
||||
|
||||
private static readonly Counter RoundNumberMetric = Metrics.CreateCounter(
|
||||
@@ -190,9 +192,6 @@ namespace Content.Server.GameTicking
|
||||
if (!_playerManager.TryGetSessionById(userId, out _))
|
||||
continue;
|
||||
|
||||
if (_banManager.GetRoleBans(userId) == null)
|
||||
continue;
|
||||
|
||||
total++;
|
||||
}
|
||||
|
||||
@@ -236,11 +235,7 @@ namespace Content.Server.GameTicking
|
||||
#if DEBUG
|
||||
DebugTools.Assert(_userDb.IsLoadComplete(session), $"Player was readied up but didn't have user DB data loaded yet??");
|
||||
#endif
|
||||
if (_banManager.GetRoleBans(userId) == null)
|
||||
{
|
||||
Logger.ErrorS("RoleBans", $"Role bans for player {session} {userId} have not been loaded yet.");
|
||||
continue;
|
||||
}
|
||||
|
||||
readyPlayers.Add(session);
|
||||
HumanoidCharacterProfile profile;
|
||||
if (_prefsManager.TryGetCachedPreferences(userId, out var preferences))
|
||||
@@ -339,8 +334,23 @@ namespace Content.Server.GameTicking
|
||||
|
||||
RunLevel = GameRunLevel.PostRound;
|
||||
|
||||
ShowRoundEndScoreboard(text);
|
||||
SendRoundEndDiscordMessage();
|
||||
try
|
||||
{
|
||||
ShowRoundEndScoreboard(text);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Error while showing round end scoreboard: {e}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SendRoundEndDiscordMessage();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error($"Error while sending round end Discord message: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowRoundEndScoreboard(string text = "")
|
||||
@@ -373,7 +383,7 @@ namespace Content.Server.GameTicking
|
||||
var userId = mind.UserId ?? mind.OriginalOwnerUserId;
|
||||
|
||||
var connected = false;
|
||||
var observer = HasComp<ObserverRoleComponent>(mindId);
|
||||
var observer = _role.MindHasRole<ObserverRoleComponent>(mindId);
|
||||
// Continuing
|
||||
if (userId != null && _playerManager.ValidSessionId(userId.Value))
|
||||
{
|
||||
@@ -400,7 +410,7 @@ namespace Content.Server.GameTicking
|
||||
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
|
||||
}
|
||||
|
||||
var roles = _roles.MindGetAllRoles(mindId);
|
||||
var roles = _roles.MindGetAllRoleInfo(mindId);
|
||||
|
||||
var playerEndRoundInfo = new RoundEndMessageEvent.RoundEndPlayerInfo()
|
||||
{
|
||||
|
||||
@@ -3,8 +3,6 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.GameTicking.Events;
|
||||
using Content.Server.Ghost;
|
||||
using Content.Server.Shuttles.Components;
|
||||
using Content.Server.Spawners.Components;
|
||||
using Content.Server.Speech.Components;
|
||||
using Content.Server.Station.Components;
|
||||
@@ -224,13 +222,12 @@ namespace Content.Server.GameTicking
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
var job = new JobComponent {Prototype = jobId};
|
||||
_roles.MindAddRole(newMind, job, silent: silent);
|
||||
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
|
||||
var jobName = _jobs.MindTryGetJobName(newMind);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, job, character);
|
||||
var mobMaybe = _stationSpawning.SpawnPlayerCharacterOnStation(station, jobId, character);
|
||||
DebugTools.AssertNotNull(mobMaybe);
|
||||
var mob = mobMaybe!.Value;
|
||||
|
||||
@@ -271,13 +268,17 @@ namespace Content.Server.GameTicking
|
||||
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
|
||||
|
||||
if (lateJoin)
|
||||
{
|
||||
_adminLogger.Add(LogType.LateJoin,
|
||||
LogImpact.Medium,
|
||||
$"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.RoundStartJoin,
|
||||
LogImpact.Medium,
|
||||
$"Player {player.Name} joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
}
|
||||
|
||||
// Make sure they're aware of extended access.
|
||||
if (Comp<StationJobsComponent>(station).ExtendedAccess
|
||||
@@ -363,7 +364,7 @@ namespace Content.Server.GameTicking
|
||||
var (mindId, mindComp) = _mind.CreateMind(player.UserId, name);
|
||||
mind = (mindId, mindComp);
|
||||
_mind.SetUserId(mind.Value, player.UserId);
|
||||
_roles.MindAddRole(mind.Value, new ObserverRoleComponent());
|
||||
_roles.MindAddRole(mind.Value, "MindRoleObserver");
|
||||
}
|
||||
|
||||
var ghost = _ghost.SpawnGhost(mind.Value);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.NPC.Prototypes;
|
||||
using Content.Shared.Random;
|
||||
using Content.Shared.Roles;
|
||||
@@ -31,6 +32,24 @@ public sealed partial class TraitorRuleComponent : Component
|
||||
[DataField]
|
||||
public ProtoId<DatasetPrototype> ObjectiveIssuers = "TraitorCorporations";
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor an Uplink on spawn.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveUplink = true;
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor the codewords.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveCodewords = true;
|
||||
|
||||
/// <summary>
|
||||
/// Give this traitor a briefing in chat.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool GiveBriefing = true;
|
||||
|
||||
public int TotalTraitors => TraitorMinds.Count;
|
||||
public string[] Codewords = new string[3];
|
||||
|
||||
@@ -68,5 +87,5 @@ public sealed partial class TraitorRuleComponent : Component
|
||||
/// The amount of TC traitors start with.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int StartingBalance = 20;
|
||||
public FixedPoint2 StartingBalance = 20;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ using Content.Server.RoundEnd;
|
||||
using Content.Server.Shuttles.Events;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Server.Store.Components;
|
||||
using Content.Server.Store.Systems;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mobs;
|
||||
@@ -31,9 +30,9 @@ namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEndSystem = default!;
|
||||
[Dependency] private readonly StoreSystem _store = default!;
|
||||
@@ -57,6 +56,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
SubscribeLocalEvent<NukeOperativeComponent, MobStateChangedEvent>(OnMobStateChanged);
|
||||
SubscribeLocalEvent<NukeOperativeComponent, EntityZombifiedEvent>(OnOperativeZombified);
|
||||
|
||||
SubscribeLocalEvent<NukeopsRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
|
||||
SubscribeLocalEvent<ConsoleFTLAttemptEvent>(OnShuttleFTLAttempt);
|
||||
SubscribeLocalEvent<WarDeclaredEvent>(OnWarDeclared);
|
||||
SubscribeLocalEvent<CommunicationConsoleCallShuttleAttemptEvent>(OnShuttleCallAttempt);
|
||||
@@ -65,7 +66,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
SubscribeLocalEvent<NukeopsRuleComponent, RuleLoadedGridsEvent>(OnRuleLoadedGrids);
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||
protected override void Started(EntityUid uid,
|
||||
NukeopsRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
GameRuleStartedEvent args)
|
||||
{
|
||||
var eligible = new List<Entity<StationEventEligibleComponent, NpcFactionMemberComponent>>();
|
||||
@@ -85,7 +88,9 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
protected override void AppendRoundEndText(EntityUid uid, NukeopsRuleComponent component, GameRuleComponent gameRule,
|
||||
protected override void AppendRoundEndText(EntityUid uid,
|
||||
NukeopsRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
var winText = Loc.GetString($"nukeops-{component.WinType.ToString().ToLower()}");
|
||||
@@ -227,7 +232,8 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
|
||||
// If the disk is currently at Central Command, the crew wins - just slightly.
|
||||
// This also implies that some nuclear operatives have died.
|
||||
SetWinType(ent, diskAtCentCom
|
||||
SetWinType(ent,
|
||||
diskAtCentCom
|
||||
? WinType.CrewMinor
|
||||
: WinType.OpsMinor);
|
||||
ent.Comp.WinConditions.Add(diskAtCentCom
|
||||
@@ -456,8 +462,11 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
: WinCondition.AllNukiesDead);
|
||||
|
||||
SetWinType(ent, WinType.CrewMajor, false);
|
||||
_roundEndSystem.DoRoundEndBehavior(
|
||||
nukeops.RoundEndBehavior, nukeops.EvacShuttleTime, nukeops.RoundEndTextSender, nukeops.RoundEndTextShuttleCall, nukeops.RoundEndTextAnnouncement);
|
||||
_roundEndSystem.DoRoundEndBehavior(nukeops.RoundEndBehavior,
|
||||
nukeops.EvacShuttleTime,
|
||||
nukeops.RoundEndTextSender,
|
||||
nukeops.RoundEndTextShuttleCall,
|
||||
nukeops.RoundEndTextAnnouncement);
|
||||
|
||||
// prevent it called multiple times
|
||||
nukeops.RoundEndBehavior = RoundEndBehavior.Nothing;
|
||||
@@ -465,16 +474,22 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
|
||||
|
||||
private void OnAfterAntagEntSelected(Entity<NukeopsRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
if (ent.Comp.TargetStation is not { } station)
|
||||
return;
|
||||
var target = (ent.Comp.TargetStation is not null) ? Name(ent.Comp.TargetStation.Value) : "the target";
|
||||
|
||||
_antag.SendBriefing(args.Session, Loc.GetString("nukeops-welcome",
|
||||
("station", station),
|
||||
_antag.SendBriefing(args.Session,
|
||||
Loc.GetString("nukeops-welcome",
|
||||
("station", target),
|
||||
("name", Name(ent))),
|
||||
Color.Red,
|
||||
ent.Comp.GreetSoundNotification);
|
||||
}
|
||||
|
||||
private void OnGetBriefing(Entity<NukeopsRoleComponent> role, ref GetBriefingEvent args)
|
||||
{
|
||||
// TODO Different character screen briefing for the 3 nukie types
|
||||
args.Append(Loc.GetString("nukeops-briefing"));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Is this method the shitty glue holding together the last of my sanity? yes.
|
||||
/// Do i have a better solution? not presently.
|
||||
|
||||
@@ -15,7 +15,6 @@ using Content.Shared.Database;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Mindshield.Components;
|
||||
using Content.Shared.Mobs;
|
||||
@@ -38,8 +37,8 @@ namespace Content.Server.GameTicking.Rules;
|
||||
public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLogManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly EuiManager _euiMan = default!;
|
||||
[Dependency] private readonly MindSystem _mind = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
@@ -49,7 +48,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
[Dependency] private readonly SharedStunSystem _stun = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly StationSystem _stationSystem = default!;
|
||||
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
//Used in OnPostFlash, no reference to the rule component is available
|
||||
public readonly ProtoId<NpcFactionPrototype> RevolutionaryNpcFaction = "Revolutionary";
|
||||
@@ -59,9 +58,12 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<CommandStaffComponent, MobStateChangedEvent>(OnCommandMobStateChanged);
|
||||
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
|
||||
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
|
||||
SubscribeLocalEvent<HeadRevolutionaryComponent, AfterFlashedEvent>(OnPostFlash);
|
||||
SubscribeLocalEvent<HeadRevolutionaryComponent, MobStateChangedEvent>(OnHeadRevMobStateChanged);
|
||||
|
||||
SubscribeLocalEvent<RevolutionaryRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
|
||||
}
|
||||
|
||||
protected override void Started(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
|
||||
@@ -85,7 +87,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
}
|
||||
}
|
||||
|
||||
protected override void AppendRoundEndText(EntityUid uid, RevolutionaryRuleComponent component, GameRuleComponent gameRule,
|
||||
protected override void AppendRoundEndText(EntityUid uid,
|
||||
RevolutionaryRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
base.AppendRoundEndText(uid, component, gameRule, ref args);
|
||||
@@ -101,7 +105,9 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
args.AddLine(Loc.GetString("rev-headrev-count", ("initialCount", sessionData.Count)));
|
||||
foreach (var (mind, data, name) in sessionData)
|
||||
{
|
||||
var count = CompOrNull<RevolutionaryRoleComponent>(mind)?.ConvertedCount ?? 0;
|
||||
_role.MindHasRole<RevolutionaryRoleComponent>(mind, out var role);
|
||||
var count = CompOrNull<RevolutionaryRoleComponent>(role)?.ConvertedCount ?? 0;
|
||||
|
||||
args.AddLine(Loc.GetString("rev-headrev-name-user",
|
||||
("name", name),
|
||||
("username", data.UserName),
|
||||
@@ -113,10 +119,8 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, RevolutionaryRoleComponent comp, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
|
||||
var head = HasComp<HeadRevolutionaryComponent>(mind.OwnedEntity);
|
||||
var ent = args.Mind.Comp.OwnedEntity;
|
||||
var head = HasComp<HeadRevolutionaryComponent>(ent);
|
||||
args.Append(Loc.GetString(head ? "head-rev-briefing" : "rev-briefing"));
|
||||
}
|
||||
|
||||
@@ -145,15 +149,20 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
|
||||
|
||||
if (ev.User != null)
|
||||
{
|
||||
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
|
||||
_adminLogManager.Add(LogType.Mind,
|
||||
LogImpact.Medium,
|
||||
$"{ToPrettyString(ev.User.Value)} converted {ToPrettyString(ev.Target)} into a Revolutionary");
|
||||
|
||||
if (_mind.TryGetRole<RevolutionaryRoleComponent>(ev.User.Value, out var headrev))
|
||||
headrev.ConvertedCount++;
|
||||
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
|
||||
{
|
||||
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
|
||||
role.Value.Comp2.ConvertedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (mindId == default || !_role.MindHasRole<RevolutionaryRoleComponent>(mindId))
|
||||
{
|
||||
_role.MindAddRole(mindId, new RevolutionaryRoleComponent { PrototypeId = RevPrototypeId });
|
||||
_role.MindAddRole(mindId, "MindRoleRevolutionary");
|
||||
}
|
||||
|
||||
if (mind?.Session != null)
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Objectives;
|
||||
using Content.Server.Roles;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -24,32 +18,33 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
|
||||
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
|
||||
}
|
||||
|
||||
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
// Greeting upon thief activation
|
||||
private void AfterAntagSelected(Entity<ThiefRuleComponent> mindId, ref AfterAntagEntitySelectedEvent args)
|
||||
{
|
||||
if (!_mindSystem.TryGetMind(args.EntityUid, out var mindId, out var mind))
|
||||
return;
|
||||
|
||||
//Generate objectives
|
||||
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
|
||||
var ent = args.EntityUid;
|
||||
_antag.SendBriefing(ent, MakeBriefing(ent), null, null);
|
||||
}
|
||||
|
||||
//Add mind briefing
|
||||
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
|
||||
// Character screen briefing
|
||||
private void OnGetBriefing(Entity<ThiefRoleComponent> role, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(thief.Owner, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
var ent = args.Mind.Comp.OwnedEntity;
|
||||
|
||||
args.Append(MakeBriefing(mind.OwnedEntity.Value));
|
||||
if (ent is null)
|
||||
return;
|
||||
args.Append(MakeBriefing(ent.Value));
|
||||
}
|
||||
|
||||
private string MakeBriefing(EntityUid thief)
|
||||
private string MakeBriefing(EntityUid ent)
|
||||
{
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(thief);
|
||||
var isHuman = HasComp<HumanoidAppearanceComponent>(ent);
|
||||
var briefing = isHuman
|
||||
? Loc.GetString("thief-role-greeting-human")
|
||||
: Loc.GetString("thief-role-greeting-animal");
|
||||
|
||||
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
|
||||
if (isHuman)
|
||||
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
|
||||
|
||||
return briefing;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Antag;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
@@ -5,12 +6,12 @@ using Content.Server.Objectives;
|
||||
using Content.Server.PDA.Ringer;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.GameTicking.Components;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.Objectives.Components;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Radio;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
using Content.Shared.Roles.RoleCodeword;
|
||||
@@ -25,29 +26,29 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
{
|
||||
private static readonly Color TraitorCodewordColor = Color.FromHex("#cc3b3b");
|
||||
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
[Dependency] private readonly MindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly SharedRoleCodewordSystem _roleCodewordSystem = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
|
||||
|
||||
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
|
||||
}
|
||||
|
||||
protected override void Added(EntityUid uid, TraitorRuleComponent component, GameRuleComponent gameRule, GameRuleAddedEvent args)
|
||||
{
|
||||
base.Added(uid, component, gameRule, args);
|
||||
SetCodewords(component);
|
||||
SetCodewords(component, args.RuleEntity);
|
||||
}
|
||||
|
||||
private void AfterEntitySelected(Entity<TraitorRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
|
||||
@@ -55,9 +56,10 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
MakeTraitor(args.EntityUid, ent);
|
||||
}
|
||||
|
||||
private void SetCodewords(TraitorRuleComponent component)
|
||||
private void SetCodewords(TraitorRuleComponent component, EntityUid ruleEntity)
|
||||
{
|
||||
component.Codewords = GenerateTraitorCodewords(component);
|
||||
_adminLogger.Add(LogType.EventStarted, LogImpact.Low, $"Codewords generated for game rule {ToPrettyString(ruleEntity)}: {string.Join(", ", component.Codewords)}");
|
||||
}
|
||||
|
||||
public string[] GenerateTraitorCodewords(TraitorRuleComponent component)
|
||||
@@ -74,47 +76,59 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return codewords;
|
||||
}
|
||||
|
||||
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
|
||||
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component)
|
||||
{
|
||||
//Grab the mind if it wasnt provided
|
||||
//Grab the mind if it wasn't provided
|
||||
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
|
||||
return false;
|
||||
|
||||
var briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||
var briefing = "";
|
||||
|
||||
if (component.GiveCodewords)
|
||||
briefing = Loc.GetString("traitor-role-codewords-short", ("codewords", string.Join(", ", component.Codewords)));
|
||||
|
||||
var issuer = _random.Pick(_prototypeManager.Index(component.ObjectiveIssuers).Values);
|
||||
|
||||
// Uplink code will go here if applicable, but we still need the variable if there aren't any
|
||||
Note[]? code = null;
|
||||
if (giveUplink)
|
||||
|
||||
if (component.GiveUplink)
|
||||
{
|
||||
// Calculate the amount of currency on the uplink.
|
||||
var startingBalance = component.StartingBalance;
|
||||
if (_jobs.MindTryGetJob(mindId, out _, out var prototype))
|
||||
startingBalance = Math.Max(startingBalance - prototype.AntagAdvantage, 0);
|
||||
if (_jobs.MindTryGetJob(mindId, out var prototype))
|
||||
{
|
||||
if (startingBalance < prototype.AntagAdvantage) // Can't use Math functions on FixedPoint2
|
||||
startingBalance = 0;
|
||||
else
|
||||
startingBalance = startingBalance - prototype.AntagAdvantage;
|
||||
}
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already
|
||||
var pda = _uplink.FindUplinkTarget(traitor);
|
||||
if (pda == null || !_uplink.AddUplink(traitor, startingBalance, giveDiscounts: true))
|
||||
return false;
|
||||
|
||||
// Give traitors their codewords and uplink code to keep in their character info menu
|
||||
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}", briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
// Choose and generate an Uplink, and return the uplink code if applicable
|
||||
var uplinkParams = RequestUplink(traitor, startingBalance, briefing);
|
||||
code = uplinkParams.Item1;
|
||||
briefing = uplinkParams.Item2;
|
||||
}
|
||||
|
||||
_antag.SendBriefing(traitor, GenerateBriefing(component.Codewords, code, issuer), null, component.GreetSoundNotification);
|
||||
string[]? codewords = null;
|
||||
if (component.GiveCodewords)
|
||||
codewords = component.Codewords;
|
||||
|
||||
if (component.GiveBriefing)
|
||||
_antag.SendBriefing(traitor, GenerateBriefing(codewords, code, issuer), null, component.GreetSoundNotification);
|
||||
|
||||
component.TraitorMinds.Add(mindId);
|
||||
|
||||
// Assign briefing
|
||||
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
|
||||
//Since this provides neither an antag/job prototype, nor antag status/roletype,
|
||||
//and is intrinsically related to the traitor role
|
||||
//it does not need to be a separate Mind Role Entity
|
||||
_roleSystem.MindHasRole<TraitorRoleComponent>(mindId, out var traitorRole);
|
||||
if (traitorRole is not null)
|
||||
{
|
||||
Briefing = briefing
|
||||
}, mind, true);
|
||||
AddComp<RoleBriefingComponent>(traitorRole.Value.Owner);
|
||||
Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing;
|
||||
}
|
||||
|
||||
// Send codewords to only the traitor client
|
||||
var color = TraitorCodewordColor; // Fall back to a dark red Syndicate color if a prototype is not found
|
||||
@@ -129,20 +143,51 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
|
||||
return true;
|
||||
}
|
||||
|
||||
private (Note[]?, string) RequestUplink(EntityUid traitor, FixedPoint2 startingBalance, string briefing)
|
||||
{
|
||||
var pda = _uplink.FindUplinkTarget(traitor);
|
||||
Note[]? code = null;
|
||||
|
||||
var uplinked = _uplink.AddUplink(traitor, startingBalance, pda, true);
|
||||
|
||||
if (pda is not null && uplinked)
|
||||
{
|
||||
// Codes are only generated if the uplink is a PDA
|
||||
code = EnsureComp<RingerUplinkComponent>(pda.Value).Code;
|
||||
|
||||
// If giveUplink is false the uplink code part is omitted
|
||||
briefing = string.Format("{0}\n{1}",
|
||||
briefing,
|
||||
Loc.GetString("traitor-role-uplink-code-short", ("code", string.Join("-", code).Replace("sharp", "#"))));
|
||||
return (code, briefing);
|
||||
}
|
||||
else if (pda is null && uplinked)
|
||||
{
|
||||
briefing += "\n" + Loc.GetString("traitor-role-uplink-implant-short");
|
||||
}
|
||||
|
||||
return (null, briefing);
|
||||
}
|
||||
|
||||
// TODO: AntagCodewordsComponent
|
||||
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
|
||||
{
|
||||
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
|
||||
if(comp.GiveCodewords)
|
||||
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
|
||||
}
|
||||
|
||||
// TODO: figure out how to handle this? add priority to briefing event?
|
||||
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
|
||||
private string GenerateBriefing(string[]? codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine(Loc.GetString("traitor-role-greeting", ("corporation", objectiveIssuer ?? Loc.GetString("objective-issuer-unknown"))));
|
||||
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
|
||||
if (codewords != null)
|
||||
sb.AppendLine(Loc.GetString("traitor-role-codewords", ("codewords", string.Join(", ", codewords))));
|
||||
if (uplinkCode != null)
|
||||
sb.AppendLine(Loc.GetString("traitor-role-uplink-code", ("code", string.Join("-", uplinkCode).Replace("sharp", "#"))));
|
||||
else
|
||||
sb.AppendLine(Loc.GetString("traitor-role-uplink-implant"));
|
||||
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.Zombies;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -22,15 +23,16 @@ namespace Content.Server.GameTicking.Rules;
|
||||
|
||||
public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
{
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly AntagSelectionSystem _antag = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||
[Dependency] private readonly RoundEndSystem _roundEnd = default!;
|
||||
[Dependency] private readonly StationSystem _station = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ZombieSystem _zombie = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -41,23 +43,20 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
|
||||
SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
|
||||
}
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args)
|
||||
private void OnGetBriefing(Entity<InitialInfectedRoleComponent> role, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
|
||||
return;
|
||||
if (HasComp<ZombieRoleComponent>(uid)) // don't show both briefings
|
||||
return;
|
||||
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
|
||||
if (!_roles.MindHasRole<ZombieRoleComponent>(args.Mind.Owner))
|
||||
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
|
||||
}
|
||||
|
||||
private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args)
|
||||
private void OnGetBriefing(Entity<ZombieRoleComponent> role, ref GetBriefingEvent args)
|
||||
{
|
||||
if (!TryComp<MindComponent>(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,
|
||||
protected override void AppendRoundEndText(EntityUid uid,
|
||||
ZombieRuleComponent component,
|
||||
GameRuleComponent gameRule,
|
||||
ref RoundEndTextAppendEvent args)
|
||||
{
|
||||
base.AppendRoundEndText(uid, component, gameRule, ref args);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user