From 0269efcda3531b8668d513913bee21c4a84c2f11 Mon Sep 17 00:00:00 2001
From: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com>
Date: Sat, 29 Jun 2024 15:46:53 -0700
Subject: [PATCH 01/56] Mirror: Throwing Knives: Syndicate Kit (#310)
## Mirror of PR #26026: [Throwing Knives: Syndicate
Kit](https://github.com/space-wizards/space-station-14/pull/26026) from
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)
###### `393bcbfc1346589075ad960473685c78bdbf46e5`
PR opened by UbaserB at
2024-03-12 06:57:41 UTC
---
PR changed 13 files with 101 additions and 8 deletions.
The PR had the following labels:
- No C#
- Changes: Sprites
---
Original Body
>
>
>
> ## About the PR
>
>
> This PR adds throwing knives and a syndicate bundle in the bundle
category for 6 TC which comes with 4 throwing knives in a box. Each
knife does 10 slash + 15 pierce damage on throw, and 5 flash per hit (10
DPS). This makes it better than a kitchen knife, but worse than a combat
knife, which is what security is fitted with.
>
> ## Why / Balance
>
>
> This kit is built for weakening an enemy from a distance (from
chasing, etc) while being able to fight back if cornered into a 1v1 CQC
fight. Sure, you can kill several people with the kit; hence why it's so
expensive, but there are drawbacks. Firstly, once it embeds into a
target they are able to pull it out. Secondly, when you are pulling them
out of a target, YOU are vulnerable. Thirdly, if you miss your hits the
target will probably not die. Good things you have four knives and can
stab them to death.. better than punches!
>
> ## Media
>
>
> - [X] I have added screenshots/videos to this PR showcasing its
changes ingame, **or** this PR does not require an ingame showcase
>
> **It embeds and kills in 4 throws on an unarmoured target.**
>
![image](https://github.com/space-wizards/space-station-14/assets/134914314/05f4112d-a08c-4bde-9b34-8a5ecde0b270)
>
> **Item description.**
>
![image](https://github.com/space-wizards/space-station-14/assets/134914314/87704a6e-8501-4f19-a158-bf89d6b4a83e)
>
> **4 knives.**
>
![image](https://github.com/space-wizards/space-station-14/assets/134914314/44dc4c40-add3-46be-b224-b71e2f14f1e9)
>
> **Kit on uplink.**
> [IMG NEEDS UPDATING]
>
> **Changelog**
>
> :cl: Ubaser
> - add: You can now purchase a set of 4 throwing knives in the uplink
as a bundle for 12 TC.
---------
Co-authored-by: SimpleStation14
Co-authored-by: VMSolidus
---
.../Locale/en-US/store/uplink-catalog.ftl | 3 ++
.../Catalog/Fills/Boxes/syndicate.yml | 37 +++++++++++++-----
.../Prototypes/Catalog/uplink_catalog.yml | 11 ++++++
.../Entities/Objects/Weapons/Melee/knife.yml | 29 ++++++++++++++
.../Objects/Storage/boxes.rsi/meta.json | 17 ++++----
.../Storage/boxes.rsi/throwing_knives.png | Bin 0 -> 1141 bytes
.../Objects/Storage/boxicons.rsi/meta.json | 5 ++-
.../Storage/boxicons.rsi/throwing_knives.png | Bin 0 -> 1392 bytes
.../throwing_knife.rsi/equipped-BELT.png | Bin 0 -> 1289 bytes
.../Weapons/Melee/throwing_knife.rsi/icon.png | Bin 0 -> 1593 bytes
.../Melee/throwing_knife.rsi/inhand-left.png | Bin 0 -> 1296 bytes
.../Melee/throwing_knife.rsi/inhand-right.png | Bin 0 -> 1298 bytes
.../Melee/throwing_knife.rsi/meta.json | 26 ++++++++++++
13 files changed, 110 insertions(+), 18 deletions(-)
create mode 100644 Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png
create mode 100644 Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png
create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/equipped-BELT.png
create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/icon.png
create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png
create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png
create mode 100644 Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json
diff --git a/Resources/Locale/en-US/store/uplink-catalog.ftl b/Resources/Locale/en-US/store/uplink-catalog.ftl
index 70eb998bb4..4836a57d6b 100644
--- a/Resources/Locale/en-US/store/uplink-catalog.ftl
+++ b/Resources/Locale/en-US/store/uplink-catalog.ftl
@@ -201,6 +201,9 @@ uplink-decoy-kit-desc = State-of-the-art distraction technology straight from RN
uplink-chemistry-kit-name = Chemical Synthesis Kit
uplink-chemistry-kit-desc = A starter kit for the aspiring chemist, includes toxin and vestine for all your criminal needs!
+uplink-knives-kit-name = Throwing Knives Kit
+uplink-knives-kit-desc = A set of 4 syndicate branded throwing knives, perfect for embedding into the body of your victims.
+
uplink-meds-bundle-name = Medical Bundle
uplink-meds-bundle-desc = All you need to get your comrades back in the fight: mainly a combat medkit, a defibrillator and three combat medipens.
diff --git a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml
index 53c526f033..7b5b05a49a 100644
--- a/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml
+++ b/Resources/Prototypes/Catalog/Fills/Boxes/syndicate.yml
@@ -38,14 +38,31 @@
name: observations kit
suffix: Filled
components:
- - type: StorageFill
- contents:
- - id: SyndiCrewMonitorEmpty
- amount: 1
- - id: PowerCellHigh
- amount: 1
- - id: ClothingEyesGlassesHiddenSecurity
- amount: 1
- - id: SurveillanceCameraMonitorCircuitboard
- amount: 1
+ - type: StorageFill
+ contents:
+ - id: SyndiCrewMonitorEmpty
+ amount: 1
+ - id: PowerCellHigh
+ amount: 1
+ - id: ClothingEyesGlassesHiddenSecurity
+ amount: 1
+ - id: SurveillanceCameraMonitorCircuitboard
+ amount: 1
+- type: entity
+ parent: BoxCardboard
+ id: ThrowingKnivesKit
+ name: throwing knives kit
+ description: A set of 4 syndicate branded throwing knives, perfect for embedding into the body of your victims.
+ components:
+ - type: Storage
+ grid:
+ - 0,0,3,1
+ - type: StorageFill
+ contents:
+ - id: ThrowingKnife
+ amount: 4
+ - type: Sprite
+ layers:
+ - state: box_of_doom
+ - state: throwing_knives
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index 1e81cdf2dd..328ace7ba1 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -65,6 +65,17 @@
categories:
- UplinkWeapons
+- type: listing
+ id: UplinkThrowingKnivesKit
+ name: uplink-knives-kit-name
+ description: uplink-knives-kit-desc
+ icon: { sprite: /Textures/Objects/Storage/boxicons.rsi, state: throwing_knives }
+ productEntity: ThrowingKnivesKit
+ cost:
+ Telecrystal: 6
+ categories:
+ - UplinkWeapons
+
- type: listing
id: UplinkGlovesNorthStar
name: uplink-gloves-north-star-name
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml
index b5d597715a..03654061ce 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/knife.yml
@@ -252,3 +252,32 @@
sprite: Objects/Weapons/Melee/uranium_shiv.rsi
- type: Sprite
sprite: Objects/Weapons/Melee/uranium_shiv.rsi
+
+- type: entity
+ name: throwing knife
+ parent: BaseKnife
+ id: ThrowingKnife
+ description: This bloodred knife is very aerodynamic and easy to throw, but good luck trying to fight someone hand-to-hand.
+ components:
+ - type: Tag
+ tags:
+ - CombatKnife
+ - Knife
+ - type: Sprite
+ sprite: Objects/Weapons/Melee/throwing_knife.rsi
+ state: icon
+ - type: MeleeWeapon
+ wideAnimationRotation: -135
+ attackRate: 2
+ damage:
+ types:
+ Slash: 5
+ - type: EmbeddableProjectile
+ sound: /Audio/Weapons/star_hit.ogg
+ - type: DamageOtherOnHit
+ damage:
+ types:
+ Slash: 10
+ Piercing: 15
+ - type: Item
+ sprite: Objects/Weapons/Melee/throwing_knife.rsi
diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json
index 23868a906f..53ac39b639 100644
--- a/Resources/Textures/Objects/Storage/boxes.rsi/meta.json
+++ b/Resources/Textures/Objects/Storage/boxes.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, vials was drawn by Ubaser, evidence_markers by moomoobeef.",
+ "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/cc65477c04f7403ca8a457bd5bae69a01abadbf0, encryptokey was taken from Baystation12 at https://github.com/infinitystation/Baystation12/blob/073f678cdce92edb8fcd55f9ffc9f0523bf31506/icons/obj/radio.dmi and modified by lapatison. boxwidetoy, shelltoy, swab, flare, inflatable, trashbag, magazine, holo and forensic created by potato1234x (github) for ss14 based on toys.rsi, mouth_swab.rsi, flare.rsi, inflatable_wall.rsi, trashbag.rsi, caseless_pistol_mag.rsi, guardians.rsi and bureaucracy.rsi respectively, candle and darts created by TheShuEd for ss14, throwing_knives and vials was drawn by Ubaser, evidence_markers by moomoobeef.",
"size": {
"x": 32,
"y": 32
@@ -35,7 +35,7 @@
"name": "sechud"
},
{
- "name": "bottle"
+ "name": "bottle"
},
{
"name": "box"
@@ -142,6 +142,9 @@
{
"name": "syringe"
},
+ {
+ "name": "throwing_knives"
+ },
{
"name": "trashbag"
},
@@ -152,12 +155,12 @@
"name": "writing_of_doom"
},
{
- "name": "headset"
- },
+ "name": "headset"
+ },
{
- "name": "encryptokey"
- },
- {
+ "name": "encryptokey"
+ },
+ {
"name": "inhand-left",
"directions": 4
},
diff --git a/Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png b/Resources/Textures/Objects/Storage/boxes.rsi/throwing_knives.png
new file mode 100644
index 0000000000000000000000000000000000000000..834410a43ef64bd0f3dbfd9d852247d464cbd5f0
GIT binary patch
literal 1141
zcmZ`&O=#0#7*2JTtvC;YZit60f{2)WZB~X2X<#$^3EEL=g=`16Hc7XzG+#)*+BgSJ
z4^u%!9HT)<=$0%*`di&DU%@%;&UsW-gtBTI$qnX
zp%I554$ONq(!q5-B8aU>&&x>89OD#NW(q1SYK&X9d=&3;_*m9Zfpp7;>2PkE@-;Yo
z_QZf9eHAp7rsPbPJOXWv>|x?eoDyOrN%FQ@;&S3Z2*h~Qs5XK)Ri);~s@R6_
zMOA=iXl2br4oENw&8P
z^E>(cRx~-Ey}kOY>*2{yt7|vq_uqb=K@7Mycl6Mok8_dE*+}P;Zi~1#+Hs@h!h`!>
N%S-*k;zHl}`F|_-V#WXf
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json b/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json
index 858fc7c4e5..935b0b9f8b 100644
--- a/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json
+++ b/Resources/Textures/Objects/Storage/boxicons.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from baystation at https://github.com/Baystation12/Baystation12/commit/bc9fbb1722530596e3aa7522ee407280b323ad43, vials drawn by Ubaser, tracks made by Fazansen(https://github.com/Fazansen).",
+ "copyright": "Taken from baystation at https://github.com/Baystation12/Baystation12/commit/bc9fbb1722530596e3aa7522ee407280b323ad43, throwing_knives and vials are drawn by Ubaser, tracks made by Fazansen(https://github.com/Fazansen).",
"size": {
"x": 32,
"y": 32
@@ -76,6 +76,9 @@
{
"name": "syringe"
},
+ {
+ "name": "throwing_knives"
+ },
{
"name": "ziptie"
},
diff --git a/Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png b/Resources/Textures/Objects/Storage/boxicons.rsi/throwing_knives.png
new file mode 100644
index 0000000000000000000000000000000000000000..b2af7bce88436ed9b060cec8bd6f97019537caa9
GIT binary patch
literal 1392
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz#^NA%Cx&(BWL^R}e5nzhX}-P;
zT0k}j5QD&_;K@Lev%n*=n1MlK76>znTPbd0U|=rE42dX-@b$4u&d=3LOvz75)vL%Y
z0PC`;umUo3Q%e#RDspr3imfVamB1>jfNYSkzLEl1NlCV?QiN}Sf^&XRs)CuGfu4bq
z9hZWFf=y9MnpKdC8&o@xXRDM^Qc_^0uU}qXu2*iXmtT~wZ)j<0sc&GUZ)BtkRH0j3
znOBlnp_^B%3^4>|j!SBBa#3bMNoIbY0?6FNr2NtnTO}osMQ{LdXG${Mo`TY%9I!1Z
z$@-}|sky0nCB^!NdWQPg^p#|$AzYYO3=Ixo!03ZyfZ7bOYV#~8Nj3q7lxqdhJy8Dv
z9hwZbx40xlA4!3}k%57Qu7Q!Rk)=M|e?aHkq$FFFWR~Qlf&&ijA8-gd=9Hj{g4Bb8
zASV+PvQ{~XdFi%F6}l;@X^EvdB}#Tl`4#c`Y4Iuf`MEaw5QV4)Ktxf^wb94o1RIdY
zts;X`i}Q0zK|yb4WM*Om4h|GiL?l3TL}1l{EQ+opAS1sdADF0$GLt>?N>Ymoihv0W
zVk6W(+$zw`LRcP}msw(G1T_Fn7)cjW1X(#2m6YcfWru(x10^JJs)CvX300(Y3dscE
z7y#x1J1!f2c#g8;T5vFZ7clEqd%8G=L~y$v&<~>y})k!b$Ij_*Boec?7c@H{qFghl3r*G!+?2O}8vz5m}+81h%=^c1PKS83O{n;1@QN}0Z7^Xpv`mc7*YUNB{2a+8@u
z6VE;{x%R3Bx1%7%JU02a%^lN%F
zE%jh-`3kPxCsNfyV%|*TT~Hz7n*90eUlmUe899+_S!k}&6*}S*Quc$Du?mypn?RQoOuZK{=k6;xQ!K#XyN7Ju|PyZWn
zMDN`Biup!2tQbXXfBj=p>r*o7QRYk6np)zppMvt=Rf6n1)cS_o?!bBEpWVW
zzxWC@_m8>zq*a)GFBhEM{dHDh?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6
zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G
zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf
zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we
z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p
zz)=9q2XMaQ|Ei{
z!xO@M-(SD!`>3)=h^2Je2J^D5F{d?VMa9-GwO_78j!x3y?Z
zAj4G~+4hAyrlqc*dNa%HszigAoqha~oP8H+mOXafayE9a!9u>WGK-y+(MulJo4!
{eFz
zK3OI`antZn#mM^|`vUTx9X{?q)pV<`M@~&xT=!J%TJAry)DFy+mU5`JKlnhdpM7PB
z*G*T;g$Ycu3|n7Vm0iv>{N65lwq#bQ@Z<*>?{c(v2M9F!WqQbu$Pe0yk&>D6*Hq-gd*mBP7>5;
hNwHl1#vxGFf@x_avz=0K&TUY!?dj_0vd$@?2>>-FL7@Nu
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-left.png
new file mode 100644
index 0000000000000000000000000000000000000000..5988d571dc12ca1c485efc87735bbba3ce635640
GIT binary patch
literal 1296
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|_);T0(|mmy
zw18|5AO?X;!IOa`XMsm#F$06fED&ZCw^H21z`$IV84^(v;p=0SoS&h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6
zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G
zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf
zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we
z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p
zz)=9q2X*mm#I&6k+g^W`6|5prbv@a}`Hz?!tn&ug}I
zy!5S_wCuBHzq<1nK*zRfE;x>U{MO(OH#J#lhTTXtx$E#ub`D;1*mDf3?|1NQ#
p_;-Kx%DB?a9Dh3C-ua-+#BaJLkNt{4Ehnfn@^tlcS?83{1OP7tpo#zh
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/inhand-right.png
new file mode 100644
index 0000000000000000000000000000000000000000..09c015efac56e7935ff49a7488bea47ee21f706a
GIT binary patch
literal 1298
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|_);T0(|mmy
zw18|5AO?X;!IOa`XMsm#F$06fED&ZCw^H21z`$IV84^(v;p=0SoS&h?X&sHg;q@=(~U%$M(T(8_%FTW^V-_X+1Qs2Nx-^fT8s6w~6
zGOr}DLN~8i8Da>`9GBGM~RsBTId_|A5Z7NlCUU$t=l91qU45Kj08_%qc+?1*r!G
zK~5$pWUX=%^U`gVDs)p)(-KQ_N|fv}^D+|iQgn+l(=$qJ^dTxyO@N4^8f>GF#SJzf
zpIb!+r55Msl!C(E&dAKf1{@wJqKH_4=!n3o16dSZM?gk?Nj@-D6=f!S=9Q!t6%+we
z7{o@XeYjPin}x7EG%vHn&IoD%nlO?sq!_YtEGj9_FUk%9#|COp;#3AT3KFbHDHW0p
zz)=9q2X1D--Z+E{R|F`Dg_uaXhW6tcgx!1%pi>paAZW9~hvW5@d6(?_2pParSE_c-*
z{|Wn+{=e67Ep6>X)9_3GCW~F>Grp7k(%8AgO9|B?RC`-lOEe^_b5>b}$HEbI&63Re#m1*}He%`O5qM
s!?yrm-qO5?#!wak2L?tE(OAW5$7GzxZoH}C0;oLlboFyt=akR{01HQ^j{pDw
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json
new file mode 100644
index 0000000000..373d2d7770
--- /dev/null
+++ b/Resources/Textures/Objects/Weapons/Melee/throwing_knife.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Drawn by Ubaser.",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "inhand-left",
+ "directions": 4
+ },
+ {
+ "name": "inhand-right",
+ "directions": 4
+ },
+ {
+ "name": "equipped-BELT",
+ "directions": 4
+ }
+ ]
+}
From 6f54ed1b28908264a848c801ae6b86ed27a21734 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Sat, 29 Jun 2024 18:26:26 -0700
Subject: [PATCH 02/56] Update Credits (#495)
This is an automated Pull Request. This PR updates the GitHub
contributors in the credits section.
Co-authored-by: SimpleStation Changelogs
---
Resources/Credits/GitHub.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Resources/Credits/GitHub.txt b/Resources/Credits/GitHub.txt
index b5381a4ce2..84796a9466 100644
--- a/Resources/Credits/GitHub.txt
+++ b/Resources/Credits/GitHub.txt
@@ -1 +1 @@
-0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, Boaz1111, BobdaBiscuit, brainfood1183, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, casperr04, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, clement-or, Clyybber, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, exincore, exp111, Fahasor, FairlySadPanda, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, Genkail, Git-Nivrak, github-actions[bot], gituhabu, GNF54, Golinth, GoodWheatley, Gotimanga, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, Hebiman, Henry12116, HerCoyote23, Hmeister-real, HoofedEar, hord-brayden, hubismal, Hugal31, Hyenh, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, IntegerTempest, Interrobang01, IProduceWidgets, ItsMeThom, j-giebel, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, joelhed, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTether, JustinTrotter, KaiShibaa, kalane15, kalanosh, KEEYNy, Keikiru, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Kmc2000, Ko4ergaPunk, komunre, koteq, Krunklehorn, Kukutis96513, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, LightVillet, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, LudwigVonChesterfield, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, M3739, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, matthst, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, Nopey, notafet, notquitehadouken, noudoit, noverd, nuke-haus, NULL882, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, SethLafuente, ShadowCommander, Shadowtheprotogen546, SignalWalker, SimpleStation14, Simyon264, SirDragooon, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, Stealthbomber16, stellar-novas, StrawberryMoses, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, tmtmtl30, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, waylon531, weaversam8, Willhelm53, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, ZelteHonor, zerorulez, zionnBE, zlodo, ZNixian, ZoldorfTheWizard, Zumorica, Zymem
+0x6273, 2013HORSEMEATSCANDAL, 20kdc, 21Melkuu, 4dplanner, 612git, 778b, Ablankmann, Acruid, actioninja, adamsong, Admiral-Obvious-001, Adrian16199, Aerocrux, Aexxie, africalimedrop, Agoichi, Ahion, AJCM-git, AjexRose, Alekshhh, AlexMorgan3817, AlmondFlour, AlphaQwerty, Altoids1, amylizzle, ancientpower, ArchPigeon, Arendian, arimah, Arteben, AruMoon, as334, AsikKEsel, asperger-sind, aspiringLich, avghdev, AzzyIsNotHere, BananaFlambe, BasedUser, BGare, BingoJohnson-zz, BismarckShuffle, Bixkitts, Blackern5000, Blazeror, Boaz1111, BobdaBiscuit, brainfood1183, Brandon-Huu, Bribrooo, Bright0, brndd, BubblegumBlue, BYONDFuckery, c4llv07e, CaasGit, CakeQ, CaptainSqrBeard, Carbonhell, Carolyn3114, casperr04, CatTheSystem, Centronias, chairbender, Charlese2, Cheackraze, cheesePizza2, Chief-Engineer, chromiumboy, Chronophylos, clement-or, Clyybber, ColdAutumnRain, Colin-Tel, collinlunn, ComicIronic, coolmankid12345, corentt, crazybrain23, creadth, CrigCrag, Crotalus, CrudeWax, CrzyPotato, Cyberboss, d34d10cc, Daemon, daerSeebaer, dahnte, dakamakat, dakimasu, DamianX, DangerRevolution, daniel-cr, Darkenson, DawBla, dch-GH, Deahaka, DEATHB4DEFEAT, DeathCamel58, deathride58, DebugOk, Decappi, deepdarkdepths, Delete69, deltanedas, DeltaV-Bot, DerbyX, DoctorBeard, DogZeroX, dontbetank, Doru991, DoubleRiceEddiedd, DrMelon, DrSmugleaf, drteaspoon420, DTanxxx, DubiousDoggo, Duddino, Dutch-VanDerLinde, Easypoller, eclips_e, EdenTheLiznerd, EEASAS, Efruit, ElectroSR, elthundercloud, Emisse, EmoGarbage404, Endecc, enumerate0, eoineoineoin, ERORR404V1, Errant-4, estacaoespacialpirata, exincore, exp111, Fahasor, FairlySadPanda, ficcialfaint, Fildrance, FillerVK, Fishfish458, Flareguy, FluffiestFloof, FluidRock, FoLoKe, fooberticus, Fortune117, FoxxoTrystan, freeman2651, Fromoriss, FungiFellow, GalacticChimp, gbasood, Geekyhobo, Genkail, geraeumig, Git-Nivrak, github-actions[bot], gituhabu, GNF54, Golinth, GoodWheatley, Gotimanga, graevy, GreyMario, Guess-My-Name, gusxyz, h3half, Hanzdegloker, Hardly3D, harikattar, Hebiman, Henry12116, HerCoyote23, Hmeister-real, HoofedEar, hord-brayden, hubismal, Hugal31, Hyenh, iacore, IamVelcroboy, icekot8, igorsaux, ike709, Illiux, Ilya246, IlyaElDunaev, Injazz, Insineer, IntegerTempest, Interrobang01, IProduceWidgets, ItsMeThom, j-giebel, Jackal298, Jackrost, jamessimo, janekvap, JerryImMouse, Jessetriesagain, jessicamaybe, Jezithyr, jicksaw, JiimBob, JoeHammad1844, joelhed, JohnGinnane, johnku1, joshepvodka, jproads, Jrpl, juliangiebel, JustArt1m, JustCone14, JustinTether, JustinTrotter, KaiShibaa, kalane15, kalanosh, KEEYNy, Keikiru, Kelrak, kerisargit, keronshb, KIBORG04, Killerqu00, KingFroozy, kira-er, Kit0vras, KittenColony, Kmc2000, Ko4ergaPunk, komunre, koteq, Krunklehorn, Kukutis96513, kxvvv, Lamrr, LankLTE, lapatison, Leander-0, leonardo-dabepis, LetterN, Level10Cybermancer, lever1209, LightVillet, liltenhead, LittleBuilderJane, Lomcastar, LordCarve, LordEclipse, LovelyLophi, LudwigVonChesterfield, Lukasz825700516, lunarcomets, luringens, lvvova1, lzimann, lzk228, M3739, MACMAN2003, Macoron, MagnusCrowe, ManelNavola, matthst, Matz05, MehimoNemo, MeltedPixel, MemeProof, Menshin, Mervill, metalgearsloth, mhamsterr, MilenVolf, Minty642, Mirino97, mirrorcult, misandrie, MishaUnity, MisterMecky, Mith-randalf, Moneyl, Moomoobeef, moony, Morb0, Mr0maks, musicmanvr, Myakot, Myctai, N3X15, Nairodian, Naive817, namespace-Memory, NickPowers43, nikthechampiongr, Nimfar11, Nirnael, nmajask, nok-ko, Nopey, notafet, notquitehadouken, noudoit, noverd, nuke-haus, NULL882, OCOtheOmega, OctoRocket, OldDanceJacket, onoira, Owai-Seek, pali6, Pangogie, patrikturi, PaulRitter, Peptide90, peptron1, Phantom-Lily, PHCodes, PixelTheKermit, PJB3005, Plykiya, pofitlo, pointer-to-null, PolterTzi, PoorMansDreams, potato1234x, ProfanedBane, PrPleGoo, ps3moira, Psychpsyo, psykzz, PuroSlavKing, quatre, QuietlyWhisper, qwerltaz, Radosvik, Radrark, Rainbeon, Rainfey, Rane, ravage123321, rbertoche, Redict, RedlineTriad, RednoWCirabrab, RemberBM, RemieRichards, RemTim, rene-descartes2021, RiceMar1244, RieBi, Rinkashikachi, Rockdtben, rolfero, rosieposieeee, Saakra, Samsterious, SaphireLattice, ScalyChimp, scrato, Scribbles0, Serkket, SethLafuente, ShadowCommander, Shadowtheprotogen546, SignalWalker, SimpleStation14, Simyon264, SirDragooon, Sirionaut, siyengar04, Skarletto, Skrauz, Skyedra, SlamBamActionman, slarticodefast, Slava0135, Snowni, snowsignal, SonicHDC, SoulSloth, SpaceManiac, SpeltIncorrectyl, spoogemonster, ssdaniel24, Stealthbomber16, stellar-novas, StrawberryMoses, SweptWasTaken, Szunti, TadJohnson00, takemysoult, TaralGit, Tayrtahn, tday93, TekuNut, TemporalOroboros, tentekal, tgrkzus, thatrandomcanadianguy, TheArturZh, theashtronaut, thedraccx, themias, Theomund, theOperand, TheShuEd, TimrodDX, Titian3, tkdrg, tmtmtl30, tom-leys, tomasalves8, Tomeno, Tornado-Technology, tosatur, Tryded, TsjipTsjip, Tunguso4ka, TurboTrackerss14, Tyler-IN, Tyzemol, UbaserB, UKNOWH, UnicornOnLSD, Uriende, UristMcDorf, Vaaankas, Varen, VasilisThePikachu, veliebm, Veritius, Verslebas, VigersRay, Visne, VMSolidus, volundr-, Voomra, Vordenburg, vulppine, waylon531, weaversam8, Willhelm53, wixoaGit, WlarusFromDaSpace, wrexbe, xRiriq, yathxyz, Ygg01, YotaXP, YuriyKiss, zach-hill, Zandario, Zap527, ZelteHonor, zerorulez, zionnBE, zlodo, ZNixian, ZoldorfTheWizard, Zumorica, Zymem
From f439eb70b897cd2f1424b1eed778b8a804c8c948 Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Sun, 30 Jun 2024 21:22:28 +0300
Subject: [PATCH 03/56] Add Equip Delays to Clothing (#499)
# Description
For centuries SS14 had equip and unequip delays on the clothing
component, and yet that feature remained unused and unheard of, not even
nyanotrasen/deltav used it when designing shock collars and headcages,
whose descriptions clearly suggested they should take time to take off.
For centuries salvage specialists could safely swap suits in space. For
centuries you could accidentally unequip your eva suit mid-spacewalk and
die. Now the time has come. The time when we change it.
This adds equip and unequip delays to the base clothing item. The
currently chosen time is half a second - it's meant to not be too
annoying, but at the same time prevent people from being able to
instantly swap clothes, headsets, other things with a single click.
EVA suits take 1.5 seconds to equip and 1 second to take off, so to swap
EVA suits, you will have to expose your body to the dangers of space for
at least 1.5 seconds. For hardsuits, both values are increased to 2.5
seconds.
The values are not final and this PR will probably need polishing before
it can be merged - for example, while recording the second preview
video, I discovered that jetpacks do not inherit from base clothing and
thus do not inherit the delays. There's probably way more such items.
---
# TODO
Add equip/unequip delays to:
- [X] most basic clothing items
- [X] hardsuits/softsuits
- [X] special items (currently headcages and shock collars, possibly
more later?)
- [ ] Everything that was missed by the above
---
Media
Basics
https://github.com/Simple-Station/Einstein-Engines/assets/69920617/3fc900b8-ee13-4968-bf5d-cddeb9a141b6
Hardsuits, eva suits, shock collars, headcages + demonstration that
aghost stripping is unaffected.
https://github.com/Simple-Station/Einstein-Engines/assets/69920617/a536578f-2ac3-40e1-9b27-3b167e006397
---
# Changelog
:cl:
- add: Most items now take time to equip and unequip, especially space
suits.
---
.../Entities/Clothing/OuterClothing/base_clothingouter.yml | 6 ++++++
Resources/Prototypes/Entities/Clothing/base_clothing.yml | 3 +++
.../Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml | 2 ++
.../Nyanotrasen/Entities/Objects/Devices/shock_collar.yml | 2 ++
4 files changed, 13 insertions(+)
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
index 13524efa9e..902c57418e 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml
@@ -93,6 +93,9 @@
- Hardsuit
- WhitelistChameleon
- HidesHarpyWings #DeltaV: Used by harpies to help render their hardsuit sprites
+ - type: Clothing
+ equipDelay: 2.5 # Hardsuits are heavy and take a while to put on/off.
+ unequipDelay: 2.5
- type: entity
abstract: true
@@ -114,6 +117,9 @@
- type: Tag
tags:
- HidesHarpyWings #DeltaV: Used by harpies to help render their hardsuit sprites
+ - type: Clothing
+ equipDelay: 1.25 # Softsuits are easier to put on and off
+ unequipDelay: 1
- type: entity
parent: ClothingOuterBase
diff --git a/Resources/Prototypes/Entities/Clothing/base_clothing.yml b/Resources/Prototypes/Entities/Clothing/base_clothing.yml
index 92a698dd30..810ada5429 100644
--- a/Resources/Prototypes/Entities/Clothing/base_clothing.yml
+++ b/Resources/Prototypes/Entities/Clothing/base_clothing.yml
@@ -11,6 +11,9 @@
- WhitelistChameleon
- type: StaticPrice
price: 15
+ - type: Clothing
+ equipDelay: 0.5
+ unequipDelay: 0.5
- type: entity
abstract: true
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml
index 8278114d26..2cd9785d98 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hats.yml
@@ -99,6 +99,8 @@
sprite: Nyanotrasen/Clothing/Head/Hats/cage.rsi
- type: Clothing
sprite: Nyanotrasen/Clothing/Head/Hats/cage.rsi
+ equipDelay: 0.5
+ unequipDelay: 6
- type: HeadCage
- type: entity
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml
index 35cdcae658..1266a721fe 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/shock_collar.yml
@@ -8,6 +8,8 @@
sprite: Nyanotrasen/Clothing/Neck/Misc/shock.rsi
- type: Clothing
sprite: Nyanotrasen/Clothing/Neck/Misc/shock.rsi
+ equipDelay: 1
+ unequipDelay: 10 # It's a collar meant to be used on prisoners (or not), so it probably has some sort of safety.
- type: ShockCollar
- type: UseDelay
delay: 3 # DeltaV: prevent clocks instakilling people
From 7c83c8b30c016b4f457d8d7398c282d9cba5a0da Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Sun, 30 Jun 2024 18:22:48 +0000
Subject: [PATCH 04/56] Automatic Changelog Update (#499)
---
Resources/Changelog/Changelog.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 3aea1324fe..6fe7b0ff68 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4251,3 +4251,9 @@ Entries:
message: 'Height and Width sliders have been added to character creation. '
id: 6131
time: '2024-06-27T17:46:51.0000000+00:00'
+- author: Mnemotechnician
+ changes:
+ - type: Add
+ message: Most items now take time to equip and unequip, especially space suits.
+ id: 6132
+ time: '2024-06-30T18:22:28.0000000+00:00'
From 89a6bb3ab5897da04c0526c32ff408b90bfc70fc Mon Sep 17 00:00:00 2001
From: SimpleStation14 <130339894+SimpleStation14@users.noreply.github.com>
Date: Mon, 1 Jul 2024 11:37:45 -0700
Subject: [PATCH 05/56] Mirror: StrippableSystem doafter overhaul (#205)
## Mirror of PR #25994: [StrippableSystem doafter
overhaul](https://github.com/space-wizards/space-station-14/pull/25994)
from
[space-wizards](https://github.com/space-wizards)/[space-station-14](https://github.com/space-wizards/space-station-14)
###### `41ca8f3dfcb986432e1e509247bf239cac137836`
PR opened by Krunklehorn at
2024-03-11 12:36:28 UTC
---
PR changed 7 files with 465 additions and 305 deletions.
The PR had the following labels:
- Status: Needs Review
---
Original Body
> ## About the PR
>
> Refactors Strippable DoAfter events to make them synchronous and
organized.
>
>
> ## Technical details
>
> ### Strippable System & Component
> - Synchronous DoAfters
> - Made use of `TimeSpan`, `GetStripTimeModifiers()` and `ByRefEvent`
> - Reorganized checks, removed some redundant ones
> - Resolve pattern where useful
> - Added more asserts
> - Lots of cleanup
>
> The DoAfters were grouped under one event to avoid copy-pasting eight
separate cancel checks, asserts and function signatures.
>
> Let me know if this is bad for performance and I'll roll them out
instead.
>
>
> ## Media
>
> - [x] I have added screenshots/videos to this PR showcasing its
changes ingame, **or** this PR does not require an ingame showcase
>
>
> ## Breaking changes
>
> ### TimeSpans
> `ThievingComponent`, `InventoryTemplatePrototype` and
`ToggleableClothingSystem` use `TimeSpan` in places where they intersect
with `StrippableComponent`.
>
>
> **Changelog**
>
> N/A
>
Signed-off-by: VMSolidus
Co-authored-by: SimpleStation14
Co-authored-by: VMSolidus
---
Content.Server/Strip/StrippableSystem.cs | 676 +++++++++++-------
.../EntitySystems/ToggleableClothingSystem.cs | 2 +-
.../Inventory/InventoryTemplatePrototype.cs | 2 +-
.../Strip/Components/StrippableComponent.cs | 81 +--
.../Strip/Components/ThievingComponent.cs | 2 +-
.../Strip/SharedStrippableSystem.cs | 6 +-
Content.Shared/Strip/ThievingSystem.cs | 1 +
7 files changed, 465 insertions(+), 305 deletions(-)
diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs
index 96b2ecc00c..950411a8e2 100644
--- a/Content.Server/Strip/StrippableSystem.cs
+++ b/Content.Server/Strip/StrippableSystem.cs
@@ -1,4 +1,3 @@
-using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Ensnaring;
using Content.Shared.CombatMode;
@@ -21,18 +20,21 @@
using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Utility;
+using System.Linq;
namespace Content.Server.Strip
{
public sealed class StrippableSystem : SharedStrippableSystem
{
- [Dependency] private readonly SharedCuffableSystem _cuffable = default!;
- [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
- [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
- [Dependency] private readonly SharedPopupSystem _popup = default!;
- [Dependency] private readonly EnsnareableSystem _ensnaring = default!;
+ [Dependency] private readonly EnsnareableSystem _ensnaringSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+
+ [Dependency] private readonly SharedCuffableSystem _cuffableSystem = default!;
+ [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popupSystem = default!;
+
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
// TODO: ECS popups. Not all of these have ECS equivalents yet.
@@ -48,64 +50,58 @@ public override void Initialize()
// BUI
SubscribeLocalEvent(OnStripButtonPressed);
SubscribeLocalEvent(OnStripEnsnareMessage);
+
+ // DoAfters
+ SubscribeLocalEvent>(OnStrippableDoAfterRunning);
+ SubscribeLocalEvent(OnStrippableDoAfterFinished);
}
- private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args)
+ private void AddStripVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args)
{
- if (args.Session.AttachedEntity is not {Valid: true} user)
+ if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
return;
- foreach (var entity in component.Container.ContainedEntities)
+ if (!HasComp(args.User))
+ return;
+
+ Verb verb = new()
{
- if (!TryComp(entity, out var ensnaring))
- continue;
+ Text = Loc.GetString("strip-verb-get-data-text"),
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
+ Act = () => StartOpeningStripper(args.User, (uid, component), true),
+ };
- _ensnaring.TryFree(uid, user, entity, ensnaring);
- return;
- }
+ args.Verbs.Add(verb);
}
- private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args)
+ private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args)
{
- if (args.Session.AttachedEntity is not {Valid: true} user ||
- !TryComp(user, out var userHands))
- return;
-
- if (args.IsHand)
- {
- StripHand(user, args.Slot, strippable, userHands);
+ if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
return;
- }
- if (!TryComp(strippable, out var inventory))
+ if (!HasComp(args.User))
return;
- var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory);
+ ExamineVerb verb = new()
+ {
+ Text = Loc.GetString("strip-verb-get-data-text"),
+ Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
+ Act = () => StartOpeningStripper(args.User, (uid, component), true),
+ Category = VerbCategory.Examine,
+ };
- if (userHands.ActiveHandEntity != null && !hasEnt)
- PlaceActiveHandItemInInventory(user, strippable, userHands.ActiveHandEntity.Value, args.Slot, strippable);
- else if (userHands.ActiveHandEntity == null && hasEnt)
- TakeItemFromInventory(user, strippable, held!.Value, args.Slot, strippable);
+ args.Verbs.Add(verb);
}
- private void StripHand(EntityUid user, string handId, Entity target, HandsComponent userHands)
+ private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args)
{
- if (!_handsSystem.TryGetHand(target, handId, out var hand))
+ if (args.Target == args.User)
return;
- // is the target a handcuff?
- if (TryComp(hand.HeldEntity, out VirtualItemComponent? virt)
- && TryComp(target, out CuffableComponent? cuff)
- && _cuffable.GetAllCuffs(cuff).Contains(virt.BlockingEntity))
- {
- _cuffable.TryUncuff(target, user, virt.BlockingEntity, cuffable: cuff);
+ if (!HasComp(args.User))
return;
- }
- if (userHands.ActiveHandEntity != null && hand.HeldEntity == null)
- PlaceActiveHandItemInHands(user, target, userHands.ActiveHandEntity.Value, handId, target);
- else if (userHands.ActiveHandEntity == null && hand.HeldEntity != null)
- TakeItemFromHands(user, target, hand.HeldEntity.Value, handId, target);
+ StartOpeningStripper(args.User, (uid, component));
}
public override void StartOpeningStripper(EntityUid user, Entity strippable, bool openInCombat = false)
@@ -123,352 +119,514 @@ public override void StartOpeningStripper(EntityUid user, Entity args)
+ private void OnStripButtonPressed(Entity strippable, ref StrippingSlotButtonPressed args)
{
- if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
+ if (args.Session.AttachedEntity is not { Valid: true } user ||
+ !TryComp(user, out var userHands) ||
+ !TryComp(strippable.Owner, out var targetHands))
return;
- if (!HasComp(args.User))
+ if (args.IsHand)
+ {
+ StripHand((user, userHands), (strippable.Owner, targetHands), args.Slot, strippable);
return;
+ }
- Verb verb = new()
- {
- Text = Loc.GetString("strip-verb-get-data-text"),
- Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
- Act = () => StartOpeningStripper(args.User, (uid, component), true),
- };
- args.Verbs.Add(verb);
+ if (!TryComp(strippable, out var inventory))
+ return;
+
+ var hasEnt = _inventorySystem.TryGetSlotEntity(strippable, args.Slot, out var held, inventory);
+
+ if (userHands.ActiveHandEntity != null && !hasEnt)
+ StartStripInsertInventory((user, userHands), strippable.Owner, userHands.ActiveHandEntity.Value, args.Slot);
+ else if (userHands.ActiveHandEntity == null && hasEnt)
+ StartStripRemoveInventory(user, strippable.Owner, held!.Value, args.Slot);
}
- private void AddStripExamineVerb(EntityUid uid, StrippableComponent component, GetVerbsEvent args)
+ private void StripHand(
+ Entity user,
+ Entity target,
+ string handId,
+ StrippableComponent? targetStrippable)
{
- if (args.Hands == null || !args.CanAccess || !args.CanInteract || args.Target == args.User)
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp) ||
+ !Resolve(target, ref targetStrippable))
return;
- if (!HasComp(args.User))
+ if (!_handsSystem.TryGetHand(target.Owner, handId, out var handSlot))
return;
- ExamineVerb verb = new()
+ // Is the target a handcuff?
+ if (TryComp(handSlot.HeldEntity, out var virtualItem) &&
+ TryComp(target.Owner, out var cuffable) &&
+ _cuffableSystem.GetAllCuffs(cuffable).Contains(virtualItem.BlockingEntity))
{
- Text = Loc.GetString("strip-verb-get-data-text"),
- Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/outfit.svg.192dpi.png")),
- Act = () => StartOpeningStripper(args.User, (uid, component), true),
- Category = VerbCategory.Examine,
- };
+ _cuffableSystem.TryUncuff(target.Owner, user, virtualItem.BlockingEntity, cuffable);
+ return;
+ }
- args.Verbs.Add(verb);
+ if (user.Comp.ActiveHandEntity != null && handSlot.HeldEntity == null)
+ StartStripInsertHand(user, target, user.Comp.ActiveHandEntity.Value, handId, targetStrippable);
+ else if (user.Comp.ActiveHandEntity == null && handSlot.HeldEntity != null)
+ StartStripRemoveHand(user, target, handSlot.HeldEntity.Value, handId, targetStrippable);
}
- private void OnActivateInWorld(EntityUid uid, StrippableComponent component, ActivateInWorldEvent args)
+ private void OnStripEnsnareMessage(EntityUid uid, EnsnareableComponent component, StrippingEnsnareButtonPressed args)
{
- if (args.Target == args.User)
+ if (args.Session.AttachedEntity is not { Valid: true } user)
return;
- if (!HasComp(args.User))
- return;
+ foreach (var entity in component.Container.ContainedEntities)
+ {
+ if (!TryComp(entity, out var ensnaring))
+ continue;
- StartOpeningStripper(args.User, (uid, component));
+ _ensnaringSystem.TryFree(uid, user, entity, ensnaring);
+ return;
+ }
}
///
- /// Places item in user's active hand to an inventory slot.
+ /// Checks whether the item is in a user's active hand and whether it can be inserted into the inventory slot.
///
- private async void PlaceActiveHandItemInInventory(
- EntityUid user,
+ private bool CanStripInsertInventory(
+ Entity user,
EntityUid target,
EntityUid held,
- string slot,
- StrippableComponent component)
+ string slot)
{
- var userHands = Comp(user);
+ if (!Resolve(user, ref user.Comp))
+ return false;
+
+ if (user.Comp.ActiveHand == null)
+ return false;
+
+ if (user.Comp.ActiveHandEntity == null)
+ return false;
+
+ if (user.Comp.ActiveHandEntity != held)
+ return false;
+
+ if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
+ {
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
+ return false;
+ }
+
+ if (_inventorySystem.TryGetSlotEntity(target, slot, out _))
+ {
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied", ("owner", target)), user);
+ return false;
+ }
- bool Check()
+ if (!_inventorySystem.CanEquip(user, target, held, slot, out _))
{
- if (userHands.ActiveHandEntity != held)
- return false;
-
- if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
- return false;
- }
-
- if (_inventorySystem.TryGetSlotEntity(target, slot, out _))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-occupied",("owner", target)), user);
- return false;
- }
-
- if (!_inventorySystem.CanEquip(user, target, held, slot, out _))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message",("owner", target)), user);
- return false;
- }
-
- return true;
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-equip-message", ("owner", target)), user);
+ return false;
}
+ return true;
+ }
+
+ ///
+ /// Begins a DoAfter to insert the item in the user's active hand into the inventory slot.
+ ///
+ private void StartStripInsertInventory(
+ Entity user,
+ EntityUid target,
+ EntityUid held,
+ string slot)
+ {
+ if (!Resolve(user, ref user.Comp))
+ return;
+
+ if (!CanStripInsertInventory(user, target, held, slot))
+ return;
+
if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
{
Log.Error($"{ToPrettyString(user)} attempted to place an item in a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
return;
}
- var userEv = new BeforeStripEvent(slotDef.StripTime);
- RaiseLocalEvent(user, userEv);
- var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
- RaiseLocalEvent(target, ev);
+ var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
+
+ if (!stealth)
+ _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-insert", ("user", Identity.Entity(user, EntityManager)), ("item", user.Comp.ActiveHandEntity!.Value)), target, target, PopupType.Large);
+
+ var prefix = stealth ? "stealthily " : "";
+ _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
- var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held)
+ var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, true, slot), user, target, held)
{
- ExtraCheck = Check,
- Hidden = ev.Stealth,
+ Hidden = stealth,
AttemptFrequency = AttemptFrequency.EveryTick,
BreakOnDamage = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
- DuplicateCondition = DuplicateConditions.SameTool // Block any other DoAfters featuring this same entity.
+ DuplicateCondition = DuplicateConditions.SameTool
};
- if (!ev.Stealth && Check() && userHands.ActiveHandEntity != null)
- {
- var message = Loc.GetString("strippable-component-alert-owner-insert",
- ("user", Identity.Entity(user, EntityManager)), ("item", userHands.ActiveHandEntity));
- _popup.PopupEntity(message, target, target, PopupType.Large);
- }
+ _doAfterSystem.TryStartDoAfter(doAfterArgs);
+ }
- var prefix = ev.Stealth ? "stealthily " : "";
- _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
+ ///
+ /// Inserts the item in the user's active hand into the inventory slot.
+ ///
+ private void StripInsertInventory(
+ Entity user,
+ EntityUid target,
+ EntityUid held,
+ string slot)
+ {
+ if (!Resolve(user, ref user.Comp))
+ return;
- var result = await _doAfter.WaitDoAfter(doAfterArgs);
- if (result != DoAfterStatus.Finished)
+ if (!CanStripInsertInventory(user, target, held, slot))
return;
- DebugTools.Assert(userHands.ActiveHand?.HeldEntity == held);
+ if (!_handsSystem.TryDrop(user, handsComp: user.Comp))
+ return;
+
+ _inventorySystem.TryEquip(user, target, held, slot);
+ _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
+ }
- if (_handsSystem.TryDrop(user, handsComp: userHands))
+ ///
+ /// Checks whether the item can be removed from the target's inventory.
+ ///
+ private bool CanStripRemoveInventory(
+ EntityUid user,
+ EntityUid target,
+ EntityUid item,
+ string slot)
+ {
+ if (!_inventorySystem.TryGetSlotEntity(target, slot, out var slotItem))
{
- _inventorySystem.TryEquip(user, target, held, slot);
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user);
+ return false;
+ }
- _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s {slot} slot");
+ if (slotItem != item)
+ return false;
+
+ if (!_inventorySystem.CanUnequip(user, target, slot, out var reason))
+ {
+ _popupSystem.PopupCursor(Loc.GetString(reason), user);
+ return false;
}
+
+ return true;
}
///
- /// Places item in user's active hand in one of the entity's hands.
+ /// Begins a DoAfter to remove the item from the target's inventory and insert it in the user's active hand.
///
- private async void PlaceActiveHandItemInHands(
+ private void StartStripRemoveInventory(
EntityUid user,
EntityUid target,
- EntityUid held,
- string handName,
- StrippableComponent component)
+ EntityUid item,
+ string slot)
{
- var hands = Comp(target);
- var userHands = Comp(user);
+ if (!CanStripRemoveInventory(user, target, item, slot))
+ return;
- bool Check()
+ if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
{
- if (userHands.ActiveHandEntity != held)
- return false;
-
- if (!_handsSystem.CanDropHeld(user, userHands.ActiveHand!))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
- return false;
- }
-
- if (!_handsSystem.TryGetHand(target, handName, out var hand, hands)
- || !_handsSystem.CanPickupToHand(target, userHands.ActiveHandEntity.Value, hand, checkActionBlocker: false, hands))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-cannot-put-message",("owner", target)), user);
- return false;
- }
-
- return true;
+ Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
+ return;
}
- var userEv = new BeforeStripEvent(component.HandStripDelay);
- RaiseLocalEvent(user, userEv);
- var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
- RaiseLocalEvent(target, ev);
+ var (time, stealth) = GetStripTimeModifiers(user, target, slotDef.StripTime);
- var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: held)
+ if (!stealth)
{
- ExtraCheck = Check,
- Hidden = ev.Stealth,
+ if (slotDef.StripHidden)
+ _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target, target, PopupType.Large);
+ else
+ _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target, PopupType.Large);
+ }
+
+ var prefix = stealth ? "stealthily " : "";
+ _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, true, slot), user, target, item)
+ {
+ Hidden = stealth,
AttemptFrequency = AttemptFrequency.EveryTick,
BreakOnDamage = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
+ BreakOnHandChange = false, // Allow simultaneously removing multiple items.
DuplicateCondition = DuplicateConditions.SameTool
};
- var prefix = ev.Stealth ? "stealthily " : "";
- _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
-
- var result = await _doAfter.WaitDoAfter(doAfterArgs);
- if (result != DoAfterStatus.Finished) return;
-
- _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: userHands);
- _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: hands);
- _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
- // hand update will trigger strippable update
+ _doAfterSystem.TryStartDoAfter(doAfterArgs);
}
///
- /// Takes an item from the inventory and places it in the user's active hand.
+ /// Removes the item from the target's inventory and inserts it in the user's active hand.
///
- private async void TakeItemFromInventory(
+ private void StripRemoveInventory(
EntityUid user,
EntityUid target,
EntityUid item,
string slot,
- Entity strippable)
+ bool stealth)
+ {
+ if (!CanStripRemoveInventory(user, target, item, slot))
+ return;
+
+ if (!_inventorySystem.TryUnequip(user, target, slot))
+ return;
+
+ RaiseLocalEvent(item, new DroppedEvent(user), true); // Gas tank internals etc.
+
+ _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth);
+ _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+ }
+
+ ///
+ /// Checks whether the item in the user's active hand can be inserted into one of the target's hands.
+ ///
+ private bool CanStripInsertHand(
+ Entity user,
+ Entity target,
+ EntityUid held,
+ string handName)
{
- bool Check()
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp))
+ return false;
+
+ if (user.Comp.ActiveHand == null)
+ return false;
+
+ if (user.Comp.ActiveHandEntity == null)
+ return false;
+
+ if (user.Comp.ActiveHandEntity != held)
+ return false;
+
+ if (!_handsSystem.CanDropHeld(user, user.Comp.ActiveHand))
{
- if (!_inventorySystem.TryGetSlotEntity(target, slot, out var ent) && ent == item)
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user);
- return false;
- }
-
- if (!_inventorySystem.CanUnequip(user, target, slot, out var reason))
- {
- _popup.PopupCursor(Loc.GetString(reason), user);
- return false;
- }
-
- return true;
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop"), user);
+ return false;
}
- if (!_inventorySystem.TryGetSlot(target, slot, out var slotDef))
+ if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp) ||
+ !_handsSystem.CanPickupToHand(target, user.Comp.ActiveHandEntity.Value, handSlot, checkActionBlocker: false, target.Comp))
{
- Log.Error($"{ToPrettyString(user)} attempted to take an item from a non-existent inventory slot ({slot}) on {ToPrettyString(target)}");
- return;
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-put-message", ("owner", target)), user);
+ return false;
}
- var userEv = new BeforeStripEvent(slotDef.StripTime);
- RaiseLocalEvent(user, userEv);
- var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
- RaiseLocalEvent(target, ev);
+ return true;
+ }
+
+ ///
+ /// Begins a DoAfter to insert the item in the user's active hand into one of the target's hands.
+ ///
+ private void StartStripInsertHand(
+ Entity user,
+ Entity target,
+ EntityUid held,
+ string handName,
+ StrippableComponent? targetStrippable = null)
+ {
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp) ||
+ !Resolve(target, ref targetStrippable))
+ return;
+
+ if (!CanStripInsertHand(user, target, held, handName))
+ return;
- var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item)
+ var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
+
+ var prefix = stealth ? "stealthily " : "";
+ _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}place the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(true, false, handName), user, target, held)
{
- ExtraCheck = Check,
- Hidden = ev.Stealth,
+ Hidden = stealth,
AttemptFrequency = AttemptFrequency.EveryTick,
BreakOnDamage = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
- BreakOnHandChange = false, // allow simultaneously removing multiple items.
DuplicateCondition = DuplicateConditions.SameTool
};
- if (!ev.Stealth && Check())
+ _doAfterSystem.TryStartDoAfter(doAfterArgs);
+ }
+
+ ///
+ /// Places the item in the user's active hand into one of the target's hands.
+ ///
+ private void StripInsertHand(
+ Entity user,
+ Entity target,
+ EntityUid held,
+ string handName,
+ bool stealth)
+ {
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp))
+ return;
+
+ _handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp);
+ _handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp);
+ _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
+
+ // Hand update will trigger strippable update.
+ }
+
+ ///
+ /// Checks whether the item is in the target's hand and whether it can be dropped.
+ ///
+ private bool CanStripRemoveHand(
+ EntityUid user,
+ Entity target,
+ EntityUid item,
+ string handName)
+ {
+ if (!Resolve(target, ref target.Comp))
+ return false;
+
+ if (!_handsSystem.TryGetHand(target, handName, out var handSlot, target.Comp))
{
- if (slotDef.StripHidden)
- {
- _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner-hidden", ("slot", slot)), target,
- target, PopupType.Large);
- }
- else if (_inventorySystem.TryGetSlotEntity(strippable, slot, out var slotItem))
- {
- _popup.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", slotItem)), target,
- target, PopupType.Large);
- }
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message", ("owner", target)), user);
+ return false;
}
- var prefix = ev.Stealth ? "stealthily " : "";
- _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+ if (HasComp(handSlot.HeldEntity))
+ return false;
- var result = await _doAfter.WaitDoAfter(doAfterArgs);
- if (result != DoAfterStatus.Finished)
- return;
+ if (handSlot.HeldEntity == null)
+ return false;
- if (!_inventorySystem.TryUnequip(user, strippable, slot))
- return;
-
- // Raise a dropped event, so that things like gas tank internals properly deactivate when stripping
- RaiseLocalEvent(item, new DroppedEvent(user), true);
+ if (handSlot.HeldEntity != item)
+ return false;
- _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth);
- _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s {slot} slot");
+ if (!_handsSystem.CanDropHeld(target, handSlot, false))
+ {
+ _popupSystem.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message", ("owner", target)), user);
+ return false;
+ }
+ return true;
}
///
- /// Takes an item from a hand and places it in the user's active hand.
+ /// Begins a DoAfter to remove the item from the target's hand and insert it in the user's active hand.
///
- private async void TakeItemFromHands(EntityUid user, EntityUid target, EntityUid item, string handName, Entity strippable)
+ private void StartStripRemoveHand(
+ Entity user,
+ Entity target,
+ EntityUid item,
+ string handName,
+ StrippableComponent? targetStrippable = null)
{
- var hands = Comp(target);
- var userHands = Comp(user);
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp) ||
+ !Resolve(target, ref targetStrippable))
+ return;
- bool Check()
- {
- if (!_handsSystem.TryGetHand(target, handName, out var hand, hands) || hand.HeldEntity != item)
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-item-slot-free-message",("owner", target)), user);
- return false;
- }
-
- if (HasComp(hand.HeldEntity))
- return false;
-
- if (!_handsSystem.CanDropHeld(target, hand, false))
- {
- _popup.PopupCursor(Loc.GetString("strippable-component-cannot-drop-message",("owner", target)), user);
- return false;
- }
-
- return true;
- }
+ if (!CanStripRemoveHand(user, target, item, handName))
+ return;
- var userEv = new BeforeStripEvent(strippable.Comp.HandStripDelay);
- RaiseLocalEvent(user, userEv);
- var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
- RaiseLocalEvent(target, ev);
+ var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
- var doAfterArgs = new DoAfterArgs(EntityManager, user, ev.Time, new AwaitedDoAfterEvent(), null, target: target, used: item)
+ if (!stealth)
+ _popupSystem.PopupEntity( Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
+
+ var prefix = stealth ? "stealthily " : "";
+ _adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
+
+ var doAfterArgs = new DoAfterArgs(EntityManager, user, time, new StrippableDoAfterEvent(false, false, handName), user, target, item)
{
- ExtraCheck = Check,
- Hidden = ev.Stealth,
+ Hidden = stealth,
AttemptFrequency = AttemptFrequency.EveryTick,
BreakOnDamage = true,
BreakOnTargetMove = true,
BreakOnUserMove = true,
NeedHand = true,
- BreakOnHandChange = false, // allow simultaneously removing multiple items.
+ BreakOnHandChange = false, // Allow simultaneously removing multiple items.
DuplicateCondition = DuplicateConditions.SameTool
};
- if (!ev.Stealth && Check() && _handsSystem.TryGetHand(target, handName, out var handSlot, hands) && handSlot.HeldEntity != null)
+ _doAfterSystem.TryStartDoAfter(doAfterArgs);
+ }
+
+ ///
+ /// Takes the item from the target's hand and inserts it in the user's active hand.
+ ///
+ private void StripRemoveHand(
+ Entity user,
+ Entity target,
+ EntityUid item,
+ bool stealth)
+ {
+ if (!Resolve(user, ref user.Comp) ||
+ !Resolve(target, ref target.Comp))
+ return;
+
+ _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp);
+ _handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp);
+ _adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
+
+ // Hand update will trigger strippable update.
+ }
+
+ private void OnStrippableDoAfterRunning(Entity entity, ref DoAfterAttemptEvent ev)
+ {
+ var args = ev.DoAfter.Args;
+
+ DebugTools.Assert(entity.Owner == args.User);
+ DebugTools.Assert(args.Target != null);
+ DebugTools.Assert(args.Used != null);
+ DebugTools.Assert(ev.Event.SlotOrHandName != null);
+
+ if (ev.Event.InventoryOrHand)
{
- _popup.PopupEntity(
- Loc.GetString("strippable-component-alert-owner",
- ("user", Identity.Entity(user, EntityManager)), ("item", item)),
- strippable.Owner,
- strippable.Owner);
+ if ( ev.Event.InsertOrRemove && !CanStripInsertInventory((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
+ !ev.Event.InsertOrRemove && !CanStripRemoveInventory(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
+ ev.Cancel();
}
+ else
+ {
+ if ( ev.Event.InsertOrRemove && !CanStripInsertHand((entity.Owner, entity.Comp), args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName) ||
+ !ev.Event.InsertOrRemove && !CanStripRemoveHand(entity.Owner, args.Target.Value, args.Used.Value, ev.Event.SlotOrHandName))
+ ev.Cancel();
+ }
+ }
- var prefix = ev.Stealth ? "stealthily " : "";
- _adminLogger.Add(LogType.Stripping, LogImpact.Low,
- $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
-
- var result = await _doAfter.WaitDoAfter(doAfterArgs);
- if (result != DoAfterStatus.Finished)
+ private void OnStrippableDoAfterFinished(Entity entity, ref StrippableDoAfterEvent ev)
+ {
+ if (ev.Cancelled)
return;
- _handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: hands);
- _handsSystem.PickupOrDrop(user, item, animateUser: !ev.Stealth, animate: !ev.Stealth, handsComp: userHands);
- // hand update will trigger strippable update
- _adminLogger.Add(LogType.Stripping, LogImpact.Medium,
- $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
+ DebugTools.Assert(entity.Owner == ev.User);
+ DebugTools.Assert(ev.Target != null);
+ DebugTools.Assert(ev.Used != null);
+ DebugTools.Assert(ev.SlotOrHandName != null);
+
+ if (ev.InventoryOrHand)
+ {
+ if (ev.InsertOrRemove)
+ StripInsertInventory((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName);
+ else StripRemoveInventory(entity.Owner, ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
+ }
+ else
+ {
+ if (ev.InsertOrRemove)
+ StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
+ else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.Args.Hidden);
+ }
}
}
}
diff --git a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
index 0138de7a98..22a1d1a8f5 100644
--- a/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
+++ b/Content.Shared/Clothing/EntitySystems/ToggleableClothingSystem.cs
@@ -95,7 +95,7 @@ private void StartDoAfter(EntityUid user, EntityUid item, EntityUid wearer, Togg
if (component.StripDelay == null)
return;
- var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, (float) component.StripDelay.Value.TotalSeconds);
+ var (time, stealth) = _strippable.GetStripTimeModifiers(user, wearer, component.StripDelay.Value);
var args = new DoAfterArgs(EntityManager, user, time, new ToggleClothingDoAfterEvent(), item, wearer, item)
{
diff --git a/Content.Shared/Inventory/InventoryTemplatePrototype.cs b/Content.Shared/Inventory/InventoryTemplatePrototype.cs
index a477969962..585f80d4ce 100644
--- a/Content.Shared/Inventory/InventoryTemplatePrototype.cs
+++ b/Content.Shared/Inventory/InventoryTemplatePrototype.cs
@@ -20,7 +20,7 @@ public sealed partial class SlotDefinition
[DataField("slotFlags")] public SlotFlags SlotFlags { get; private set; } = SlotFlags.PREVENTEQUIP;
[DataField("showInWindow")] public bool ShowInWindow { get; private set; } = true;
[DataField("slotGroup")] public string SlotGroup { get; private set; } = "Default";
- [DataField("stripTime")] public float StripTime { get; private set; } = 4f;
+ [DataField("stripTime")] public TimeSpan StripTime { get; private set; } = TimeSpan.FromSeconds(4f);
[DataField("uiWindowPos", required: true)]
public Vector2i UIWindowPosition { get; private set; }
diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs
index fbf99992e3..8bf09c3f4c 100644
--- a/Content.Shared/Strip/Components/StrippableComponent.cs
+++ b/Content.Shared/Strip/Components/StrippableComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.DoAfter;
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -8,10 +9,10 @@ namespace Content.Shared.Strip.Components
public sealed partial class StrippableComponent : Component
{
///
- /// The strip delay for hands.
+ /// The strip delay for hands.
///
[ViewVariables(VVAccess.ReadWrite), DataField("handDelay")]
- public float HandStripDelay = 4f;
+ public TimeSpan HandStripDelay = TimeSpan.FromSeconds(4f);
}
[NetSerializable, Serializable]
@@ -21,63 +22,63 @@ public enum StrippingUiKey : byte
}
[NetSerializable, Serializable]
- public sealed class StrippingSlotButtonPressed : BoundUserInterfaceMessage
+ public sealed class StrippingSlotButtonPressed(string slot, bool isHand) : BoundUserInterfaceMessage
{
- public readonly string Slot;
-
- public readonly bool IsHand;
-
- public StrippingSlotButtonPressed(string slot, bool isHand)
- {
- Slot = slot;
- IsHand = isHand;
- }
+ public readonly string Slot = slot;
+ public readonly bool IsHand = isHand;
}
[NetSerializable, Serializable]
- public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage
- {
- public StrippingEnsnareButtonPressed()
- {
- }
- }
+ public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage;
- public abstract class BaseBeforeStripEvent : EntityEventArgs, IInventoryRelayEvent
+ [ByRefEvent]
+ public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent
{
- public readonly float InitialTime;
- public float Time => MathF.Max(InitialTime * Multiplier + Additive, 0f);
- public float Additive = 0;
- public float Multiplier = 1f;
- public bool Stealth;
+ public readonly TimeSpan InitialTime = initialTime;
+ public TimeSpan Multiplier = TimeSpan.FromSeconds(1f);
+ public TimeSpan Additive = TimeSpan.Zero;
+ public bool Stealth = stealth;
- public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES;
+ public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier.Seconds + Additive.Seconds, 0f));
- public BaseBeforeStripEvent(float initialTime, bool stealth = false)
- {
- InitialTime = initialTime;
- Stealth = stealth;
- }
+ public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES;
}
///
- /// Used to modify strip times. Raised directed at the user.
+ /// Used to modify strip times. Raised directed at the user.
///
///
- /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
+ /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
///
- public sealed class BeforeStripEvent : BaseBeforeStripEvent
- {
- public BeforeStripEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { }
- }
+ [ByRefEvent]
+ public sealed class BeforeStripEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth);
///
- /// Used to modify strip times. Raised directed at the target.
+ /// Used to modify strip times. Raised directed at the target.
///
///
- /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
+ /// This is also used by some stripping related interactions, i.e., interactions with items that are currently equipped by another player.
///
- public sealed class BeforeGettingStrippedEvent : BaseBeforeStripEvent
+ [ByRefEvent]
+ public sealed class BeforeGettingStrippedEvent(TimeSpan initialTime, bool stealth = false) : BaseBeforeStripEvent(initialTime, stealth);
+
+ ///
+ /// Organizes the behavior of DoAfters for .
+ ///
+ [Serializable, NetSerializable]
+ public sealed partial class StrippableDoAfterEvent : DoAfterEvent
{
- public BeforeGettingStrippedEvent(float initialTime, bool stealth = false) : base(initialTime, stealth) { }
+ public readonly bool InsertOrRemove;
+ public readonly bool InventoryOrHand;
+ public readonly string SlotOrHandName;
+
+ public StrippableDoAfterEvent(bool insertOrRemove, bool inventoryOrHand, string slotOrHandName)
+ {
+ InsertOrRemove = insertOrRemove;
+ InventoryOrHand = inventoryOrHand;
+ SlotOrHandName = slotOrHandName;
+ }
+
+ public override DoAfterEvent Clone() => this;
}
}
diff --git a/Content.Shared/Strip/Components/ThievingComponent.cs b/Content.Shared/Strip/Components/ThievingComponent.cs
index 83679f132c..a851dd5ef6 100644
--- a/Content.Shared/Strip/Components/ThievingComponent.cs
+++ b/Content.Shared/Strip/Components/ThievingComponent.cs
@@ -11,7 +11,7 @@ public sealed partial class ThievingComponent : Component
///
[ViewVariables(VVAccess.ReadWrite)]
[DataField("stripTimeReduction")]
- public float StripTimeReduction = 0.5f;
+ public TimeSpan StripTimeReduction = TimeSpan.FromSeconds(0.5f);
///
/// Should it notify the user if they're stripping a pocket?
diff --git a/Content.Shared/Strip/SharedStrippableSystem.cs b/Content.Shared/Strip/SharedStrippableSystem.cs
index a698ae5035..7afd4f245a 100644
--- a/Content.Shared/Strip/SharedStrippableSystem.cs
+++ b/Content.Shared/Strip/SharedStrippableSystem.cs
@@ -14,12 +14,12 @@ public override void Initialize()
SubscribeLocalEvent(OnDragDrop);
}
- public (float Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, float initialTime)
+ public (TimeSpan Time, bool Stealth) GetStripTimeModifiers(EntityUid user, EntityUid target, TimeSpan initialTime)
{
var userEv = new BeforeStripEvent(initialTime);
- RaiseLocalEvent(user, userEv);
+ RaiseLocalEvent(user, ref userEv);
var ev = new BeforeGettingStrippedEvent(userEv.Time, userEv.Stealth);
- RaiseLocalEvent(target, ev);
+ RaiseLocalEvent(target, ref ev);
return (ev.Time, ev.Stealth);
}
diff --git a/Content.Shared/Strip/ThievingSystem.cs b/Content.Shared/Strip/ThievingSystem.cs
index 0ef4b66571..2b3d3b38a0 100644
--- a/Content.Shared/Strip/ThievingSystem.cs
+++ b/Content.Shared/Strip/ThievingSystem.cs
@@ -1,4 +1,5 @@
using Content.Shared.Inventory;
+using Content.Shared.Strip;
using Content.Shared.Strip.Components;
namespace Content.Shared.Strip;
From 5f4df32bb532b1f7d86750b266e12135bea6729e Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 15:24:31 -0400
Subject: [PATCH 06/56] Fixes handcuff bug
---
Content.Shared/Cuffs/Components/HandcuffComponent.cs | 6 +++---
Content.Shared/Cuffs/SharedCuffableSystem.cs | 2 +-
Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/Content.Shared/Cuffs/Components/HandcuffComponent.cs b/Content.Shared/Cuffs/Components/HandcuffComponent.cs
index a5593cd271..72663fd8fa 100644
--- a/Content.Shared/Cuffs/Components/HandcuffComponent.cs
+++ b/Content.Shared/Cuffs/Components/HandcuffComponent.cs
@@ -86,11 +86,11 @@ public sealed partial class HandcuffComponent : Component
public SoundSpecifier EndUncuffSound = new SoundPathSpecifier("/Audio/Items/Handcuffs/cuff_takeoff_end.ogg");
///
- /// Both a bool and a multiplier combined. If it is 0, handcuffs are unaffected by mass contests. The absolute value of any nonzero acts as a multiplier on how much mass affects uncuff speed.
- /// A value of 1 provides the full modifier from MassContest. 0.5 is half the effect of mass contests, and so on.
+ /// Acts as a two-state option for handcuff speed. When true, handcuffs will be easier to get out of if you are larger than average. Representing the use of strength to break things like zipties.
+ /// When false, handcuffs are easier to get out of if you are smaller than average, representing the use of dexterity to slip the cuffs.
///
[DataField]
- public float UncuffMassModifier = 0f;
+ public bool UncuffMassMultiplies = false;
}
///
diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs
index 0fa77b182c..92f4576c8b 100644
--- a/Content.Shared/Cuffs/SharedCuffableSystem.cs
+++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs
@@ -561,7 +561,7 @@ public void TryUncuff(EntityUid target, EntityUid user, EntityUid? cuffsToRemove
return;
}
- var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (_contests.MassContest(user) * Math.Abs(cuff.UncuffMassModifier));
+ var uncuffTime = (isOwner ? cuff.BreakoutTime : cuff.UncuffTime) * (cuff.UncuffMassMultiplies ? 1 / _contests.MassContest(user) : _contests.MassContest(user));
if (isOwner)
{
diff --git a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml
index 32269d3888..711ddbc0c2 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/handcuffs.yml
@@ -52,7 +52,7 @@
path: /Audio/Items/Handcuffs/rope_breakout.ogg
startBreakoutSound:
path: /Audio/Items/Handcuffs/rope_takeoff.ogg
- uncuffMassModifier: 1
+ uncuffMassMultiplies: true
- type: Construction
graph: makeshifthandcuffs
node: cuffscable
@@ -94,7 +94,7 @@
path: /Audio/Items/Handcuffs/rope_breakout.ogg
startBreakoutSound:
path: /Audio/Items/Handcuffs/rope_takeoff.ogg
- uncuffMassModifier: 1
+ uncuffMassMultiplies: true
- type: Sprite
sprite: Objects/Misc/zipties.rsi
state: cuff
From ba2d53845e3f365a7423ac8f9ab6ee491e76d60d Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 18:37:29 -0400
Subject: [PATCH 07/56] Make Spent Casing Ejection Not Suck (#478)
# Description
Part of #467, #460, and #474
This is a small PR that corrects a math error in SharedGunSystem,
causing shell casings to be "Thrown" directly downwards instead of in an
actually cinematic and exciting arc. While I'm at it, I also corrected
the fixture of base shell casings to favor "Bounciness", and decreased
its mass to approximately 100 grams. Finally, I added a sound for when
casings bounce off of walls, which wasn't present before.
https://github.com/Simple-Station/Einstein-Engines/assets/16548818/56bb4ecc-d5eb-4b36-853b-42f05374150d
:cl:
- fix: Spent bullet casings now fly away from a shooter in a cinematic
manner, rather than fall at their feet.
Co-authored-by: Danger Revolution! <142105406+DangerRevolution@users.noreply.github.com>
---
Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs | 2 +-
.../Guns/Ammunition/Cartridges/base_cartridge.yml | 9 +++++++--
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
index cadb0a4b21..ff8b102bb5 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
@@ -443,7 +443,7 @@ protected void EjectCartridge(
{
Angle ejectAngle = angle.Value;
ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun
- ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f);
+ ThrowingSystem.TryThrow(entity, ejectAngle.ToVec(), 625f);
}
if (playSound && TryComp(entity, out var cartridge))
{
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml
index e188ee8c65..3bef413dff 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Cartridges/base_cartridge.yml
@@ -11,10 +11,10 @@
shape:
!type:PhysShapeAabb
bounds: "-0.10,-0.05,0.10,0.05"
- density: 20
+ density: 0.5
mask:
- ItemMask
- restitution: 0.3 # fite me
+ restitution: 0.7 # Small and bouncy
friction: 0.2
- type: Tag
tags:
@@ -23,6 +23,11 @@
size: Tiny
- type: SpaceGarbage
- type: EmitSoundOnLand
+ sound:
+ path: /Audio/Weapons/Guns/Casings/casing_fall_1.ogg
+ params:
+ volume: -1
+ - type: EmitSoundOnCollide
sound:
path: /Audio/Weapons/Guns/Casings/casing_fall_2.ogg
params:
From 2e712c00249cfe5bd254462a1efa1b6c8a1ec6bd Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Mon, 1 Jul 2024 22:37:52 +0000
Subject: [PATCH 08/56] Automatic Changelog Update (#478)
---
Resources/Changelog/Changelog.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 6fe7b0ff68..515c1832db 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4257,3 +4257,11 @@ Entries:
message: Most items now take time to equip and unequip, especially space suits.
id: 6132
time: '2024-06-30T18:22:28.0000000+00:00'
+- author: VMSolidus
+ changes:
+ - type: Fix
+ message: >-
+ Spent bullet casings now fly away from a shooter in a cinematic manner,
+ rather than fall at their feet.
+ id: 6133
+ time: '2024-07-01T22:37:29.0000000+00:00'
From 8bbcafa009cc04e44f25dac57c56d4492c87afa3 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 18:45:04 -0400
Subject: [PATCH 09/56] New glimmer functions
---
.../Abilities/PyrokinesisPowerSystem.cs | 15 ++--
.../Structures/GlimmerStructuresSystem.cs | 12 ++-
.../Psionics/Glimmer/GlimmerSystem.cs | 74 +++++++++++++++++--
Content.Shared/Psionics/PsionicComponent.cs | 14 +++-
4 files changed, 96 insertions(+), 19 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
index 90a28eb6ae..07ada4c0c5 100644
--- a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
+++ b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
@@ -11,6 +11,7 @@
using Robust.Shared.Timing;
using Content.Shared.Popups;
using Content.Shared.Psionics.Events;
+using Content.Shared.Psionics;
namespace Content.Server.Psionics.Abilities
{
@@ -38,7 +39,7 @@ public override void Initialize()
private void OnInit(EntityUid uid, PyrokinesisPowerComponent component, ComponentInit args)
{
_actions.AddAction(uid, ref component.PyrokinesisPrechargeActionEntity, component.PyrokinesisPrechargeActionId);
- _actions.TryGetActionData( component.PyrokinesisPrechargeActionEntity, out var actionData);
+ _actions.TryGetActionData(component.PyrokinesisPrechargeActionEntity, out var actionData);
if (actionData is { UseDelay: not null })
_actions.StartUseDelay(component.PyrokinesisPrechargeActionEntity);
if (TryComp(uid, out var psionic))
@@ -107,13 +108,17 @@ private void OnPowerUsed(PyrokinesisPowerActionEvent args)
var ent = Spawn("ProjectileAnomalyFireball", spawnCoords);
+ if (_glimmerSystem.GlimmerOutput >= 25 * psionic.Dampening)
+ EnsureComp(ent);
+
if (TryComp(ent, out var fireball))
{
- fireball.MaxIntensity = (int) MathF.Round(2 * psionic.Amplification);
- fireball.IntensitySlope = (int) MathF.Round(1 * psionic.Amplification);
- fireball.TotalIntensity = (int) MathF.Round(25 * psionic.Amplification);
+ var psionicFactor = psionic.Amplification * _glimmerSystem.GetGlimmerEquilibriumRatio();
+ fireball.MaxIntensity = 2 * psionicFactor;
+ fireball.IntensitySlope = 1 * psionicFactor;
+ fireball.TotalIntensity = 25 * psionicFactor;
- if (_glimmerSystem.GlimmerOutput >= 500)
+ if (_glimmerSystem.GlimmerOutput >= _glimmerSystem.GlimmerEquilibrium)
fireball.CanCreateVacuum = true;
else fireball.CanCreateVacuum = false;
diff --git a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
index 09e33dd414..b8649f71a5 100644
--- a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
+++ b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
@@ -76,16 +76,20 @@ public override void Update(float frameTime)
if (source.Accumulator > source.SecondsPerGlimmer)
{
+ var glimmerEquilibrium = _glimmerSystem.GlimmerEquilibrium;
source.Accumulator -= source.SecondsPerGlimmer;
+
+ // Shorthand explanation:
+ // This makes glimmer far more "Swingy", by making both positive and negative glimmer sources scale quite dramatically with glimmer
if (source.AddToGlimmer)
{
- //If we're above the equilibrium point of 500, increase the output of passive glimmer sources to help fight against linear decay
- //Even with this, don't expect probers to ever get to mind swap levels of power without any help
- _glimmerSystem.DeltaGlimmerInput(_glimmerSystem.GlimmerOutput > 500 ? MathF.Round(_glimmerSystem.GlimmerOutput / 100) : 1f);
+ _glimmerSystem.DeltaGlimmerInput((_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerOutputInteger() : 1f)
+ * (_glimmerSystem.GlimmerOutput < glimmerEquilibrium ? _glimmerSystem.GetGlimmerEquilibriumRatio() : 1f));
}
else
{
- _glimmerSystem.DeltaGlimmerInput(-1);
+ _glimmerSystem.DeltaGlimmerInput(-(_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerOutputInteger() : 1f)
+ * (_glimmerSystem.GlimmerOutput > glimmerEquilibrium ? _glimmerSystem.GetGlimmerEquilibriumRatio() : 1f));
}
}
}
diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
index d50b6fb2f6..f40a916f48 100644
--- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
+++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
@@ -2,7 +2,6 @@
using Robust.Shared.Configuration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
-using Content.Shared.Mobs;
namespace Content.Shared.Psionics.Glimmer
{
@@ -12,6 +11,7 @@ namespace Content.Shared.Psionics.Glimmer
public sealed class GlimmerSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
+
private float _glimmerInput = 0;
///
/// GlimmerInput represents the system-facing value of the station's glimmer, and is given by f(y) for this graph: https://www.desmos.com/calculator/posutiq38e
@@ -27,6 +27,19 @@ public float GlimmerInput
}
private float _glimmerOutput = 0;
+ ///
+ /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X.
+ ///
+ private const float _glimmerEquilibrium = 502.941f;
+
+ ///
+ /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X.
+ ///
+ public float GlimmerEquilibrium
+ {
+ get { return _glimmerEquilibrium; }
+ }
+
///
/// Glimmer Output represents the player-facing value of the station's glimmer, and is given by f(x) for this graph: https://www.desmos.com/calculator/posutiq38e
/// Where x = GlimmerInput and y = GlimmerOutput
@@ -55,7 +68,8 @@ private void Reset(RoundRestartCleanupEvent args)
}
///
- /// Return an abstracted range of a glimmer count.
+ /// Return an abstracted range of a glimmer count. This is a legacy system used to support the Prober,
+ /// and is the lowest form of abstracted glimmer. It's meant more for sprite states than math.
///
/// What glimmer count to check. Uses the current glimmer by default.
public GlimmerTier GetGlimmerTier(float? glimmer = null)
@@ -66,14 +80,30 @@ public GlimmerTier GetGlimmerTier(float? glimmer = null)
return glimmer switch
{
<= 49 => GlimmerTier.Minimal,
- >= 50 and <= 99 => GlimmerTier.Low,
- >= 100 and <= 299 => GlimmerTier.Moderate,
- >= 300 and <= 499 => GlimmerTier.High,
- >= 500 and <= 899 => GlimmerTier.Dangerous,
+ >= 50 and <= 399 => GlimmerTier.Low,
+ >= 400 and <= 599 => GlimmerTier.Moderate,
+ >= 600 and <= 699 => GlimmerTier.High,
+ >= 700 and <= 899 => GlimmerTier.Dangerous,
_ => GlimmerTier.Critical,
};
}
+ ///
+ /// Returns a 0 through 10 range of glimmer. Do not divide by this for any reason.
+ ///
+ ///
+ public int GetGlimmerOutputInteger()
+ {
+ if (!_enabled)
+ return 1;
+ else return (int) MathF.Round(GlimmerOutput / 1000);
+ }
+
+ ///
+ /// This is the public facing function for modifying Glimmer based on the log scale. Simply add or subtract to this with any nonzero number
+ /// Go through this if you want glimmer to be modified faster if its below 502.941f, and slower if above said equilibrium
+ ///
+ ///
public void DeltaGlimmerInput(float delta)
{
if (_enabled && delta != 0)
@@ -83,6 +113,11 @@ public void DeltaGlimmerInput(float delta)
}
}
+ ///
+ /// This is the public facing function for modifying Glimmer based on a linear scale. Simply add or subtract to this with any nonzero number.
+ /// This is primarily intended for load bearing systems such as Probers and Drainers, and should not be called by most things by design.
+ ///
+ ///
public void DeltaGlimmerOutput(float delta)
{
if (_enabled && delta != 0)
@@ -92,23 +127,46 @@ public void DeltaGlimmerOutput(float delta)
}
}
+ ///
+ /// This directly sets the Player-Facing side of Glimmer to a given value, and is not intended to be called by anything other than admin commands.
+ /// This is clamped between 0 and 999.999f
+ ///
+ ///
public void SetGlimmerOutput(float set)
{
if (_enabled && set != 0)
{
- GlimmerOutput = set;
+ GlimmerOutput = Math.Clamp(set, 0, 999.999f);
GlimmerInput = 2000 / (1 + MathF.Pow(MathF.E, -.0022f * GlimmerOutput)) - 1000;
}
}
+ ///
+ /// This directly sets the code-facing side of Glimmer to a given value, and is not intended to be called by anything other than admin commands.
+ /// This accepts any positive float input.
+ ///
+ ///
public void SetGlimmerInput(float set)
{
- if (_enabled && set != 0)
+ if (_enabled && set >= 0)
{
GlimmerInput = set;
GlimmerOutput = 2000 / (1 + MathF.Pow(MathF.E, -.0022f * GlimmerOutput)) - 1000;
}
}
+
+ ///
+ /// Outputs the ratio between actual glimmer and glimmer equilibrium(The intersection of the Glimmer Equation and the line y = x).
+ /// This will return 0.01f if glimmer is 0, and 1 if glimmer is disabled.
+ ///
+ public float GetGlimmerEquilibriumRatio()
+ {
+ if (!_enabled)
+ return 1;
+ else if (GlimmerOutput == 0)
+ return 0.01f;
+ else return GlimmerOutput / GlimmerEquilibrium;
+ }
}
[Serializable, NetSerializable]
diff --git a/Content.Shared/Psionics/PsionicComponent.cs b/Content.Shared/Psionics/PsionicComponent.cs
index c955626c53..748b8ddd92 100644
--- a/Content.Shared/Psionics/PsionicComponent.cs
+++ b/Content.Shared/Psionics/PsionicComponent.cs
@@ -19,10 +19,20 @@ public sealed partial class PsionicComponent : Component
[DataField("psychicFeedback")]
public List PsychicFeedback = new();
- [DataField("amplification")]
+ ///
+ /// An abstraction of how "Powerful" a psychic is. This is most commonly used as a multiplier on numerical outputs for psychic powers.
+ ///
+ ///
+ /// For an ordinary human, this will be between 0.5 and 1.2, but may be higher for some entities.
+ ///
+ [DataField]
public float Amplification = 0.1f;
- [DataField("dampening")]
+ ///
+ /// An abstraction of how much "Control" a psychic has over their powers. This is most commonly used to decrease glimmer output of powers,
+ /// or to make obvious powers less likely to be obvious.
+ ///
+ [DataField]
public float Dampening = 0.1f;
public bool Telepath = true;
public bool TelepathicMute = false;
From c8e0572c675b51d78681637c89de8b4c7d3c5a9f Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 19:22:48 -0400
Subject: [PATCH 10/56] simplify GlimmerEquilibrium constant
---
.../Psionics/Abilities/PyrokinesisPowerSystem.cs | 2 +-
.../Glimmer/Structures/GlimmerStructuresSystem.cs | 2 +-
Content.Shared/Psionics/Glimmer/GlimmerSystem.cs | 9 +--------
3 files changed, 3 insertions(+), 10 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
index 07ada4c0c5..f3e2cc69fd 100644
--- a/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
+++ b/Content.Server/Psionics/Abilities/PyrokinesisPowerSystem.cs
@@ -118,7 +118,7 @@ private void OnPowerUsed(PyrokinesisPowerActionEvent args)
fireball.IntensitySlope = 1 * psionicFactor;
fireball.TotalIntensity = 25 * psionicFactor;
- if (_glimmerSystem.GlimmerOutput >= _glimmerSystem.GlimmerEquilibrium)
+ if (_glimmerSystem.GlimmerOutput >= GlimmerSystem.GlimmerEquilibrium)
fireball.CanCreateVacuum = true;
else fireball.CanCreateVacuum = false;
diff --git a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
index b8649f71a5..309bd732ef 100644
--- a/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
+++ b/Content.Server/Psionics/Glimmer/Structures/GlimmerStructuresSystem.cs
@@ -76,7 +76,7 @@ public override void Update(float frameTime)
if (source.Accumulator > source.SecondsPerGlimmer)
{
- var glimmerEquilibrium = _glimmerSystem.GlimmerEquilibrium;
+ var glimmerEquilibrium = GlimmerSystem.GlimmerEquilibrium;
source.Accumulator -= source.SecondsPerGlimmer;
// Shorthand explanation:
diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
index f40a916f48..b376f8aab7 100644
--- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
+++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
@@ -30,15 +30,8 @@ public float GlimmerInput
///
/// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X.
///
- private const float _glimmerEquilibrium = 502.941f;
+ public const float GlimmerEquilibrium = 502.941f;
- ///
- /// This constant is equal to the intersection of the Glimmer Equation(https://www.desmos.com/calculator/posutiq38e) and the line Y = X.
- ///
- public float GlimmerEquilibrium
- {
- get { return _glimmerEquilibrium; }
- }
///
/// Glimmer Output represents the player-facing value of the station's glimmer, and is given by f(x) for this graph: https://www.desmos.com/calculator/posutiq38e
From 35eb6f6a0266b26ba605ab330c3c5ee0cf757aaf Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 19:40:53 -0400
Subject: [PATCH 11/56] Some more small optimizations
---
Content.Server/Psionics/Abilities/DispelPowerSystem.cs | 6 +++++-
Content.Shared/Psionics/Glimmer/GlimmerSystem.cs | 8 ++++++++
Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs | 3 ++-
3 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/DispelPowerSystem.cs b/Content.Server/Psionics/Abilities/DispelPowerSystem.cs
index 33c6b5dcae..73c6f5d339 100644
--- a/Content.Server/Psionics/Abilities/DispelPowerSystem.cs
+++ b/Content.Server/Psionics/Abilities/DispelPowerSystem.cs
@@ -84,7 +84,11 @@ private void OnPowerUsed(DispelPowerActionEvent args)
{
args.Handled = true;
_psionics.LogPowerUsed(args.Performer, "dispel", psionic, 1, 1, true);
- _glimmerSystem.DeltaGlimmerInput(-_random.NextFloat(2 * psionic.Dampening - psionic.Amplification, 4 * psionic.Dampening - psionic.Amplification));
+
+ // Redundant check here as well for a small performance optimization
+ // Dispel has its own niche equations for glimmer.
+ if (_glimmerSystem.GetGlimmerEnabled())
+ _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat(2 * psionic.Dampening - psionic.Amplification, 4 * psionic.Dampening - psionic.Amplification));
}
}
diff --git a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
index b376f8aab7..9b8d20b295 100644
--- a/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
+++ b/Content.Shared/Psionics/Glimmer/GlimmerSystem.cs
@@ -160,6 +160,14 @@ public float GetGlimmerEquilibriumRatio()
return 0.01f;
else return GlimmerOutput / GlimmerEquilibrium;
}
+
+ ///
+ /// Returns the GlimmerEnabled CVar, useful for niche early exits in systems that otherwise don't have any calls to CVars.
+ ///
+ public bool GetGlimmerEnabled()
+ {
+ return _enabled;
+ }
}
[Serializable, NetSerializable]
diff --git a/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs b/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs
index f905ec4dd4..4dd8fa2442 100644
--- a/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs
+++ b/Content.Shared/Psionics/SharedPsionicAbilitiesSystem.cs
@@ -88,7 +88,8 @@ public void LogPowerUsed(EntityUid uid, string power, PsionicComponent? psionic
var ev = new PsionicPowerUsedEvent(uid, power);
RaiseLocalEvent(uid, ev, false);
- if (!overrideGlimmer)
+ //Redundant check for the GlimmerEnabled CVar because I want to skip this math too if its turned off.
+ if (_glimmerSystem.GetGlimmerEnabled() && !overrideGlimmer)
{
if (psionic == null)
_glimmerSystem.DeltaGlimmerInput(_robustRandom.NextFloat(minGlimmer, maxGlimmer));
From 631a4bfa46fd58b5045380d8de06370a7af416ff Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 20:09:16 -0400
Subject: [PATCH 12/56] Removing the single power requirement
We now have a glimmer system that can accommodate and account for lots of psychics with multiple powers.
---
Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs | 3 ---
1 file changed, 3 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
index 9de4e412c8..ed75d097c0 100644
--- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
@@ -37,9 +37,6 @@ public void AddPsionics(EntityUid uid)
if (Deleted(uid))
return;
- if (HasComp(uid))
- return;
-
AddRandomPsionicPower(uid);
}
public void AddRandomPsionicPower(EntityUid uid)
From 2f2f658a09e62f5e63196097f92121dffa8ffdb6 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 20:10:44 -0400
Subject: [PATCH 13/56] Ye
---
.../Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs | 2 +-
Content.Server/Psionics/PsionicsSystem.cs | 3 ---
2 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs
index c26ab1481a..021c959102 100644
--- a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericStormRule.cs
@@ -30,7 +30,7 @@ protected override void Started(EntityUid uid, NoosphericStormRuleComponent comp
continue;
// Skip over those who are already psionic or those who are insulated, or zombies.
- if (HasComp(potentialPsionic) || HasComp(potentialPsionic) || HasComp(potentialPsionic))
+ if (HasComp(potentialPsionic) || HasComp(potentialPsionic))
continue;
validList.Add(potentialPsionic);
diff --git a/Content.Server/Psionics/PsionicsSystem.cs b/Content.Server/Psionics/PsionicsSystem.cs
index 06250f77ba..6799b0bd3f 100644
--- a/Content.Server/Psionics/PsionicsSystem.cs
+++ b/Content.Server/Psionics/PsionicsSystem.cs
@@ -127,9 +127,6 @@ private void OnStamHit(EntityUid uid, AntiPsionicWeaponComponent component, Stam
public void RollPsionics(EntityUid uid, PotentialPsionicComponent component, bool applyGlimmer = true, float multiplier = 1f)
{
- if (HasComp(uid))
- return;
-
if (!_cfg.GetCVar(CCVars.PsionicRollsEnabled))
return;
From 7c7ab3f240a96e5633273168481d1158de67d6ef Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 21:11:01 -0400
Subject: [PATCH 14/56] Update PsionicAbilitiesSystem.cs
---
.../Abilities/PsionicAbilitiesSystem.cs | 31 +++++++++++++------
1 file changed, 21 insertions(+), 10 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
index ed75d097c0..27e93678f2 100644
--- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
@@ -7,6 +7,7 @@
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Content.Shared.Popups;
+using Content.Server.MassMedia.Components;
namespace Content.Server.Psionics.Abilities
{
@@ -37,25 +38,35 @@ public void AddPsionics(EntityUid uid)
if (Deleted(uid))
return;
- AddRandomPsionicPower(uid);
+ var rollCount = 0;
+ while (rollCount < 3)
+ {
+ if (AddRandomPsionicPower(uid))
+ break;
+
+ rollCount++;
+ }
+
}
- public void AddRandomPsionicPower(EntityUid uid)
+ public bool AddRandomPsionicPower(EntityUid uid)
{
EnsureComp(uid, out var psionic);
if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool))
{
Logger.Error("Can't index the random psionic power pool!");
- return;
+ return false;
}
- // uh oh, stinky!
- var newComponent = (Component) _componentFactory.GetComponent(pool.Pick());
- newComponent.Owner = uid;
-
- EntityManager.AddComponent(uid, newComponent);
+ var newComponent = _componentFactory.GetComponent(pool.Pick());
- _glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5));
+ if (!EntityManager.HasComponent(uid, newComponent.GetType()))
+ {
+ EntityManager.AddComponent(uid, newComponent);
+ _glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5));
+ return true;
+ }
+ return false;
}
public void RemovePsionics(EntityUid uid)
@@ -93,7 +104,7 @@ public void RemovePsionics(EntityUid uid)
_statusEffectsSystem.TryAddStatusEffect(uid, "Stutter", TimeSpan.FromMinutes(5), false, "StutteringAccent");
- _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat((int) MathF.Round(psionic.Amplification * psionic.Dampening * 5), (int) MathF.Round(psionic.Amplification * psionic.Dampening * 10)));
+ _glimmerSystem.DeltaGlimmerOutput(-_random.NextFloat(psionic.Amplification * psionic.Dampening * 5, psionic.Amplification * psionic.Dampening * 10));
RemComp(uid);
RemComp(uid);
}
From 10ea788420d744d3a5ea39a0d34ed10bd89808fc Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 21:32:50 -0400
Subject: [PATCH 15/56] 1984 a while loop and make this actually work without
needing one.
---
.../Abilities/PsionicAbilitiesSystem.cs | 28 +++++++++----------
1 file changed, 14 insertions(+), 14 deletions(-)
diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
index 27e93678f2..b839622014 100644
--- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
@@ -38,35 +38,35 @@ public void AddPsionics(EntityUid uid)
if (Deleted(uid))
return;
- var rollCount = 0;
- while (rollCount < 3)
- {
- if (AddRandomPsionicPower(uid))
- break;
-
- rollCount++;
- }
+ AddRandomPsionicPower(uid);
}
- public bool AddRandomPsionicPower(EntityUid uid)
+ public void AddRandomPsionicPower(EntityUid uid)
{
EnsureComp(uid, out var psionic);
if (!_prototypeManager.TryIndex("RandomPsionicPowerPool", out var pool))
{
Logger.Error("Can't index the random psionic power pool!");
- return false;
+ return;
}
- var newComponent = _componentFactory.GetComponent(pool.Pick());
+ var newPool = pool;
+ foreach (var component in pool.Weights.Keys)
+ {
+ var checkedComponent = _componentFactory.GetComponent(component);
+ if (EntityManager.HasComponent(uid, checkedComponent.GetType()))
+ newPool.Weights.Remove(component);
+ }
- if (!EntityManager.HasComponent(uid, newComponent.GetType()))
+ if (newPool.Weights.Keys != null)
{
+ var newComponent = _componentFactory.GetComponent(newPool.Pick());
+
EntityManager.AddComponent(uid, newComponent);
_glimmerSystem.DeltaGlimmerInput(_random.NextFloat(psionic.Amplification * psionic.Dampening, psionic.Amplification * psionic.Dampening * 5));
- return true;
}
- return false;
+ return;
}
public void RemovePsionics(EntityUid uid)
From 8e9afa0c7ade0e79e3e959cf911051687c5b8d8c Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 21:35:05 -0400
Subject: [PATCH 16/56] Update PsionicAbilitiesSystem.cs
---
Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
index b839622014..24e6bbb8d3 100644
--- a/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
+++ b/Content.Server/Psionics/Abilities/PsionicAbilitiesSystem.cs
@@ -7,7 +7,6 @@
using Robust.Shared.Random;
using Robust.Shared.Prototypes;
using Content.Shared.Popups;
-using Content.Server.MassMedia.Components;
namespace Content.Server.Psionics.Abilities
{
From 44e29dd8ce0b32b70dad42780583fbdc3e5b49b5 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 22:43:20 -0400
Subject: [PATCH 17/56] Zap now unlocks the next power roll.
---
.../StationEvents/Events/NoosphericZapRule.cs | 18 +++++++-----------
1 file changed, 7 insertions(+), 11 deletions(-)
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs
index 3672d317d9..a11faa0693 100644
--- a/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/NoosphericZapRule.cs
@@ -35,19 +35,15 @@ protected override void Started(EntityUid uid, NoosphericZapRuleComponent compon
_stunSystem.TryParalyze(psion, TimeSpan.FromSeconds(5), false);
_statusEffectsSystem.TryAddStatusEffect(psion, "Stutter", TimeSpan.FromSeconds(10), false, "StutteringAccent");
- if (HasComp(psion))
- _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution);
+ if (potentialPsionicComponent.Rerolled)
+ {
+ potentialPsionicComponent.Rerolled = false;
+ _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize-potential-regained"), psion, psion, Shared.Popups.PopupType.LargeCaution);
+ }
else
{
- if (potentialPsionicComponent.Rerolled)
- {
- potentialPsionicComponent.Rerolled = false;
- _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize-potential-regained"), psion, psion, Shared.Popups.PopupType.LargeCaution);
- } else
- {
- _psionicsSystem.RollPsionics(psion, potentialPsionicComponent, multiplier: 0.25f);
- _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution);
- }
+ _psionicsSystem.RollPsionics(psion, potentialPsionicComponent, multiplier: 0.25f);
+ _popupSystem.PopupEntity(Loc.GetString("noospheric-zap-seize"), psion, psion, Shared.Popups.PopupType.LargeCaution);
}
}
}
From 864ba53a5cf17963608a4325a23b6a27afc6f5ea Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 22:45:33 -0400
Subject: [PATCH 18/56] Revenant moved to new crit threshold
---
Resources/Prototypes/Nyanotrasen/GameRules/events.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
index f5626a0406..decdcfdda2 100644
--- a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
+++ b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
@@ -145,7 +145,7 @@
noSpawn: true
components:
- type: GlimmerEvent
- minimumGlimmer: 500
+ minimumGlimmer: 700
maximumGlimmer: 900
report: glimmer-event-report-signatures
- type: GlimmerRevenantRule
From c5946ee4b845580821bbb1f016e791eca9e8be53 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Mon, 1 Jul 2024 23:02:19 -0400
Subject: [PATCH 19/56] Update AtmosphereSystem.HighPressureDelta.cs
---
.../Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs
index ac9b5c2b0a..461435f062 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.HighPressureDelta.cs
@@ -217,11 +217,11 @@ public void ExperiencePressureDifference(
if (throwTarget != EntityCoordinates.Invalid)
{
var pos = throwTarget.ToMap(EntityManager, _transformSystem).Position - xform.WorldPosition + dirVec;
- _throwing.TryThrow(uid, pos.Normalized() * moveForce, moveForce);
+ _throwing.TryThrow(uid, pos.Normalized() * MathF.Min(moveForce, SpaceWindMaxVelocity), moveForce);
}
else
{
- _throwing.TryThrow(uid, dirVec.Normalized() * moveForce, moveForce);
+ _throwing.TryThrow(uid, dirVec.Normalized() * MathF.Min(moveForce, SpaceWindMaxVelocity), moveForce);
}
component.LastHighPressureMovementAirCycle = cycle;
From d461f4fe176ab24b8278d17b500424c840a6348d Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 01:35:29 -0400
Subject: [PATCH 20/56] Don't look at this please.
---
Content.Client/Entry/EntryPoint.cs | 1 +
.../TypingIndicator/TypingIndicatorSystem.cs | 2 +-
.../Components/NPCConversationComponent.cs | 152 +++++
.../NPC/Events/NPCConversationEvents.cs | 63 ++
.../NPCConversationTreePrototype.cs | 154 +++++
.../NPC/Systems/NPCConversationSystem.cs | 558 ++++++++++++++++++
.../SophicScribe/SophicScribeSystem.cs | 36 ++
.../Locale/en-US/npc/conversation/sophia.ftl | 82 +++
.../Structures/Research/sophicscribe.yml | 194 +++++-
9 files changed, 1240 insertions(+), 2 deletions(-)
create mode 100644 Content.Server/NPC/Components/NPCConversationComponent.cs
create mode 100644 Content.Server/NPC/Events/NPCConversationEvents.cs
create mode 100644 Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
create mode 100644 Content.Server/NPC/Systems/NPCConversationSystem.cs
create mode 100644 Resources/Locale/en-US/npc/conversation/sophia.ftl
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index a1fc68bbd2..8636e0eb6a 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -125,6 +125,7 @@ public override void Init()
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("stationGoal");
+ _prototypeManager.RegisterIgnore("npcConversationTree");
_componentFactory.GenerateNetIds();
_adminManager.Initialize();
diff --git a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
index c923738930..443923f675 100644
--- a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
+++ b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
@@ -54,7 +54,7 @@ private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs
SetTypingIndicatorEnabled(uid.Value, ev.IsTyping);
}
- private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
+ public void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref appearance, false))
return;
diff --git a/Content.Server/NPC/Components/NPCConversationComponent.cs b/Content.Server/NPC/Components/NPCConversationComponent.cs
new file mode 100644
index 0000000000..c2a8ca31d7
--- /dev/null
+++ b/Content.Server/NPC/Components/NPCConversationComponent.cs
@@ -0,0 +1,152 @@
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
+using Content.Server.NPC.Events;
+using Content.Server.NPC.Prototypes;
+using Content.Server.NPC.Systems;
+
+namespace Content.Server.NPC.Components;
+
+[RegisterComponent]
+[Access(typeof(NPCConversationSystem))]
+public sealed partial class NPCConversationComponent : Component
+{
+ ///
+ /// Whether or not the listening logic is turned on.
+ ///
+ ///
+ /// Queued responses will still play through, but no new attempts to listen will be made.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("enabled")]
+ public bool Enabled = true;
+
+ /* NYI:
+ ///
+ /// The NPC will pay attention when one of these words are said.
+ ///
+ [ViewVariables]
+ [DataField("aliases")]
+ public List Aliases = new();
+ */
+
+ [ViewVariables]
+ [DataField("tree", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string? ConversationTreeId;
+
+ ///
+ /// This is the cached prototype.
+ ///
+ [ViewVariables]
+ public NPCConversationTreePrototype ConversationTree = default!;
+
+ ///
+ /// Topics that are unlocked in the NPC's conversation tree.
+ ///
+ [ViewVariables]
+ public HashSet UnlockedTopics = new();
+
+ ///
+ /// How long until we stop paying attention to someone for a prompt.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("attentionSpan")]
+ public TimeSpan AttentionSpan = TimeSpan.FromSeconds(20);
+
+ ///
+ /// This is the minimum delay before the NPC makes a response.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("delayBeforeResponse")]
+ public TimeSpan DelayBeforeResponse = TimeSpan.FromSeconds(0.3);
+
+ ///
+ /// This is the approximate delay per letter typed in text.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("typingDelay")]
+ public TimeSpan TypingDelay = TimeSpan.FromSeconds(0.05);
+
+ [ViewVariables]
+ public Stack ResponseQueue = new();
+
+ ///
+ /// This is when the NPC will respond with its top response.
+ ///
+ [ViewVariables]
+ [DataField("nextResponse", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextResponse;
+
+ ///
+ /// This is the direction the NPC was facing before looking towards a conversation partner.
+ ///
+ [ViewVariables]
+ public Angle OriginalFacing;
+
+ ///
+ /// This is who the NPC is paying attention to for conversation.
+ ///
+ [ViewVariables]
+ public EntityUid? AttendingTo;
+
+ ///
+ /// This is when the NPC will stop paying attention to a specific person.
+ ///
+ [ViewVariables]
+ [DataField("nextAttentionLoss", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextAttentionLoss;
+
+ ///
+ /// This event is fired the next time the NPC hears something from the
+ /// person they're speaking with and it takes control of the response.
+ ///
+ [ViewVariables]
+ public NPCConversationListenEvent? ListeningEvent;
+
+#region Idle Chatter
+
+ ///
+ /// Whether or not the NPC will say things unprompted.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("idleEnabled")]
+ public bool IdleEnabled = true;
+
+ ///
+ /// This is the approximate delay between idle chats.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("idleChatDelay")]
+ public TimeSpan IdleChatDelay = TimeSpan.FromMinutes(3);
+
+ ///
+ /// This is the order in which idle chat lines are given.
+ ///
+ ///
+ /// This is randomized both on init and when the lines have been exhausted
+ /// to prevent repeating lines twice in a row and to avoid predictable patterns.
+ ///
+ /// It technically reduces randomness, with the benefit of less repetition.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public List IdleChatOrder = new();
+
+ ///
+ /// This is the next idle chat line that will be used.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ public int IdleChatIndex = 0;
+
+ ///
+ /// This is when the NPC will say something out of its list of idle lines.
+ ///
+ ///
+ /// This is reset every time the NPC speaks.
+ ///
+ [ViewVariables]
+ [DataField("nextIdleChat", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan NextIdleChat;
+
+#endregion
+
+}
+
diff --git a/Content.Server/NPC/Events/NPCConversationEvents.cs b/Content.Server/NPC/Events/NPCConversationEvents.cs
new file mode 100644
index 0000000000..eb04f59bdd
--- /dev/null
+++ b/Content.Server/NPC/Events/NPCConversationEvents.cs
@@ -0,0 +1,63 @@
+using Robust.Shared.Audio;
+using Content.Server.NPC.Systems;
+
+namespace Content.Server.NPC.Events;
+
+///
+/// This is used for dynamic responses and post-response events.
+///
+[ImplicitDataDefinitionForInheritors]
+[Access(typeof(NPCConversationSystem))]
+public abstract partial class NPCConversationEvent : EntityEventArgs
+{
+ ///
+ /// This is the entity that the NPC is speaking to.
+ ///
+ public EntityUid? TalkingTo;
+}
+
+///
+/// This event type is raised when an NPC hears a response when it was set to listen for one.
+///
+///
+/// Set Handled to true when you want the NPC to stop listening.
+/// The NPC will otherwise keep listening and block any attempt to find a prompt in the speaker's words.
+///
+[ImplicitDataDefinitionForInheritors]
+[Access(typeof(NPCConversationSystem))]
+public abstract partial class NPCConversationListenEvent : HandledEntityEventArgs
+{
+ ///
+ /// This is the entity that said the message.
+ ///
+ public EntityUid? Speaker;
+
+ ///
+ /// This is the original message that the NPC heard.
+ ///
+ public string Message = default!;
+
+ ///
+ /// This is the message, parsed into separate words.
+ ///
+ public List Words = default!;
+}
+
+public sealed partial class NPCConversationHelpEvent : NPCConversationEvent
+{
+ [DataField("text")]
+ public string? Text;
+
+ [DataField("audio")]
+ public SoundSpecifier? Audio;
+}
+
+///
+/// This event can be raised after a response to cause an NPC to stop paying attention to someone.
+///
+public sealed partial class NPCConversationByeEvent : NPCConversationEvent { }
+
+// The following classes help demonstrate some of the features of the system.
+// They may be separated out at some point.
+public sealed partial class NPCConversationToldNameEvent : NPCConversationListenEvent { }
+
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
new file mode 100644
index 0000000000..20a616d830
--- /dev/null
+++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
@@ -0,0 +1,154 @@
+using Robust.Shared.Audio;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Content.Server.NPC.Events;
+
+namespace Content.Server.NPC.Prototypes;
+
+[Prototype("npcConversationTree")]
+public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks
+{
+ [ViewVariables]
+ [IdDataField]
+ public string ID { get; } = default!;
+
+ ///
+ /// Dialogue contains all the topics to which an NPC can discuss.
+ ///
+ [ViewVariables]
+ [DataField("dialogue", required: true)]
+ public readonly NPCTopic[] Dialogue = default!;
+
+ ///
+ /// Attention responses are what the NPC says when they start paying
+ /// attention to you without a specific question or prompt to respond to.
+ ///
+ [ViewVariables]
+ [DataField("attention", required: true)]
+ public readonly NPCResponse[] Attention = default!;
+
+ ///
+ /// Idle responses are just things the NPC will say when nothing else is
+ /// going on, after some time.
+ ///
+ [ViewVariables]
+ [DataField("idle", required: true)]
+ public readonly NPCResponse[] Idle = default!;
+
+ ///
+ /// Unknown responses are what the NPC says when they can't respond to a
+ /// particular question or prompt.
+ ///
+ [ViewVariables]
+ [DataField("unknown", required: true)]
+ public readonly NPCResponse[] Unknown = default!;
+
+ ///
+ /// Custom responses are available to use in extensions to the NPC
+ /// Conversation system.
+ ///
+ // NOTE: This may be removed in favor of storing NPCResponses on custom
+ // components, i.e. an NPCShopkeeperComponent, but for now, it lives here
+ // to help demonstrate some features.
+ [ViewVariables]
+ [DataField("custom")]
+ public readonly Dictionary Custom = default!;
+
+ ///
+ /// This exists as a quick way to map a prompt to a topic.
+ ///
+ public readonly Dictionary PromptToTopic = new();
+
+ // ISerializationHooks _is_ obsolete, but ConstructionGraphPrototype is using it as of this commit,
+ // and I'm not quite sure how to otherwise do this.
+ //
+ // I will look at that prototype when ISerializationHooks is phased out.
+ void ISerializationHooks.AfterDeserialization()
+ {
+ // Cache the strings mapping to prompts.
+ foreach (var topic in Dialogue)
+ {
+ foreach (var prompt in topic.Prompts)
+ {
+ PromptToTopic[prompt] = topic;
+ }
+ }
+ }
+}
+
+[DataDefinition]
+public sealed partial class NPCTopic
+{
+ [DataField]
+ public string[] Prompts = default!;
+
+ ///
+ /// This determines the likelihood of this topic being selected over any
+ /// other, given the existence of multiple candidates.
+ ///
+ [DataField]
+ public float Weight = 1.0f;
+
+ ///
+ /// Locked topics will not be accessible through dialogue until unlocked.
+ ///
+ [DataField]
+ public bool Locked;
+
+ ///
+ /// Hidden topics won't show up in any form of "help" question.
+ ///
+ [DataField]
+ public bool Hidden;
+
+ [DataField("responses", required: true)]
+ public NPCResponse[] Responses = default!;
+}
+
+[DataDefinition]
+public sealed partial class NPCResponse
+{
+ public NPCResponse() { }
+
+ public NPCResponse(string? text, SoundSpecifier? audio = null, NPCConversationEvent? ev = null)
+ {
+ Text = text;
+ Audio = audio;
+ Event = ev;
+ }
+
+ public override string ToString()
+ {
+ return $"NPCResponse({Text})";
+ }
+
+ [DataField]
+ public string? Text;
+
+ [DataField]
+ public SoundSpecifier? Audio;
+
+ /* [DataField("emote")] */
+ /* public string? Emote; */
+
+ ///
+ /// This event is raised when the response is queued,
+ /// for the purpose of dynamic responses.
+ ///
+ [DataField]
+ public NPCConversationEvent? Is;
+
+ ///
+ /// This event is raised after the response is made.
+ ///
+ [DataField]
+ public NPCConversationEvent? Event;
+
+ ///
+ /// This event is raised when the NPC next hears a response,
+ /// allowing the response to be processed by other systems.
+ ///
+ [DataField]
+ public NPCConversationListenEvent? ListenEvent;
+}
+
diff --git a/Content.Server/NPC/Systems/NPCConversationSystem.cs b/Content.Server/NPC/Systems/NPCConversationSystem.cs
new file mode 100644
index 0000000000..015adb19de
--- /dev/null
+++ b/Content.Server/NPC/Systems/NPCConversationSystem.cs
@@ -0,0 +1,558 @@
+using System.Collections.Immutable;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Robust.Server.GameObjects;
+using Robust.Shared.Audio;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Robust.Shared.Timing;
+using Content.Server.Chat.Systems;
+using Content.Server.Chat.TypingIndicator;
+using Content.Server.NPC.HTN;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.Events;
+using Content.Server.NPC.Prototypes;
+using Content.Server.Speech;
+using Content.Shared.Interaction;
+using Content.Server.Radio.Components;
+
+namespace Content.Server.NPC.Systems;
+
+public sealed class NPCConversationSystem : EntitySystem
+{
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly NPCSystem _npcSystem = default!;
+ [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
+ [Dependency] private readonly TransformSystem _transformSystem = default!;
+ [Dependency] private readonly TypingIndicatorSystem _typingIndicatorSystem = default!;
+
+ private ISawmill _sawmill = default!;
+
+ // TODO: attention attenuation. distance, facing, visible
+ // TODO: attending to multiple people, multiple streams of conversation
+ // TODO: multi-word prompts
+ // TODO: nameless prompting (pointing is good)
+ // TODO: aliases
+
+ public static readonly string[] QuestionWords = { "who", "what", "when", "why", "where", "how" };
+ public static readonly string[] Copulae = { "is", "are" };
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("npc.conversation");
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(OnUnpaused);
+ SubscribeLocalEvent(OnListenAttempt);
+ SubscribeLocalEvent(OnListen);
+
+ SubscribeLocalEvent(OnBye);
+ SubscribeLocalEvent(OnHelp);
+
+ SubscribeLocalEvent(OnToldName);
+ }
+
+#region API
+
+ ///
+ /// Toggle the ability of an NPC to listen for topics.
+ ///
+ public void EnableConversation(EntityUid uid, bool enable = true, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.Enabled = enable;
+ }
+
+ ///
+ /// Toggle the NPC's willingness to make idle comments.
+ ///
+ public void EnableIdleChat(EntityUid uid, bool enable = true, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.IdleEnabled = enable;
+ }
+
+ ///
+ /// Return locked status of a dialogue topic.
+ ///
+ public bool IsDialogueLocked(EntityUid uid, string option, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return true;
+
+ if (!component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic))
+ {
+ _sawmill.Warning($"Tried to check locked status of missing dialogue option `{option}` on {ToPrettyString(uid)}");
+ return true;
+ }
+
+ if (component.UnlockedTopics.Contains(topic))
+ return false;
+
+ return topic.Locked;
+ }
+
+ ///
+ /// Unlock dialogue options normally locked in an NPC's conversation tree.
+ ///
+ public void UnlockDialogue(EntityUid uid, string option, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic))
+ component.UnlockedTopics.Add(topic);
+ else
+ _sawmill.Warning($"Tried to unlock missing dialogue option `{option}` on {ToPrettyString(uid)}");
+ }
+
+ ///
+ public void UnlockDialogue(EntityUid uid, HashSet options, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ foreach (var option in options)
+ UnlockDialogue(uid, option, component);
+ }
+
+ ///
+ /// Queue a response for an NPC with a visible typing indicator and delay between messages.
+ ///
+ ///
+ /// This can be used as opposed to the typical method.
+ ///
+ public void QueueResponse(EntityUid uid, NPCResponse response, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ if (response.Is is {} ev)
+ {
+ // This is a dynamic response which will call QueueResponse with static responses of its own.
+ ev.TalkingTo = component.AttendingTo;
+ RaiseLocalEvent(uid, (object) ev);
+ return;
+ }
+
+ if (component.ResponseQueue.Count == 0)
+ {
+ DelayResponse(uid, component, response);
+ _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, true);
+ }
+
+ component.ResponseQueue.Push(response);
+ }
+
+ ///
+ /// Make an NPC stop paying attention to someone.
+ ///
+ public void LoseAttention(EntityUid uid, NPCConversationComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ component.AttendingTo = null;
+ component.ListeningEvent = null;
+ _rotateToFaceSystem.TryFaceAngle(uid, component.OriginalFacing);
+ }
+
+#endregion
+
+ private void DelayResponse(EntityUid uid, NPCConversationComponent component, NPCResponse response)
+ {
+ if (response.Text == null)
+ return;
+
+ component.NextResponse = _gameTiming.CurTime +
+ component.DelayBeforeResponse +
+ component.TypingDelay.TotalSeconds * TimeSpan.FromSeconds(response.Text.Length) *
+ _random.NextDouble(0.9, 1.1);
+ }
+
+ private IEnumerable GetAvailableTopics(EntityUid uid, NPCConversationComponent component)
+ {
+ HashSet availableTopics = new();
+
+ foreach (var topic in component.ConversationTree.Dialogue)
+ {
+ if (!topic.Locked || component.UnlockedTopics.Contains(topic))
+ availableTopics.Add(topic);
+ }
+
+ return availableTopics;
+ }
+
+ private IEnumerable GetVisibleTopics(EntityUid uid, NPCConversationComponent component)
+ {
+ HashSet visibleTopics = new();
+
+ foreach (var topic in component.ConversationTree.Dialogue)
+ {
+ if (!topic.Hidden && (!topic.Locked || component.UnlockedTopics.Contains(topic)))
+ visibleTopics.Add(topic);
+ }
+
+ return visibleTopics;
+ }
+
+ private void OnInit(EntityUid uid, NPCConversationComponent component, ComponentInit args)
+ {
+ if (component.ConversationTreeId == null)
+ return;
+
+ component.ConversationTree = _prototype.Index(component.ConversationTreeId);
+ component.NextIdleChat = _gameTiming.CurTime + component.IdleChatDelay;
+
+ for (var i = 0; i < component.ConversationTree.Idle.Length; ++i)
+ component.IdleChatOrder.Add(i);
+
+ _random.Shuffle(component.IdleChatOrder);
+ }
+
+ private void OnUnpaused(EntityUid uid, NPCConversationComponent component, ref EntityUnpausedEvent args)
+ {
+ component.NextResponse += args.PausedTime;
+ component.NextAttentionLoss += args.PausedTime;
+ component.NextIdleChat += args.PausedTime;
+ }
+
+ private bool TryGetIdleChatLine(EntityUid uid, NPCConversationComponent component, [NotNullWhen(true)] out NPCResponse? line)
+ {
+ line = null;
+
+ if (component.IdleChatOrder.Count() == 0)
+ return false;
+
+ if (++component.IdleChatIndex == component.IdleChatOrder.Count())
+ {
+ // Exhausted all lines in the pre-shuffled order.
+ // Reset the index and shuffle again.
+ component.IdleChatIndex = 0;
+ _random.Shuffle(component.IdleChatOrder);
+ }
+
+ var index = component.IdleChatOrder[component.IdleChatIndex];
+
+ line = component.ConversationTree.Idle[index];
+
+ return true;
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var component))
+ {
+ var curTime = _gameTiming.CurTime;
+
+ if (curTime >= component.NextResponse && component.ResponseQueue.Count > 0)
+ {
+ // Make a response.
+ Respond(uid, component, component.ResponseQueue.Pop());
+ }
+
+ if (curTime >= component.NextAttentionLoss && component.AttendingTo != null)
+ {
+ // Forget who we were talking to.
+ LoseAttention(uid, component);
+ }
+
+ if (component.IdleEnabled &&
+ curTime >= component.NextIdleChat &&
+ TryGetIdleChatLine(uid, component, out var line))
+ {
+ Respond(uid, component, line);
+ }
+ }
+ }
+
+ private void OnListenAttempt(EntityUid uid, NPCConversationComponent component, ListenAttemptEvent args)
+ {
+ if (!component.Enabled ||
+ // Don't listen to myself...
+ uid == args.Source ||
+ // Don't bother listening to other NPCs. For now.
+ HasComp(args.Source) ||
+ // We're already "typing" a response, so do that first.
+ component.ResponseQueue.Count > 0)
+ {
+ args.Cancel();
+ }
+ }
+
+ private void PayAttentionTo(EntityUid uid, NPCConversationComponent component, EntityUid speaker)
+ {
+ component.AttendingTo = speaker;
+ component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan;
+ component.OriginalFacing = _transformSystem.GetWorldRotation(uid);
+ }
+
+ private void Respond(EntityUid uid, NPCConversationComponent component, NPCResponse response)
+ {
+ if (component.ResponseQueue.Count == 0)
+ _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, false);
+ else
+ DelayResponse(uid, component, component.ResponseQueue.Peek());
+
+ if (component.AttendingTo != null)
+ {
+ // TODO: This line is a mouthful. Maybe write a public API that supports EntityCoordinates later?
+ var speakerCoords = Transform(component.AttendingTo.Value).Coordinates.ToMap(EntityManager, _transformSystem).Position;
+ _rotateToFaceSystem.TryFaceCoordinates(uid, speakerCoords);
+ }
+
+ if (response.Event is {} ev)
+ {
+ ev.TalkingTo = component.AttendingTo;
+ RaiseLocalEvent(uid, (object) ev);
+ }
+
+ if (response.ListenEvent != null)
+ component.ListeningEvent = response.ListenEvent;
+
+ if (response.Text != null)
+ _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(response.Text), InGameICChatType.Speak, false);
+
+ if (response.Audio != null)
+ _audioSystem.PlayPvs(response.Audio, uid,
+ // TODO: Allow this to be configured per NPC/response.
+ AudioParams.Default
+ .WithVolume(8f)
+ .WithMaxDistance(9f)
+ .WithRolloffFactor(0.5f));
+
+ // Refresh our attention.
+ component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan;
+ component.NextIdleChat = component.NextAttentionLoss + component.IdleChatDelay;
+ }
+
+ private List ParseMessageIntoWords(string message)
+ {
+ return Regex.Replace(message.Trim().ToLower(), @"(\p{P})", "")
+ .Split()
+ .ToList();
+ }
+
+ private bool FindResponse(EntityUid uid, NPCConversationComponent component, List words, [NotNullWhen(true)] out NPCResponse? response)
+ {
+ response = null;
+
+ var availableTopics = GetAvailableTopics(uid, component);
+
+ // Some topics are more interesting than others.
+ var greatestWeight = 0f;
+ NPCTopic? candidate = null;
+
+ foreach (var word in words)
+ {
+ if (component.ConversationTree.PromptToTopic.TryGetValue(word, out var topic) &&
+ availableTopics.Contains(topic) &&
+ topic.Weight > greatestWeight)
+ {
+ greatestWeight = topic.Weight;
+ candidate = topic;
+ }
+ }
+
+ if (candidate != null)
+ {
+ response = _random.Pick(candidate.Responses);
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool JudgeQuestionLikelihood(EntityUid uid, NPCConversationComponent component, List words, string message)
+ {
+ if (message.Length > 0 && message[^1] == '?')
+ // A question mark is an absolute mark of a question.
+ return true;
+
+ if (words.Count == 1)
+ // The usefulness of this is dubious, but it's definitely a question.
+ return QuestionWords.Contains(words[0]);
+
+ if (words.Count >= 2)
+ return QuestionWords.Contains(words[0]) && Copulae.Contains(words[1]);
+
+ return false;
+ }
+
+ private void OnBye(EntityUid uid, NPCConversationComponent component, NPCConversationByeEvent args)
+ {
+ LoseAttention(uid, component);
+ }
+
+ private void OnHelp(EntityUid uid, NPCConversationComponent component, NPCConversationHelpEvent args)
+ {
+ if (args.Text == null)
+ {
+ _sawmill.Error($"{ToPrettyString(uid)} heard a Help prompt but has no text for it.");
+ return;
+ }
+
+ var availableTopics = GetVisibleTopics(uid, component);
+ var availablePrompts = availableTopics.Select(topic => topic.Prompts.FirstOrDefault()).ToArray();
+
+ string availablePromptsText;
+ if (availablePrompts.Count() <= 2)
+ {
+ availablePromptsText = Loc.GetString(args.Text,
+ ("availablePrompts", string.Join(" or ", availablePrompts))
+ );
+ }
+ else
+ {
+ availablePrompts[^1] = $"or {availablePrompts[^1]}";
+ availablePromptsText = Loc.GetString(args.Text,
+ ("availablePrompts", string.Join(", ", availablePrompts))
+ );
+ }
+
+ // Unlikely we'll be able to do audio that isn't hard-coded,
+ // so best to keep it general.
+ var response = new NPCResponse(availablePromptsText, args.Audio);
+ QueueResponse(uid, response, component);
+ }
+
+ private void OnToldName(EntityUid uid, NPCConversationComponent component, NPCConversationListenEvent args)
+ {
+ if (!component.ConversationTree.Custom.TryGetValue("toldName", out var responses))
+ return;
+
+ var response = _random.Pick(responses);
+ if (response.Text == null)
+ {
+ _sawmill.Error($"{ToPrettyString(uid)} was told a name but had no text response.");
+ return;
+ }
+
+ // The world's simplest heuristic for names:
+ if (args.Words.Count > 3)
+ {
+ // It didn't seem like a name, so wait for something that does.
+ return;
+ }
+
+ var cleanedName = string.Join(" ", args.Words);
+ cleanedName = char.ToUpper(cleanedName[0]) + cleanedName.Remove(0, 1);
+
+ var formattedResponse = new NPCResponse(Loc.GetString(response.Text,
+ ("name", cleanedName)),
+ response.Audio);
+
+ QueueResponse(uid, formattedResponse, component);
+ args.Handled = true;
+ }
+
+ private void OnListen(EntityUid uid, NPCConversationComponent component, ListenEvent args)
+ {
+ if (HasComp(args.Source))
+ return;
+
+ if (component.AttendingTo != null && component.AttendingTo != args.Source)
+ // Ignore someone speaking to us if we're already paying attention to someone else.
+ return;
+
+ var words = ParseMessageIntoWords(args.Message);
+ if (words.Count == 0)
+ return;
+
+ if (component.AttendingTo == args.Source)
+ {
+ // The person we're talking to said something to us.
+
+ if (component.ListeningEvent is {} ev)
+ {
+ // We were waiting on this person to say something, and they've said something.
+ ev.Handled = false;
+ ev.Speaker = component.AttendingTo;
+ ev.Message = args.Message;
+ ev.Words = words;
+ RaiseLocalEvent(uid, (object) ev);
+
+ if (ev.Handled)
+ component.ListeningEvent = null;
+
+ return;
+ }
+
+ // We're already paying attention to this person,
+ // so try to figure out if they said something we can talk about.
+ if (FindResponse(uid, component, words, out var response))
+ {
+ // A response was found so go ahead with it.
+ QueueResponse(uid, response, component);
+ }
+ else if(JudgeQuestionLikelihood(uid, component, words, args.Message))
+ {
+ // The message didn't match any of the prompts, but it seemed like a question.
+ var unknownResponse = _random.Pick(component.ConversationTree.Unknown);
+ QueueResponse(uid, unknownResponse, component);
+ }
+
+ // If the message didn't seem like a question,
+ // and it didn't raise any of our topics,
+ // then politely ignore who we're talking with.
+ //
+ // It's better than spamming them with "I don't understand."
+ return;
+ }
+
+ // See if someone said our name.
+ var myName = MetaData(uid).EntityName.ToLower();
+
+ // So this is a rough heuristic, but if our name occurs within the first three words,
+ // or is the very last one, someone might be trying to talk to us.
+ var payAttention = words[0] == myName || words[^1] == myName;
+ if (!payAttention)
+ {
+ for (int i = 1; i < Math.Min(2, words.Count); ++i)
+ {
+ if (words[i] == myName)
+ {
+ payAttention = true;
+ break;
+ }
+ }
+ }
+
+ if (payAttention)
+ {
+ PayAttentionTo(uid, component, args.Source);
+
+ if (!FindResponse(uid, component, words, out var response))
+ {
+ if(JudgeQuestionLikelihood(uid, component, words, args.Message) &&
+ // This subcondition exists to block our name being interpreted as a question in its own right.
+ words.Count > 1)
+ {
+ response = _random.Pick(component.ConversationTree.Unknown);
+ }
+ else
+ {
+ response = _random.Pick(component.ConversationTree.Attention);
+ }
+ }
+
+ QueueResponse(uid, response, component);
+ }
+ }
+}
+
diff --git a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
index b1a6c1e9de..ba5ff0a056 100644
--- a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
+++ b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
@@ -1,5 +1,8 @@
using Content.Server.Psionics.Abilities;
using Content.Server.Chat.Systems;
+using Content.Server.NPC.Events;
+using Content.Server.NPC.Systems;
+using Content.Server.NPC.Prototypes;
using Content.Server.Radio.Components;
using Content.Server.Radio.EntitySystems;
using Content.Server.StationEvents.Events;
@@ -18,6 +21,8 @@ public sealed partial class SophicScribeSystem : EntitySystem
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
+ [Dependency] private readonly NPCConversationSystem _conversationSystem = default!;
+ protected ISawmill Sawmill = default!;
public override void Update(float frameTime)
{
@@ -51,6 +56,32 @@ public override void Initialize()
SubscribeLocalEvent(OnInteractHand);
SubscribeLocalEvent(OnGlimmerEventEnded);
+ SubscribeLocalEvent(OnGetGlimmer);
+ }
+
+ private void OnGetGlimmer(EntityUid uid, SophicScribeComponent component, NPCConversationGetGlimmerEvent args)
+ {
+ if (args.Text == null)
+ {
+ Sawmill.Error($"{uid} heard a glimmer reading prompt but has no text for it");
+ return;
+ }
+
+ var tier = _glimmerSystem.GetGlimmerTier() switch
+ {
+ GlimmerTier.Minimal => Loc.GetString("glimmer-reading-minimal"),
+ GlimmerTier.Low => Loc.GetString("glimmer-reading-low"),
+ GlimmerTier.Moderate => Loc.GetString("glimmer-reading-moderate"),
+ GlimmerTier.High => Loc.GetString("glimmer-reading-high"),
+ GlimmerTier.Dangerous => Loc.GetString("glimmer-reading-dangerous"),
+ _ => Loc.GetString("glimmer-reading-critical"),
+ };
+
+ var glimmerReadingText = Loc.GetString(args.Text,
+ ("glimmer", (int) Math.Round(_glimmerSystem.GlimmerOutput)), ("tier", tier));
+
+ var response = new NPCResponse(glimmerReadingText);
+ _conversationSystem.QueueResponse(uid, response);
}
private void OnInteractHand(EntityUid uid, SophicScribeComponent component, InteractHandEvent args)
@@ -83,4 +114,9 @@ private void OnGlimmerEventEnded(GlimmerEventEndedEvent args)
_radioSystem.SendRadioMessage(speaker, message, channel, speaker);
}
}
+ public sealed partial class NPCConversationGetGlimmerEvent : NPCConversationEvent
+ {
+ [DataField]
+ public string? Text;
+ }
}
diff --git a/Resources/Locale/en-US/npc/conversation/sophia.ftl b/Resources/Locale/en-US/npc/conversation/sophia.ftl
new file mode 100644
index 0000000000..c832d9fc17
--- /dev/null
+++ b/Resources/Locale/en-US/npc/conversation/sophia.ftl
@@ -0,0 +1,82 @@
+sophia-response-name = You may call me Sophia.
+sophia-response-help = You may inquire about one of the following topics: {$availablePrompts}.
+
+sophia-response-hello-1 = Greetings.
+sophia-response-hello-2 = Salutations.
+
+sophia-response-bye-1 = Fare thee well.
+sophia-response-bye-2 = Gods be with you.
+sophia-response-bye-3 = Come back wiser.
+
+sophia-idle-phrase-1 = Mmmm, another portent.
+sophia-idle-phrase-2 = The noösphere is quite beautiful today. However, I don't think I could describe it in a way you could understand.
+sophia-idle-phrase-3 = I've been here before. You have, too.
+
+sophia-response-attention-1 = What is it?
+sophia-response-attention-2 = What do you seek?
+sophia-response-attention-3 = Out with it.
+
+sophia-response-sorry-1 = That's not a question for me.
+sophia-response-sorry-2 = Ask someone else.
+sophia-response-sorry-3 = Maybe I know the answer, maybe I do not. Either way, I will not be answering that question.
+
+sophia-response-nature = My nature doesn't really matter, does it? I'm fulfilling my purpose. Can you say the same, or are you just wasting time?
+
+sophia-response-epi = 'Epistemics' is a word. Aspiring Hellenes they are, they wished to displace the Latin 'science.' However, in English, epistemics has undesired connotations as a study of knowledge itself, even though the Greek word is a literal replacement for 'science.'
+
+sophia-response-mantis = 'Mantis' means seer, soothsayer, or prophet. They must be so named because they seek to uncover the truth. And, fittingly with their psionic aptitude, 'mantis' and 'mind' both descend, to the best of our knowledge, from an absolutely ancient word that sounded something like 'mentis.'
+
+sophia-response-mystagogue = 'Mystagogue' literally means 'leader of the mystics.' You may know the suffix -gogue from 'demogogue.'
+
+sophia-response-oracle = Oracle? I don't know much about her, and she isn't keen to share her secrets with me.
+
+sophia-response-psionics = Psionics are extraordinary abilities originating from one's mind. There doesn't seem to be any dominant word to refer to someone with the ability to practice these, although I prefer 'psion' or 'psychic.'
+
+sophia-response-noosphere = The noösphere is a field connecting all of consciousness. It's the medium through which psionics works. Its strength and effects on the illusory world of the material are based on its pressure. Colloquially, noöspheric pressure is called 'glimmer.'
+
+sophia-response-god = 'God' is such a vague term. There are so many entities out there that have defeated mortality. How you choose to regard them is your business.
+
+sophia-response-morphotype = In the first century PCC, several entities reshaped men into their image. I had done the same, if you would believe it. I can offer no evidence of their existence, other than faint memories. Any specific morphotype you want to know about?
+
+sophia-response-calendar = It's currently 417 PCC. The casuality crisis neccesitated a new year to count from. Due to the nature of the crisis, it can only be said with certainty that 1 PCC is between 2400 and 2700 CE.
+
+sophia-response-crisis = The first FTL travel was incompatible with the old ways. Fortunately, its resolution made more apparent the inherent futility in trying to give one history, one narrative, one account. Truth cannot be found in the material world, only higher ones.
+
+sophia-response-metempsychosis = You've died thousands of times, and you'll die thousands more. Some of those lives you may dedicate to trying to stop the cycle. We all carry at least some memory of past lives, usually temporally recent ones. One of the great mysteries of the persistence of fragments is the high concentration of memories from the early 21st century CE, which, inverse to other periods, seem to be more common among the ignorant.
+
+sophia-response-truth = If you seek the truth, you're in the wrong place. From a perspective tainted by material reality, the best you can hope is to try and divine higher truths that are not dependent on it.
+
+sophia-response-job = I observe the glimmer here, and record it.
+
+sophia-response-human = Humans were the base for all the others. But they, too, were shaped. Long, long before the others.
+
+sophia-response-felinid = Felinids were the first, and the most willing. In true feline nature, they shaped themselves.
+
+sophia-response-oni = Oni, it is said, originated in Sirius. The brightest star in the night sky from Earth may have attracted some chromatically inclined entities, explaining their vivid coloring. But, that's just speculation.
+
+sophia-response-arachne = Arachne are the strangest of them. They're not fully mortal. They took the form of humans, but not their genes. Their creator wrote his name in their stead.
+
+sophia-response-moth = Moths scarecely look human, but, strangely, their genes confirm they are. Their creator shares his name with a genus of moths, and was responsible for the other outlier.
+
+sophia-response-lamiae = So, you remember? You must be remembering their mythological namesake. If you've really retained that fleeting memory over so many metempsychoses... Perhaps I've said too much.
+
+sophia-response-cyno = Were those... no... So faint. Ignorance! You cannot remember them! It's impossible!
+
+sophia-response-harpy = Harpies, it is said, were once men and women, sculpted by greed for a purpose long gone. They were abandoned by their creators on a world named Valerian 4b.
+
+sophia-response-valerian = The Harpy homeworld? Magestic mountains gleaming in white, forests of brilliant scarlet, oceans wine dark, yet no light to be seen by mortal eyes. The Harpies were made to thrive there. To them, their world was bathed in beautiful silver light.
+
+sophia-response-grue = You do not know of those. You cannot. I had so hoped to live a few cycles under normal causality.
+
+sophia-response-abraxas = That's a name of power, and I avoid speaking of him. He's the least content to rest, and the most infatuated with creating things from ignorance.
+
+sophia-response-zork = You wander into the slavering fangs of a hungry grue. There, did you enjoy this game?
+
+sophia-response-glimmer = The current glimmer reading is {$glimmer}. {$tier}
+
+glimmer-reading-minimal = That is extremely low. Nothing bad will happen, but I hope this is not at the cost of progression in your understanding of the universe.
+glimmer-reading-low = That is quite low. Just barely enough to register any psionic activity here.
+glimmer-reading-moderate = That is about the expected level on a psionically active station. You may notice manageable, minor effects.
+glimmer-reading-high = That is sure to start attracting attention, although still quite manageable.
+glimmer-reading-dangerous = That's a bit concerning. You may want to redirect efforts to reducing it.
+glimmer-reading-critical = That's apocalyptic, in the original sense of the word. That is, to say, revealing. This is the sort of time and place to acquire secret knowledge.
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
index 8e34a07ea5..5213608d95 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
@@ -1,7 +1,7 @@
- type: entity
parent: BaseStructure
id: SophicScribe
- name: sophie
+ name: Sophie
description: Latest reports on the Noösphere!
components:
- type: Sprite
@@ -27,6 +27,10 @@
channels:
- Common
- Science
+ - type: ActiveListener
+ - type: TypingIndicator
+ - type: NPCConversation
+ tree: SophiaTree
- type: PotentialPsionic #this makes her easier to access for glimmer events, dw about it
- type: Psionic
psychicFeedback:
@@ -39,3 +43,191 @@
- type: GuideHelp
guides:
- Psionics
+
+- type: npcConversationTree
+ id: SophiaTree
+ dialogue:
+ - prompts: [glimmer, reading]
+ responses:
+ - is: !type:NPCConversationGetGlimmerEvent
+ text: sophia-response-glimmer
+
+ - prompts: [purpose, job, occupation, profession]
+ weight: 0.5
+ responses:
+ - text: sophia-response-job
+
+ - prompts: [help, topics]
+ weight: 0.5
+ hidden: true
+ responses:
+ - is: !type:NPCConversationHelpEvent
+ text: sophia-response-help
+
+ - prompts: [nature, statue, snake, being]
+ weight: 0.3
+ responses:
+ - text: sophia-response-nature
+
+ - prompts: [epistemics, epi]
+ weight: 0.2
+ responses:
+ - text: sophia-response-epi
+
+ - prompts: [mantis]
+ weight: 0.2
+ responses:
+ - text: sophia-response-mantis
+
+ - prompts: [mystagogue, mysta]
+ weight: 0.2
+ responses:
+ - text: sophia-response-mystagogue
+
+ - prompts: [psionics, psychic]
+ weight: 0.2
+ responses:
+ - text: sophia-response-psionics
+
+ - prompts: [noösphere, noosphere]
+ weight: 0.2
+ responses:
+ - text: sophia-response-noosphere
+
+ - prompts: [metempsychosis, metempsychoses, reincarnation, death, dying, afterlife]
+ weight: 0.2
+ responses:
+ - text: sophia-response-metempsychosis
+
+ - prompts: [calendar]
+ weight: 0.2
+ responses:
+ - text: sophia-response-calendar
+
+ - prompts: [morphotypes, morphotype, species]
+ weight: 0.2
+ responses:
+ - text: sophia-response-morphotype
+
+ - prompts: [gods, god]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-god
+
+ - prompts: [truth, "true", "false", falsity, falsehood]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-truth
+
+ - prompts: [human, humans, humanoid, unmutated]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-human
+
+ - prompts: [felinid, felinids, felid, felids, catperson, catpeople]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-felinid
+
+ - prompts: [oni, onis]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-oni
+
+ - prompts: [arachne, arachnid, arachnids, spiderperson, spiderpeople]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-arachne
+
+ - prompts: [moth, moths, moff, moths]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-moth
+
+ - prompts: [lamiae, lamia, lamias]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-lamiae
+
+ - prompts: [grue, grues, batperson, batpeople]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-grue
+
+ - prompts: [cynocephalus, cynocephali, cyno, cynos]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-cyno
+
+ - prompts: [harpy, harpies]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-harpy
+
+ - prompts: [valerian, Valerian, 4b]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-valerian
+
+ - prompts: [crisis, causality]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-crisis
+
+ - prompts: [oracle]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-oracle
+
+ - prompts: [abraxas]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-abraxas
+
+ - prompts: [hi, hello, hey, greetings, salutations]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-hello-1
+ - text: sophia-response-hello-2
+
+ - prompts: [bye, goodbye, done, farewell, later, seeya]
+ weight: 0.1
+ hidden: true
+ responses:
+ - text: sophia-response-bye-1
+ event: !type:NPCConversationByeEvent
+ - text: sophia-response-bye-2
+ event: !type:NPCConversationByeEvent
+ - text: sophia-response-bye-3
+ event: !type:NPCConversationByeEvent
+
+ attention:
+ - text: sophia-response-attention-1
+ - text: sophia-response-attention-2
+ - text: sophia-response-attention-3
+
+ idle:
+ - text: sophia-idle-phrase-1
+ - text: sophia-idle-phrase-2
+ - text: sophia-idle-phrase-3
+
+ unknown:
+ - text: sophia-response-sorry-1
+ - text: sophia-response-sorry-2
+ - text: sophia-response-sorry-3
From 4752115623ee410cd30942a1c67beab3c4f073ae Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 01:54:53 -0400
Subject: [PATCH 21/56] First attempt at the error
---
Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
index 20a616d830..a147cf1f74 100644
--- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
+++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
@@ -68,8 +68,12 @@ void ISerializationHooks.AfterDeserialization()
// Cache the strings mapping to prompts.
foreach (var topic in Dialogue)
{
+ if (topic == null)
+ continue;
foreach (var prompt in topic.Prompts)
{
+ if (prompt == null)
+ continue;
PromptToTopic[prompt] = topic;
}
}
From 10803e1cbce75b27750646842ecffeb7071598b3 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 03:01:49 -0400
Subject: [PATCH 22/56] Trying this
---
Content.Server/Entry/EntryPoint.cs | 1 +
Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 28687d17cd..2e8b298d08 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -70,6 +70,7 @@ public override void Init()
prototypes.RegisterIgnore("parallax");
prototypes.RegisterIgnore("guideEntry");
+ prototypes.RegisterIgnore("npcConversationTree");
ServerContentIoC.Register();
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
index a147cf1f74..d1280723ec 100644
--- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
+++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
@@ -6,11 +6,11 @@
namespace Content.Server.NPC.Prototypes;
[Prototype("npcConversationTree")]
-public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks
+public sealed partial class NPCConversationTreePrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[IdDataField]
- public string ID { get; } = default!;
+ public string ID { get; private set; } = default!;
///
/// Dialogue contains all the topics to which an NPC can discuss.
From 7b89ce1326c74e9d813ac56b497951c867b3af3e Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 03:15:06 -0400
Subject: [PATCH 23/56] Cherrypick "Fix StrippableSystem Blunders" (#504)
---
Content.Server/Strip/StrippableSystem.cs | 16 +++++++++++-----
.../Strip/Components/StrippableComponent.cs | 4 ++--
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/Content.Server/Strip/StrippableSystem.cs b/Content.Server/Strip/StrippableSystem.cs
index 950411a8e2..3b38b65a19 100644
--- a/Content.Server/Strip/StrippableSystem.cs
+++ b/Content.Server/Strip/StrippableSystem.cs
@@ -122,13 +122,12 @@ public override void StartOpeningStripper(EntityUid user, Entity strippable, ref StrippingSlotButtonPressed args)
{
if (args.Session.AttachedEntity is not { Valid: true } user ||
- !TryComp(user, out var userHands) ||
- !TryComp(strippable.Owner, out var targetHands))
+ !TryComp(user, out var userHands))
return;
if (args.IsHand)
{
- StripHand((user, userHands), (strippable.Owner, targetHands), args.Slot, strippable);
+ StripHand((user, userHands), (strippable.Owner, null), args.Slot, strippable);
return;
}
@@ -478,6 +477,9 @@ private void StripInsertHand(
!Resolve(target, ref target.Comp))
return;
+ if (!CanStripInsertHand(user, target, held, handName))
+ return;
+
_handsSystem.TryDrop(user, checkActionBlocker: false, handsComp: user.Comp);
_handsSystem.TryPickup(target, held, handName, checkActionBlocker: false, animateUser: stealth, animate: stealth, handsComp: target.Comp);
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has placed the item {ToPrettyString(held):item} in {ToPrettyString(target):target}'s hands");
@@ -542,7 +544,7 @@ private void StartStripRemoveHand(
var (time, stealth) = GetStripTimeModifiers(user, target, targetStrippable.HandStripDelay);
if (!stealth)
- _popupSystem.PopupEntity( Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
+ _popupSystem.PopupEntity(Loc.GetString("strippable-component-alert-owner", ("user", Identity.Entity(user, EntityManager)), ("item", item)), target, target);
var prefix = stealth ? "stealthily " : "";
_adminLogger.Add(LogType.Stripping, LogImpact.Low, $"{ToPrettyString(user):actor} is trying to {prefix}strip the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
@@ -569,12 +571,16 @@ private void StripRemoveHand(
Entity user,
Entity target,
EntityUid item,
+ string handName,
bool stealth)
{
if (!Resolve(user, ref user.Comp) ||
!Resolve(target, ref target.Comp))
return;
+ if (!CanStripRemoveHand(user, target, item, handName))
+ return;
+
_handsSystem.TryDrop(target, item, checkActionBlocker: false, handsComp: target.Comp);
_handsSystem.PickupOrDrop(user, item, animateUser: stealth, animate: stealth, handsComp: user.Comp);
_adminLogger.Add(LogType.Stripping, LogImpact.Medium, $"{ToPrettyString(user):actor} has stripped the item {ToPrettyString(item):item} from {ToPrettyString(target):target}'s hands");
@@ -625,7 +631,7 @@ private void OnStrippableDoAfterFinished(Entity entity, ref Stri
{
if (ev.InsertOrRemove)
StripInsertHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
- else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.Args.Hidden);
+ else StripRemoveHand((entity.Owner, entity.Comp), ev.Target.Value, ev.Used.Value, ev.SlotOrHandName, ev.Args.Hidden);
}
}
}
diff --git a/Content.Shared/Strip/Components/StrippableComponent.cs b/Content.Shared/Strip/Components/StrippableComponent.cs
index 8bf09c3f4c..4faca4d8f2 100644
--- a/Content.Shared/Strip/Components/StrippableComponent.cs
+++ b/Content.Shared/Strip/Components/StrippableComponent.cs
@@ -35,11 +35,11 @@ public sealed class StrippingEnsnareButtonPressed : BoundUserInterfaceMessage;
public abstract class BaseBeforeStripEvent(TimeSpan initialTime, bool stealth = false) : EntityEventArgs, IInventoryRelayEvent
{
public readonly TimeSpan InitialTime = initialTime;
- public TimeSpan Multiplier = TimeSpan.FromSeconds(1f);
+ public float Multiplier = 1f;
public TimeSpan Additive = TimeSpan.Zero;
public bool Stealth = stealth;
- public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier.Seconds + Additive.Seconds, 0f));
+ public TimeSpan Time => TimeSpan.FromSeconds(MathF.Max(InitialTime.Seconds * Multiplier + Additive.Seconds, 0f));
public SlotFlags TargetSlots { get; } = SlotFlags.GLOVES;
}
From 606a28d11e706681ec23f0db32eb01ddc0534a2d Mon Sep 17 00:00:00 2001
From: WarMechanic <69510347+WarMechanic@users.noreply.github.com>
Date: Tue, 2 Jul 2024 17:18:27 +1000
Subject: [PATCH 24/56] Fix Loadouts Breaking when You Spend All Your Points
(#506)
# Description
Self explanatory
P.S. i genuinely dont know what the fuck i did, who wrote this?
# TODO
- [X] Fix shit
# Media
https://youtu.be/hbJbd5SgZ54
# Changelog
:cl:
- fix: Fixed loadouts becoming uneditable after spending all your points
---
Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
index c797f02a75..04810b0771 100644
--- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
@@ -2086,7 +2086,7 @@ void AddSelector(LoadoutPreferenceSelector selector, int points, string id)
selector.PreferenceChanged += preference =>
{
// Make sure they have enough loadout points
- preference = preference ? CheckPoints(points, preference) : CheckPoints(-points, preference);
+ preference = preference ? CheckPoints(-points, preference) : CheckPoints(points, preference);
// Update Preferences
Profile = Profile?.WithLoadoutPreference(id, preference);
From 0d0dd4c01bfd859004b3b399d482187e6ba58ea8 Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Tue, 2 Jul 2024 07:18:51 +0000
Subject: [PATCH 25/56] Automatic Changelog Update (#506)
---
Resources/Changelog/Changelog.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index 515c1832db..e4e812edeb 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4265,3 +4265,9 @@ Entries:
rather than fall at their feet.
id: 6133
time: '2024-07-01T22:37:29.0000000+00:00'
+- author: WarMechanic
+ changes:
+ - type: Fix
+ message: Fixed loadouts becoming uneditable after spending all your points
+ id: 6134
+ time: '2024-07-02T07:18:27.0000000+00:00'
From eaacdeaa57ee06826f18060553ef78abe575828d Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 03:37:28 -0400
Subject: [PATCH 26/56] Revert "Trying this"
This reverts commit 10803e1cbce75b27750646842ecffeb7071598b3.
---
Content.Server/Entry/EntryPoint.cs | 1 -
Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ++--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index 2e8b298d08..28687d17cd 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -70,7 +70,6 @@ public override void Init()
prototypes.RegisterIgnore("parallax");
prototypes.RegisterIgnore("guideEntry");
- prototypes.RegisterIgnore("npcConversationTree");
ServerContentIoC.Register();
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
index d1280723ec..a147cf1f74 100644
--- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
+++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
@@ -6,11 +6,11 @@
namespace Content.Server.NPC.Prototypes;
[Prototype("npcConversationTree")]
-public sealed partial class NPCConversationTreePrototype : IPrototype, ISerializationHooks
+public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks
{
[ViewVariables]
[IdDataField]
- public string ID { get; private set; } = default!;
+ public string ID { get; } = default!;
///
/// Dialogue contains all the topics to which an NPC can discuss.
From 5fc1ce81388dc91f3a1ae1887e91d9835651bf14 Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Tue, 2 Jul 2024 03:37:31 -0400
Subject: [PATCH 27/56] Revert "First attempt at the error"
This reverts commit 4752115623ee410cd30942a1c67beab3c4f073ae.
---
Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs | 4 ----
1 file changed, 4 deletions(-)
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
index a147cf1f74..20a616d830 100644
--- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
+++ b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
@@ -68,12 +68,8 @@ void ISerializationHooks.AfterDeserialization()
// Cache the strings mapping to prompts.
foreach (var topic in Dialogue)
{
- if (topic == null)
- continue;
foreach (var prompt in topic.Prompts)
{
- if (prompt == null)
- continue;
PromptToTopic[prompt] = topic;
}
}
From ffb9ec979ff9ffe76d81eeffa84cea0be45ac2fc Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Tue, 2 Jul 2024 11:00:14 +0300
Subject: [PATCH 28/56] Unflip Hands for Felinid, Vulpkanin, Harpy (#503)
# Description
Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1194
This is a minor issue in the yml files of custom species that will
become a problem if we ever merge wizden's better hand indicators.
This PR shouldn't require a preview; all credit goes to the original
author of the fix.
---
# Changelog
Too minor for a cl. Or as some say, no cl no fun.
Co-authored-by: Angelo Fallaria
---
Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml | 5 ++---
Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml | 6 +++---
.../Nyanotrasen/Entities/Body/Prototypes/felinid.yml | 4 ++--
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml b/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml
index 5b3615c55d..25988f4a3a 100644
--- a/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml
+++ b/Resources/Prototypes/DeltaV/Body/Prototypes/harpy.yml
@@ -13,10 +13,10 @@
torso:
part: TorsoHarpy
connections:
- - left arm
- right arm
- - left leg
+ - left arm
- right leg
+ - left leg
organs:
heart: OrganHumanHeart
lungs: OrganHarpyLungs
@@ -47,4 +47,3 @@
part: RightFootHarpy
left foot:
part: LeftFootHarpy
-
diff --git a/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml b/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml
index 3d1552ac81..cdf787e473 100644
--- a/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml
+++ b/Resources/Prototypes/DeltaV/Body/Prototypes/vulpkanin.yml
@@ -1,4 +1,4 @@
-- type: body
+- type: body
name: "vulpkanin"
id: Vulpkanin
root: torso
@@ -19,10 +19,10 @@
liver: OrganAnimalLiver
kidneys: OrganHumanKidneys
connections:
- - left arm
- right arm
- - left leg
+ - left arm
- right leg
+ - left leg
right arm:
part: RightArmVulpkanin
connections:
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml
index 6dd2a89e5a..a09f3b6ab7 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Body/Prototypes/felinid.yml
@@ -13,10 +13,10 @@
torso:
part: TorsoHuman
connections:
- - left arm
- right arm
- - left leg
+ - left arm
- right leg
+ - left leg
organs:
heart: OrganAnimalHeart
lungs: OrganHumanLungs
From 7a124612a1b08b0d46f9baa2025c764d1b3a08de Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Tue, 2 Jul 2024 11:01:35 +0300
Subject: [PATCH 29/56] Cherry-Pick the Secwatch Pda App (#502)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
# Description
Cherry-picks https://github.com/DeltaV-Station/Delta-v/pull/1237
All credit goes to the original author, deltanedas
Adds a PDA app that lets seccies know who's wanted and who's about to be
thrown out of an airlock without relying on the sechud and people having
their IDs on them.
# Media
![image](https://github.com/Simple-Station/Einstein-Engines/assets/69920617/37f5fa1a-27a5-4392-b4bb-be0f1016b499)
(see the original PR for a better preview)
# Changelog
:cl: deltanedas
- add: Security can find the new SecWatch™ app in their PDAs to see
current suspects and wanted criminals.
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Azzy
---
.../Cartridges/CrimeAssistUi.cs | 9 ---
.../Cartridges/CrimeAssistUiFragment.xaml.cs | 3 -
.../Cartridges/SecWatchEntryControl.xaml | 19 +++++
.../Cartridges/SecWatchEntryControl.xaml.cs | 21 ++++++
.../CartridgeLoader/Cartridges/SecWatchUi.cs | 27 +++++++
.../Cartridges/SecWatchUiFragment.xaml | 13 ++++
.../Cartridges/SecWatchUiFragment.xaml.cs | 25 +++++++
.../Cartridges/SecWatchCartridgeComponent.cs | 23 ++++++
.../Cartridges/SecWatchCartridgeSystem.cs | 73 +++++++++++++++++++
.../CrimeAssistCartridgeComponent.cs | 5 --
.../CrimeAssistCartridgeSystem.cs | 16 ----
.../Cartridges/CrimeAssistUiState.cs | 18 -----
.../Cartridges/SecWatchUiState.cs | 24 ++++++
.../deltav/cartridge-loader/secwatch.ftl | 5 ++
.../Entities/Objects/Devices/cartridges.yml | 22 +++++-
.../DeltaV/Entities/Objects/Devices/pda.yml | 3 +-
.../Entities/Objects/Devices/pda.yml | 24 ++++--
.../Entities/Objects/Devices/pda.yml | 3 +-
18 files changed, 271 insertions(+), 62 deletions(-)
create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml
create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs
create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs
create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml
create mode 100644 Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs
create mode 100644 Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs
create mode 100644 Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs
delete mode 100644 Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs
delete mode 100644 Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs
delete mode 100644 Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs
create mode 100644 Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs
create mode 100644 Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs
index ea5aa3cf25..2dbe923b2a 100644
--- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUi.cs
@@ -18,15 +18,6 @@ public override Control GetUIFragmentRoot()
public override void Setup(BoundUserInterface userInterface, EntityUid? fragmentOwner)
{
_fragment = new CrimeAssistUiFragment();
-
- _fragment.OnSync += _ => SendSyncMessage(userInterface);
- }
-
- private void SendSyncMessage(BoundUserInterface userInterface)
- {
- var syncMessage = new CrimeAssistSyncMessageEvent();
- var message = new CartridgeUiMessage(syncMessage);
- userInterface.SendMessage(message);
}
public override void UpdateState(BoundUserInterfaceState state)
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs
index e3163975d1..fb085a8a79 100644
--- a/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiFragment.xaml.cs
@@ -1,7 +1,6 @@
using Content.Client.Message;
using Content.Shared.DeltaV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
-using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
@@ -13,9 +12,7 @@ namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
public sealed partial class CrimeAssistUiFragment : BoxContainer
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
- [Dependency] private readonly IResourceCache _resourceCache = default!;
- public event Action? OnSync;
private CrimeAssistPage _currentPage;
private List? _pages;
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml
new file mode 100644
index 0000000000..2de8a37ff7
--- /dev/null
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs
new file mode 100644
index 0000000000..e8dd4eea44
--- /dev/null
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchEntryControl.xaml.cs
@@ -0,0 +1,21 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class SecWatchEntryControl : BoxContainer
+{
+ public SecWatchEntryControl(SecWatchEntry entry)
+ {
+ RobustXamlLoader.Load(this);
+
+ Status.Text = Loc.GetString($"criminal-records-status-{entry.Status.ToString().ToLower()}");
+ Title.Text = Loc.GetString("sec-watch-entry", ("name", entry.Name), ("job", entry.Job));
+
+ Reason.Text = entry.Reason ?? Loc.GetString("sec-watch-no-reason");
+ }
+}
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs
new file mode 100644
index 0000000000..da5ff825b9
--- /dev/null
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUi.cs
@@ -0,0 +1,27 @@
+using Content.Client.UserInterface.Fragments;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.UserInterface;
+
+namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
+
+public sealed partial class SecWatchUi : UIFragment
+{
+ private SecWatchUiFragment? _fragment;
+
+ public override Control GetUIFragmentRoot()
+ {
+ return _fragment!;
+ }
+
+ public override void Setup(BoundUserInterface ui, EntityUid? owner)
+ {
+ _fragment = new SecWatchUiFragment();
+ }
+
+ public override void UpdateState(BoundUserInterfaceState state)
+ {
+ if (state is SecWatchUiState cast)
+ _fragment?.UpdateState(cast);
+ }
+}
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml
new file mode 100644
index 0000000000..7fb2c42deb
--- /dev/null
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs
new file mode 100644
index 0000000000..ad15284052
--- /dev/null
+++ b/Content.Client/DeltaV/CartridgeLoader/Cartridges/SecWatchUiFragment.xaml.cs
@@ -0,0 +1,25 @@
+using Content.Shared.CartridgeLoader.Cartridges;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.DeltaV.CartridgeLoader.Cartridges;
+
+[GenerateTypedNameReferences]
+public sealed partial class SecWatchUiFragment : BoxContainer
+{
+ public SecWatchUiFragment()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void UpdateState(SecWatchUiState state)
+ {
+ NoEntries.Visible = state.Entries.Count == 0;
+ Entries.RemoveAllChildren();
+ foreach (var entry in state.Entries)
+ {
+ Entries.AddChild(new SecWatchEntryControl(entry));
+ }
+ }
+}
diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs
new file mode 100644
index 0000000000..7ccc90ef79
--- /dev/null
+++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeComponent.cs
@@ -0,0 +1,23 @@
+using Content.Shared.Security;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+[RegisterComponent, Access(typeof(SecWatchCartridgeSystem))]
+public sealed partial class SecWatchCartridgeComponent : Component
+{
+ ///
+ /// Only show people with these statuses.
+ ///
+ [DataField]
+ public List Statuses = new()
+ {
+ SecurityStatus.Suspected,
+ SecurityStatus.Wanted
+ };
+
+ ///
+ /// Station entity thats getting its records checked.
+ ///
+ [DataField]
+ public EntityUid? Station;
+}
diff --git a/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs
new file mode 100644
index 0000000000..16da24514c
--- /dev/null
+++ b/Content.Server/DeltaV/CartridgeLoader/Cartridges/SecWatchCartridgeSystem.cs
@@ -0,0 +1,73 @@
+using Content.Server.Station.Systems;
+using Content.Server.StationRecords;
+using Content.Server.StationRecords.Systems;
+using Content.Shared.CartridgeLoader;
+using Content.Shared.CartridgeLoader.Cartridges;
+using Content.Shared.CriminalRecords;
+using Content.Shared.StationRecords;
+
+namespace Content.Server.CartridgeLoader.Cartridges;
+
+public sealed class SecWatchCartridgeSystem : EntitySystem
+{
+ [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!;
+ [Dependency] private readonly StationRecordsSystem _records = default!;
+ [Dependency] private readonly StationSystem _station = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnRecordModified);
+
+ SubscribeLocalEvent(OnUiReady);
+ }
+
+ private void OnRecordModified(RecordModifiedEvent args)
+ {
+ // when a record is modified update the ui of every loaded cartridge tuned to the same station
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp, out var cartridge))
+ {
+ if (cartridge.LoaderUid is not {} loader || comp.Station != args.Station)
+ continue;
+
+ UpdateUI((uid, comp), loader);
+ }
+ }
+
+ private void OnUiReady(Entity ent, ref CartridgeUiReadyEvent args)
+ {
+ UpdateUI(ent, args.Loader);
+ }
+
+ private void UpdateUI(Entity ent, EntityUid loader)
+ {
+ // if the loader is on a grid, update the station
+ // if it is off grid use the cached station
+ if (_station.GetOwningStation(loader) is {} station)
+ ent.Comp.Station = station;
+
+ if (!TryComp(ent.Comp.Station, out var records))
+ return;
+
+ station = ent.Comp.Station.Value;
+
+ var entries = new List();
+ foreach (var (id, criminal) in _records.GetRecordsOfType(station, records))
+ {
+ if (!ent.Comp.Statuses.Contains(criminal.Status))
+ continue;
+
+ var key = new StationRecordKey(id, station);
+ if (!_records.TryGetRecord(key, out var general, records))
+ continue;
+
+ var status = criminal.Status;
+ entries.Add(new SecWatchEntry(general.Name, general.JobTitle, criminal.Status, criminal.Reason));
+ }
+
+ var state = new SecWatchUiState(entries);
+ _cartridgeLoader.UpdateCartridgeUiState(loader, state);
+ }
+}
diff --git a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs b/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs
deleted file mode 100644
index 741a613458..0000000000
--- a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeComponent.cs
+++ /dev/null
@@ -1,5 +0,0 @@
-namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
-
-[RegisterComponent]
-public sealed partial class CrimeAssistCartridgeComponent : Component
-{ }
diff --git a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs b/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs
deleted file mode 100644
index 06732c2c53..0000000000
--- a/Content.Server/DeltaV/CartridgeLoader/CrimeAssistCartridgeSystem.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Content.Shared.CartridgeLoader;
-using Content.Server.DeltaV.CartridgeLoader;
-using Content.Server.CartridgeLoader.Cartridges;
-using Content.Server.CartridgeLoader;
-
-namespace Content.Server.DeltaV.CartridgeLoader.Cartridges;
-
-public sealed class CrimeAssistCartridgeSystem : EntitySystem
-{
- [Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
-
- public override void Initialize()
- {
- base.Initialize();
- }
-}
diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs
deleted file mode 100644
index dd820f1a0b..0000000000
--- a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/CrimeAssistUiState.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-using Content.Shared.CartridgeLoader;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.DeltaV.CartridgeLoader.Cartridges;
-
-[Serializable, NetSerializable]
-public sealed class CrimeAssistUiState : BoundUserInterfaceState
-{
- public CrimeAssistUiState()
- { }
-}
-
-[Serializable, NetSerializable]
-public sealed class CrimeAssistSyncMessageEvent : CartridgeMessageEvent
-{
- public CrimeAssistSyncMessageEvent()
- { }
-}
diff --git a/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs
new file mode 100644
index 0000000000..068b54a6ff
--- /dev/null
+++ b/Content.Shared/DeltaV/CartridgeLoader/Cartridges/SecWatchUiState.cs
@@ -0,0 +1,24 @@
+using Content.Shared.Security;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.CartridgeLoader.Cartridges;
+
+///
+/// Show a list of wanted and suspected people from criminal records.
+///
+[Serializable, NetSerializable]
+public sealed class SecWatchUiState : BoundUserInterfaceState
+{
+ public readonly List Entries;
+
+ public SecWatchUiState(List entries)
+ {
+ Entries = entries;
+ }
+}
+
+///
+/// Entry for a person who is wanted or suspected.
+///
+[Serializable, NetSerializable]
+public record struct SecWatchEntry(string Name, string Job, SecurityStatus Status, string? Reason);
diff --git a/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl b/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl
new file mode 100644
index 0000000000..a5b96eab08
--- /dev/null
+++ b/Resources/Locale/en-US/deltav/cartridge-loader/secwatch.ftl
@@ -0,0 +1,5 @@
+sec-watch-program-name = SecWatch
+sec-watch-title = SecWatch 1.0
+sec-watch-no-entries = Everything's calm. Why not enjoy a Monkin Donut?
+sec-watch-entry = {$name}, {$job}
+sec-watch-no-reason = None given???
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml
index e3d5e9d213..def215cee4 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/cartridges.yml
@@ -17,4 +17,24 @@
icon:
sprite: DeltaV/Icons/cri.rsi
state: cri
- - type: CrimeAssistCartridge
+
+- type: entity
+ parent: BaseItem
+ id: SecWatchCartridge
+ name: sec watch cartridge
+ description: A cartridge that tracks the status of currently wanted individuals.
+ components:
+ - type: Sprite
+ sprite: DeltaV/Objects/Devices/cartridge.rsi
+ state: cart-cri
+ - type: Icon
+ sprite: DeltaV/Objects/Devices/cartridge.rsi
+ state: cart-cri
+ - type: UIFragment
+ ui: !type:SecWatchUi
+ - type: Cartridge
+ programName: sec-watch-program-name
+ icon:
+ sprite: Objects/Weapons/Melee/stunbaton.rsi
+ state: stunbaton_on
+ - type: SecWatchCartridge
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml
index 6ee3a7543f..d9607390cd 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Devices/pda.yml
@@ -20,12 +20,13 @@
map: [ "enum.PdaVisualLayers.IdLight" ]
shader: "unshaded"
visible: false
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: Pda
id: BrigmedicIDCard
state: pda-corpsman
diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
index 706cbd5dbb..7155be68d7 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml
@@ -173,12 +173,13 @@
accentVColor: "#A32D26"
- type: Icon
state: pda-interncadet
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
@@ -430,12 +431,13 @@
borderColor: "#6f6192"
- type: Icon
state: pda-lawyer
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
@@ -643,12 +645,13 @@
accentHColor: "#447987"
- type: Icon
state: pda-hos
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
@@ -664,12 +667,13 @@
accentVColor: "#949137"
- type: Icon
state: pda-warden
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
@@ -684,12 +688,13 @@
borderColor: "#A32D26"
- type: Icon
state: pda-security
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
@@ -979,12 +984,13 @@
borderColor: "#774705"
- type: Icon
state: pda-detective
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BaseMedicalPDA
@@ -1001,12 +1007,13 @@
accentVColor: "#d7d7d0"
- type: Icon
state: pda-brigmedic
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: ClownPDA
@@ -1092,12 +1099,13 @@
accentVColor: "#DFDFDF"
- type: Icon
state: pda-seniorofficer
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: SyndiPDA
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml
index c2fd8786af..4e6115ba33 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Devices/pda.yml
@@ -32,12 +32,13 @@
accentVColor: "#DFDFDF"
- type: Icon
state: pda-security
- - type: CartridgeLoader # DeltaV - Crime Assist
+ - type: CartridgeLoader # DeltaV - Crime Assist + SecWatch
preinstalled:
- CrewManifestCartridge
- NotekeeperCartridge
- NewsReaderCartridge
- CrimeAssistCartridge
+ - SecWatchCartridge
- type: entity
parent: BasePDA
From 2dcab4d57f74e25f4a1aafa23f48463ad6251516 Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Tue, 2 Jul 2024 08:01:57 +0000
Subject: [PATCH 30/56] Automatic Changelog Update (#502)
---
Resources/Changelog/Changelog.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index e4e812edeb..cd2ca0c929 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4271,3 +4271,11 @@ Entries:
message: Fixed loadouts becoming uneditable after spending all your points
id: 6134
time: '2024-07-02T07:18:27.0000000+00:00'
+- author: deltanedas
+ changes:
+ - type: Add
+ message: >-
+ Security can find the new SecWatch™ app in their PDAs to see current
+ suspects and wanted criminals.
+ id: 6135
+ time: '2024-07-02T08:01:36.0000000+00:00'
From 2e8e56f763a5cda63511a1aaae0d649ff64eedad Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Wed, 3 Jul 2024 22:48:28 +0300
Subject: [PATCH 31/56] Fix Clothing Quick-Equip (#507)
# Description
For some reason, ClothingSystem.TryEquip would return false if there's a
do-after to equip the clothing. This had caused all quick-equip attempts
on SMALL ITEMS to fail and ended up with every SMALL clothing item being
equipped into one of the pocket slots (which have no equip delays).
Also fixes quick swap - see the comments below.
# Changelog
:cl:
- fix: Equipping clothing using the Z key works correctly again.
---
Content.Shared/Clothing/EntitySystems/ClothingSystem.cs | 5 +++++
Content.Shared/Inventory/InventorySystem.Equip.cs | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
index f189db005b..976682c990 100644
--- a/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
+++ b/Content.Shared/Clothing/EntitySystems/ClothingSystem.cs
@@ -62,6 +62,11 @@ private void QuickEquip(
{
foreach (var slotDef in userEnt.Comp1.Slots)
{
+ // Do not attempt to quick-equip clothing in pocket slots.
+ // We should probably add a special flag to SlotDefinition to skip quick equip if more similar slots get added.
+ if (slotDef.SlotFlags.HasFlag(SlotFlags.POCKET))
+ continue;
+
if (!_invSystem.CanEquip(userEnt, toEquipEnt, slotDef.Name, out _, slotDef, userEnt, toEquipEnt))
continue;
diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs
index 24006b0c9f..7bdd17ee6f 100644
--- a/Content.Shared/Inventory/InventorySystem.Equip.cs
+++ b/Content.Shared/Inventory/InventorySystem.Equip.cs
@@ -176,7 +176,7 @@ public bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, strin
};
_doAfter.TryStartDoAfter(args);
- return false;
+ return true; // Changed to return true even if the item wasn't equipped instantly
}
if (!_containerSystem.Insert(itemUid, slotContainer))
From 6aaf4664ad1b9d150da9af30c27bfc31858de8d6 Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Wed, 3 Jul 2024 19:48:50 +0000
Subject: [PATCH 32/56] Automatic Changelog Update (#507)
---
Resources/Changelog/Changelog.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index cd2ca0c929..c4056561c9 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4279,3 +4279,9 @@ Entries:
suspects and wanted criminals.
id: 6135
time: '2024-07-02T08:01:36.0000000+00:00'
+- author: Mnemotechnician
+ changes:
+ - type: Fix
+ message: Equipping clothing using the Z key works correctly again.
+ id: 6136
+ time: '2024-07-03T19:48:29.0000000+00:00'
From 8897a4b4732e16baee30be42dd9925419f4cdd0a Mon Sep 17 00:00:00 2001
From: VMSolidus
Date: Thu, 4 Jul 2024 00:16:17 -0400
Subject: [PATCH 33/56] Revert "Don't look at this please."
This reverts commit d461f4fe176ab24b8278d17b500424c840a6348d.
---
Content.Client/Entry/EntryPoint.cs | 1 -
.../TypingIndicator/TypingIndicatorSystem.cs | 2 +-
.../Components/NPCConversationComponent.cs | 152 -----
.../NPC/Events/NPCConversationEvents.cs | 63 --
.../NPCConversationTreePrototype.cs | 154 -----
.../NPC/Systems/NPCConversationSystem.cs | 558 ------------------
.../SophicScribe/SophicScribeSystem.cs | 36 --
.../Locale/en-US/npc/conversation/sophia.ftl | 82 ---
.../Structures/Research/sophicscribe.yml | 194 +-----
9 files changed, 2 insertions(+), 1240 deletions(-)
delete mode 100644 Content.Server/NPC/Components/NPCConversationComponent.cs
delete mode 100644 Content.Server/NPC/Events/NPCConversationEvents.cs
delete mode 100644 Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
delete mode 100644 Content.Server/NPC/Systems/NPCConversationSystem.cs
delete mode 100644 Resources/Locale/en-US/npc/conversation/sophia.ftl
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 8636e0eb6a..a1fc68bbd2 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -125,7 +125,6 @@ public override void Init()
_prototypeManager.RegisterIgnore("alertLevels");
_prototypeManager.RegisterIgnore("nukeopsRole");
_prototypeManager.RegisterIgnore("stationGoal");
- _prototypeManager.RegisterIgnore("npcConversationTree");
_componentFactory.GenerateNetIds();
_adminManager.Initialize();
diff --git a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
index 443923f675..c923738930 100644
--- a/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
+++ b/Content.Server/Chat/TypingIndicator/TypingIndicatorSystem.cs
@@ -54,7 +54,7 @@ private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs
SetTypingIndicatorEnabled(uid.Value, ev.IsTyping);
}
- public void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
+ private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref appearance, false))
return;
diff --git a/Content.Server/NPC/Components/NPCConversationComponent.cs b/Content.Server/NPC/Components/NPCConversationComponent.cs
deleted file mode 100644
index c2a8ca31d7..0000000000
--- a/Content.Server/NPC/Components/NPCConversationComponent.cs
+++ /dev/null
@@ -1,152 +0,0 @@
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Content.Server.NPC.Events;
-using Content.Server.NPC.Prototypes;
-using Content.Server.NPC.Systems;
-
-namespace Content.Server.NPC.Components;
-
-[RegisterComponent]
-[Access(typeof(NPCConversationSystem))]
-public sealed partial class NPCConversationComponent : Component
-{
- ///
- /// Whether or not the listening logic is turned on.
- ///
- ///
- /// Queued responses will still play through, but no new attempts to listen will be made.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("enabled")]
- public bool Enabled = true;
-
- /* NYI:
- ///
- /// The NPC will pay attention when one of these words are said.
- ///
- [ViewVariables]
- [DataField("aliases")]
- public List Aliases = new();
- */
-
- [ViewVariables]
- [DataField("tree", required: true, customTypeSerializer: typeof(PrototypeIdSerializer))]
- public string? ConversationTreeId;
-
- ///
- /// This is the cached prototype.
- ///
- [ViewVariables]
- public NPCConversationTreePrototype ConversationTree = default!;
-
- ///
- /// Topics that are unlocked in the NPC's conversation tree.
- ///
- [ViewVariables]
- public HashSet UnlockedTopics = new();
-
- ///
- /// How long until we stop paying attention to someone for a prompt.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("attentionSpan")]
- public TimeSpan AttentionSpan = TimeSpan.FromSeconds(20);
-
- ///
- /// This is the minimum delay before the NPC makes a response.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("delayBeforeResponse")]
- public TimeSpan DelayBeforeResponse = TimeSpan.FromSeconds(0.3);
-
- ///
- /// This is the approximate delay per letter typed in text.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("typingDelay")]
- public TimeSpan TypingDelay = TimeSpan.FromSeconds(0.05);
-
- [ViewVariables]
- public Stack ResponseQueue = new();
-
- ///
- /// This is when the NPC will respond with its top response.
- ///
- [ViewVariables]
- [DataField("nextResponse", customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan NextResponse;
-
- ///
- /// This is the direction the NPC was facing before looking towards a conversation partner.
- ///
- [ViewVariables]
- public Angle OriginalFacing;
-
- ///
- /// This is who the NPC is paying attention to for conversation.
- ///
- [ViewVariables]
- public EntityUid? AttendingTo;
-
- ///
- /// This is when the NPC will stop paying attention to a specific person.
- ///
- [ViewVariables]
- [DataField("nextAttentionLoss", customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan NextAttentionLoss;
-
- ///
- /// This event is fired the next time the NPC hears something from the
- /// person they're speaking with and it takes control of the response.
- ///
- [ViewVariables]
- public NPCConversationListenEvent? ListeningEvent;
-
-#region Idle Chatter
-
- ///
- /// Whether or not the NPC will say things unprompted.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("idleEnabled")]
- public bool IdleEnabled = true;
-
- ///
- /// This is the approximate delay between idle chats.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("idleChatDelay")]
- public TimeSpan IdleChatDelay = TimeSpan.FromMinutes(3);
-
- ///
- /// This is the order in which idle chat lines are given.
- ///
- ///
- /// This is randomized both on init and when the lines have been exhausted
- /// to prevent repeating lines twice in a row and to avoid predictable patterns.
- ///
- /// It technically reduces randomness, with the benefit of less repetition.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public List IdleChatOrder = new();
-
- ///
- /// This is the next idle chat line that will be used.
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- public int IdleChatIndex = 0;
-
- ///
- /// This is when the NPC will say something out of its list of idle lines.
- ///
- ///
- /// This is reset every time the NPC speaks.
- ///
- [ViewVariables]
- [DataField("nextIdleChat", customTypeSerializer: typeof(TimeOffsetSerializer))]
- public TimeSpan NextIdleChat;
-
-#endregion
-
-}
-
diff --git a/Content.Server/NPC/Events/NPCConversationEvents.cs b/Content.Server/NPC/Events/NPCConversationEvents.cs
deleted file mode 100644
index eb04f59bdd..0000000000
--- a/Content.Server/NPC/Events/NPCConversationEvents.cs
+++ /dev/null
@@ -1,63 +0,0 @@
-using Robust.Shared.Audio;
-using Content.Server.NPC.Systems;
-
-namespace Content.Server.NPC.Events;
-
-///
-/// This is used for dynamic responses and post-response events.
-///
-[ImplicitDataDefinitionForInheritors]
-[Access(typeof(NPCConversationSystem))]
-public abstract partial class NPCConversationEvent : EntityEventArgs
-{
- ///
- /// This is the entity that the NPC is speaking to.
- ///
- public EntityUid? TalkingTo;
-}
-
-///
-/// This event type is raised when an NPC hears a response when it was set to listen for one.
-///
-///
-/// Set Handled to true when you want the NPC to stop listening.
-/// The NPC will otherwise keep listening and block any attempt to find a prompt in the speaker's words.
-///
-[ImplicitDataDefinitionForInheritors]
-[Access(typeof(NPCConversationSystem))]
-public abstract partial class NPCConversationListenEvent : HandledEntityEventArgs
-{
- ///
- /// This is the entity that said the message.
- ///
- public EntityUid? Speaker;
-
- ///
- /// This is the original message that the NPC heard.
- ///
- public string Message = default!;
-
- ///
- /// This is the message, parsed into separate words.
- ///
- public List Words = default!;
-}
-
-public sealed partial class NPCConversationHelpEvent : NPCConversationEvent
-{
- [DataField("text")]
- public string? Text;
-
- [DataField("audio")]
- public SoundSpecifier? Audio;
-}
-
-///
-/// This event can be raised after a response to cause an NPC to stop paying attention to someone.
-///
-public sealed partial class NPCConversationByeEvent : NPCConversationEvent { }
-
-// The following classes help demonstrate some of the features of the system.
-// They may be separated out at some point.
-public sealed partial class NPCConversationToldNameEvent : NPCConversationListenEvent { }
-
diff --git a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs b/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
deleted file mode 100644
index 20a616d830..0000000000
--- a/Content.Server/NPC/Prototypes/NPCConversationTreePrototype.cs
+++ /dev/null
@@ -1,154 +0,0 @@
-using Robust.Shared.Audio;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization;
-using Content.Server.NPC.Events;
-
-namespace Content.Server.NPC.Prototypes;
-
-[Prototype("npcConversationTree")]
-public sealed class NPCConversationTreePrototype : IPrototype, ISerializationHooks
-{
- [ViewVariables]
- [IdDataField]
- public string ID { get; } = default!;
-
- ///
- /// Dialogue contains all the topics to which an NPC can discuss.
- ///
- [ViewVariables]
- [DataField("dialogue", required: true)]
- public readonly NPCTopic[] Dialogue = default!;
-
- ///
- /// Attention responses are what the NPC says when they start paying
- /// attention to you without a specific question or prompt to respond to.
- ///
- [ViewVariables]
- [DataField("attention", required: true)]
- public readonly NPCResponse[] Attention = default!;
-
- ///
- /// Idle responses are just things the NPC will say when nothing else is
- /// going on, after some time.
- ///
- [ViewVariables]
- [DataField("idle", required: true)]
- public readonly NPCResponse[] Idle = default!;
-
- ///
- /// Unknown responses are what the NPC says when they can't respond to a
- /// particular question or prompt.
- ///
- [ViewVariables]
- [DataField("unknown", required: true)]
- public readonly NPCResponse[] Unknown = default!;
-
- ///
- /// Custom responses are available to use in extensions to the NPC
- /// Conversation system.
- ///
- // NOTE: This may be removed in favor of storing NPCResponses on custom
- // components, i.e. an NPCShopkeeperComponent, but for now, it lives here
- // to help demonstrate some features.
- [ViewVariables]
- [DataField("custom")]
- public readonly Dictionary Custom = default!;
-
- ///
- /// This exists as a quick way to map a prompt to a topic.
- ///
- public readonly Dictionary PromptToTopic = new();
-
- // ISerializationHooks _is_ obsolete, but ConstructionGraphPrototype is using it as of this commit,
- // and I'm not quite sure how to otherwise do this.
- //
- // I will look at that prototype when ISerializationHooks is phased out.
- void ISerializationHooks.AfterDeserialization()
- {
- // Cache the strings mapping to prompts.
- foreach (var topic in Dialogue)
- {
- foreach (var prompt in topic.Prompts)
- {
- PromptToTopic[prompt] = topic;
- }
- }
- }
-}
-
-[DataDefinition]
-public sealed partial class NPCTopic
-{
- [DataField]
- public string[] Prompts = default!;
-
- ///
- /// This determines the likelihood of this topic being selected over any
- /// other, given the existence of multiple candidates.
- ///
- [DataField]
- public float Weight = 1.0f;
-
- ///
- /// Locked topics will not be accessible through dialogue until unlocked.
- ///
- [DataField]
- public bool Locked;
-
- ///
- /// Hidden topics won't show up in any form of "help" question.
- ///
- [DataField]
- public bool Hidden;
-
- [DataField("responses", required: true)]
- public NPCResponse[] Responses = default!;
-}
-
-[DataDefinition]
-public sealed partial class NPCResponse
-{
- public NPCResponse() { }
-
- public NPCResponse(string? text, SoundSpecifier? audio = null, NPCConversationEvent? ev = null)
- {
- Text = text;
- Audio = audio;
- Event = ev;
- }
-
- public override string ToString()
- {
- return $"NPCResponse({Text})";
- }
-
- [DataField]
- public string? Text;
-
- [DataField]
- public SoundSpecifier? Audio;
-
- /* [DataField("emote")] */
- /* public string? Emote; */
-
- ///
- /// This event is raised when the response is queued,
- /// for the purpose of dynamic responses.
- ///
- [DataField]
- public NPCConversationEvent? Is;
-
- ///
- /// This event is raised after the response is made.
- ///
- [DataField]
- public NPCConversationEvent? Event;
-
- ///
- /// This event is raised when the NPC next hears a response,
- /// allowing the response to be processed by other systems.
- ///
- [DataField]
- public NPCConversationListenEvent? ListenEvent;
-}
-
diff --git a/Content.Server/NPC/Systems/NPCConversationSystem.cs b/Content.Server/NPC/Systems/NPCConversationSystem.cs
deleted file mode 100644
index 015adb19de..0000000000
--- a/Content.Server/NPC/Systems/NPCConversationSystem.cs
+++ /dev/null
@@ -1,558 +0,0 @@
-using System.Collections.Immutable;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text.RegularExpressions;
-using Robust.Server.GameObjects;
-using Robust.Shared.Audio;
-using Robust.Shared.Audio.Systems;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
-using Content.Server.Chat.Systems;
-using Content.Server.Chat.TypingIndicator;
-using Content.Server.NPC.HTN;
-using Content.Server.NPC.Components;
-using Content.Server.NPC.Events;
-using Content.Server.NPC.Prototypes;
-using Content.Server.Speech;
-using Content.Shared.Interaction;
-using Content.Server.Radio.Components;
-
-namespace Content.Server.NPC.Systems;
-
-public sealed class NPCConversationSystem : EntitySystem
-{
- [Dependency] private readonly IRobustRandom _random = default!;
- [Dependency] private readonly IPrototypeManager _prototype = default!;
- [Dependency] private readonly IGameTiming _gameTiming = default!;
- [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
- [Dependency] private readonly ChatSystem _chatSystem = default!;
- [Dependency] private readonly NPCSystem _npcSystem = default!;
- [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!;
- [Dependency] private readonly TransformSystem _transformSystem = default!;
- [Dependency] private readonly TypingIndicatorSystem _typingIndicatorSystem = default!;
-
- private ISawmill _sawmill = default!;
-
- // TODO: attention attenuation. distance, facing, visible
- // TODO: attending to multiple people, multiple streams of conversation
- // TODO: multi-word prompts
- // TODO: nameless prompting (pointing is good)
- // TODO: aliases
-
- public static readonly string[] QuestionWords = { "who", "what", "when", "why", "where", "how" };
- public static readonly string[] Copulae = { "is", "are" };
-
- public override void Initialize()
- {
- base.Initialize();
-
- _sawmill = Logger.GetSawmill("npc.conversation");
-
- SubscribeLocalEvent(OnInit);
- SubscribeLocalEvent(OnUnpaused);
- SubscribeLocalEvent(OnListenAttempt);
- SubscribeLocalEvent(OnListen);
-
- SubscribeLocalEvent(OnBye);
- SubscribeLocalEvent(OnHelp);
-
- SubscribeLocalEvent(OnToldName);
- }
-
-#region API
-
- ///
- /// Toggle the ability of an NPC to listen for topics.
- ///
- public void EnableConversation(EntityUid uid, bool enable = true, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.Enabled = enable;
- }
-
- ///
- /// Toggle the NPC's willingness to make idle comments.
- ///
- public void EnableIdleChat(EntityUid uid, bool enable = true, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.IdleEnabled = enable;
- }
-
- ///
- /// Return locked status of a dialogue topic.
- ///
- public bool IsDialogueLocked(EntityUid uid, string option, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return true;
-
- if (!component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic))
- {
- _sawmill.Warning($"Tried to check locked status of missing dialogue option `{option}` on {ToPrettyString(uid)}");
- return true;
- }
-
- if (component.UnlockedTopics.Contains(topic))
- return false;
-
- return topic.Locked;
- }
-
- ///
- /// Unlock dialogue options normally locked in an NPC's conversation tree.
- ///
- public void UnlockDialogue(EntityUid uid, string option, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (component.ConversationTree.PromptToTopic.TryGetValue(option, out var topic))
- component.UnlockedTopics.Add(topic);
- else
- _sawmill.Warning($"Tried to unlock missing dialogue option `{option}` on {ToPrettyString(uid)}");
- }
-
- ///
- public void UnlockDialogue(EntityUid uid, HashSet options, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- foreach (var option in options)
- UnlockDialogue(uid, option, component);
- }
-
- ///
- /// Queue a response for an NPC with a visible typing indicator and delay between messages.
- ///
- ///
- /// This can be used as opposed to the typical method.
- ///
- public void QueueResponse(EntityUid uid, NPCResponse response, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- if (response.Is is {} ev)
- {
- // This is a dynamic response which will call QueueResponse with static responses of its own.
- ev.TalkingTo = component.AttendingTo;
- RaiseLocalEvent(uid, (object) ev);
- return;
- }
-
- if (component.ResponseQueue.Count == 0)
- {
- DelayResponse(uid, component, response);
- _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, true);
- }
-
- component.ResponseQueue.Push(response);
- }
-
- ///
- /// Make an NPC stop paying attention to someone.
- ///
- public void LoseAttention(EntityUid uid, NPCConversationComponent? component = null)
- {
- if (!Resolve(uid, ref component))
- return;
-
- component.AttendingTo = null;
- component.ListeningEvent = null;
- _rotateToFaceSystem.TryFaceAngle(uid, component.OriginalFacing);
- }
-
-#endregion
-
- private void DelayResponse(EntityUid uid, NPCConversationComponent component, NPCResponse response)
- {
- if (response.Text == null)
- return;
-
- component.NextResponse = _gameTiming.CurTime +
- component.DelayBeforeResponse +
- component.TypingDelay.TotalSeconds * TimeSpan.FromSeconds(response.Text.Length) *
- _random.NextDouble(0.9, 1.1);
- }
-
- private IEnumerable GetAvailableTopics(EntityUid uid, NPCConversationComponent component)
- {
- HashSet availableTopics = new();
-
- foreach (var topic in component.ConversationTree.Dialogue)
- {
- if (!topic.Locked || component.UnlockedTopics.Contains(topic))
- availableTopics.Add(topic);
- }
-
- return availableTopics;
- }
-
- private IEnumerable GetVisibleTopics(EntityUid uid, NPCConversationComponent component)
- {
- HashSet visibleTopics = new();
-
- foreach (var topic in component.ConversationTree.Dialogue)
- {
- if (!topic.Hidden && (!topic.Locked || component.UnlockedTopics.Contains(topic)))
- visibleTopics.Add(topic);
- }
-
- return visibleTopics;
- }
-
- private void OnInit(EntityUid uid, NPCConversationComponent component, ComponentInit args)
- {
- if (component.ConversationTreeId == null)
- return;
-
- component.ConversationTree = _prototype.Index(component.ConversationTreeId);
- component.NextIdleChat = _gameTiming.CurTime + component.IdleChatDelay;
-
- for (var i = 0; i < component.ConversationTree.Idle.Length; ++i)
- component.IdleChatOrder.Add(i);
-
- _random.Shuffle(component.IdleChatOrder);
- }
-
- private void OnUnpaused(EntityUid uid, NPCConversationComponent component, ref EntityUnpausedEvent args)
- {
- component.NextResponse += args.PausedTime;
- component.NextAttentionLoss += args.PausedTime;
- component.NextIdleChat += args.PausedTime;
- }
-
- private bool TryGetIdleChatLine(EntityUid uid, NPCConversationComponent component, [NotNullWhen(true)] out NPCResponse? line)
- {
- line = null;
-
- if (component.IdleChatOrder.Count() == 0)
- return false;
-
- if (++component.IdleChatIndex == component.IdleChatOrder.Count())
- {
- // Exhausted all lines in the pre-shuffled order.
- // Reset the index and shuffle again.
- component.IdleChatIndex = 0;
- _random.Shuffle(component.IdleChatOrder);
- }
-
- var index = component.IdleChatOrder[component.IdleChatIndex];
-
- line = component.ConversationTree.Idle[index];
-
- return true;
- }
-
- public override void Update(float frameTime)
- {
- base.Update(frameTime);
-
- var query = EntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var component))
- {
- var curTime = _gameTiming.CurTime;
-
- if (curTime >= component.NextResponse && component.ResponseQueue.Count > 0)
- {
- // Make a response.
- Respond(uid, component, component.ResponseQueue.Pop());
- }
-
- if (curTime >= component.NextAttentionLoss && component.AttendingTo != null)
- {
- // Forget who we were talking to.
- LoseAttention(uid, component);
- }
-
- if (component.IdleEnabled &&
- curTime >= component.NextIdleChat &&
- TryGetIdleChatLine(uid, component, out var line))
- {
- Respond(uid, component, line);
- }
- }
- }
-
- private void OnListenAttempt(EntityUid uid, NPCConversationComponent component, ListenAttemptEvent args)
- {
- if (!component.Enabled ||
- // Don't listen to myself...
- uid == args.Source ||
- // Don't bother listening to other NPCs. For now.
- HasComp(args.Source) ||
- // We're already "typing" a response, so do that first.
- component.ResponseQueue.Count > 0)
- {
- args.Cancel();
- }
- }
-
- private void PayAttentionTo(EntityUid uid, NPCConversationComponent component, EntityUid speaker)
- {
- component.AttendingTo = speaker;
- component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan;
- component.OriginalFacing = _transformSystem.GetWorldRotation(uid);
- }
-
- private void Respond(EntityUid uid, NPCConversationComponent component, NPCResponse response)
- {
- if (component.ResponseQueue.Count == 0)
- _typingIndicatorSystem.SetTypingIndicatorEnabled(uid, false);
- else
- DelayResponse(uid, component, component.ResponseQueue.Peek());
-
- if (component.AttendingTo != null)
- {
- // TODO: This line is a mouthful. Maybe write a public API that supports EntityCoordinates later?
- var speakerCoords = Transform(component.AttendingTo.Value).Coordinates.ToMap(EntityManager, _transformSystem).Position;
- _rotateToFaceSystem.TryFaceCoordinates(uid, speakerCoords);
- }
-
- if (response.Event is {} ev)
- {
- ev.TalkingTo = component.AttendingTo;
- RaiseLocalEvent(uid, (object) ev);
- }
-
- if (response.ListenEvent != null)
- component.ListeningEvent = response.ListenEvent;
-
- if (response.Text != null)
- _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(response.Text), InGameICChatType.Speak, false);
-
- if (response.Audio != null)
- _audioSystem.PlayPvs(response.Audio, uid,
- // TODO: Allow this to be configured per NPC/response.
- AudioParams.Default
- .WithVolume(8f)
- .WithMaxDistance(9f)
- .WithRolloffFactor(0.5f));
-
- // Refresh our attention.
- component.NextAttentionLoss = _gameTiming.CurTime + component.AttentionSpan;
- component.NextIdleChat = component.NextAttentionLoss + component.IdleChatDelay;
- }
-
- private List ParseMessageIntoWords(string message)
- {
- return Regex.Replace(message.Trim().ToLower(), @"(\p{P})", "")
- .Split()
- .ToList();
- }
-
- private bool FindResponse(EntityUid uid, NPCConversationComponent component, List words, [NotNullWhen(true)] out NPCResponse? response)
- {
- response = null;
-
- var availableTopics = GetAvailableTopics(uid, component);
-
- // Some topics are more interesting than others.
- var greatestWeight = 0f;
- NPCTopic? candidate = null;
-
- foreach (var word in words)
- {
- if (component.ConversationTree.PromptToTopic.TryGetValue(word, out var topic) &&
- availableTopics.Contains(topic) &&
- topic.Weight > greatestWeight)
- {
- greatestWeight = topic.Weight;
- candidate = topic;
- }
- }
-
- if (candidate != null)
- {
- response = _random.Pick(candidate.Responses);
- return true;
- }
-
- return false;
- }
-
- private bool JudgeQuestionLikelihood(EntityUid uid, NPCConversationComponent component, List words, string message)
- {
- if (message.Length > 0 && message[^1] == '?')
- // A question mark is an absolute mark of a question.
- return true;
-
- if (words.Count == 1)
- // The usefulness of this is dubious, but it's definitely a question.
- return QuestionWords.Contains(words[0]);
-
- if (words.Count >= 2)
- return QuestionWords.Contains(words[0]) && Copulae.Contains(words[1]);
-
- return false;
- }
-
- private void OnBye(EntityUid uid, NPCConversationComponent component, NPCConversationByeEvent args)
- {
- LoseAttention(uid, component);
- }
-
- private void OnHelp(EntityUid uid, NPCConversationComponent component, NPCConversationHelpEvent args)
- {
- if (args.Text == null)
- {
- _sawmill.Error($"{ToPrettyString(uid)} heard a Help prompt but has no text for it.");
- return;
- }
-
- var availableTopics = GetVisibleTopics(uid, component);
- var availablePrompts = availableTopics.Select(topic => topic.Prompts.FirstOrDefault()).ToArray();
-
- string availablePromptsText;
- if (availablePrompts.Count() <= 2)
- {
- availablePromptsText = Loc.GetString(args.Text,
- ("availablePrompts", string.Join(" or ", availablePrompts))
- );
- }
- else
- {
- availablePrompts[^1] = $"or {availablePrompts[^1]}";
- availablePromptsText = Loc.GetString(args.Text,
- ("availablePrompts", string.Join(", ", availablePrompts))
- );
- }
-
- // Unlikely we'll be able to do audio that isn't hard-coded,
- // so best to keep it general.
- var response = new NPCResponse(availablePromptsText, args.Audio);
- QueueResponse(uid, response, component);
- }
-
- private void OnToldName(EntityUid uid, NPCConversationComponent component, NPCConversationListenEvent args)
- {
- if (!component.ConversationTree.Custom.TryGetValue("toldName", out var responses))
- return;
-
- var response = _random.Pick(responses);
- if (response.Text == null)
- {
- _sawmill.Error($"{ToPrettyString(uid)} was told a name but had no text response.");
- return;
- }
-
- // The world's simplest heuristic for names:
- if (args.Words.Count > 3)
- {
- // It didn't seem like a name, so wait for something that does.
- return;
- }
-
- var cleanedName = string.Join(" ", args.Words);
- cleanedName = char.ToUpper(cleanedName[0]) + cleanedName.Remove(0, 1);
-
- var formattedResponse = new NPCResponse(Loc.GetString(response.Text,
- ("name", cleanedName)),
- response.Audio);
-
- QueueResponse(uid, formattedResponse, component);
- args.Handled = true;
- }
-
- private void OnListen(EntityUid uid, NPCConversationComponent component, ListenEvent args)
- {
- if (HasComp(args.Source))
- return;
-
- if (component.AttendingTo != null && component.AttendingTo != args.Source)
- // Ignore someone speaking to us if we're already paying attention to someone else.
- return;
-
- var words = ParseMessageIntoWords(args.Message);
- if (words.Count == 0)
- return;
-
- if (component.AttendingTo == args.Source)
- {
- // The person we're talking to said something to us.
-
- if (component.ListeningEvent is {} ev)
- {
- // We were waiting on this person to say something, and they've said something.
- ev.Handled = false;
- ev.Speaker = component.AttendingTo;
- ev.Message = args.Message;
- ev.Words = words;
- RaiseLocalEvent(uid, (object) ev);
-
- if (ev.Handled)
- component.ListeningEvent = null;
-
- return;
- }
-
- // We're already paying attention to this person,
- // so try to figure out if they said something we can talk about.
- if (FindResponse(uid, component, words, out var response))
- {
- // A response was found so go ahead with it.
- QueueResponse(uid, response, component);
- }
- else if(JudgeQuestionLikelihood(uid, component, words, args.Message))
- {
- // The message didn't match any of the prompts, but it seemed like a question.
- var unknownResponse = _random.Pick(component.ConversationTree.Unknown);
- QueueResponse(uid, unknownResponse, component);
- }
-
- // If the message didn't seem like a question,
- // and it didn't raise any of our topics,
- // then politely ignore who we're talking with.
- //
- // It's better than spamming them with "I don't understand."
- return;
- }
-
- // See if someone said our name.
- var myName = MetaData(uid).EntityName.ToLower();
-
- // So this is a rough heuristic, but if our name occurs within the first three words,
- // or is the very last one, someone might be trying to talk to us.
- var payAttention = words[0] == myName || words[^1] == myName;
- if (!payAttention)
- {
- for (int i = 1; i < Math.Min(2, words.Count); ++i)
- {
- if (words[i] == myName)
- {
- payAttention = true;
- break;
- }
- }
- }
-
- if (payAttention)
- {
- PayAttentionTo(uid, component, args.Source);
-
- if (!FindResponse(uid, component, words, out var response))
- {
- if(JudgeQuestionLikelihood(uid, component, words, args.Message) &&
- // This subcondition exists to block our name being interpreted as a question in its own right.
- words.Count > 1)
- {
- response = _random.Pick(component.ConversationTree.Unknown);
- }
- else
- {
- response = _random.Pick(component.ConversationTree.Attention);
- }
- }
-
- QueueResponse(uid, response, component);
- }
- }
-}
-
diff --git a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
index ba5ff0a056..b1a6c1e9de 100644
--- a/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
+++ b/Content.Server/Nyanotrasen/Research/SophicScribe/SophicScribeSystem.cs
@@ -1,8 +1,5 @@
using Content.Server.Psionics.Abilities;
using Content.Server.Chat.Systems;
-using Content.Server.NPC.Events;
-using Content.Server.NPC.Systems;
-using Content.Server.NPC.Prototypes;
using Content.Server.Radio.Components;
using Content.Server.Radio.EntitySystems;
using Content.Server.StationEvents.Events;
@@ -21,8 +18,6 @@ public sealed partial class SophicScribeSystem : EntitySystem
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
- [Dependency] private readonly NPCConversationSystem _conversationSystem = default!;
- protected ISawmill Sawmill = default!;
public override void Update(float frameTime)
{
@@ -56,32 +51,6 @@ public override void Initialize()
SubscribeLocalEvent(OnInteractHand);
SubscribeLocalEvent(OnGlimmerEventEnded);
- SubscribeLocalEvent(OnGetGlimmer);
- }
-
- private void OnGetGlimmer(EntityUid uid, SophicScribeComponent component, NPCConversationGetGlimmerEvent args)
- {
- if (args.Text == null)
- {
- Sawmill.Error($"{uid} heard a glimmer reading prompt but has no text for it");
- return;
- }
-
- var tier = _glimmerSystem.GetGlimmerTier() switch
- {
- GlimmerTier.Minimal => Loc.GetString("glimmer-reading-minimal"),
- GlimmerTier.Low => Loc.GetString("glimmer-reading-low"),
- GlimmerTier.Moderate => Loc.GetString("glimmer-reading-moderate"),
- GlimmerTier.High => Loc.GetString("glimmer-reading-high"),
- GlimmerTier.Dangerous => Loc.GetString("glimmer-reading-dangerous"),
- _ => Loc.GetString("glimmer-reading-critical"),
- };
-
- var glimmerReadingText = Loc.GetString(args.Text,
- ("glimmer", (int) Math.Round(_glimmerSystem.GlimmerOutput)), ("tier", tier));
-
- var response = new NPCResponse(glimmerReadingText);
- _conversationSystem.QueueResponse(uid, response);
}
private void OnInteractHand(EntityUid uid, SophicScribeComponent component, InteractHandEvent args)
@@ -114,9 +83,4 @@ private void OnGlimmerEventEnded(GlimmerEventEndedEvent args)
_radioSystem.SendRadioMessage(speaker, message, channel, speaker);
}
}
- public sealed partial class NPCConversationGetGlimmerEvent : NPCConversationEvent
- {
- [DataField]
- public string? Text;
- }
}
diff --git a/Resources/Locale/en-US/npc/conversation/sophia.ftl b/Resources/Locale/en-US/npc/conversation/sophia.ftl
deleted file mode 100644
index c832d9fc17..0000000000
--- a/Resources/Locale/en-US/npc/conversation/sophia.ftl
+++ /dev/null
@@ -1,82 +0,0 @@
-sophia-response-name = You may call me Sophia.
-sophia-response-help = You may inquire about one of the following topics: {$availablePrompts}.
-
-sophia-response-hello-1 = Greetings.
-sophia-response-hello-2 = Salutations.
-
-sophia-response-bye-1 = Fare thee well.
-sophia-response-bye-2 = Gods be with you.
-sophia-response-bye-3 = Come back wiser.
-
-sophia-idle-phrase-1 = Mmmm, another portent.
-sophia-idle-phrase-2 = The noösphere is quite beautiful today. However, I don't think I could describe it in a way you could understand.
-sophia-idle-phrase-3 = I've been here before. You have, too.
-
-sophia-response-attention-1 = What is it?
-sophia-response-attention-2 = What do you seek?
-sophia-response-attention-3 = Out with it.
-
-sophia-response-sorry-1 = That's not a question for me.
-sophia-response-sorry-2 = Ask someone else.
-sophia-response-sorry-3 = Maybe I know the answer, maybe I do not. Either way, I will not be answering that question.
-
-sophia-response-nature = My nature doesn't really matter, does it? I'm fulfilling my purpose. Can you say the same, or are you just wasting time?
-
-sophia-response-epi = 'Epistemics' is a word. Aspiring Hellenes they are, they wished to displace the Latin 'science.' However, in English, epistemics has undesired connotations as a study of knowledge itself, even though the Greek word is a literal replacement for 'science.'
-
-sophia-response-mantis = 'Mantis' means seer, soothsayer, or prophet. They must be so named because they seek to uncover the truth. And, fittingly with their psionic aptitude, 'mantis' and 'mind' both descend, to the best of our knowledge, from an absolutely ancient word that sounded something like 'mentis.'
-
-sophia-response-mystagogue = 'Mystagogue' literally means 'leader of the mystics.' You may know the suffix -gogue from 'demogogue.'
-
-sophia-response-oracle = Oracle? I don't know much about her, and she isn't keen to share her secrets with me.
-
-sophia-response-psionics = Psionics are extraordinary abilities originating from one's mind. There doesn't seem to be any dominant word to refer to someone with the ability to practice these, although I prefer 'psion' or 'psychic.'
-
-sophia-response-noosphere = The noösphere is a field connecting all of consciousness. It's the medium through which psionics works. Its strength and effects on the illusory world of the material are based on its pressure. Colloquially, noöspheric pressure is called 'glimmer.'
-
-sophia-response-god = 'God' is such a vague term. There are so many entities out there that have defeated mortality. How you choose to regard them is your business.
-
-sophia-response-morphotype = In the first century PCC, several entities reshaped men into their image. I had done the same, if you would believe it. I can offer no evidence of their existence, other than faint memories. Any specific morphotype you want to know about?
-
-sophia-response-calendar = It's currently 417 PCC. The casuality crisis neccesitated a new year to count from. Due to the nature of the crisis, it can only be said with certainty that 1 PCC is between 2400 and 2700 CE.
-
-sophia-response-crisis = The first FTL travel was incompatible with the old ways. Fortunately, its resolution made more apparent the inherent futility in trying to give one history, one narrative, one account. Truth cannot be found in the material world, only higher ones.
-
-sophia-response-metempsychosis = You've died thousands of times, and you'll die thousands more. Some of those lives you may dedicate to trying to stop the cycle. We all carry at least some memory of past lives, usually temporally recent ones. One of the great mysteries of the persistence of fragments is the high concentration of memories from the early 21st century CE, which, inverse to other periods, seem to be more common among the ignorant.
-
-sophia-response-truth = If you seek the truth, you're in the wrong place. From a perspective tainted by material reality, the best you can hope is to try and divine higher truths that are not dependent on it.
-
-sophia-response-job = I observe the glimmer here, and record it.
-
-sophia-response-human = Humans were the base for all the others. But they, too, were shaped. Long, long before the others.
-
-sophia-response-felinid = Felinids were the first, and the most willing. In true feline nature, they shaped themselves.
-
-sophia-response-oni = Oni, it is said, originated in Sirius. The brightest star in the night sky from Earth may have attracted some chromatically inclined entities, explaining their vivid coloring. But, that's just speculation.
-
-sophia-response-arachne = Arachne are the strangest of them. They're not fully mortal. They took the form of humans, but not their genes. Their creator wrote his name in their stead.
-
-sophia-response-moth = Moths scarecely look human, but, strangely, their genes confirm they are. Their creator shares his name with a genus of moths, and was responsible for the other outlier.
-
-sophia-response-lamiae = So, you remember? You must be remembering their mythological namesake. If you've really retained that fleeting memory over so many metempsychoses... Perhaps I've said too much.
-
-sophia-response-cyno = Were those... no... So faint. Ignorance! You cannot remember them! It's impossible!
-
-sophia-response-harpy = Harpies, it is said, were once men and women, sculpted by greed for a purpose long gone. They were abandoned by their creators on a world named Valerian 4b.
-
-sophia-response-valerian = The Harpy homeworld? Magestic mountains gleaming in white, forests of brilliant scarlet, oceans wine dark, yet no light to be seen by mortal eyes. The Harpies were made to thrive there. To them, their world was bathed in beautiful silver light.
-
-sophia-response-grue = You do not know of those. You cannot. I had so hoped to live a few cycles under normal causality.
-
-sophia-response-abraxas = That's a name of power, and I avoid speaking of him. He's the least content to rest, and the most infatuated with creating things from ignorance.
-
-sophia-response-zork = You wander into the slavering fangs of a hungry grue. There, did you enjoy this game?
-
-sophia-response-glimmer = The current glimmer reading is {$glimmer}. {$tier}
-
-glimmer-reading-minimal = That is extremely low. Nothing bad will happen, but I hope this is not at the cost of progression in your understanding of the universe.
-glimmer-reading-low = That is quite low. Just barely enough to register any psionic activity here.
-glimmer-reading-moderate = That is about the expected level on a psionically active station. You may notice manageable, minor effects.
-glimmer-reading-high = That is sure to start attracting attention, although still quite manageable.
-glimmer-reading-dangerous = That's a bit concerning. You may want to redirect efforts to reducing it.
-glimmer-reading-critical = That's apocalyptic, in the original sense of the word. That is, to say, revealing. This is the sort of time and place to acquire secret knowledge.
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
index 5213608d95..8e34a07ea5 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Research/sophicscribe.yml
@@ -1,7 +1,7 @@
- type: entity
parent: BaseStructure
id: SophicScribe
- name: Sophie
+ name: sophie
description: Latest reports on the Noösphere!
components:
- type: Sprite
@@ -27,10 +27,6 @@
channels:
- Common
- Science
- - type: ActiveListener
- - type: TypingIndicator
- - type: NPCConversation
- tree: SophiaTree
- type: PotentialPsionic #this makes her easier to access for glimmer events, dw about it
- type: Psionic
psychicFeedback:
@@ -43,191 +39,3 @@
- type: GuideHelp
guides:
- Psionics
-
-- type: npcConversationTree
- id: SophiaTree
- dialogue:
- - prompts: [glimmer, reading]
- responses:
- - is: !type:NPCConversationGetGlimmerEvent
- text: sophia-response-glimmer
-
- - prompts: [purpose, job, occupation, profession]
- weight: 0.5
- responses:
- - text: sophia-response-job
-
- - prompts: [help, topics]
- weight: 0.5
- hidden: true
- responses:
- - is: !type:NPCConversationHelpEvent
- text: sophia-response-help
-
- - prompts: [nature, statue, snake, being]
- weight: 0.3
- responses:
- - text: sophia-response-nature
-
- - prompts: [epistemics, epi]
- weight: 0.2
- responses:
- - text: sophia-response-epi
-
- - prompts: [mantis]
- weight: 0.2
- responses:
- - text: sophia-response-mantis
-
- - prompts: [mystagogue, mysta]
- weight: 0.2
- responses:
- - text: sophia-response-mystagogue
-
- - prompts: [psionics, psychic]
- weight: 0.2
- responses:
- - text: sophia-response-psionics
-
- - prompts: [noösphere, noosphere]
- weight: 0.2
- responses:
- - text: sophia-response-noosphere
-
- - prompts: [metempsychosis, metempsychoses, reincarnation, death, dying, afterlife]
- weight: 0.2
- responses:
- - text: sophia-response-metempsychosis
-
- - prompts: [calendar]
- weight: 0.2
- responses:
- - text: sophia-response-calendar
-
- - prompts: [morphotypes, morphotype, species]
- weight: 0.2
- responses:
- - text: sophia-response-morphotype
-
- - prompts: [gods, god]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-god
-
- - prompts: [truth, "true", "false", falsity, falsehood]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-truth
-
- - prompts: [human, humans, humanoid, unmutated]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-human
-
- - prompts: [felinid, felinids, felid, felids, catperson, catpeople]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-felinid
-
- - prompts: [oni, onis]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-oni
-
- - prompts: [arachne, arachnid, arachnids, spiderperson, spiderpeople]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-arachne
-
- - prompts: [moth, moths, moff, moths]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-moth
-
- - prompts: [lamiae, lamia, lamias]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-lamiae
-
- - prompts: [grue, grues, batperson, batpeople]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-grue
-
- - prompts: [cynocephalus, cynocephali, cyno, cynos]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-cyno
-
- - prompts: [harpy, harpies]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-harpy
-
- - prompts: [valerian, Valerian, 4b]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-valerian
-
- - prompts: [crisis, causality]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-crisis
-
- - prompts: [oracle]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-oracle
-
- - prompts: [abraxas]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-abraxas
-
- - prompts: [hi, hello, hey, greetings, salutations]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-hello-1
- - text: sophia-response-hello-2
-
- - prompts: [bye, goodbye, done, farewell, later, seeya]
- weight: 0.1
- hidden: true
- responses:
- - text: sophia-response-bye-1
- event: !type:NPCConversationByeEvent
- - text: sophia-response-bye-2
- event: !type:NPCConversationByeEvent
- - text: sophia-response-bye-3
- event: !type:NPCConversationByeEvent
-
- attention:
- - text: sophia-response-attention-1
- - text: sophia-response-attention-2
- - text: sophia-response-attention-3
-
- idle:
- - text: sophia-idle-phrase-1
- - text: sophia-idle-phrase-2
- - text: sophia-idle-phrase-3
-
- unknown:
- - text: sophia-response-sorry-1
- - text: sophia-response-sorry-2
- - text: sophia-response-sorry-3
From e092203d1134521f95c44c424bfa57f1ec7ffe53 Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Fri, 5 Jul 2024 19:53:25 +0300
Subject: [PATCH 34/56] Frictionfull Space (#514)
# Description
Makes it so that the station and the ATS get a very tiny bit of friction
to prevent cargo tech pros from sending either of those out of this
galaxy cluster (which has actually happened multiple times on two
servers and required either admin intervention or early round ending).
# Technical details
Added a PassiveDampeningComponent which defines how much friction an
entity receives while in 0g. FrictionRemoverSystem was updated to try to
fetch this component from an entity before updating its dampening. A new
system was added to automatically add this component (if it's not
already defined) to all station grids.
# Media
See the #when-you-code-it channel for a preview. It's kinda hard to
demonstrate, but after a few minutes, stations and the ATS come to an
almost complete stop.
# Changelog
:cl:
- tweak: Space stations now have a tiny bit of velocity dampening to
prevent them from being flunged into the void.
---
.../Station/Systems/StationDampeningSystem.cs | 28 +++++++++++++++++++
.../Physics/FrictionRemoverSystem.cs | 13 +++++++--
.../Physics/PassiveDampeningComponent.cs | 18 ++++++++++++
Resources/Maps/Shuttles/trading_outpost.yml | 3 ++
4 files changed, 60 insertions(+), 2 deletions(-)
create mode 100644 Content.Server/Station/Systems/StationDampeningSystem.cs
create mode 100644 Content.Shared/Physics/PassiveDampeningComponent.cs
diff --git a/Content.Server/Station/Systems/StationDampeningSystem.cs b/Content.Server/Station/Systems/StationDampeningSystem.cs
new file mode 100644
index 0000000000..f499127031
--- /dev/null
+++ b/Content.Server/Station/Systems/StationDampeningSystem.cs
@@ -0,0 +1,28 @@
+using Content.Server.Station.Events;
+using Content.Shared.Physics;
+
+namespace Content.Server.Station.Systems;
+
+public sealed class StationDampeningSystem : EntitySystem
+{
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnInitStation);
+ }
+
+ private void OnInitStation(ref StationPostInitEvent ev)
+ {
+ foreach (var grid in ev.Station.Comp.Grids)
+ {
+ // If the station grid doesn't have defined dampening, give it a small dampening by default
+ // This will ensure cargo tech pros won't fling the station 1000 megaparsec away from the galaxy
+ if (!TryComp(grid, out var dampening))
+ {
+ dampening = AddComp(grid);
+ dampening.Enabled = true;
+ dampening.LinearDampening = 0.01f;
+ dampening.AngularDampening = 0.01f;
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Physics/FrictionRemoverSystem.cs b/Content.Shared/Physics/FrictionRemoverSystem.cs
index 65bbe9e4d2..c8d7521eb0 100644
--- a/Content.Shared/Physics/FrictionRemoverSystem.cs
+++ b/Content.Shared/Physics/FrictionRemoverSystem.cs
@@ -1,3 +1,4 @@
+using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
@@ -19,7 +20,15 @@ public override void Initialize()
private void RemoveDampening(EntityUid uid, PhysicsComponent component, PhysicsSleepEvent args)
{
- _physics.SetAngularDamping(uid, component, 0f, false);
- _physics.SetLinearDamping(uid, component, 0f);
+ var linear = 0f;
+ var angular = 0f;
+ if (TryComp(uid, out var dampening) && dampening.Enabled)
+ {
+ linear = dampening.LinearDampening;
+ angular = dampening.AngularDampening;
+ }
+
+ _physics.SetAngularDamping(uid, component, angular, false);
+ _physics.SetLinearDamping(uid, component, linear);
}
}
diff --git a/Content.Shared/Physics/PassiveDampeningComponent.cs b/Content.Shared/Physics/PassiveDampeningComponent.cs
new file mode 100644
index 0000000000..834569195e
--- /dev/null
+++ b/Content.Shared/Physics/PassiveDampeningComponent.cs
@@ -0,0 +1,18 @@
+namespace Content.Shared.Physics;
+
+///
+/// A component that allows an entity to have friction (linear and angular dampening)
+/// even when not being affected by gravity.
+///
+[RegisterComponent]
+public sealed partial class PassiveDampeningComponent : Component
+{
+ [DataField]
+ public bool Enabled = true;
+
+ [DataField]
+ public float LinearDampening = 0.2f;
+
+ [DataField]
+ public float AngularDampening = 0.2f;
+}
diff --git a/Resources/Maps/Shuttles/trading_outpost.yml b/Resources/Maps/Shuttles/trading_outpost.yml
index f040d58253..7b968b5c13 100644
--- a/Resources/Maps/Shuttles/trading_outpost.yml
+++ b/Resources/Maps/Shuttles/trading_outpost.yml
@@ -60,6 +60,9 @@ entities:
linearDamping: 0.05
fixedRotation: False
bodyType: Dynamic
+ - type: PassiveDampening # To prevent cargotechs from flingling it away.
+ linearDampening: 0.01
+ angularDampening: 0.01
- type: Fixtures
fixtures: {}
- type: OccluderTree
From 81dd78259e66b6fda06fcb7c4b76b365eb4f652a Mon Sep 17 00:00:00 2001
From: SimpleStation Changelogs
Date: Fri, 5 Jul 2024 16:53:46 +0000
Subject: [PATCH 35/56] Automatic Changelog Update (#514)
---
Resources/Changelog/Changelog.yml | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml
index c4056561c9..847b9c90fb 100644
--- a/Resources/Changelog/Changelog.yml
+++ b/Resources/Changelog/Changelog.yml
@@ -4285,3 +4285,11 @@ Entries:
message: Equipping clothing using the Z key works correctly again.
id: 6136
time: '2024-07-03T19:48:29.0000000+00:00'
+- author: Mnemotechnician
+ changes:
+ - type: Tweak
+ message: >-
+ Space stations now have a tiny bit of velocity dampening to prevent them
+ from being flunged into the void.
+ id: 6137
+ time: '2024-07-05T16:53:25.0000000+00:00'
From 58be8504857c54d311e9100ee89b921d2437ee35 Mon Sep 17 00:00:00 2001
From: Mnemotechnican <69920617+Mnemotechnician@users.noreply.github.com>
Date: Fri, 5 Jul 2024 20:28:44 +0300
Subject: [PATCH 36/56] Port All Carrying/PseudoItem/EscapeInventory Tweaks
From DeltaV (#484)
# Description
This cherry-picks the following two PRs of mine from delta-v:
- https://github.com/DeltaV-Station/Delta-v/pull/1118 (this one is a
port of two other frontier PRs, see the original description for
details)
- https://github.com/DeltaV-Station/Delta-v/pull/1232
Encompassing a total of 8 distinct changes:
1. Fixes dropping the carried person when walking to a different grid
(from station to shuttle or vice versa. Walking into space however will
still make you drop them) and also makes sure that pressing shift while
being carried does not make you escape.
2. Ensures that the carried person is always centered relative to the
parent (under certain conditions, such as walking near a gravitational
anomaly, their position can change, and that leads to really weird
effects)
3. Fixes the mass contest in CarryingSystem that caused stronger
entities to take longer to escape than weaker ones.
4. Adds popups for when you're getting picked up or being stuffed into a
bag as a pseudo-item (e.g. a felinid)
5. Adds an action to stop escaping an inventory. This action gets added
to your action bar when you attempt escaping and gets removed when you
stop or escape. It applies both to carrying and items (hampsters,
felinids, whatever else).
6. Adds a sleep action for pseudo-items stuffed inside a suitable bag.
The bag must have a special component, which is added to the base
backpack item and thus inherited by all soft bags (duffels, satchels,
etc). Contrary to a popular belief, sleeping IS PURELY COSMETICAL and
does not provide healing. (Beds provide healing when you buckle into
them and that healing does not depend on whether or not you're sleeping)
7. Makes it so that when you try to take a pseudo-item out of the bag
(e.g. a felinid), you automatically try to carry them (if you don't have
enough free hands, they will be dropped on the floor like usually), and
enables you to insert the carried person into a bag, but only if they're
a pseudo-item (e.g. felinid).
8. Allows pseudoitems to be inserted into bags even when there are other
items (as long as there's enough space)
---
## For technical details and video showcases, see the original PRs
This PR is split into separate commits so different parts can be
reverted if deemed unneccessary.
---
# Changelog
:cl:
- fix: Carrying is less likely to behave erratically or suddenly
interrupt now.
- add: You can now see when someone is trying to pick you up, and also
you can interrupt your attempt at escaping from their hands or
inventory.
- add: You can now properly take Felinids out of bags and place them
inside.
- add: Scientists have discovered that Felinids can sleep in bags.
---
.../Nyanotrasen/Carrying/CarryingSystem.cs | 116 +++++++++++-
.../Item/PseudoItem/PseudoItemSystem.cs | 30 +++-
.../Resist/CanEscapeInventoryComponent.cs | 6 +
.../Resist/EscapeInventorySystem.cs | 26 +++
.../PseudoItem/AllowsSleepInsideComponent.cs | 9 +
.../Item/PseudoItem/PseudoItemComponent.cs | 14 +-
.../SharedPseudoItemSystem.Checks.cs | 166 ++----------------
.../Item/PseudoItem/SharedPseudoItemSystem.cs | 24 ++-
.../Resist/EscapeInventoryCancelEvent.cs | 5 +
.../Locale/en-US/actions/actions/sleep.ftl | 2 +
.../en-US/nyanotrasen/carrying/carry.ftl | 1 +
Resources/Prototypes/Actions/misc.yml | 10 ++
.../Entities/Clothing/Back/backpacks.yml | 3 +-
.../escapeinventory.rsi/cancel-escape.png | Bin 0 -> 559 bytes
.../Actions/escapeinventory.rsi/meta.json | 14 ++
15 files changed, 259 insertions(+), 167 deletions(-)
create mode 100644 Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs
create mode 100644 Content.Shared/Resist/EscapeInventoryCancelEvent.cs
create mode 100644 Resources/Prototypes/Actions/misc.yml
create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/cancel-escape.png
create mode 100644 Resources/Textures/Actions/escapeinventory.rsi/meta.json
diff --git a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs
index bb071334fa..103731b1b0 100644
--- a/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs
+++ b/Content.Server/Nyanotrasen/Carrying/CarryingSystem.cs
@@ -1,3 +1,4 @@
+using System.Numerics;
using System.Threading;
using Content.Server.DoAfter;
using Content.Server.Body.Systems;
@@ -5,6 +6,7 @@
using Content.Server.Resist;
using Content.Server.Popups;
using Content.Server.Inventory;
+using Content.Server.Nyanotrasen.Item.PseudoItem;
using Content.Shared.Climbing; // Shared instead of Server
using Content.Shared.Mobs;
using Content.Shared.DoAfter;
@@ -23,9 +25,12 @@
using Content.Shared.Standing;
using Content.Shared.ActionBlocker;
using Content.Shared.Inventory.VirtualItem;
+using Content.Shared.Item;
using Content.Shared.Throwing;
using Content.Shared.Physics.Pull;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Nyanotrasen.Item.PseudoItem;
+using Content.Shared.Storage;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
@@ -44,11 +49,13 @@ public sealed class CarryingSystem : EntitySystem
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly RespiratorSystem _respirator = default!;
+ [Dependency] private readonly PseudoItemSystem _pseudoItem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent>(AddCarryVerb);
+ SubscribeLocalEvent>(AddInsertCarriedVerb);
SubscribeLocalEvent(OnVirtualItemDeleted);
SubscribeLocalEvent(OnThrow);
SubscribeLocalEvent(OnParentChanged);
@@ -64,7 +71,6 @@ public override void Initialize()
SubscribeLocalEvent(OnDoAfter);
}
-
private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsEvent args)
{
if (!args.CanInteract || !args.CanAccess)
@@ -97,6 +103,33 @@ private void AddCarryVerb(EntityUid uid, CarriableComponent component, GetVerbsE
args.Verbs.Add(verb);
}
+ private void AddInsertCarriedVerb(EntityUid uid, CarryingComponent component, GetVerbsEvent args)
+ {
+ // If the person is carrying someone, and the carried person is a pseudo-item, and the target entity is a storage,
+ // then add an action to insert the carried entity into the target
+ var toInsert = args.Using;
+ if (toInsert is not { Valid: true } || !args.CanAccess || !TryComp(toInsert, out var pseudoItem))
+ return;
+
+ if (!TryComp(args.Target, out var storageComp))
+ return;
+
+ if (!_pseudoItem.CheckItemFits((toInsert.Value, pseudoItem), (args.Target, storageComp)))
+ return;
+
+ InnateVerb verb = new()
+ {
+ Act = () =>
+ {
+ DropCarried(uid, toInsert.Value);
+ _pseudoItem.TryInsert(args.Target, toInsert.Value, pseudoItem, storageComp);
+ },
+ Text = Loc.GetString("action-name-insert-other", ("target", toInsert)),
+ Priority = 2
+ };
+ args.Verbs.Add(verb);
+ }
+
///
/// Since the carried entity is stored as 2 virtual items, when deleted we want to drop them.
///
@@ -125,7 +158,12 @@ private void OnThrow(EntityUid uid, CarryingComponent component, BeforeThrowEven
private void OnParentChanged(EntityUid uid, CarryingComponent component, ref EntParentChangedMessage args)
{
- if (Transform(uid).MapID != args.OldMapId)
+ var xform = Transform(uid);
+ if (xform.MapID != args.OldMapId)
+ return;
+
+ // Do not drop the carried entity if the new parent is a grid
+ if (xform.ParentUid == xform.GridUid)
return;
DropCarried(uid, component.Carried);
@@ -158,9 +196,13 @@ private void OnMoveInput(EntityUid uid, BeingCarriedComponent component, ref Mov
if (!TryComp(uid, out var escape))
return;
+ if (!args.HasDirectionalMovement)
+ return;
+
if (_actionBlockerSystem.CanInteract(uid, component.Carrier))
{
- _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(uid, component.Carrier));
+ // Note: the mass contest is inverted because weaker entities are supposed to take longer to escape
+ _escapeInventorySystem.AttemptEscape(uid, component.Carrier, escape, MassContest(component.Carrier, uid));
}
}
@@ -209,12 +251,7 @@ private void OnDoAfter(EntityUid uid, CarriableComponent component, CarryDoAfter
}
private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableComponent component)
{
- TimeSpan length = TimeSpan.FromSeconds(3);
-
- var mod = MassContest(carrier, carried);
-
- if (mod != 0)
- length /= mod;
+ TimeSpan length = GetPickupDuration(carrier, carried);
if (length >= TimeSpan.FromSeconds(9))
{
@@ -236,6 +273,9 @@ private void StartCarryDoAfter(EntityUid carrier, EntityUid carried, CarriableCo
};
_doAfterSystem.TryStartDoAfter(args);
+
+ // Show a popup to the person getting picked up
+ _popupSystem.PopupEntity(Loc.GetString("carry-started", ("carrier", carrier)), carried, carried);
}
private void Carry(EntityUid carrier, EntityUid carried)
@@ -260,6 +300,26 @@ private void Carry(EntityUid carrier, EntityUid carried)
_actionBlockerSystem.UpdateCanMove(carried);
}
+ public bool TryCarry(EntityUid carrier, EntityUid toCarry, CarriableComponent? carriedComp = null)
+ {
+ if (!Resolve(toCarry, ref carriedComp, false))
+ return false;
+
+ if (!CanCarry(carrier, toCarry, carriedComp))
+ return false;
+
+ // The second one means that carrier is a pseudo-item and is inside a bag.
+ if (HasComp(carrier) || HasComp(carrier))
+ return false;
+
+ if (GetPickupDuration(carrier, toCarry) > TimeSpan.FromSeconds(9))
+ return false;
+
+ Carry(carrier, toCarry);
+
+ return true;
+ }
+
public void DropCarried(EntityUid carrier, EntityUid carried)
{
RemComp(carrier); // get rid of this first so we don't recusrively fire that event
@@ -323,5 +383,43 @@ private float MassContest(EntityUid roller, EntityUid target, PhysicsComponent?
return rollerPhysics.FixturesMass / targetPhysics.FixturesMass;
}
+
+ private TimeSpan GetPickupDuration(EntityUid carrier, EntityUid carried)
+ {
+ var length = TimeSpan.FromSeconds(3);
+
+ var mod = MassContest(carrier, carried);
+ if (mod != 0)
+ length /= mod;
+
+ return length;
+ }
+
+ public override void Update(float frameTime)
+ {
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var carried, out var comp))
+ {
+ var carrier = comp.Carrier;
+ if (carrier is not { Valid: true } || carried is not { Valid: true })
+ continue;
+
+ // SOMETIMES - when an entity is inserted into disposals, or a cryosleep chamber - it can get re-parented without a proper reparent event
+ // when this happens, it needs to be dropped because it leads to weird behavior
+ if (Transform(carried).ParentUid != carrier)
+ {
+ DropCarried(carrier, carried);
+ continue;
+ }
+
+ // Make sure the carried entity is always centered relative to the carrier, as gravity pulls can offset it otherwise
+ var xform = Transform(carried);
+ if (!xform.LocalPosition.Equals(Vector2.Zero))
+ {
+ xform.LocalPosition = Vector2.Zero;
+ }
+ }
+ query.Dispose();
+ }
}
}
diff --git a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs
index 76cfe7d904..6df387e6ba 100644
--- a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs
+++ b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs
@@ -1,6 +1,9 @@
-using Content.Server.DoAfter;
+using Content.Server.Carrying;
+using Content.Server.DoAfter;
using Content.Server.Item;
+using Content.Server.Popups;
using Content.Server.Storage.EntitySystems;
+using Content.Shared.Bed.Sleep;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Item;
@@ -17,12 +20,14 @@ public sealed class PseudoItemSystem : SharedPseudoItemSystem
[Dependency] private readonly StorageSystem _storage = default!;
[Dependency] private readonly ItemSystem _item = default!;
[Dependency] private readonly DoAfterSystem _doAfter = default!;
-
+ [Dependency] private readonly CarryingSystem _carrying = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent>(AddInsertAltVerb);
+ SubscribeLocalEvent(OnTrySleeping);
}
private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetVerbsEvent args)
@@ -53,4 +58,25 @@ private void AddInsertAltVerb(EntityUid uid, PseudoItemComponent component, GetV
};
args.Verbs.Add(verb);
}
+
+ protected override void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args)
+ {
+ // Try to pick the entity up instead first
+ if (args.User != args.Item && _carrying.TryCarry(args.User, uid))
+ {
+ args.Cancel();
+ return;
+ }
+
+ // If could not pick up, just take it out onto the ground as per default
+ base.OnGettingPickedUpAttempt(uid, component, args);
+ }
+
+ // Show a popup when a pseudo-item falls asleep inside a bag.
+ private void OnTrySleeping(EntityUid uid, PseudoItemComponent component, TryingToSleepEvent args)
+ {
+ var parent = Transform(uid).ParentUid;
+ if (!HasComp(uid) && parent is { Valid: true } && HasComp(parent))
+ _popup.PopupEntity(Loc.GetString("popup-sleep-in-bag", ("entity", uid)), uid);
+ }
}
diff --git a/Content.Server/Resist/CanEscapeInventoryComponent.cs b/Content.Server/Resist/CanEscapeInventoryComponent.cs
index 19b4abf7d0..978e03d95f 100644
--- a/Content.Server/Resist/CanEscapeInventoryComponent.cs
+++ b/Content.Server/Resist/CanEscapeInventoryComponent.cs
@@ -15,4 +15,10 @@ public sealed partial class CanEscapeInventoryComponent : Component
[DataField("doAfter")]
public DoAfterId? DoAfter;
+
+ ///
+ /// Action to cancel inventory escape.
+ ///
+ [DataField]
+ public EntityUid? EscapeCancelAction;
}
diff --git a/Content.Server/Resist/EscapeInventorySystem.cs b/Content.Server/Resist/EscapeInventorySystem.cs
index 127db7d2b3..95a470e909 100644
--- a/Content.Server/Resist/EscapeInventorySystem.cs
+++ b/Content.Server/Resist/EscapeInventorySystem.cs
@@ -5,6 +5,7 @@
using Content.Shared.Hands.EntitySystems;
using Content.Server.Storage.Components;
using Content.Shared.ActionBlocker;
+using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction.Events;
@@ -13,6 +14,7 @@
using Content.Shared.Resist;
using Content.Shared.Storage;
using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
namespace Content.Server.Resist;
@@ -24,11 +26,17 @@ public sealed class EscapeInventorySystem : EntitySystem
[Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly CarryingSystem _carryingSystem = default!; // Carrying system from Nyanotrasen.
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
///
/// You can't escape the hands of an entity this many times more massive than you.
///
public const float MaximumMassDisadvantage = 6f;
+ ///
+ /// Action to cancel inventory escape
+ ///
+ [ValidatePrototypeId]
+ private readonly string _escapeCancelAction = "ActionCancelEscape";
public override void Initialize()
{
@@ -37,6 +45,7 @@ public override void Initialize()
SubscribeLocalEvent(OnRelayMovement);
SubscribeLocalEvent(OnEscape);
SubscribeLocalEvent(OnDropped);
+ SubscribeLocalEvent(OnCancelEscape);
}
private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent component, ref MoveInputEvent args)
@@ -84,12 +93,20 @@ private void OnRelayMovement(EntityUid uid, CanEscapeInventoryComponent componen
_popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting"), user, user);
_popupSystem.PopupEntity(Loc.GetString("escape-inventory-component-start-resisting-target"), container, container);
+
+ // Add an escape cancel action
+ if (component.EscapeCancelAction is not { Valid: true })
+ _actions.AddAction(user, ref component.EscapeCancelAction, _escapeCancelAction);
}
private void OnEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryEvent args)
{
component.DoAfter = null;
+ // Remove the cancel action regardless of do-after result
+ _actions.RemoveAction(uid, component.EscapeCancelAction);
+ component.EscapeCancelAction = null;
+
if (args.Handled || args.Cancelled)
return;
@@ -109,4 +126,13 @@ private void OnDropped(EntityUid uid, CanEscapeInventoryComponent component, Dro
if (component.DoAfter != null)
_doAfterSystem.Cancel(component.DoAfter);
}
+
+ private void OnCancelEscape(EntityUid uid, CanEscapeInventoryComponent component, EscapeInventoryCancelActionEvent args)
+ {
+ if (component.DoAfter != null)
+ _doAfterSystem.Cancel(component.DoAfter);
+
+ _actions.RemoveAction(uid, component.EscapeCancelAction);
+ component.EscapeCancelAction = null;
+ }
}
diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs
new file mode 100644
index 0000000000..a28c7698fc
--- /dev/null
+++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/AllowsSleepInsideComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Shared.Nyanotrasen.Item.PseudoItem;
+
+///
+/// Signifies that pseudo-item creatures can sleep inside the container to which this component is applied.
+///
+[RegisterComponent]
+public sealed partial class AllowsSleepInsideComponent : Component
+{
+}
diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs
index d3774439d3..458b514b96 100644
--- a/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs
+++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/PseudoItemComponent.cs
@@ -3,10 +3,10 @@
namespace Content.Shared.Nyanotrasen.Item.PseudoItem;
- ///
- /// For entities that behave like an item under certain conditions,
- /// but not under most conditions.
- ///
+///
+/// For entities that behave like an item under certain conditions,
+/// but not under most conditions.
+///
[RegisterComponent, AutoGenerateComponentState]
public sealed partial class PseudoItemComponent : Component
{
@@ -24,4 +24,10 @@ public sealed partial class PseudoItemComponent : Component
public Vector2i StoredOffset;
public bool Active = false;
+
+ ///
+ /// Action for sleeping while inside a container with .
+ ///
+ [DataField]
+ public EntityUid? SleepAction;
}
diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs
index 7000c65404..906503b370 100644
--- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs
+++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.Checks.cs
@@ -3,163 +3,33 @@
namespace Content.Shared.Nyanotrasen.Item.PseudoItem;
-///
-/// Almost all of this is code taken from other systems, but adapted to use PseudoItem.
-/// I couldn't use the original functions because the resolve would fuck shit up, even if I passed a constructed itemcomp
-///
-/// This is horrible, and I hate it. But such is life
-///
public partial class SharedPseudoItemSystem
{
- protected bool CheckItemFits(Entity itemEnt, Entity storageEnt)
+ ///
+ /// Checks if the pseudo-item can be inserted into the specified storage entity.
+ ///
+ ///
+ /// This function creates and uses a fake item component if the entity doesn't have one.
+ ///
+ public bool CheckItemFits(Entity itemEnt, Entity