Лоадауты + апстрим, ебанешься (#421)

* Game server api (#24015)

* Revert "Revert "Game server api (#23129)""

* Review pt.1

* Reviews pt.2

* Reviews pt. 3

* Reviews pt. 4

(cherry picked from commit 297853929b7b3859760dcdda95e21888672ce8e1)

* Revert "Game server api" (#26871)

Revert "Game server api (#24015)"

This reverts commit 297853929b7b3859760dcdda95e21888672ce8e1.

(cherry picked from commit 3aee19792391cbfb06edb65d6f16f77da0f36f13)

* Give botanists droppers (#26839)

Start botanists with droppers so that they can better dose robust harvest or mutagen.

(cherry picked from commit 935127f25fef5cce6e0d5c4b73db5c6077badf56)

* Automatic changelog update

(cherry picked from commit 7d599a7199f21d71f3befa26e7ffec003a887dd3)

* Automatic changelog update

(cherry picked from commit 57911975c70dafcc3af9dfa08a86c9acda472497)

* fix lots of door access (#26858)

* dirty after calling SetAccesses

* fix door access

* D

* pro ops

* nukeop

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>

(cherry picked from commit 1cdf05a7a7169ed655a294b6bbe24a75f58f62fa)

* Add emergency nitrogen lockers (#26752)

(cherry picked from commit 4017f9bd28251a3cbf3c0fb34499c6b821051498)

* Automatic changelog update

(cherry picked from commit 8c16b466132d946578e7ab4c8c92dd3ddfe94f89)

* Update ashtray to allow all cigarettes / cigars (#26864)

* Update ashtray to allow all cigarettes / cigars

This also includes joints (as they are technically cigarettes)

* ?

(cherry picked from commit 9eb1e12022c22d0a8b431e4def3a8178aa1924d8)

* Fix door electronics configurator usage (#26888)

* allow usage of network configurator for door electronics

* add checks for "allowed" items

(cherry picked from commit 2bcdb608a3ddd9c91a1169d3f0d2d5b31aaebc88)

* Fix TEG assert (#26881)

It's possible to trigger this by stacking it weirdly with the spawn panel. Just make it bail instead.

(cherry picked from commit 210ed3ece4230a7fa31c12a43f4fdee0f0614915)

* Automatic changelog update

(cherry picked from commit 00dc99769c2442410204c1ea57bb24cc8353c16a)

* Bug fix for deconstructing tiles and lattice with RCDs (#26863)

* Fixed mixed deconstruction times for tiles and lattice

* Lattice and power cables can be deconstructed instantly

(cherry picked from commit fc5a90be0da4801aa7ff1fbc996c2f55f8cb7ae7)

* Immovable Rod changes (#26757)

(cherry picked from commit 036abacbb731c0d1128a4c6cd1658f64dd488985)

* fix evil roleplay changelog (#26893)

agh

(cherry picked from commit 75d3502d267d2050b4f9db1a4c0260c9fb6205e9)

* Cryogenic storage tweaks (#26813)

* make cryo remove crewmember's station record when going to cryo

* Revert "make cryo remove crewmember's station record when going to cryo"

This reverts commit 9ac9707289b5e553e3015c8c3ef88a78439977c6.

* make cryo remove crewmember from station records when the mind is removed from the body

* add stationwide announcement for people cryoing (remember to change pr title and desc)

* minor changes

* announcement actually shows job now

* requested changes

* get outta here derivative

(cherry picked from commit 9d62b3c3e690cdda48143774a5e5db853894e1b8)

* Automatic changelog update

(cherry picked from commit 6fa90e06c737d449f354b727550a5d1e13aeae44)

* Allow advertisement timers to prewarm (#26900)

Allow advertisement timers to prewarm.

(cherry picked from commit 264bf7199d805bd07dbdccc4345c672b19df9333)

* Fix shaker sprites (#26899)

* Change basefoodshaker to parent from basefoodcondiment instead

* Make them still refillable

(cherry picked from commit b895e557d4503074622ec1ca60e1f7749783a29e)

* Automatic changelog update

(cherry picked from commit 4627c7c859f12da60b75880c50b761b4646ea3a0)

* Update .editorconfig to correspond Code Conventions (#26824)

Update editorconfig to Code Style

End of line is: CRLF (suggestion)
Namespace declarations are: file scoped (suggestion). Instead of block scoped

(cherry picked from commit 882aeb03143d07a4cef91412008c81c9902075d8)

* Remove reagent slimes from ghost role pool (#26840)

reagentslimeghostrole

(cherry picked from commit e12223c355b3b452d6d6043ec126124189b64f84)

* Automatic changelog update

(cherry picked from commit 8f17bf1a3d96c8392c227ecc27b0e7d32c971126)

* Fix grammar in changelog (#26894)

Grammar

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
(cherry picked from commit 261e5354d3601b5ed1a8ea6c6a161a6f79c3f791)

* NoticeBoard is craftable now (#26847)

* NoticeBoard is craftable now

* Fix notice board to proper name capitalization

* Fix notice board proper name in description

* Update Resources/Prototypes/Recipes/Construction/furniture.yml

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
(cherry picked from commit e88b2467ca36c25c15095b7e7e0357c55ed581e2)

* Automatic changelog update

(cherry picked from commit 85aef16954725a72a2c590e9cf7445b15b93d23e)

* Add drink container suffixes (#26835)

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
(cherry picked from commit 7d480acb0c859e0f186df41b7f2940b2ef5789bb)

* uplink and store freshening (#26444)

* uplink and store freshening

* more

* im gonna POOOOOOGGGGGGG

* we love it

(cherry picked from commit 9d5a3992fa635194cfe1d9fbfa60a5ea72288f4e)

* Sterile swab dispenser instead of box (#24986)

* sterile swab dispenser

* trust

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit 3a00e8c59cc595d8f85bdf0319d56bf0741d2dec)

* Automatic changelog update

(cherry picked from commit 2a5d23f3f1fdc72268419bd09fd29d6c265b3e51)

* Strobes added (#26083)

* Done

* Adds new

* empty

* attributions

* empty

* strobe admin deleted

(cherry picked from commit 279e01c3d26ce8332114103b2045abb61886c614)

* Automatic changelog update

(cherry picked from commit c8f75d99186301810a3c16d99ad138107637e723)

* Health analyzer UI unit correction (#26903)

Correct Kelvin displayed on health analyzer UI, use T0C constant.

(cherry picked from commit 998bf453684ed91ae5971e41be72820c87b441ef)

* Fix the stripping menu being openable without StrippingComponent (#26908)

(cherry picked from commit 24b6456735ae83dd9de53097d625f20b723c578f)

* Fixed magboot activation distance (#26912)

(cherry picked from commit 0a1ce9dd437f162a59c826904241bf15ad6f2838)

* Automatic changelog update

(cherry picked from commit 2360376b4064db390275f407fcb89190c1e78bd7)

* Uncooked animal proteins is safe for animal stomachs only (#26906)

Uncooked animal proteins is safe for animal stomachs

(cherry picked from commit a49a576b031d2f90b7e127fd2b639a20bb31aaf4)

* Automatic changelog update

(cherry picked from commit 5d00305a2279b455a7ff5196f62050abeebc4085)

* Fix incorrect "Cycled" and "Bolted" popups when (un)wielding a gun (#26924)

(cherry picked from commit b51482f51a67bb90f6ccc7fb355c316e200bb36d)

* Fix guns that spawn without a magazine looking like they have one (#26922)

(cherry picked from commit 2d53cfeabcc977938f217c303ce46a79d7fe80c9)

* Fixes polymorph cooldowns (#26914)

fixes polymorph cooldowns

(cherry picked from commit d4b7bc5aa328e200bb087672d00c07776f98edce)

* Automatic changelog update

(cherry picked from commit b08677916557f34740a9b06e09493751b0af7a82)

* Glowy lights - light fixtures now actually glow (#23428)

* glowy lights - makes light fixtures actually physically glow when active

* serviced the lights

* ya sure

* fixes

* Removed Salv Borg Crusher Dagger (#26865)

(cherry picked from commit eeb460fb2956381b44ead42f0d435ade11ec0ba3)

* Fix pulling a new entity when already pulling an entity (#26499)

Fix pulling when already pulling

The TryStopPull were failing due to wrong arguments provided.
Replacing the virtual item in hand with a different pull was failing due to the hand not being cleared.

Fix stop pulling checks that had the wrong variables provided.

VirtualItems are already queue deleted at the end of HandleEntityRemoved.

(cherry picked from commit 037a7d7d3d53a623f70a07908299e8fc4df1b4a5)

* Replace SetDamage call with TryChangeDamage in ImmovableRodSystem.cs (#26902)

(cherry picked from commit 48330745147aba1ce89cdd6e7949da62dc9cbb78)

* Automatic changelog update

(cherry picked from commit 154b8606f9d9cc543c6b8ad422c43f563079327f)

* Fix for the salvage ice labs map. (#26928)

* done

* more work

(cherry picked from commit 1bf97c94eecfb9b76d4e2f08765ee5e8a7472151)

* Automatic changelog update

(cherry picked from commit ed065e8a3dbecb688b8aef1c1d597794de1503ef)

* Update Credits (#26938)

Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
(cherry picked from commit 1f4a01aa3857af6d924c4ad9cb2c105b793c2e53)

* Fix cryostorage identifying unknown characters as captain (#26927)

Fixed cryostorage getting captain's record for unknown jobs.
Also localized Unknown job string.

(cherry picked from commit 9b97a2e05d5fe65b1d81aefcf37b69033c78f21f)

* Automatic changelog update

(cherry picked from commit d44db87bfb9e84fb8181d8dbd2ee250d0c6829d3)

* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai. (#26939)

* Fixed Honkbot/jonkbot honking like crazy, gave honkbot/jonkbot standard idle ai.

* Update Resources/Prototypes/Entities/Mobs/NPCs/silicon.yml

---------

Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
(cherry picked from commit 8272d7a345264e3c10a895f838b3f5f4fa6b35e1)

* Automatic changelog update

(cherry picked from commit ba9091ff59251f20aa3d21615d52b40cb937f01c)

* Bug fix: Force cancellation of RCD constructions if the construction type is changed (#26935)

Force cancellation of RCD constructions if the construction type is changed

(cherry picked from commit 33e5e4e581a8d224cc2c44465db188ded0302e4e)

* Fix standart -> standard and dressfilled test fail (#26942)

Fix standart -> standard

(cherry picked from commit dc19964d84149354bb10fd186283f1a9d7da9cf5)

* Add Ability to stop sound when MobState is Dead (#26905)

* Add stopsWhenEntityDead to sound components

* Convert component

* Review

* Fix dupe sub

---------

Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
(cherry picked from commit da618d791a48efacebe441eee9398c0d1c571ec8)

* Automatic changelog update

(cherry picked from commit bbff00cd2af7a5f5700489b42d94c9d99bc88a52)

* Fix rockets and lasers looking like they have nothing loaded (#26933)

(cherry picked from commit 13cef85a6e7d0f5873d1f66358ef742356541582)

* Automatic changelog update

(cherry picked from commit cb4561fe96adacac3283e9af138fff6eb349eb9f)

* You can now see paper on crates (with color!) (#26834)

* Implement changes on not-cooked branch

* Made it work

* Fix update appearance calls

* Fix extra indents, clean-up code, fix tests hopefully

* Fix hammy cagecrate

* Fix messing up the yml, add artifact crate specific labels back in

* Visual Studio hates yml, sad

* Seperate the colors for cargonia

* sorry json

* make label move with artifact door

* Apply suggestion changes

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>

* Fix remaining crate offsets, add a few for livestock and graves (why are you labeling graves) and coffin label sprites (why are you labeling coffins??)

---------

Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
(cherry picked from commit 96ad9002f1d6390bf666c74ddfd871a45a8fe1df)

* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled (#25826)

* Make UtensilSystem and SharpSystem not run AfterInteract if it has already been handled

* merge conflicts

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>

(cherry picked from commit c67948407ec9ee21c0459ca9679b6fac904aad63)

* Automatic changelog update

(cherry picked from commit f1d1e6c6fdf169b11e950feeac5d68f7f0aedddd)

* Add two-message overload to PopupPredicted (#26907)

Added two-message overload to PopupPredicted

(cherry picked from commit 9107d421bdd2600883780d02d521ebbbec06ac68)

* Update submodule to 218.0.0 (#26945)

(cherry picked from commit 54b3d7fe45d8c491acea785aa8081c752cd6d91f)

* Autism pins! (#25597)

* hee hee he ha ha

* added gold varients, forgive me for my spritework

* maints loot, copying from past PRs

* Trying to fix RSI

* speedran these sprites in break time, pictures will be later

* Fixed/Tweaked glows

* consensus

* gregregation

* dam copiryte

* oops i forgot to delete 2 fields hope this works

(cherry picked from commit d2d62b97ac904fd486edd85a885ce682f362576c)

* Automatic changelog update

(cherry picked from commit dbf8a036ecf5a933270127d133927f42a88e3586)

* Fix database round start date issues (#26838)

How can ONE DATABASE COLUMN have so many cursed issues I don't know, but it certainly pissed off the devil in its previous life.

The start_date column on round entities in the database was added by https://github.com/space-wizards/space-station-14/pull/21153. For some reason, this PR gave the column a nonsensical default value instead of making it nullable. This default value causes the code from #25280 to break. It actually trips an assert though that's not what the original issue report ran into.

This didn't get noticed on wizden servers because we at some point backfilled the start_date column based on the stored admin logs.

So I change the database model to make this column nullable, updated the C# code to match, and made the existing migration set the invalid values to be NULL instead. Cool.

Wait how's SQLite handle in this scenario anyways? Well actually turns out the column was *completely broken* in the first place!

The code for inserting into the round table was copy pasted between SQLite and PostgreSQL, with the only difference being that the SQLite key manually assigned the primary key instead of letting SQLite AUTOINCREMENT it. And then the code to give a start_date value was only added to the PostgreSQL version (which is actually in the base class already). So for SQLite that column's been filled up with the same invalid default the whole time.

Why was the code manually assigning a PK? I checked the SQLite docs for AUTOINCREMENT[1], and the behavior seems appropriate.

I removed the SQLite-specific code path and it just seems to work regardless. The migration just sets the old values to NULL too.

BUT WAIT, THERE'S MORE!

Turns out just doing the migration on SQLite is a pain in the ass! EF Core has to create a new table to apply the nullability change, because SQLite doesn't support proper ALTER COLUMN. This causes the generated SQL commands to be weird and the UPDATE for the migration goes BEFORE the nullability change... I ended up having to make TWO migrations for SQLite. Yay.

Fixes #26800

[1]: https://www.sqlite.org/autoinc.html

(cherry picked from commit d3ac3d06bb6eddd9c2076a586974aabf514a5c86)

* Fix options menu crashing in replays (#26911)

Not having the nullable set properly is annoying but fixing that would probably be a significant amount of work.

(cherry picked from commit 4cb344cc307c33247492ba64cccca6dfe720540f)

* Greyscale color clothing (#26943)

* greyscales color gloves, color jumpsuits, and shoes

* remove popbob

* fix test fails

(cherry picked from commit 2b8e26fa2ebd1946211b245ebd10c1863b82e66d)

* Automatic changelog update

(cherry picked from commit 7787a82d032185b2e165bafe1f342d1a39aeb9fc)

* WT550 Buffs + Burst Mode for WT550 & C-20R (#26886)

* Slightly increased WT550 Firerate, drastically reduced recoil, and given it the option to fire in 5 round bursts.

* Given the C-20 a 5 round burst aswell

(cherry picked from commit 7b0dd31b1fa3a91225c1b3d312ac88dc03ea9959)

* Automatic changelog update

(cherry picked from commit e0ff7f7625db366751000018c298d0ca97df085b)

* make holoparasites actually holographic (#26862)

it's over

(cherry picked from commit c6ef37cc5f717116a59f465daa119d023a15c262)

* Automatic changelog update

(cherry picked from commit 734b6f321d83541c9282bc264a2512f95a686ccb)

* Add character sheets to board game crate (#26926)

add character sheets to board game crate

(cherry picked from commit 7a86b1d0977d51c3a43752a7a8bfe055fadadd56)

* Automatic changelog update

(cherry picked from commit 9752746775021f6076fdd88ae384c7df44ba858b)

* Game server admin API (#26880)

* Reapply "Game server api" (#26871)

This reverts commit 3aee19792391cbfb06edb65d6f16f77da0f36f13.

* Rewrite 75% of the code it's good now

(cherry picked from commit 9d0dfcf2b9fa1b6ba54b3aa26a3f41982b945323)

* Wield recoil components (#26915)

* WieldRecoilComponents

* WieldRecoilComponents

* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>

* Update Content.Shared/Weapons/Ranged/Components/GunWieldBonusComponent.cs

---------

Co-authored-by: Whisper <121047731+QuietlyWhisper@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
(cherry picked from commit a7fad5d43953ea679418d7c39272bf3224f7767f)

* Clown shoes make you waddle, as God intended (#26338)

* Clown shoes make you waddle, as God intended

* OOPS

* Toned down, client system name fix

* Tidy namespacing for @deltanedas

* Refactor to handle prediction better, etc.

* Resolve PR comments.

(cherry picked from commit ef42fb38061b29d7202eb6d65f1b658aecc43452)

* Automatic changelog update

(cherry picked from commit 055c5ab323168314095b45b532440508c1447303)

* Use round time instead of server time for criminal history (#26949)

make criminal records computer use round time for history instead of the server time

(cherry picked from commit fbec5d18cf175d9418fed77fcb38b673692771c6)

* Rotate and Offset station CCVar nuke (#26175)

* no content

* add noRot to Europa

* bruh. and this

* yay

* fix

(cherry picked from commit 44b20f60ff178813ebbc5b449229b0bbba81f649)

* Fixed cybersun pen attacking noise (#26951)

* Uupdated the cyberpen

* Updated noise

* Removed flashlight

(cherry picked from commit 0a29508f431e2f08213ca3d2554dc21a80c15061)

* Automatic changelog update

(cherry picked from commit 5270e6f5f9e639b2c55dc26e01b5a184f468101f)

* Revert "Game server admin API (#26880)"

This reverts commit 087e013406cdd45201ebd73056fdbe5852485658.

* fixes

* Fix rubber hammer being unshaded (#26956)

(cherry picked from commit cf8f68c7e51df1c11b38b0efffda39996a4c4e73)

* Make lockers can be deconstructed only when unlocked now (#26961)

* Fix lockers are not deconstrucable now

Lockers are was deconstructable event when they closed and you didn't have access to them. In short - get stuff by 5 seconds, 5 sec it's time to screw down any locker, except LockerBaseSecure one

* Revert un-destructable lockers fix

Make lockers destructable again

* Fix lockers that deconstructable only when unlocked now

(cherry picked from commit 7a6067989fde5b5e1b791d40d566adf7a74d203f)

* Automatic changelog update

(cherry picked from commit 563f304ac57a61bba618c8ec1ffec77151ffc687)

* nerf incendiary grenade (#26959)

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit 516f5f3161396f8a759d9b44c35d5e9d59f6249f)

* meatWall incorrect node fixed (#26966)

changed node in construction meatWall

(cherry picked from commit 41d2f06ffcfb8a67bfeb863ff24889e199971bb6)

* Automatic changelog update

(cherry picked from commit 637fc2d47571cec3c4d2509ac5aa285d2ccd2ab9)

* SS14-26950 Fix Waddling During Improper States (#26965)

* SS14-26950 Fix Waddling During Improper States

Fix some states when a clown can waddle when no clown should be able to waddle, no-matter their clowning powers.

1. You cannot waddle whilst weightless
2. You cannot waddle whilst stunned
3. You cannot waddle whilst slowed down due to stam damage
4. You cannot waddle whilst you're knocked down
5. You cannot waddle whilst you're buckled
6. You cannot waddle whilst crit
7. You cannot waddle whilst dead

There's some argument for being able to waddle whilst on the floor
and doing some bizarre floor-humping exercise but I'm not coding an animation layer system just to handle clowns doing the worm.

* Use a nicer "can move" check

(cherry picked from commit 3d0fc1067303635a743af04ef4c463abd0dba343)

* Mobs burn to ashes on excessive heat damage (#26971)

* mobs burn to ashes on excessive heat damage

* remove comment, remove random lines I didn't mean to add

* combine code into behavior

* clean unused

* fix namespace

* drop next to

* fix spawn entities behavior spawning entities outside container

(cherry picked from commit 4a6cf480cc557447a89ec98b6305b4b461f494ca)

* Fix dragon slowdown on damage (#26975)

Fix dragon slow on damage

(cherry picked from commit 34fbd2874e3f2602f8cf1cb55bf4c20588aa8764)

* Fix some airlocks with multiple access types (#26980)

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
(cherry picked from commit cd46282e51b0368b63344d655564315a709c7355)

* Automatic changelog update

(cherry picked from commit faba129e780859336cfa1a97bb492e7044348ed2)

* Fix some TryGetMind overrides relying on player data (#26992)

* Fix some TryGetMind overrides relying on player data

* A

* Rider has bamboozled me

* Update `data.Mind` before attaching to entity.

(cherry picked from commit 229caa10bf3417858d2cbd7c1290af12dcad4acc)

* Give names to solution & identity entities (#26993)

(cherry picked from commit faec39ced4d6a65b1ecbf373ba282202171f5224)

* Add QM maintenance airlock (#26982)

Co-authored-by: Velcroboy <velcroboy333@hotmail.com>
(cherry picked from commit b23ef00d37f1c1782e8ca2ff7f3cbea6cffefa7b)

* Loadouts redux (#25715)

* Loadouts redux

* Loadout window mockup

* More workout

* rent

* validation

* Developments

* bcs

* More cleanup

* Rebuild working

* Fix model and loading

* obsession

* efcore

* We got a stew goin

* Cleanup

* Optional + SeniorEngineering fix

* Fixes

* Update science.yml

* add

add

* Automatic naming

* Update nukeops

* Coming together

* Right now

* stargate

* rejig the UI

* weh

* Loadouts tweaks

* Merge conflicts + ordering fix

* yerba mate

* chocolat

* More updates

* Add multi-selection support

* test

h

* fikss

* a

* add tech assistant and hazard suit

* huh

* Latest changes

* add medical loadouts

* and science

* finish security loadouts

* cargo

* service done

* added wildcards

* add command

* Move restrictions

* Finalising

* Fix existing work

* Localise next batch

* clothing fix

* Fix storage names

* review

* the scooping room

* Test fixes

* Xamlify

* Xamlify this too

* Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

* ben

* Margins

---------

Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com>
Co-authored-by: Mr. 27 <koolthunder019@gmail.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>

(cherry picked from commit 12766fe6e37bb600a53693cfa5392892bc100685)

* Fix starting gear (#27008)

Slight blunder on the loadout prototype being used and all the names aligning means playtesting didn't catch it earlier.

Ideally player spawning code wouldn't have sucked so I could add tests like I wanted but it is what it is.

(cherry picked from commit 9bc3e076288c4e4ede6e757aa59ded9cef413340)

* Reduce clown snore volume (#27012)

* reduced gain by 25dB

* changed volume again

---------

Co-authored-by: Martin Petkovski <63034378+martin69420@users.noreply.github.com>
(cherry picked from commit 11207a06490e593acab1121a9db200931b672c32)

* Automatic changelog update

(cherry picked from commit 9102a065a98faf4501f79e0fefc23d3dcc269c11)

* Add changelog for loadouts (#27020)

(cherry picked from commit 46cfd63c4fcba29c5fb62a9c13b269c9b39508fa)

* Automatic changelog update

(cherry picked from commit 33888b64d6a128823b60c6ce228726ce38a69d31)

* Fix senior ID cards and other loadout shit (#27017)

* remove senior backpacks

* fix ID cards

* Update atmospheric_technician.yml

(cherry picked from commit e2be85bc52b51b7c736b2f0c7bfb4a2a0fc39e71)

* Automatic changelog update

(cherry picked from commit 5e7f2244fc52a3a0d1a0e6ea41800c1abf482a47)

* fixes 1

* fixed double SlowOnDamage component

* may be fixed

* update engine

* fix db

* fix tts

* fix setup gui

* fix sponsor loadouts

* fix: fix loadout gui

* Fix potted plant popup/sfx spam (#26901)

Fixed potted plant hide popup/sfx spam.

(cherry picked from commit 8e9d2744f3d196fc11e88a4755f98cac8ad8dbee)

* Fix StepTrigger blacklist not working (#26968)

(cherry picked from commit 7810cbe41101fe152c6b30fea937cc4351d4ae29)

* Re-add IAdminRemarksCommon to DB model for SS14.Admin (#27028)

This was removed in #25280 as the relevant DB entities didn't go outside the DB layer anymore. SS14.Admin however still uses them directly (as it only supports Postgres), so the interface is still useful there.

(cherry picked from commit bbf0505fdc0f49a40a66473296f912cb4d580cb9)

* Add autism pins to loadout (#27034)

add autism pins to loadout

(cherry picked from commit d5b7e4baf2b867ea184c6566b1e00b5f1384867e)

* Add winter coats and shoes to loadouts (#27022)

* inital

* Update loadout-groups.ftl

* fix order

* add winter boots

* fix test fails

(cherry picked from commit 34fa48bff9b9af20843152debd40f4a1b0226d3a)

* Automatic changelog update

(cherry picked from commit 30f73cfb6cd0565816801d860b816ce5a0a5e739)

* LobbyUI fixes (#27033)

* LobbyUI fixes

I have no idea which were bugs prior but anyway fix stuff.

* More fixes

* Test moment

(cherry picked from commit fcd6c25242c195266c3b4c4aa2ed78922683567c)

* fix: fix loadout prototype

* fix: fix music label

* fix: add title and artist to lobby music metadata

* Automatic changelog update

(cherry picked from commit 4fa245f723dffc64b7f5fd1ffaec5f5b498fb48a)

* Automatic changelog update

(cherry picked from commit 5ee597d98c73af26ecf3985ea921b350fa834c53)

* Add Nun Hood to Chaplain loadout options (#27025)

Adds Nun Hood to starting Chaplain loadout options

(cherry picked from commit 7114b1939c82acb593dad32f8bf34d557b8e722e)

* Automatic changelog update

(cherry picked from commit f825e5e38bbfc517e40f9ea99ce075187a9310b5)

* add ancient jumpsuit to passenger loadout (#27035)

inital

(cherry picked from commit 5742dee84a04623e2b2626f49b6ab303863e2575)

* update engine 2

* Added Jukebox (#26736)

* Added Jukebox, along with music for jukebox

* Fixed Jukebox meta.json copyright

* Removed songs I couldn't find a license for.

* Renamed files to solve check failures from spaces

* Added missing attributions.yml

* Fixed lack of description in Jukebox

* Jukebox is now constructable.

* Change Jukebox menu to FancyWindow

* Moved Jukebox messages out of jukebox component

* Removed Jukebox OnValueChanged.

* JukeboxComp now uses AutoGenerateComponentState

* Removed state code, since it's auto generated

* Fixed various Jukebox code to match conventions.

* Updated Standard.yml to match changed song list.

* fixes

* Jukebox workin

* Fix

* Polishing

* Finalising

* Revert

* bad

* jukey

* Reviews

* name

* Update submodule to 218.2.0

---------

Co-authored-by: iNVERTED <alextjorgensen@gmail.com>

(cherry picked from commit 2db374988c91c6ce5f932b9cee1ba251cbfb22e5)

* fix: fix loadout prototype

* fix: fix jukebox

* Add jani gloves loadout (#27011)

(cherry picked from commit e2341c0089a136deec40520595914086c428f466)

* Mobs auto state handlers (#26957)

* Autogenerate MobStateComponentState

* changed CurrentState to DataField, updated DataField attribute for AllowedStates

* Update Content.Shared/Mobs/Components/MobStateComponent.cs

* Update Content.Shared/Mobs/Components/MobStateComponent.cs

---------

Co-authored-by: BuildTools <unconfigured@null.spigotmc.org>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
(cherry picked from commit 5b8468c9d8463e68dbf4b40226895cfed7d7e143)

* Fix capitalization for pirates and rats (#26644)

* Fix capitalization for pirates and rats

* Deal with replacements better

* Be smarter about caps

* Do last word properly

* Variables named a bit better

* Fix Consistency

* Undo change that's not needed anymore

* Fix up pirate since it doesn't need to check early either

* Make mobster replacin' a bit better anyway

* Remove extra space

* Use capture groups for mobster in', add comments for first and last words

* Slightly more clarification with comments

(cherry picked from commit 432e6ec45dedd2982d0dcea0fe30945113835705)

* Automatic changelog update

(cherry picked from commit 03f51ca3aa9ed71aa99476e3a4d256b17b78be53)

* Fix character preview not updating on character change (#27043)

I love lobby code :3

(cherry picked from commit f9f204a6d03e08cb0d189f7d64c031a025711430)

* Automatic changelog update

(cherry picked from commit 0785516eacfdc06a0c568fdd96d7c1a291efd557)

* Fixed gloved weapons being able to attack when not equipped. (#26762)

* Initial commit. No evil hidden files this time :)

* Added newline because I forgot :(

* We <3 tags :)

* Fixed! Works now

* Update Content.Shared/Weapons/Melee/MeleeWeaponComponent.cs

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
(cherry picked from commit afcdc8b8668a0504fd0392eaa1b6dcd36b4f1108)

* Automatic changelog update

(cherry picked from commit 9f461dec5a281adab5634bed4e587a0458c82b6e)

* Make cargo pallets smoothing with eachother (#27049)

(cherry picked from commit 24390ef51bdf00f65d199d74300504391a83c521)

* Revert "Update .editorconfig to correspond Code Conventions" (#27051)

Revert "Update .editorconfig to correspond Code Conventions (#26824)"

This reverts commit 882aeb03143d07a4cef91412008c81c9902075d8.

(cherry picked from commit ef72d3cf7fac8ed8c21d1ea43c7740a2c49ccf03)

* Show volume on the gas analyzer (#25720)

The gas analyzer now shows the volume of pipes, tanks, canisters and the environment.

Adjust gas analyzers so that the volume and number of moles shown corresponds to only the scanned element, e.g. a canister or single pipe in a pipenet.

(cherry picked from commit 5a5efa11cf1d68708a70d48a286cb97fde96afdf)

* Automatic changelog update

(cherry picked from commit 20b16944adc63d810186c18ed2a4cafc7c026a48)

* Navmap rework (#26713)

* Optimized the drawing of lines and tracked entities

* Optimized nav map updating and added thin wall support

* Added support for thin doors

* Removed floor tile seams, more line drawing optimizations

* Fixed split grids not updating correctly

* Cleaned up NavMapControl code

* Fix nav map header

* Converted nav map updates from system network messages to delta-states

* Addressed review comments

* Fixed timing issue where NavMapSystem would update before AirtightSystem did

(cherry picked from commit 009d06d97833b7700bcfaa534b20575fdbbd7db5)

* fland update (#27064)

* fland update

* n2 lockers

(cherry picked from commit e0589a1350db5f129c8b5ab5950bc8932cfdefb1)

* meta update (#27067)

(cherry picked from commit f2ed5085628a06813d7831bad826782633d06a7d)

* saltern update (#26507)

* saltern update

* update atmos too

* run fixgridatmos

* fix power outside botany, fix botany air alarm

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit d08600240f81d86dee98c51823cebfb094637c1b)

* atlas artifact  update (#26506)

atlas update

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit 46733616df769c04244c778aa2e6dd010b02886b)

* prop hunt ss14 (real) (#26691)

* texture appropriation

* add code for projector

* add chameleon projector yml

* damage and actions

* prevent small props being killed round removing you (700 damage from a single shot)

* tweak default

* oop

* do appearance properly, need engine update

* fix bugs, blacklist pda

* remove status icons

* amou

* sus

* fix test + make props fast

* amouuuung

* remove funny log

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
(cherry picked from commit 395c33024cb9d4e80078e3ab3c70c1e92e7fd5bc)

* Automatic changelog update

(cherry picked from commit a376f4784a5f14f1587a47d1c10327327f78aaf6)

* box update (#27069)

* box update

* argh

(cherry picked from commit da136826e7d1ca51f8f2d51d7f16d976a493805a)

* Fix PDA and ID card data not getting set on loadouts (#27062)

(cherry picked from commit a95fc86f7a73c95383bcc41cb2cf63c4fe4ce575)

* Automatic changelog update

(cherry picked from commit d2dc0734df6a686f2415134183ec019097876376)

* Standardize HoS/Warden winter coats, add unarmored variants for printing (#24865)

* Both winter coats with same armor as their counterparts

* Matching description for HoS's, unarmored variants for balancing the uniform printer

* Forgot some text

* New sprite provided by PursuitinAshes, old sprite moved to unarmored version

* Removed the 'unarmored' specifier, in line with the rest of the winter coats

* Remove unarmored warden, no sprite

* Re-implemented the warden's unarmored coat, with sprites from Dutch-VanDerLinde

* CRLF to LF

* Move armor values to abstract

(cherry picked from commit d67d782f99f5d26558dbafaf43ec4b95202d16d6)

* Chances of triggering effects (#27056)

* electrocution

* slippery

* flashibg

* Update SlipperyComponent.cs

* Update SlipperySystem.cs

(cherry picked from commit 5659edd207c36b1148ba675204c74f94821abb87)

* Automatic changelog update

(cherry picked from commit 92089da19240f20ced7fd91f09677ac3edabe799)

* cargo console radio messages on approving (#27038)

* 1

* void --> "Unknown"

(cherry picked from commit fd109d61b88468a1b623975ce7258462fc40b7f6)

* Automatic changelog update

(cherry picked from commit 45cefc9643d447e9594fd683ec1cb5312437d78a)

* Low-Key Zombie Rebalance (#27060)

initial soft zombie rebalance - lower infection chance, damage, and chance of outbreak

Co-authored-by: Bellwether <null>
(cherry picked from commit 8213c89fdb127eba36c2c5d44e92bdae62308f2b)

* Automatic changelog update

(cherry picked from commit d76211514bc5697d5c1b7c93309704f73d6083b2)

* Partially reverts the remote signaller resprite (#27073)

de-sprites the remote signaller

(cherry picked from commit 66f32d428959a1c8b5b17bd0f1346bdfbc34e8f0)

* fix soap popup (#27054)

fix soap

(cherry picked from commit 4453fe50cfdd380f76f2c41d314e6c7ecce729fb)

* Automatic changelog update

(cherry picked from commit ac8d3d55ccc524f5972619502d7928f88764987e)

* add greysec loadout (#27023)

* inital

* George orwell

(cherry picked from commit 06ecf2af052b11cdc82c000be77b916109197ada)

* Automatic changelog update

(cherry picked from commit 8d64d2bc1e49b9fd2b2679e6a03a9bec59c5c3b8)

* Add ability to shake fizzy drinks so they spray in peoples' faces (#25574)

* Implemented Shakeable

* Prevent shaking open Openables

* Prevent shaking empty drinks. Moved part of DrinkSystem to Shared.

* DrinkSystem can have a little more prediction, as a treat

* Cleanup

* Overhauled PressurizedDrink

* Make soda cans/bottles and champagne shakeable. The drink shaker too, for fun.

* We do a little refactoring.
PressurizedDrink is now PressurizedSolution, and fizziness now only works on solutions containing a reagent marked as fizzy.

* Documentation, cleanup, and tweaks.

* Changed fizziness calculation to use a cubic-out easing curve.

* Removed broken YAML that has avoid the linter's wrath for far too long

* Changed reagent fizzy bool to fizziness float.
Solution fizzability now scales with reagent proportion.

* Rename file to match changed class name

* DoAfter improvements. Cancel if the user moves away; block if no hands.

* Match these filenames too

* And this one

* guh

* Updated to use Shared puddle methods

* Various fixes and improvements.

* Made AttemptShakeEvent a struct

* AttemptAddFizzinessEvent too

(cherry picked from commit cfa94be4c2044146298d07c703f3b71bc377ca63)

* Automatic changelog update

(cherry picked from commit b672ea73f00a6f760f8fee720c1fe1f4160523fa)

* Add Hardhats to Station Engineer Loadouts (#27044)

* Add hardhats to loadout.

* Update Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml

* Update Resources/Prototypes/Loadouts/loadout_groups.yml

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

(cherry picked from commit 3c1ec9ac3435600f547c30d77512744b003a1f27)

* Fix loadouts UI not refreshing on char change (#27075)

I love lobby code. Refreshing the entire jobs UI doesn't seem to cause issues. At least jobpriorityselector was my fault when I was far fucking stupider writing this shit.

(cherry picked from commit 9b1a8b07d734853c8cfcb83a6e2b7aad4e6dd764)

* fixes

* fixed translation for jukebox

* loadout improved part 1

* Fucking prototypes

* loadout improved part 2

* fucking prototypes for fucking loadouts

* remove hardcoded equipment

* loadouts finally done

* fixes

* Clown shoes make you waddle, as God intended (#26338)

* Clown shoes make you waddle, as God intended

* OOPS

* Toned down, client system name fix

* Tidy namespacing for @deltanedas

* Refactor to handle prediction better, etc.

* Resolve PR comments.

* fixes build

* better timings for clown animation

* new jukebox music and songs

* Maps jukeboxes update

* Emergency toolbox fill rework (#29202)

* emergency toolbox fill rework

* Fuck

---------

Co-authored-by: Simon <63975668+simyon264@users.noreply.github.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: Джексон Миссиссиппи <tripwiregamer@gmail.com>
Co-authored-by: PJBot <pieterjan.briers+bot@gmail.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
Co-authored-by: Flareguy <78941145+flareguy@users.noreply.github.com>
Co-authored-by: Ghagliiarghii <68826635+Ghagliiarghii@users.noreply.github.com>
Co-authored-by: Jark255 <jaroslav.asanov@gmail.com>
Co-authored-by: chromiumboy <50505512+chromiumboy@users.noreply.github.com>
Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
Co-authored-by: lunarcomets <140772713+lunarcomets@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Verm <32827189+Vermidia@users.noreply.github.com>
Co-authored-by: Token <esil.bektay@yandex.com>
Co-authored-by: liltenhead <104418166+liltenhead@users.noreply.github.com>
Co-authored-by: Brandon Hu <103440971+Brandon-Huu@users.noreply.github.com>
Co-authored-by: Velcroboy <107660393+IamVelcroboy@users.noreply.github.com>
Co-authored-by: Nemanja <98561806+emogarbage404@users.noreply.github.com>
Co-authored-by: Ko4ergaPunk <62609550+Ko4ergaPunk@users.noreply.github.com>
Co-authored-by: TsjipTsjip <19798667+TsjipTsjip@users.noreply.github.com>
Co-authored-by: DrSmugleaf <drsmugleaf@gmail.com>
Co-authored-by: osjarw <62134478+osjarw@users.noreply.github.com>
Co-authored-by: Vasilis <vasilis@pikachu.systems>
Co-authored-by: DrSmugleaf <10968691+DrSmugleaf@users.noreply.github.com>
Co-authored-by: deathride58 <deathride58@users.noreply.github.com>
Co-authored-by: FungiFellow <151778459+FungiFellow@users.noreply.github.com>
Co-authored-by: ShadowCommander <shadowjjt@gmail.com>
Co-authored-by: Boaz1111 <149967078+Boaz1111@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: superjj18 <gagnonjake@gmail.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: GreaseMonk <1354802+GreaseMonk@users.noreply.github.com>
Co-authored-by: Terraspark4941 <terraspark4941@gmail.com>
Co-authored-by: BramvanZijp <56019239+BramvanZijp@users.noreply.github.com>
Co-authored-by: Tyzemol <85772526+Tyzemol@users.noreply.github.com>
Co-authored-by: Froffy025 <78222136+Froffy025@users.noreply.github.com>
Co-authored-by: Hannah Giovanna Dawson <karakkaraz@gmail.com>
Co-authored-by: ilya.mikheev.coder <imc-ext+github@ilyamikcoder.com>
Co-authored-by: Ed <96445749+theshued@users.noreply.github.com>
Co-authored-by: beck-thompson <107373427+beck-thompson@users.noreply.github.com>
Co-authored-by: lzk <124214523+lzk228@users.noreply.github.com>
Co-authored-by: HS <81934438+HolySSSS@users.noreply.github.com>
Co-authored-by: Whisper <121047731+quietlywhisper@users.noreply.github.com>
Co-authored-by: MilenVolf <63782763+MilenVolf@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: tosatur <63034378+tosatur@users.noreply.github.com>
Co-authored-by: rhailrake <splatt.pr@gmail.com>
Co-authored-by: no <165581243+pissdemon@users.noreply.github.com>
Co-authored-by: Doctor-Cpu <77215380+Doctor-Cpu@users.noreply.github.com>
Co-authored-by: Bellwether <157836624+bellwetherlogic@users.noreply.github.com>
Co-authored-by: Alfred Baumann <93665570+CheesePlated@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
Co-authored-by: Emisse <99158783+Emisse@users.noreply.github.com>
Co-authored-by: Krunklehorn <42424291+krunklehorn@users.noreply.github.com>
Co-authored-by: icekot8 <93311212+icekot8@users.noreply.github.com>
Co-authored-by: MisterMecky <mrmecky@hotmail.com>
Co-authored-by: Pancake <pangogie@users.noreply.github.com>
Co-authored-by: Jabak <163307958+Jabaks@users.noreply.github.com>
Co-authored-by: PointPNG <edvard232005@gmail.com>
This commit is contained in:
ThereDrD0
2024-07-16 18:30:56 +03:00
committed by GitHub
parent c044e4f964
commit 192d2d5de7
934 changed files with 43263 additions and 27201 deletions

View File

@@ -58,7 +58,7 @@ public class SpawnEquipDeleteBenchmark
for (var i = 0; i < N; i++)
{
_entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
_spawnSys.EquipStartingGear(_entity, _gear, null);
_spawnSys.EquipStartingGear(_entity, _gear);
server.EntMan.DeleteEntity(_entity);
}
});

View File

@@ -163,6 +163,26 @@ namespace Content.Client.Atmos.UI
parent.AddChild(panel);
panel.AddChild(dataContainer);
// Volume label
var volBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };
volBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-volume-text")
});
volBox.AddChild(new Control
{
MinSize = new Vector2(10, 0),
HorizontalExpand = true
});
volBox.AddChild(new Label
{
Text = Loc.GetString("gas-analyzer-window-volume-val-text", ("volume", $"{gasMix.Volume:0.##}")),
Align = Label.AlignMode.Right,
HorizontalExpand = true
});
dataContainer.AddChild(volBox);
// Pressure label
var presBox = new BoxContainer { Orientation = BoxContainer.LayoutOrientation.Horizontal };

View File

@@ -0,0 +1,119 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Audio;
using Robust.Client.Player;
using Robust.Shared.Audio.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Audio.Jukebox;
public sealed class JukeboxBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[ViewVariables]
private JukeboxMenu? _menu;
public JukeboxBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_menu = new JukeboxMenu();
_menu.OnClose += Close;
_menu.OpenCentered();
_menu.OnPlayPressed += args =>
{
if (args)
{
SendMessage(new JukeboxPlayingMessage());
}
else
{
SendMessage(new JukeboxPauseMessage());
}
};
_menu.OnStopPressed += () =>
{
SendMessage(new JukeboxStopMessage());
};
_menu.OnSongSelected += SelectSong;
_menu.SetTime += SetTime;
PopulateMusic();
Reload();
}
/// <summary>
/// Reloads the attached menu if it exists.
/// </summary>
public void Reload()
{
if (_menu == null || !EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox))
return;
_menu.SetAudioStream(jukebox.AudioStream);
if (_protoManager.TryIndex(jukebox.SelectedSongId, out var songProto))
{
var length = EntMan.System<AudioSystem>().GetAudioLength(songProto.Path.Path.ToString());
_menu.SetSelectedSong(songProto.Name, (float) length.TotalSeconds);
}
else
{
_menu.SetSelectedSong(string.Empty, 0f);
}
}
public void PopulateMusic()
{
_menu?.Populate(_protoManager.EnumeratePrototypes<JukeboxPrototype>());
}
public void SelectSong(ProtoId<JukeboxPrototype> songid)
{
SendMessage(new JukeboxSelectedMessage(songid));
}
public void SetTime(float time)
{
var sentTime = time;
// You may be wondering, what the fuck is this
// Well we want to be able to predict the playback slider change, of which there are many ways to do it
// We can't just use SendPredictedMessage because it will reset every tick and audio updates every frame
// so it will go BRRRRT
// Using ping gets us close enough that it SHOULD, MOST OF THE TIME, fall within the 0.1 second tolerance
// that's still on engine so our playback position never gets corrected.
if (EntMan.TryGetComponent(Owner, out JukeboxComponent? jukebox) &&
EntMan.TryGetComponent(jukebox.AudioStream, out AudioComponent? audioComp))
{
audioComp.PlaybackPosition = time;
}
SendMessage(new JukeboxSetTimeMessage(sentTime));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (_menu == null)
return;
_menu.OnClose -= Close;
_menu.Dispose();
_menu = null;
}
}

View File

@@ -0,0 +1,18 @@
<ui:FancyWindow xmlns="https://spacestation14.io" xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="400 500" Title="{Loc 'jukebox-menu-title'}">
<BoxContainer Margin="4 0" Orientation="Vertical">
<ItemList Name="MusicList" SelectMode="Button" Margin="3 3 3 3"
HorizontalExpand="True" VerticalExpand="True" SizeFlagsStretchRatio="8"/>
<BoxContainer Orientation="Vertical">
<Label Name="SongSelected" Text="{Loc 'jukebox-menu-selectedsong'}" />
<Label Name="SongName" Text="---" />
<Slider Name="PlaybackSlider" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True"
VerticalExpand="False" SizeFlagsStretchRatio="1">
<Button Name="PlayButton" Text="{Loc 'jukebox-menu-buttonplay'}" />
<Button Name="StopButton" Text="{Loc 'jukebox-menu-buttonstop'}" />
<Label Name="DurationLabel" Text="00:00 / 00:00" HorizontalAlignment="Right" HorizontalExpand="True"/>
</BoxContainer>
</BoxContainer>
</ui:FancyWindow>

View File

@@ -0,0 +1,162 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Audio.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
namespace Content.Client.Audio.Jukebox;
[GenerateTypedNameReferences]
public sealed partial class JukeboxMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _entManager = default!;
private AudioSystem _audioSystem;
/// <summary>
/// Are we currently 'playing' or paused for the play / pause button.
/// </summary>
private bool _playState;
/// <summary>
/// True if playing, false if paused.
/// </summary>
public event Action<bool>? OnPlayPressed;
public event Action? OnStopPressed;
public event Action<ProtoId<JukeboxPrototype>>? OnSongSelected;
public event Action<float>? SetTime;
private EntityUid? _audio;
private float _lockTimer;
public JukeboxMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_audioSystem = _entManager.System<AudioSystem>();
MusicList.OnItemSelected += args =>
{
var entry = MusicList[args.ItemIndex];
if (entry.Metadata is not string juke)
return;
OnSongSelected?.Invoke(juke);
};
PlayButton.OnPressed += args =>
{
OnPlayPressed?.Invoke(!_playState);
};
StopButton.OnPressed += args =>
{
OnStopPressed?.Invoke();
};
PlaybackSlider.OnReleased += PlaybackSliderKeyUp;
SetPlayPauseButton(_audioSystem.IsPlaying(_audio), force: true);
}
public JukeboxMenu(AudioSystem audioSystem)
{
_audioSystem = audioSystem;
}
public void SetAudioStream(EntityUid? audio)
{
_audio = audio;
}
private void PlaybackSliderKeyUp(Slider args)
{
SetTime?.Invoke(PlaybackSlider.Value);
_lockTimer = 0.5f;
}
/// <summary>
/// Re-populates the list of jukebox prototypes available.
/// </summary>
public void Populate(IEnumerable<JukeboxPrototype> jukeboxProtos)
{
MusicList.Clear();
foreach (var entry in jukeboxProtos)
{
MusicList.AddItem(entry.Name, metadata: entry.ID);
}
}
public void SetPlayPauseButton(bool playing, bool force = false)
{
if (_playState == playing && !force)
return;
_playState = playing;
if (playing)
{
PlayButton.Text = Loc.GetString("jukebox-menu-buttonpause");
return;
}
PlayButton.Text = Loc.GetString("jukebox-menu-buttonplay");
}
public void SetSelectedSong(string name, float length)
{
SetSelectedSongText(name);
PlaybackSlider.MaxValue = length;
PlaybackSlider.SetValueWithoutEvent(0);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_lockTimer > 0f)
{
_lockTimer -= args.DeltaSeconds;
}
if (_entManager.TryGetComponent(_audio, out AudioComponent? audio))
{
DurationLabel.Text = $@"{TimeSpan.FromSeconds(audio.PlaybackPosition):mm\:ss} / {_audioSystem.GetAudioLength(audio.FileName):mm\:ss}";
}
else
{
DurationLabel.Text = $"00:00 / 00:00";
}
if (PlaybackSlider.Grabbed)
return;
if (audio != null || _entManager.TryGetComponent(_audio, out audio))
{
PlaybackSlider.SetValueWithoutEvent(audio.PlaybackPosition);
}
else
{
PlaybackSlider.SetValueWithoutEvent(0f);
}
SetPlayPauseButton(_audioSystem.IsPlaying(_audio, audio));
}
public void SetSelectedSongText(string? text)
{
if (!string.IsNullOrEmpty(text))
{
SongName.Text = text;
}
else
{
SongName.Text = "---";
}
}
}

View File

@@ -0,0 +1,153 @@
using Content.Shared.Audio.Jukebox;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Audio.Jukebox;
public sealed class JukeboxSystem : SharedJukeboxSystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JukeboxComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<JukeboxComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<JukeboxComponent, AfterAutoHandleStateEvent>(OnJukeboxAfterState);
_protoManager.PrototypesReloaded += OnProtoReload;
}
public override void Shutdown()
{
base.Shutdown();
_protoManager.PrototypesReloaded -= OnProtoReload;
}
private void OnProtoReload(PrototypesReloadedEventArgs obj)
{
if (!obj.WasModified<JukeboxPrototype>())
return;
var query = AllEntityQuery<JukeboxComponent, UserInterfaceComponent>();
while (query.MoveNext(out _, out var ui))
{
if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
baseBui is not JukeboxBoundUserInterface bui)
{
continue;
}
bui.PopulateMusic();
}
}
private void OnJukeboxAfterState(Entity<JukeboxComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!TryComp(ent, out UserInterfaceComponent? ui))
return;
if (!ui.OpenInterfaces.TryGetValue(JukeboxUiKey.Key, out var baseBui) ||
baseBui is not JukeboxBoundUserInterface bui)
{
return;
}
bui.Reload();
}
private void OnAnimationCompleted(EntityUid uid, JukeboxComponent component, AnimationCompletedEvent args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite))
return;
if (!TryComp<AppearanceComponent>(uid, out var appearance) ||
!_appearanceSystem.TryGetData<JukeboxVisualState>(uid, JukeboxVisuals.VisualState, out var visualState, appearance))
{
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, sprite);
}
private void OnAppearanceChange(EntityUid uid, JukeboxComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!args.AppearanceData.TryGetValue(JukeboxVisuals.VisualState, out var visualStateObject) ||
visualStateObject is not JukeboxVisualState visualState)
{
visualState = JukeboxVisualState.On;
}
UpdateAppearance(uid, visualState, component, args.Sprite);
}
private void UpdateAppearance(EntityUid uid, JukeboxVisualState visualState, JukeboxComponent component, SpriteComponent sprite)
{
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
switch (visualState)
{
case JukeboxVisualState.On:
SetLayerState(JukeboxVisualLayers.Base, component.OnState, sprite);
break;
case JukeboxVisualState.Off:
SetLayerState(JukeboxVisualLayers.Base, component.OffState, sprite);
break;
case JukeboxVisualState.Select:
PlayAnimation(uid, JukeboxVisualLayers.Base, component.SelectState, 1.0f, sprite);
break;
}
}
private void PlayAnimation(EntityUid uid, JukeboxVisualLayers layer, string? state, float animationTime, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
if (!_animationPlayer.HasRunningAnimation(uid, state))
{
var animation = GetAnimation(layer, state, animationTime);
sprite.LayerSetVisible(layer, true);
_animationPlayer.Play(uid, animation, state);
}
}
private static Animation GetAnimation(JukeboxVisualLayers layer, string state, float animationTime)
{
return new Animation
{
Length = TimeSpan.FromSeconds(animationTime),
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = layer,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state, 0f)
}
}
}
};
}
private void SetLayerState(JukeboxVisualLayers layer, string? state, SpriteComponent sprite)
{
if (string.IsNullOrEmpty(state))
return;
sprite.LayerSetVisible(layer, true);
sprite.LayerSetAutoAnimated(layer, true);
sprite.LayerSetState(layer, state);
}
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Body.Components;
using Content.Shared.CardboardBox;
using Content.Shared.CardboardBox.Components;
using Content.Shared.Examine;
@@ -13,9 +14,14 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
private EntityQuery<BodyComponent> _bodyQuery;
public override void Initialize()
{
base.Initialize();
_bodyQuery = GetEntityQuery<BodyComponent>();
SubscribeNetworkEvent<PlayBoxEffectMessage>(OnBoxEffect);
}
@@ -59,6 +65,10 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem
if (!_examine.InRangeUnOccluded(sourcePos, mapPos, box.Distance, null))
continue;
// no effect for anything too exotic
if (!_bodyQuery.HasComp(mob))
continue;
var ent = Spawn(box.Effect, mapPos);
if (!xformQuery.TryGetComponent(ent, out var entTransform) || !TryComp<SpriteComponent>(ent, out var sprite))

View File

@@ -0,0 +1,31 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Inventory.Events;
namespace Content.Client.Clothing.Systems;
public sealed class WaddleClothingSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<WaddleWhenWornComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<WaddleWhenWornComponent, GotUnequippedEvent>(OnGotUnequipped);
}
private void OnGotEquipped(EntityUid entity, WaddleWhenWornComponent comp, GotEquippedEvent args)
{
var waddleAnimComp = EnsureComp<WaddleAnimationComponent>(args.Equipee);
waddleAnimComp.AnimationLength = comp.AnimationLength;
waddleAnimComp.HopIntensity = comp.HopIntensity;
waddleAnimComp.RunAnimationLengthMultiplier = comp.RunAnimationLengthMultiplier;
waddleAnimComp.TumbleIntensity = comp.TumbleIntensity;
}
private void OnGotUnequipped(EntityUid entity, WaddleWhenWornComponent comp, GotUnequippedEvent args)
{
RemComp<WaddleAnimationComponent>(args.Equipee);
}
}

View File

@@ -1,5 +1,6 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Atmos;
using Content.Client.UserInterface.Controls;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
@@ -79,7 +80,7 @@ namespace Content.Client.HealthAnalyzer.UI
);
Temperature.Text = Loc.GetString("health-analyzer-window-entity-temperature-text",
("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - 273f:F1} °C ({msg.Temperature:F1} °K)")
("temperature", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - Atmospherics.T0C:F1} °C ({msg.Temperature:F1} K)")
);
BloodLevel.Text = Loc.GetString("health-analyzer-window-entity-blood-level-text",

View File

@@ -10,6 +10,7 @@ using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.Clickable;
using Content.Client.Options;
using Content.Client.Eui;
using Content.Client.GhostKick;
using Content.Client.Info;
@@ -22,10 +23,14 @@ using Content.Client.Fullscreen;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Administration;
using Content.Shared.Administration.Logs;
using Content.Shared.Module;
using Content.Client.Guidebook;
using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC
{
@@ -33,26 +38,29 @@ namespace Content.Client.IoC
{
public static void Register()
{
IoCManager.Register<IParallaxManager, ParallaxManager>();
IoCManager.Register<IChatManager, ChatManager>();
IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
IoCManager.Register<IStylesheetManager, StylesheetManager>();
IoCManager.Register<IScreenshotHook, ScreenshotHook>();
IoCManager.Register<FullscreenHook, FullscreenHook>();
IoCManager.Register<IClickMapManager, ClickMapManager>();
IoCManager.Register<IClientAdminManager, ClientAdminManager>();
IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
IoCManager.Register<EuiManager, EuiManager>();
IoCManager.Register<IVoteManager, VoteManager>();
IoCManager.Register<ChangelogManager, ChangelogManager>();
IoCManager.Register<RulesManager, RulesManager>();
IoCManager.Register<ViewportManager, ViewportManager>();
IoCManager.Register<ISharedAdminLogManager, SharedAdminLogManager>();
IoCManager.Register<GhostKickManager>();
IoCManager.Register<ExtendedDisconnectInformationManager>();
IoCManager.Register<JobRequirementsManager>();
IoCManager.Register<DocumentParsingManager>();
IoCManager.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
var collection = IoCManager.Instance!;
collection.Register<IParallaxManager, ParallaxManager>();
collection.Register<IChatManager, ChatManager>();
collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
collection.Register<IStylesheetManager, StylesheetManager>();
collection.Register<IScreenshotHook, ScreenshotHook>();
collection.Register<FullscreenHook, FullscreenHook>();
collection.Register<IClickMapManager, ClickMapManager>();
collection.Register<IClientAdminManager, ClientAdminManager>();
collection.Register<ISharedAdminManager, ClientAdminManager>();
collection.Register<EuiManager, EuiManager>();
collection.Register<IVoteManager, VoteManager>();
collection.Register<ChangelogManager, ChangelogManager>();
collection.Register<RulesManager, RulesManager>();
collection.Register<ViewportManager, ViewportManager>();
collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
collection.Register<GhostKickManager>();
collection.Register<ExtendedDisconnectInformationManager>();
collection.Register<JobRequirementsManager>();
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
//WD-EDIT
IoCManager.Register<JoinQueueManager>();

View File

@@ -30,6 +30,9 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
if (comp.SpriteStateMap.TryGetValue(state, out var spriteState))
args.Sprite.LayerSetState(PoweredLightLayers.Base, spriteState);
if (args.Sprite.LayerExists(PoweredLightLayers.Glow))
args.Sprite.LayerSetVisible(PoweredLightLayers.Glow, state == PoweredLightState.On);
SetBlinkingAnimation(
uid,
state == PoweredLightState.On

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using Content.Client._Ohio.Buttons;
using Content.Client.Audio;
using Content.Client.Changelog;
using Content.Client.GameTicking.Managers;
using Content.Client.LateJoin;
@@ -40,6 +41,7 @@ namespace Content.Client.Lobby
[ViewVariables] private CharacterSetupGui? _characterSetup;
private ClientGameTicker _gameTicker = default!;
private ContentAudioSystem _contentAudioSystem = default!;
protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
@@ -57,7 +59,8 @@ namespace Content.Client.Lobby
var chatController = _userInterfaceManager.GetUIController<ChatUIController>();
_gameTicker = _entityManager.System<ClientGameTicker>();
_contentAudioSystem = _entityManager.System<ContentAudioSystem>();
_contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
_characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager,
_prototypeManager, _configurationManager);
@@ -71,13 +74,19 @@ namespace Content.Client.Lobby
_characterSetup.CloseButton.OnPressed += _ =>
{
// Reset sliders etc.
_characterSetup?.UpdateControls();
var controller = _userInterfaceManager.GetUIController<LobbyUIController>();
controller.SetClothes(true);
controller.UpdateProfile();
_lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
};
_characterSetup.SaveButton.OnPressed += _ =>
{
_characterSetup.Save();
//_lobby.CharacterPreview.UpdateUI();
_userInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
};
LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
@@ -110,6 +119,7 @@ namespace Content.Client.Lobby
_gameTicker.InfoBlobUpdated -= UpdateLobbyUi;
_gameTicker.LobbyStatusUpdated -= LobbyStatusUpdated;
_gameTicker.LobbyLateJoinStatusUpdated -= LobbyLateJoinStatusUpdated;
_contentAudioSystem.LobbySoundtrackChanged -= UpdateLobbySoundtrackInfo;
_voteManager.ClearPopupContainer();
@@ -233,10 +243,51 @@ namespace Content.Client.Lobby
}
_lobby!.LabelName.SetMarkup("[font=\"Bedstead\" size=20] White Dream [/font]");
_lobby!.Version.SetMarkup("Version: 1.0");
_lobby!.ChangelogLabel.SetMarkup("Список изменений:");
}
private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)
{
if (ev.SoundtrackFilename == null)
{
_lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
}
else if (
ev.SoundtrackFilename != null
&& _resourceCache.TryGetResource<AudioResource>(ev.SoundtrackFilename, out var lobbySongResource)
)
{
var lobbyStream = lobbySongResource.AudioStream;
var title = string.IsNullOrEmpty(lobbyStream.Title)
? Loc.GetString("lobby-state-song-unknown-title")
: lobbyStream.Title;
var artist = string.IsNullOrEmpty(lobbyStream.Artist)
? Loc.GetString("lobby-state-song-unknown-artist")
: lobbyStream.Artist;
var markup = Loc.GetString("lobby-state-song-text",
("songTitle", title),
("songArtist", artist));
_lobby!.LobbySong.SetMarkup(markup);
}
}
private void UpdateLobbyBackground()
{
if (_gameTicker.LobbyBackground != null)
{
_lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
}
else
{
_lobby!.Background.Texture = null;
}
}
private void SetReady(bool newReady)
{
if (_gameTicker.IsGameStarted)

View File

@@ -0,0 +1,286 @@
using System.Linq;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Lobby.UI;
using Content.Client.Preferences;
using Content.Client.Preferences.UI;
using Content.Client.Station;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby;
public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
{
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
private LobbyCharacterPreviewPanel? _previewPanel;
private bool _showClothes = true;
/*
* Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
* that is shared too.
*/
/// <summary>
/// Preview dummy for role gear.
/// </summary>
private EntityUid? _previewDummy;
/// <summary>
/// If we currently have a job prototype selected.
/// </summary>
private JobPrototype? _dummyJob;
// TODO: Load the species directly and don't update entity ever.
public event Action<EntityUid>? PreviewDummyUpdated;
private HumanoidCharacterProfile? _profile;
public override void Initialize()
{
base.Initialize();
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
}
private void PreferencesDataLoaded()
{
UpdateProfile();
}
public void OnStateEntered(LobbyState state)
{
}
public void OnStateExited(LobbyState state)
{
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
}
public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
{
_previewPanel = panel;
ReloadProfile();
}
public void SetClothes(bool value)
{
if (_showClothes == value)
return;
_showClothes = value;
ReloadCharacterUI();
}
public void SetDummyJob(JobPrototype? job)
{
_dummyJob = job;
ReloadCharacterUI();
}
/// <summary>
/// Updates the character only with the specified profile change.
/// </summary>
public void ReloadProfile()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
// Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
var previewDummy = EnsurePreviewDummy(_profile);
_humanoid.LoadProfile(previewDummy, _profile);
}
/// <summary>
/// Updates the currently selected character's preview.
/// </summary>
public void ReloadCharacterUI()
{
// Test moment
if (_profile == null || _stateManager.CurrentState is not LobbyState)
return;
EntityManager.DeleteEntity(_previewDummy);
_previewDummy = null;
_previewDummy = EnsurePreviewDummy(_profile);
_previewPanel?.SetSprite(_previewDummy.Value);
_previewPanel?.SetSummaryText(_profile.Summary);
_humanoid.LoadProfile(_previewDummy.Value, _profile);
if (_showClothes)
GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
}
/// <summary>
/// Updates character profile to the default.
/// </summary>
public void UpdateProfile()
{
if (!_preferencesManager.ServerDataLoaded)
{
_profile = null;
return;
}
if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
{
_profile = selectedCharacter;
_previewPanel?.SetLoaded(true);
}
else
{
_previewPanel?.SetSummaryText(string.Empty);
_previewPanel?.SetLoaded(false);
}
ReloadCharacterUI();
}
public void UpdateProfile(HumanoidCharacterProfile? profile)
{
if (_profile?.Equals(profile) == true)
return;
if (_stateManager.CurrentState is not LobbyState)
return;
_profile = profile;
}
private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile)
{
if (_previewDummy != null)
return _previewDummy.Value;
_previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(profile.Species).DollPrototype, MapCoordinates.Nullspace);
PreviewDummyUpdated?.Invoke(_previewDummy.Value);
return _previewDummy.Value;
}
/// <summary>
/// Applies the highest priority job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
{
var job = _dummyJob ?? GetPreferredJob(profile);
GiveDummyJobClothes(dummy, profile, job);
if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
GiveDummyLoadout(dummy, loadout);
}
}
/// <summary>
/// Gets the highest priority job for the profile.
/// </summary>
public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
{
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
}
public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
{
if (roleLoadout == null)
return;
foreach (var group in roleLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in group)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
_spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
}
}
}
/// <summary>
/// Applies the specified job's clothes to the dummy.
/// </summary>
public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
{
if (!_inventory.TryGetSlots(dummy, out var slots))
return;
// Apply loadout
if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
{
foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
{
foreach (var loadout in loadouts)
{
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
// TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
foreach (var slot in slots)
{
var itemType = loadoutGear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
if (job.StartingGear == null)
return;
var gear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
public EntityUid? GetPreviewDummy()
{
return _previewDummy;
}
}

View File

@@ -1,167 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Inventory;
using Content.Client.Preferences;
using Content.Client.UserInterface.Controls;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Lobby.UI
{
public sealed class LobbyCharacterPreviewPanel : Control
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private EntityUid? _previewDummy;
private readonly Label _summaryLabel;
private readonly BoxContainer _loaded;
private readonly BoxContainer _viewBox;
private readonly Label _unloaded;
public LobbyCharacterPreviewPanel()
{
IoCManager.InjectDependencies(this);
var header = new NanoHeading
{
Text = Loc.GetString("lobby-character-preview-panel-header")
};
// CharacterSetupButton = new Button
// {
// Text = Loc.GetString("lobby-character-preview-panel-character-setup-button"),
// HorizontalAlignment = HAlignment.Center,
// Margin = new Thickness(0, 5, 0, 0),
// };
_summaryLabel = new Label
{
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(3, 3),
};
var vBox = new BoxContainer
{
Orientation = LayoutOrientation.Vertical
};
_unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
_loaded = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Visible = false
};
_viewBox = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
};
var vSpacer = new VSpacer();
_loaded.AddChild(_summaryLabel);
_loaded.AddChild(_viewBox);
_loaded.AddChild(vSpacer);
//_loaded.AddChild(CharacterSetupButton);
vBox.AddChild(header);
vBox.AddChild(_loaded);
vBox.AddChild(_unloaded);
AddChild(vBox);
UpdateUI();
}
// public Button CharacterSetupButton { get; }
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
if (_previewDummy != null)
_entityManager.DeleteEntity(_previewDummy.Value);
_previewDummy = default;
}
public void UpdateUI()
{
if (!_preferencesManager.ServerDataLoaded)
{
_loaded.Visible = false;
_unloaded.Visible = true;
}
else
{
_loaded.Visible = true;
_unloaded.Visible = false;
if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
{
_summaryLabel.Text = string.Empty;
}
else
{
_previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
_viewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(_previewDummy.Value);
_viewBox.AddChild(spriteView);
_summaryLabel.Text = selectedCharacter.Summary;
_entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
}
}
}
public static void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile)
{
var protoMan = IoCManager.Resolve<IPrototypeManager>();
var entMan = IoCManager.Resolve<IEntityManager>();
var invSystem = entMan.System<ClientInventorySystem>();
var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
var job = protoMan.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
if (job.StartingGear != null && invSystem.TryGetSlots(dummy, out var slots))
{
var gear = protoMan.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name, profile);
if (invSystem.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
entMan.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = entMan.SpawnEntity(itemType, MapCoordinates.Nullspace);
invSystem.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
}

View File

@@ -0,0 +1,22 @@
<Control
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Name="VBox" Orientation="Vertical">
<controls:NanoHeading Name="Header" Text="{Loc 'lobby-character-preview-panel-header'}">
</controls:NanoHeading>
<BoxContainer Name="Loaded" Orientation="Vertical"
Visible="False">
<Label Name="Summary" HorizontalAlignment="Center" Margin="3 3"/>
<BoxContainer Name="ViewBox" Orientation="Horizontal" HorizontalAlignment="Center">
</BoxContainer>
<controls:VSpacer/>
<Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
HorizontalAlignment="Center"
Margin="0 5 0 0"/>
</BoxContainer>
<Label Name="Unloaded" Text="{Loc 'lobby-character-preview-panel-unloaded-preferences-label'}"/>
</BoxContainer>
</Control>

View File

@@ -0,0 +1,45 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Lobby.UI;
[GenerateTypedNameReferences]
public sealed partial class LobbyCharacterPreviewPanel : Control
{
public Button CharacterSetupButton => CharacterSetup;
public LobbyCharacterPreviewPanel()
{
RobustXamlLoader.Load(this);
UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this);
}
public void SetLoaded(bool value)
{
Loaded.Visible = value;
Unloaded.Visible = !value;
}
public void SetSummaryText(string value)
{
Summary.Text = string.Empty;
}
public void SetSprite(EntityUid uid)
{
ViewBox.DisposeAllChildren();
var spriteView = new SpriteView
{
OverrideDirection = Direction.South,
Scale = new Vector2(4f, 4f),
MaxSize = new Vector2(112, 112),
Stretch = SpriteView.StretchMode.Fill,
};
spriteView.SetEntity(uid);
ViewBox.AddChild(spriteView);
}
}

View File

@@ -60,6 +60,13 @@
<RichTextLabel Name="LabelName" Access="Public" HorizontalAlignment="Left"
VerticalAlignment="Center" Margin="0 0 0 420" />
<!-- Left Bot Panel -->
<BoxContainer Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Bottom">
<PanelContainer StyleClasses="AngleRect">
<RichTextLabel Name="LobbySong" Access="Public" HorizontalAlignment="Center" />
</PanelContainer>
</BoxContainer>
<!-- Ohio Container -->
<PanelContainer HorizontalAlignment="Left" Name="Center" VerticalAlignment="Center">
@@ -108,16 +115,6 @@
</PanelContainer>
<BoxContainer Name="VersionLabel" Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Bottom">
<PanelContainer StyleClasses="LobbyGayBackground">
<RichTextLabel Name="Version" Access="Public" HorizontalAlignment="Center" />
</PanelContainer>
</BoxContainer>
<!-- Changelog -->
<PanelContainer Name="Changelog" StyleClasses="LobbyGayBackground" HorizontalAlignment="Right"
VerticalAlignment="Top" Visible="False">

View File

@@ -1,4 +1,5 @@
using Content.Client._White.Stalin;
using Content.Client.Message;
using Content.Client.UserInterface.Systems.EscapeMenu;
using Robust.Client.AutoGenerated;
using Robust.Client.Console;
@@ -21,6 +22,8 @@ namespace Content.Client.Lobby.UI
SetAnchorPreset(MainContainer, LayoutPreset.Wide);
SetAnchorPreset(Background, LayoutPreset.Wide);
LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
DiscordButton.OnPressed += _ => _stalinManager.RequestUri();
QuitButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
@@ -34,19 +37,15 @@ namespace Content.Client.Lobby.UI
CharacterSetupState.Visible = false;
Center.Visible = true;
RightSide.Visible = true;
Version.Visible = true;
LabelName.Visible = true;
Changelog.Visible = true;
VersionLabel.Visible = true;
break;
case LobbyGuiState.CharacterSetup:
CharacterSetupState.Visible = true;
Center.Visible = false;
RightSide.Visible = false;
Version.Visible = false;
LabelName.Visible = false;
Changelog.Visible = false;
VersionLabel.Visible = false;
break;
}
}

View File

@@ -0,0 +1,135 @@
using System.Numerics;
using Content.Client.Gravity;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
public sealed class WaddleAnimationSystem : EntitySystem
{
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly GravitySystem _gravity = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
SubscribeLocalEvent<WaddleAnimationComponent, MoveInputEvent>(OnMovementInput);
SubscribeLocalEvent<WaddleAnimationComponent, StartedWaddlingEvent>(OnStartedWalking);
SubscribeLocalEvent<WaddleAnimationComponent, StoppedWaddlingEvent>(OnStoppedWalking);
SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
}
private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args)
{
// Prediction mitigation. Prediction means that MoveInputEvents are spammed repeatedly, even though you'd assume
// they're once-only for the user actually doing something. As such do nothing if we're just repeating this FoR.
if (!_timing.IsFirstTimePredicted)
{
return;
}
if (!args.HasDirectionalMovement && component.IsCurrentlyWaddling)
{
component.IsCurrentlyWaddling = false;
var stopped = new StoppedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref stopped);
return;
}
// Only start waddling if we're not currently AND we're actually moving.
if (component.IsCurrentlyWaddling || !args.HasDirectionalMovement)
return;
component.IsCurrentlyWaddling = true;
var started = new StartedWaddlingEvent(entity);
RaiseLocalEvent(entity, ref started);
}
private void OnStartedWalking(EntityUid uid, WaddleAnimationComponent component, StartedWaddlingEvent args)
{
if (_animation.HasRunningAnimation(uid, component.KeyName))
{
return;
}
if (!TryComp<InputMoverComponent>(uid, out var mover))
{
return;
}
if (_gravity.IsWeightless(uid))
{
return;
}
var tumbleIntensity = component.LastStep ? 360 - component.TumbleIntensity : component.TumbleIntensity;
var len = mover.Sprinting ? component.AnimationLength * component.RunAnimationLengthMultiplier : component.AnimationLength;
component.LastStep = !component.LastStep;
component.IsCurrentlyWaddling = true;
var anim = new Animation()
{
Length = TimeSpan.FromSeconds(len),
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Rotation),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), 0),
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(tumbleIntensity), len/3),
new AnimationTrackProperty.KeyFrame(Angle.FromDegrees(0), len/3),
}
},
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Offset),
InterpolationMode = AnimationInterpolationMode.Linear,
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(new Vector2(), 0),
new AnimationTrackProperty.KeyFrame(component.HopIntensity, len/3),
new AnimationTrackProperty.KeyFrame(new Vector2(), len/3),
}
}
}
};
_animation.Play(uid, anim, component.KeyName);
}
private void OnStoppedWalking(EntityUid uid, WaddleAnimationComponent component, StoppedWaddlingEvent args)
{
_animation.Stop(uid, component.KeyName);
if (!TryComp<SpriteComponent>(uid, out var sprite))
{
return;
}
sprite.Offset = new Vector2();
sprite.Rotation = Angle.FromDegrees(0);
component.IsCurrentlyWaddling = false;
}
private void OnAnimationCompleted(EntityUid uid, WaddleAnimationComponent component, AnimationCompletedEvent args)
{
var started = new StartedWaddlingEvent(uid);
RaiseLocalEvent(uid, ref started);
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Nutrition.EntitySystems;
namespace Content.Client.Nutrition.EntitySystems;
public sealed class DrinkSystem : SharedDrinkSystem
{
}

View File

@@ -59,7 +59,9 @@ namespace Content.Client.Options.UI.Tabs
UpdateApplyButton();
};
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel.UserData.PatronTier is { } patron;
// Channel can be null in replays so.
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel?.UserData.PatronTier is { };
HudThemeOption.OnItemSelected += OnHudThemeChanged;
DiscordRich.OnToggled += OnCheckBoxToggled;

View File

@@ -1,18 +1,14 @@
using System.Numerics;
using Content.Shared.Pinpointer;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Client.Pinpointer;
public sealed class NavMapSystem : SharedNavMapSystem
public sealed partial class NavMapSystem : SharedNavMapSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NavMapComponent, ComponentHandleState>(OnHandleState);
}
@@ -21,89 +17,47 @@ public sealed class NavMapSystem : SharedNavMapSystem
if (args.Current is not NavMapComponentState state)
return;
component.Chunks.Clear();
foreach (var (origin, data) in state.TileData)
if (!state.FullState)
{
component.Chunks.Add(origin, new NavMapChunk(origin)
foreach (var index in component.Chunks.Keys)
{
TileData = data,
});
}
if (!state.AllChunks!.Contains(index))
component.Chunks.Remove(index);
}
component.Beacons.Clear();
component.Beacons.AddRange(state.Beacons);
component.Airlocks.Clear();
component.Airlocks.AddRange(state.Airlocks);
}
}
public sealed class NavMapOverlay : Overlay
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
private List<Entity<MapGridComponent>> _grids = new();
public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
{
_entManager = entManager;
_mapManager = mapManager;
}
protected override void Draw(in OverlayDrawArgs args)
{
var query = _entManager.GetEntityQuery<NavMapComponent>();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
_grids.Clear();
_mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds, ref _grids);
foreach (var grid in _grids)
{
if (!query.TryGetComponent(grid, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
continue;
// TODO: Faster helper method
var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
var localAABB = invMatrix.TransformBox(args.WorldBounds);
Matrix3.Multiply(in scale, in matrix, out var matty);
args.WorldHandle.SetTransform(matty);
for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
foreach (var beacon in component.Beacons)
{
for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.Comp.TileSize)
{
var floored = new Vector2i((int) x, (int) y);
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
if (mask == 0x0)
continue;
var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
args.WorldHandle.DrawRect(new Box2(tile * grid.Comp.TileSize, (tile + 1) * grid.Comp.TileSize), Color.Aqua, false);
}
}
if (!state.AllBeacons!.Contains(beacon))
component.Beacons.Remove(beacon);
}
}
args.WorldHandle.SetTransform(Matrix3.Identity);
else
{
foreach (var index in component.Chunks.Keys)
{
if (!state.Chunks.ContainsKey(index))
component.Chunks.Remove(index);
}
foreach (var beacon in component.Beacons)
{
if (!state.Beacons.Contains(beacon))
component.Beacons.Remove(beacon);
}
}
foreach (var ((category, origin), chunk) in state.Chunks)
{
var newChunk = new NavMapChunk(origin);
foreach (var (atmosDirection, value) in chunk)
newChunk.TileData[atmosDirection] = value;
component.Chunks[(category, origin)] = newChunk;
}
foreach (var beacon in state.Beacons)
component.Beacons.Add(beacon);
}
}

View File

@@ -16,6 +16,8 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using System.Numerics;
using JetBrains.Annotations;
using Content.Shared.Atmos;
using System.Linq;
namespace Content.Client.Pinpointer.UI;
@@ -27,6 +29,7 @@ public partial class NavMapControl : MapGridControl
{
[Dependency] private IResourceCache _cache = default!;
private readonly SharedTransformSystem _transformSystem;
private readonly SharedNavMapSystem _navMapSystem;
public EntityUid? Owner;
public EntityUid? MapUid;
@@ -40,7 +43,10 @@ public partial class NavMapControl : MapGridControl
// Tracked data
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
public Dictionary<NetEntity, NavMapBlip> TrackedEntities = new();
public Dictionary<Vector2i, List<NavMapLine>>? TileGrid = default!;
public List<(Vector2, Vector2)> TileLines = new();
public List<(Vector2, Vector2)> TileRects = new();
public List<(Vector2[], Color)> TilePolygons = new();
// Default colors
public Color WallColor = new(102, 217, 102);
@@ -53,14 +59,23 @@ public partial class NavMapControl : MapGridControl
protected static float MinDisplayedRange = 8f;
protected static float MaxDisplayedRange = 128f;
protected static float DefaultDisplayedRange = 48f;
protected float MinmapScaleModifier = 0.075f;
protected float FullWallInstep = 0.165f;
protected float ThinWallThickness = 0.165f;
protected float ThinDoorThickness = 0.30f;
// Local variables
private float _updateTimer = 0.25f;
private float _updateTimer = 1.0f;
private Dictionary<Color, Color> _sRGBLookUp = new();
protected Color BackgroundColor;
protected float BackgroundOpacity = 0.9f;
private int _targetFontsize = 8;
protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookup = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> HorizLinesLookupReversed = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookup = new();
protected Dictionary<(int, Vector2i), (int, Vector2i)> VertLinesLookupReversed = new();
// Components
private NavMapComponent? _navMap;
private MapGridComponent? _grid;
@@ -72,6 +87,7 @@ public partial class NavMapControl : MapGridControl
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
HorizontalExpand = true,
Margin = new Thickness(8f, 8f),
};
@@ -80,6 +96,7 @@ public partial class NavMapControl : MapGridControl
Text = Loc.GetString("navmap-recenter"),
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
HorizontalExpand = true,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
@@ -87,9 +104,10 @@ public partial class NavMapControl : MapGridControl
private readonly CheckBox _beacons = new()
{
Text = Loc.GetString("navmap-toggle-beacons"),
Margin = new Thickness(4f, 0f),
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
HorizontalExpand = true,
Margin = new Thickness(4f, 0f),
Pressed = true,
};
@@ -98,6 +116,8 @@ public partial class NavMapControl : MapGridControl
IoCManager.InjectDependencies(this);
_transformSystem = EntManager.System<SharedTransformSystem>();
_navMapSystem = EntManager.System<SharedNavMapSystem>();
BackgroundColor = Color.FromSrgb(TileColor.WithAlpha(BackgroundOpacity));
RectClipContent = true;
@@ -112,6 +132,8 @@ public partial class NavMapControl : MapGridControl
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
HorizontalExpand = true,
SetWidth = 650f,
Children =
{
new BoxContainer()
@@ -130,6 +152,7 @@ public partial class NavMapControl : MapGridControl
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
HorizontalExpand = true,
Children =
{
topPanel,
@@ -157,6 +180,9 @@ public partial class NavMapControl : MapGridControl
{
EntManager.TryGetComponent(MapUid, out _navMap);
EntManager.TryGetComponent(MapUid, out _grid);
EntManager.TryGetComponent(MapUid, out _xform);
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
UpdateNavMap();
}
@@ -251,119 +277,93 @@ public partial class NavMapControl : MapGridControl
EntManager.TryGetComponent(MapUid, out _physics);
EntManager.TryGetComponent(MapUid, out _fixtures);
if (_navMap == null || _grid == null || _xform == null)
return;
// Map re-centering
_recenter.Disabled = DrawRecenter();
_zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange ):0.0}"));
if (_navMap == null || _xform == null)
return;
// Update zoom text
_zoom.Text = Loc.GetString("navmap-zoom", ("value", $"{(DefaultDisplayedRange / WorldRange):0.0}"));
// Update offset with physics local center
var offset = Offset;
if (_physics != null)
offset += _physics.LocalCenter;
// Draw tiles
if (_fixtures != null)
var offsetVec = new Vector2(offset.X, -offset.Y);
// Wall sRGB
if (!_sRGBLookUp.TryGetValue(WallColor, out var wallsRGB))
{
wallsRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = wallsRGB;
}
// Draw floor tiles
if (TilePolygons.Any())
{
Span<Vector2> verts = new Vector2[8];
foreach (var fixture in _fixtures.Fixtures.Values)
foreach (var (polygonVerts, polygonColor) in TilePolygons)
{
if (fixture.Shape is not PolygonShape poly)
continue;
for (var i = 0; i < poly.VertexCount; i++)
for (var i = 0; i < polygonVerts.Length; i++)
{
var vert = poly.Vertices[i] - offset;
var vert = polygonVerts[i] - offset;
verts[i] = ScalePosition(new Vector2(vert.X, -vert.Y));
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], TileColor);
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..polygonVerts.Length], polygonColor);
}
}
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
// Drawing lines can be rather expensive due to the number of neighbors that need to be checked in order
// to figure out where they should be drawn. However, we don't *need* to do check these every frame.
// Instead, lets periodically update where to draw each line and then store these points in a list.
// Then we can just run through the list each frame and draw the lines without any extra computation.
// Draw walls
if (TileGrid != null && TileGrid.Count > 0)
// Draw map lines
if (TileLines.Any())
{
var walls = new ValueList<Vector2>();
var lines = new ValueList<Vector2>(TileLines.Count * 2);
foreach ((var chunk, var chunkedLines) in TileGrid)
foreach (var (o, t) in TileLines)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
var origin = ScalePosition(o - offsetVec);
var terminus = ScalePosition(t - offsetVec);
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
foreach (var chunkedLine in chunkedLines)
{
var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
walls.Add(start);
walls.Add(end);
}
lines.Add(origin);
lines.Add(terminus);
}
if (walls.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, walls.Span, sRGB);
}
if (lines.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, lines.Span, wallsRGB);
}
var airlockBuffer = Vector2.One * (MinimapScale / 2.25f) * 0.75f;
var airlockLines = new ValueList<Vector2>();
var foobarVec = new Vector2(1, -1);
foreach (var airlock in _navMap.Airlocks)
// Draw map rects
if (TileRects.Any())
{
var position = airlock.Position - offset;
position = ScalePosition(position with { Y = -position.Y });
airlockLines.Add(position + airlockBuffer);
airlockLines.Add(position - airlockBuffer * foobarVec);
var rects = new ValueList<Vector2>(TileRects.Count * 8);
airlockLines.Add(position + airlockBuffer);
airlockLines.Add(position + airlockBuffer * foobarVec);
airlockLines.Add(position - airlockBuffer);
airlockLines.Add(position + airlockBuffer * foobarVec);
airlockLines.Add(position - airlockBuffer);
airlockLines.Add(position - airlockBuffer * foobarVec);
airlockLines.Add(position + airlockBuffer * -Vector2.UnitY);
airlockLines.Add(position - airlockBuffer * -Vector2.UnitY);
}
if (airlockLines.Count > 0)
{
if (!_sRGBLookUp.TryGetValue(WallColor, out var sRGB))
foreach (var (lt, rb) in TileRects)
{
sRGB = Color.ToSrgb(WallColor);
_sRGBLookUp[WallColor] = sRGB;
var leftTop = ScalePosition(lt - offsetVec);
var rightBottom = ScalePosition(rb - offsetVec);
var rightTop = new Vector2(rightBottom.X, leftTop.Y);
var leftBottom = new Vector2(leftTop.X, rightBottom.Y);
rects.Add(leftTop);
rects.Add(rightTop);
rects.Add(rightTop);
rects.Add(rightBottom);
rects.Add(rightBottom);
rects.Add(leftBottom);
rects.Add(leftBottom);
rects.Add(leftTop);
}
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, airlockLines.Span, sRGB);
if (rects.Count > 0)
handle.DrawPrimitives(DrawPrimitiveTopology.LineList, rects.Span, wallsRGB);
}
// Invoke post wall drawing action
if (PostWallDrawingAction != null)
PostWallDrawingAction.Invoke(handle);
@@ -373,7 +373,7 @@ public partial class NavMapControl : MapGridControl
var rectBuffer = new Vector2(5f, 3f);
// Calculate font size for current zoom level
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize , 0);
var fontSize = (int) Math.Round(1 / WorldRange * DefaultDisplayedRange * UIScale * _targetFontsize, 0);
var font = new VectorFont(_cache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Bold.ttf"), fontSize);
foreach (var beacon in _navMap.Beacons)
@@ -409,8 +409,6 @@ public partial class NavMapControl : MapGridControl
}
// Tracked entities (can use a supplied sprite as a marker instead; should probably just replace TrackedCoordinates with this eventually)
var iconVertexUVs = new Dictionary<(Texture, Color), ValueList<DrawVertexUV2D>>();
foreach (var blip in TrackedEntities.Values)
{
if (blip.Blinks && !lit)
@@ -419,9 +417,6 @@ public partial class NavMapControl : MapGridControl
if (blip.Texture == null)
continue;
if (!iconVertexUVs.TryGetValue((blip.Texture, blip.Color), out var vertexUVs))
vertexUVs = new();
var mapPos = blip.Coordinates.ToMap(EntManager, _transformSystem);
if (mapPos.MapId != MapId.Nullspace)
@@ -429,29 +424,11 @@ public partial class NavMapControl : MapGridControl
var position = _transformSystem.GetInvWorldMatrix(_xform).Transform(mapPos.Position) - offset;
position = ScalePosition(new Vector2(position.X, -position.Y));
var scalingCoefficient = 2.5f;
var positionOffset = scalingCoefficient * float.Sqrt(MinimapScale);
var scalingCoefficient = MinmapScaleModifier * float.Sqrt(MinimapScale);
var positionOffset = new Vector2(scalingCoefficient * blip.Texture.Width, scalingCoefficient * blip.Texture.Height);
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y - positionOffset), new Vector2(1f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X - positionOffset, position.Y + positionOffset), new Vector2(1f, 0f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y - positionOffset), new Vector2(0f, 1f)));
vertexUVs.Add(new DrawVertexUV2D(new Vector2(position.X + positionOffset, position.Y + positionOffset), new Vector2(0f, 0f)));
handle.DrawTextureRect(blip.Texture, new UIBox2(position - positionOffset, position + positionOffset), blip.Color);
}
iconVertexUVs[(blip.Texture, blip.Color)] = vertexUVs;
}
foreach ((var (texture, color), var vertexUVs) in iconVertexUVs)
{
if (!_sRGBLookUp.TryGetValue(color, out var sRGB))
{
sRGB = Color.ToSrgb(color);
_sRGBLookUp[color] = sRGB;
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture, vertexUVs.Span, sRGB);
}
}
@@ -469,124 +446,294 @@ public partial class NavMapControl : MapGridControl
}
protected virtual void UpdateNavMap()
{
// Clear stale values
TilePolygons.Clear();
TileLines.Clear();
TileRects.Clear();
UpdateNavMapFloorTiles();
UpdateNavMapWallLines();
UpdateNavMapAirlocks();
}
private void UpdateNavMapFloorTiles()
{
if (_fixtures == null)
return;
var verts = new Vector2[8];
foreach (var fixture in _fixtures.Fixtures.Values)
{
if (fixture.Shape is not PolygonShape poly)
continue;
for (var i = 0; i < poly.VertexCount; i++)
{
var vert = poly.Vertices[i];
verts[i] = new Vector2(MathF.Round(vert.X), MathF.Round(vert.Y));
}
TilePolygons.Add((verts[..poly.VertexCount], TileColor));
}
}
private void UpdateNavMapWallLines()
{
if (_navMap == null || _grid == null)
return;
TileGrid = GetDecodedWallChunks(_navMap.Chunks, _grid);
}
// We'll use the following dictionaries to combine collinear wall lines
HorizLinesLookup.Clear();
HorizLinesLookupReversed.Clear();
VertLinesLookup.Clear();
VertLinesLookupReversed.Clear();
public Dictionary<Vector2i, List<NavMapLine>> GetDecodedWallChunks
(Dictionary<Vector2i, NavMapChunk> chunks,
MapGridComponent grid)
{
var decodedOutput = new Dictionary<Vector2i, List<NavMapLine>>();
foreach ((var chunkOrigin, var chunk) in chunks)
foreach ((var (category, chunkOrigin), var chunk) in _navMap.Chunks)
{
var list = new List<NavMapLine>();
if (category != NavMapChunkType.Wall)
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
var value = (ushort) Math.Pow(2, i);
var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
if (mask == 0x0)
continue;
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relativeTile))
{
AddRectForThinWall(chunk.TileData, tile);
continue;
}
tile = tile with { Y = -tile.Y };
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, 1)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.South] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.South] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, -grid.TileSize)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, -_grid.TileSize), HorizLinesLookup, HorizLinesLookupReversed);
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(1, 0)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.West] &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.West] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(_grid.TileSize, -_grid.TileSize), tile + new Vector2i(_grid.TileSize, 0), VertLinesLookup, VertLinesLookupReversed);
// South edge
if (relativeTile.Y == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(0, -1)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.North] &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.North] & flag) != 0x0;
}
if (!neighbor)
{
// Add points
list.Add(new NavMapLine(position + new Vector2(grid.TileSize, 0f), position));
}
AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed);
// West edge
if (relativeTile.X == 0)
{
neighbor = chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
neighbor = _navMap.Chunks.TryGetValue((NavMapChunkType.Wall, chunkOrigin + new Vector2i(-1, 0)), out neighborChunk) &&
(neighborChunk.TileData[AtmosDirection.East] &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
neighbor = (chunk.TileData[AtmosDirection.East] & flag) != 0x0;
}
if (!neighbor)
{
// Add point
list.Add(new NavMapLine(position, position + new Vector2(0f, -grid.TileSize)));
}
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed);
// Draw a diagonal line for interiors.
list.Add(new NavMapLine(position + new Vector2(0f, -grid.TileSize), position + new Vector2(grid.TileSize, 0f)));
// Add a diagonal line for interiors. Unless there are a lot of double walls, there is no point combining these
TileLines.Add((tile + new Vector2(0, -_grid.TileSize), tile + new Vector2(_grid.TileSize, 0)));
}
decodedOutput.Add(chunkOrigin, list);
}
return decodedOutput;
// Record the combined lines
foreach (var (origin, terminal) in HorizLinesLookup)
TileLines.Add((origin.Item2, terminal.Item2));
foreach (var (origin, terminal) in VertLinesLookup)
TileLines.Add((origin.Item2, terminal.Item2));
}
private void UpdateNavMapAirlocks()
{
if (_navMap == null || _grid == null)
return;
foreach (var ((category, _), chunk) in _navMap.Chunks)
{
if (category != NavMapChunkType.Airlock)
continue;
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = _navMapSystem.GetCombinedEdgesForChunk(chunk.TileData) & value;
if (mask == 0x0)
continue;
var relative = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relative) * _grid.TileSize;
// If the edges of an airlock tile are not all occupied, draw a thin airlock for each edge
if (!_navMapSystem.AllTileEdgesAreOccupied(chunk.TileData, relative))
{
AddRectForThinAirlock(chunk.TileData, tile);
continue;
}
// Otherwise add a single full tile airlock
TileRects.Add((new Vector2(tile.X + FullWallInstep, -tile.Y - FullWallInstep),
new Vector2(tile.X - FullWallInstep + 1f, -tile.Y + FullWallInstep - 1)));
TileLines.Add((new Vector2(tile.X + 0.5f, -tile.Y - FullWallInstep),
new Vector2(tile.X + 0.5f, -tile.Y + FullWallInstep - 1)));
}
}
}
private void AddRectForThinWall(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
{
if (_navMapSystem == null || _grid == null)
return;
var leftTop = new Vector2(-0.5f, -0.5f + ThinWallThickness);
var rightBottom = new Vector2(0.5f, -0.5f);
foreach (var (direction, mask) in tileData)
{
var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
if ((mask & flag) == 0)
continue;
var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
var angle = new Angle(0);
switch (direction)
{
case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f); break;
case AtmosDirection.South: angle = new Angle(MathF.PI); break;
case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
}
TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
}
}
private void AddRectForThinAirlock(Dictionary<AtmosDirection, ushort> tileData, Vector2i tile)
{
if (_navMapSystem == null || _grid == null)
return;
var leftTop = new Vector2(-0.5f + FullWallInstep, -0.5f + FullWallInstep + ThinDoorThickness);
var rightBottom = new Vector2(0.5f - FullWallInstep, -0.5f + FullWallInstep);
var centreTop = new Vector2(0f, -0.5f + FullWallInstep + ThinDoorThickness);
var centreBottom = new Vector2(0f, -0.5f + FullWallInstep);
foreach (var (direction, mask) in tileData)
{
var relative = SharedMapSystem.GetChunkRelative(tile, SharedNavMapSystem.ChunkSize);
var flag = (ushort) SharedNavMapSystem.GetFlag(relative);
if ((mask & flag) == 0)
continue;
var tilePosition = new Vector2(tile.X + 0.5f, -tile.Y - 0.5f);
var angle = new Angle(0);
switch (direction)
{
case AtmosDirection.East: angle = new Angle(MathF.PI * 0.5f);break;
case AtmosDirection.South: angle = new Angle(MathF.PI); break;
case AtmosDirection.West: angle = new Angle(MathF.PI * -0.5f); break;
}
TileRects.Add((angle.RotateVec(leftTop) + tilePosition, angle.RotateVec(rightBottom) + tilePosition));
TileLines.Add((angle.RotateVec(centreTop) + tilePosition, angle.RotateVec(centreBottom) + tilePosition));
}
}
protected void AddOrUpdateNavMapLine
(Vector2i origin,
Vector2i terminus,
Dictionary<(int, Vector2i), (int, Vector2i)> lookup,
Dictionary<(int, Vector2i), (int, Vector2i)> lookupReversed,
int index = 0)
{
(int, Vector2i) foundTermiusTuple;
(int, Vector2i) foundOriginTuple;
if (lookup.TryGetValue((index, terminus), out foundTermiusTuple) &&
lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
{
lookup[foundOriginTuple] = foundTermiusTuple;
lookupReversed[foundTermiusTuple] = foundOriginTuple;
lookup.Remove((index, terminus));
lookupReversed.Remove((index, origin));
}
else if (lookup.TryGetValue((index, terminus), out foundTermiusTuple))
{
lookup[(index, origin)] = foundTermiusTuple;
lookup.Remove((index, terminus));
lookupReversed[foundTermiusTuple] = (index, origin);
}
else if (lookupReversed.TryGetValue((index, origin), out foundOriginTuple))
{
lookupReversed[(index, terminus)] = foundOriginTuple;
lookupReversed.Remove(foundOriginTuple);
lookup[foundOriginTuple] = (index, terminus);
}
else
{
lookup.Add((index, origin), (index, terminus));
lookupReversed.Add((index, terminus), (index, origin));
}
}
protected Vector2 GetOffset()
@@ -612,15 +759,3 @@ public struct NavMapBlip
Selectable = selectable;
}
}
public struct NavMapLine
{
public readonly Vector2 Origin;
public readonly Vector2 Terminus;
public NavMapLine(Vector2 origin, Vector2 terminus)
{
Origin = origin;
Terminus = terminus;
}
}

View File

@@ -8,12 +8,13 @@ using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Players.PlayTimeTracking;
public sealed class JobRequirementsManager
public sealed class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
@@ -137,5 +138,13 @@ public sealed class JobRequirementsManager
}
}
public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
{
if (session != _playerManager.LocalSession)
{
return new Dictionary<string, TimeSpan>();
}
return _roles;
}
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Polymorph.Components;
using Content.Shared.Polymorph.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Polymorph.Systems;
public sealed class ChameleonProjectorSystem : SharedChameleonProjectorSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private EntityQuery<AppearanceComponent> _appearanceQuery;
public override void Initialize()
{
base.Initialize();
_appearanceQuery = GetEntityQuery<AppearanceComponent>();
SubscribeLocalEvent<ChameleonDisguiseComponent, AfterAutoHandleStateEvent>(OnHandleState);
}
private void OnHandleState(Entity<ChameleonDisguiseComponent> ent, ref AfterAutoHandleStateEvent args)
{
CopyComp<SpriteComponent>(ent);
CopyComp<GenericVisualizerComponent>(ent);
CopyComp<SolutionContainerVisualsComponent>(ent);
// reload appearance to hopefully prevent any invisible layers
if (_appearanceQuery.TryComp(ent, out var appearance))
_appearance.QueueUpdate(ent, appearance);
}
}

View File

@@ -220,6 +220,12 @@ namespace Content.Client.Popups
PopupEntity(message, uid, recipient.Value, type);
}
public override void PopupPredicted(string? recipientMessage, string? othersMessage, EntityUid uid, EntityUid? recipient, PopupType type = PopupType.Small)
{
if (recipient != null && _timing.IsFirstTimePredicted)
PopupEntity(recipientMessage, uid, recipient.Value, type);
}
#endregion
#region Network Event Handlers

View File

@@ -23,8 +23,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
public PowerMonitoringCableNetworksComponent? PowerMonitoringCableNetworks;
public List<PowerMonitoringConsoleLineGroup> HiddenLineGroups = new();
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? PowerCableNetwork;
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? FocusCableNetwork;
public List<PowerMonitoringConsoleLine> PowerCableNetwork = new();
public List<PowerMonitoringConsoleLine> FocusCableNetwork = new();
private MapGridComponent? _grid;
@@ -48,15 +48,15 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (!_entManager.TryGetComponent<PowerMonitoringCableNetworksComponent>(Owner, out var cableNetworks))
return;
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks, _grid);
FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks, _grid);
PowerCableNetwork = GetDecodedPowerCableChunks(cableNetworks.AllChunks);
FocusCableNetwork = GetDecodedPowerCableChunks(cableNetworks.FocusChunks);
}
public void DrawAllCableNetworks(DrawingHandleScreen handle)
{
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
// Draw full cable network
if (PowerCableNetwork != null && PowerCableNetwork.Count > 0)
{
@@ -69,36 +69,29 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
DrawCableNetwork(handle, FocusCableNetwork, Color.White);
}
public void DrawCableNetwork(DrawingHandleScreen handle, Dictionary<Vector2i, List<PowerMonitoringConsoleLine>> fullCableNetwork, Color modulator)
public void DrawCableNetwork(DrawingHandleScreen handle, List<PowerMonitoringConsoleLine> fullCableNetwork, Color modulator)
{
if (!_entManager.TryGetComponent(MapUid, out _grid))
return;
var offset = GetOffset();
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
offset = offset with { Y = -offset.Y };
if (WorldRange / WorldMaxRange > 0.5f)
{
var cableNetworks = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
foreach (var line in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
if (HiddenLineGroups.Contains(line.Group))
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
var cableOffset = _powerCableOffsets[(int) line.Group];
var start = ScalePosition(line.Origin + cableOffset - offset);
var end = ScalePosition(line.Terminus + cableOffset - offset);
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var start = ScalePosition(chunkedLine.Origin - new Vector2(offset.X, -offset.Y));
var end = ScalePosition(chunkedLine.Terminus - new Vector2(offset.X, -offset.Y));
cableNetworks[(int) chunkedLine.Group].Add(start);
cableNetworks[(int) chunkedLine.Group].Add(end);
}
cableNetworks[(int) line.Group].Add(start);
cableNetworks[(int) line.Group].Add(end);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableNetworks.Length; cableNetworkIdx++)
@@ -124,48 +117,39 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
{
var cableVertexUVs = new ValueList<Vector2>[3];
foreach ((var chunk, var chunkedLines) in fullCableNetwork)
foreach (var line in fullCableNetwork)
{
var offsetChunk = new Vector2(chunk.X, chunk.Y) * SharedNavMapSystem.ChunkSize;
if (offsetChunk.X < area.Left - SharedNavMapSystem.ChunkSize || offsetChunk.X > area.Right)
if (HiddenLineGroups.Contains(line.Group))
continue;
if (offsetChunk.Y < area.Bottom - SharedNavMapSystem.ChunkSize || offsetChunk.Y > area.Top)
continue;
var cableOffset = _powerCableOffsets[(int) line.Group];
foreach (var chunkedLine in chunkedLines)
{
if (HiddenLineGroups.Contains(chunkedLine.Group))
continue;
var leftTop = ScalePosition(new Vector2
(Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ cableOffset - offset);
var leftTop = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightTop = ScalePosition(new Vector2
(Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
Math.Min(line.Origin.Y, line.Terminus.Y) - 0.1f)
+ cableOffset - offset);
var rightTop = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Min(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) - 0.1f)
- new Vector2(offset.X, -offset.Y));
var leftBottom = ScalePosition(new Vector2
(Math.Min(line.Origin.X, line.Terminus.X) - 0.1f,
Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ cableOffset - offset);
var leftBottom = ScalePosition(new Vector2
(Math.Min(chunkedLine.Origin.X, chunkedLine.Terminus.X) - 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
var rightBottom = ScalePosition(new Vector2
(Math.Max(line.Origin.X, line.Terminus.X) + 0.1f,
Math.Max(line.Origin.Y, line.Terminus.Y) + 0.1f)
+ cableOffset - offset);
var rightBottom = ScalePosition(new Vector2
(Math.Max(chunkedLine.Origin.X, chunkedLine.Terminus.X) + 0.1f,
Math.Max(chunkedLine.Origin.Y, chunkedLine.Terminus.Y) + 0.1f)
- new Vector2(offset.X, -offset.Y));
cableVertexUVs[(int) chunkedLine.Group].Add(leftBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(leftTop);
cableVertexUVs[(int) chunkedLine.Group].Add(rightBottom);
cableVertexUVs[(int) chunkedLine.Group].Add(rightTop);
}
cableVertexUVs[(int) line.Group].Add(leftBottom);
cableVertexUVs[(int) line.Group].Add(leftTop);
cableVertexUVs[(int) line.Group].Add(rightBottom);
cableVertexUVs[(int) line.Group].Add(leftTop);
cableVertexUVs[(int) line.Group].Add(rightBottom);
cableVertexUVs[(int) line.Group].Add(rightTop);
}
for (int cableNetworkIdx = 0; cableNetworkIdx < cableVertexUVs.Length; cableNetworkIdx++)
@@ -188,23 +172,28 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
}
}
public Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>? GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks, MapGridComponent? grid)
public List<PowerMonitoringConsoleLine> GetDecodedPowerCableChunks(Dictionary<Vector2i, PowerCableChunk>? chunks)
{
if (chunks == null || grid == null)
return null;
var decodedOutput = new List<PowerMonitoringConsoleLine>();
var decodedOutput = new Dictionary<Vector2i, List<PowerMonitoringConsoleLine>>();
if (!_entManager.TryGetComponent(MapUid, out _grid))
return decodedOutput;
if (chunks == null)
return decodedOutput;
// We'll use the following dictionaries to combine collinear power cable lines
HorizLinesLookup.Clear();
HorizLinesLookupReversed.Clear();
VertLinesLookup.Clear();
VertLinesLookupReversed.Clear();
foreach ((var chunkOrigin, var chunk) in chunks)
{
var list = new List<PowerMonitoringConsoleLine>();
for (int cableIdx = 0; cableIdx < chunk.PowerCableData.Length; cableIdx++)
{
var chunkMask = chunk.PowerCableData[cableIdx];
Vector2 offset = _powerCableOffsets[cableIdx];
for (var chunkIdx = 0; chunkIdx < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; chunkIdx++)
{
var value = (int) Math.Pow(2, chunkIdx);
@@ -214,8 +203,8 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
continue;
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize;
var position = new Vector2(tile.X, -tile.Y);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * _grid.TileSize;
tile = tile with { Y = -tile.Y };
PowerCableChunk neighborChunk;
bool neighbor;
@@ -237,12 +226,7 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(1f, 0f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
AddOrUpdateNavMapLine(tile, tile + new Vector2i(_grid.TileSize, 0), HorizLinesLookup, HorizLinesLookupReversed, cableIdx);
}
// North
@@ -260,21 +244,21 @@ public sealed partial class PowerMonitoringConsoleNavMapControl : NavMapControl
if (neighbor)
{
// Add points
var line = new PowerMonitoringConsoleLine
(position + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
position + new Vector2(0f, -1f) + offset + new Vector2(grid.TileSize * 0.5f, -grid.TileSize * 0.5f),
(PowerMonitoringConsoleLineGroup) cableIdx);
list.Add(line);
AddOrUpdateNavMapLine(tile + new Vector2i(0, -_grid.TileSize), tile, VertLinesLookup, VertLinesLookupReversed, cableIdx);
}
}
}
if (list.Count > 0)
decodedOutput.Add(chunkOrigin, list);
}
var gridOffset = new Vector2(_grid.TileSize * 0.5f, -_grid.TileSize * 0.5f);
foreach (var (origin, terminal) in HorizLinesLookup)
decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
foreach (var (origin, terminal) in VertLinesLookup)
decodedOutput.Add(new PowerMonitoringConsoleLine(origin.Item2 + gridOffset, terminal.Item2 + gridOffset, (PowerMonitoringConsoleLineGroup) origin.Item1));
return decodedOutput;
}
}

View File

@@ -170,9 +170,6 @@ public sealed partial class PowerMonitoringWindow : FancyWindow
NavMap.TrackedEntities[mon.Value] = blip;
}
// Update nav map
NavMap.ForceNavMapUpdate();
// If the entry group doesn't match the current tab, the data is out dated, do not use it
if (allEntries.Length > 0 && allEntries[0].Group != GetCurrentPowerMonitoringConsoleGroup())
return;

View File

@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Content.Client._White.Sponsors;
using Content.Client.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Preferences;
using Robust.Client;
using Robust.Client.Player;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -22,6 +26,7 @@ namespace Content.Client.Preferences
[Dependency] private readonly IBaseClient _baseClient = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
//WD-EDIT
[Dependency] private readonly SponsorsManager _sponsorsManager = default!;
@@ -69,11 +74,14 @@ namespace Content.Client.Preferences
public void UpdateCharacter(ICharacterProfile profile, int slot)
{
var collection = IoCManager.Instance!;
//WD-EDIT
var allowedMarkings = _sponsorsManager.TryGetInfo(out var sponsor) ? sponsor.AllowedMarkings : new string[]{};
var isAdminSpecies = _adminManager.HasFlag(AdminFlags.AdminSpecies);
profile.EnsureValid(_cfg, _prototypes, allowedMarkings, isAdminSpecies);
//WD-EDIT
profile.EnsureValid(_playerManager.LocalSession!, collection, allowedMarkings, isAdminSpecies); // WD
var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile};
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor);
var msg = new MsgUpdateCharacter

View File

@@ -0,0 +1,41 @@
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Preferences.UI;
public sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
// Not supported yet get fucked.
Setup(null, items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}

View File

@@ -3,27 +3,23 @@ using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Info;
using Content.Client.Info.PlaytimeStats;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Resources;
using Content.Client.Stylesheets;
using Content.Shared.Clothing;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Direction = Robust.Shared.Maths.Direction;
@@ -36,7 +32,6 @@ namespace Content.Client.Preferences.UI
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IConfigurationManager _configurationManager;
private readonly Button _createNewCharacterButton;
private readonly HumanoidProfileEditor _humanoidProfileEditor;
@@ -51,7 +46,6 @@ namespace Content.Client.Preferences.UI
_entityManager = entityManager;
_prototypeManager = prototypeManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
var back = new StyleBoxTexture
@@ -74,7 +68,7 @@ namespace Content.Client.Preferences.UI
args.Event.Handle();
};
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, entityManager, configurationManager);
_humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
_humanoidProfileEditor.OnProfileChanged += ProfileChanged;
// WD-EDIT start
@@ -109,6 +103,12 @@ namespace Content.Client.Preferences.UI
UpdateUI();
}
public void UpdateControls()
{
// Reset sliders etc. upon going going back to GUI.
_humanoidProfileEditor.LoadServerData();
}
private void UpdateUI()
{
var numberOfFullSlots = 0;
@@ -127,11 +127,6 @@ namespace Content.Client.Preferences.UI
var isDisplayedMaxSlots = false;
foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
{
if (character is null)
{
continue;
}
numberOfFullSlots++;
isDisplayedMaxSlots = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
@@ -150,6 +145,9 @@ namespace Content.Client.Preferences.UI
_humanoidProfileEditor.CharacterSlot = characterIndexCopy;
_humanoidProfileEditor.UpdateControls();
_preferencesManager.SelectCharacter(character);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(_humanoidProfileEditor.Profile);
controller.ReloadCharacterUI();
UpdateUI();
args.Event.Handle();
};
@@ -158,8 +156,12 @@ namespace Content.Client.Preferences.UI
_createNewCharacterButton.Disabled = isDisplayedMaxSlots;
Characters.AddChild(_createNewCharacterButton);
// TODO: Move this shit to the Lobby UI controller
}
/// <summary>
/// Shows individual characters on the side of the character GUI.
/// </summary>
private sealed class CharacterPickerButton : ContainerButton
{
private EntityUid _previewDummy;
@@ -190,7 +192,15 @@ namespace Content.Client.Preferences.UI
if (humanoid != null)
{
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy, humanoid);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
var job = controller.GetPreferredJob(humanoid);
controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
if (prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
{
var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
controller.GiveDummyLoadout(_previewDummy, loadout);
}
}
var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;

View File

@@ -0,0 +1,11 @@
<PanelContainer
xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#2F2F35"
ContentMarginTopOverride="10"
ContentMarginBottomOverride="10"
ContentMarginLeftOverride="10"
ContentMarginRightOverride="10"/>
</PanelContainer.PanelOverride>
</PanelContainer>

View File

@@ -0,0 +1,23 @@
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
RobustXamlLoader.Load(this);
PanelOverride = new StyleBoxFlat
{
ContentMarginTopOverride = 10,
ContentMarginBottomOverride = 10,
ContentMarginLeftOverride = 10,
ContentMarginRightOverride = 10
};
}
}

View File

@@ -7,8 +7,6 @@ namespace Content.Client.Preferences.UI
{
public sealed partial class HumanoidProfileEditor
{
private readonly IPrototypeManager _prototypeManager;
private void RandomizeEverything()
{
var species = _prototypeManager.EnumeratePrototypes<SpeciesPrototype>();

View File

@@ -1,7 +1,8 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prefUi="clr-namespace:Content.Client.Preferences.UI"
xmlns:humanoid="clr-namespace:Content.Client.Humanoid">
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
<BoxContainer Orientation="Horizontal">
<!-- Left side -->
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" HorizontalAlignment="Stretch" Margin="10 10 10 10">
@@ -137,20 +138,6 @@
<Button Name="ShowClothes" Pressed="True" ToggleMode="True" Text="{Loc 'humanoid-profile-editor-clothing-show'}" HorizontalAlignment="Right" />
</BoxContainer>
</prefUi:HighlightedContainer>
<!-- Clothing -->
<prefUi:HighlightedContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'humanoid-profile-editor-clothing-label'}" />
<OptionButton Name="CClothingButton" />
</BoxContainer>
</prefUi:HighlightedContainer>
<!-- Backpack -->
<prefUi:HighlightedContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'humanoid-profile-editor-backpack-label'}" />
<OptionButton Name="CBackpackButton" />
</BoxContainer>
</prefUi:HighlightedContainer>
<!-- Spawn Priority -->
<prefUi:HighlightedContainer>
<BoxContainer HorizontalExpand="True">

View File

@@ -4,7 +4,7 @@ using Content.Client._White.Sponsors;
using Content.Client.Administration.Managers;
using Content.Client.Guidebook;
using Content.Client.Humanoid;
using Content.Client.Lobby.UI;
using Content.Client.Lobby;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Stylesheets;
@@ -12,24 +12,24 @@ using Content.Client.UserInterface.Controls;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Clothing;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Content.Shared.Traits;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -38,20 +38,6 @@ using Direction = Robust.Shared.Maths.Direction;
namespace Content.Client.Preferences.UI
{
public sealed class HighlightedContainer : PanelContainer
{
public HighlightedContainer()
{
PanelOverride = new StyleBoxFlat
{
ContentMarginTopOverride = 10,
ContentMarginBottomOverride = 10,
ContentMarginLeftOverride = 10,
ContentMarginRightOverride = 10
};
}
}
[GenerateTypedNameReferences]
public sealed partial class HumanoidProfileEditor : Control
{
@@ -61,15 +47,16 @@ namespace Content.Client.Preferences.UI
private readonly IClientAdminManager _adminManager;
//WD-EDIT
private readonly IPrototypeManager _prototypeManager;
private readonly IClientPreferencesManager _preferencesManager;
private readonly IEntityManager _entMan;
private readonly IConfigurationManager _configurationManager;
private readonly IEntityManager _entMan = default!;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private LineEdit _ageEdit => CAgeEdit;
private LineEdit _nameEdit => CNameEdit;
private TextEdit _flavorTextEdit = null!;
private TextEdit? _flavorTextEdit;
private LineEdit _nameClownEdit => CClownNameEdit;
private LineEdit _nameMimeEdit => CMimeNameEdit;
private LineEdit _nameBorgEdit => CBorgNameEdit;
@@ -89,8 +76,6 @@ namespace Content.Client.Preferences.UI
//WD-EDIT
private Slider _skinColor => CSkin;
private OptionButton _clothingButton => CClothingButton;
private OptionButton _backpackButton => CBackpackButton;
private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
private SingleMarkingPicker _hairPicker => CHairStylePicker;
private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
@@ -105,7 +90,7 @@ namespace Content.Client.Preferences.UI
private readonly Dictionary<string, BoxContainer> _jobCategories;
// Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony
private readonly List<SpeciesPrototype> _speciesList;
private readonly List<AntagPreferenceSelector> _antagPreferences;
private readonly List<AntagPreferenceSelector> _antagPreferences = new();
private readonly List<TraitPreferenceSelector> _traitPreferences;
private List<BodyTypePrototype> _bodyTypesList = new();
@@ -126,19 +111,23 @@ namespace Content.Client.Preferences.UI
public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
IEntityManager entityManager, IConfigurationManager configurationManager)
[ValidatePrototypeId<GuideEntryPrototype>]
private const string DefaultSpeciesGuidebook = "Species";
public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_sponsorsManager = IoCManager.Resolve<SponsorsManager>();
_adminManager = IoCManager.Resolve<IClientAdminManager>();
_prototypeManager = prototypeManager;
_entMan = entityManager;
_preferencesManager = preferencesManager;
_configurationManager = configurationManager;
_markingManager = IoCManager.Resolve<MarkingManager>();
_entMan = IoCManager.Resolve<IEntityManager>(); // WD
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated += OnDummyUpdate;
SpeciesInfoButton.ToolTip = Loc.GetString("humanoid-profile-editor-guidebook-button-tooltip");
_previewSpriteView.SetEntity(controller.GetPreviewDummy());
#region Left
@@ -161,8 +150,6 @@ namespace Content.Client.Preferences.UI
_tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
ShowClothes.OnPressed += ToggleClothes;
#region Sex
_sexButton.OnItemSelected += args =>
@@ -263,7 +250,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairStyleName(newStyle.id));
IsDirty = true;
SetDirty();
};
_hairPicker.OnColorChanged += newColor =>
@@ -273,7 +260,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnMarkingSelect += newStyle =>
@@ -282,7 +269,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairStyleName(newStyle.id));
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnColorChanged += newColor =>
@@ -292,7 +279,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
_hairPicker.OnSlotRemove += _ =>
@@ -304,7 +291,7 @@ namespace Content.Client.Preferences.UI
);
UpdateHairPickers();
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnSlotRemove += _ =>
@@ -316,7 +303,7 @@ namespace Content.Client.Preferences.UI
);
UpdateHairPickers();
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
_hairPicker.OnSlotAdd += delegate()
@@ -336,7 +323,7 @@ namespace Content.Client.Preferences.UI
UpdateHairPickers();
UpdateCMarkingsHair();
IsDirty = true;
SetDirty();
};
_facialHairPicker.OnSlotAdd += delegate()
@@ -356,38 +343,11 @@ namespace Content.Client.Preferences.UI
UpdateHairPickers();
UpdateCMarkingsFacialHair();
IsDirty = true;
SetDirty();
};
#endregion Hair
#region Clothing
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
_clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
_clothingButton.OnItemSelected += args =>
{
_clothingButton.SelectId(args.Id);
SetClothing((ClothingPreference) args.Id);
};
#endregion Clothing
#region Backpack
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
_backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
_backpackButton.OnItemSelected += args =>
{
_backpackButton.SelectId(args.Id);
SetBackpack((BackpackPreference) args.Id);
};
#endregion Backpack
#region SpawnPriority
foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
@@ -412,7 +372,7 @@ namespace Content.Client.Preferences.UI
Profile = Profile.WithCharacterAppearance(
Profile.Appearance.WithEyeColor(newColor));
CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
IsDirty = true;
SetDirty();
};
#endregion Eyes
@@ -436,13 +396,16 @@ namespace Content.Client.Preferences.UI
_preferenceUnavailableButton.SelectId(args.Id);
Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
IsDirty = true;
SetDirty();
};
_jobPriorities = new List<JobPrioritySelector>();
_jobCategories = new Dictionary<string, BoxContainer>();
_requirements = IoCManager.Resolve<JobRequirementsManager>();
// TODO: Move this to the LobbyUIController instead of being spaghetti everywhere.
_requirements.Updated += UpdateAntagRequirements;
_requirements.Updated += UpdateRoleRequirements;
UpdateAntagRequirements();
UpdateRoleRequirements();
#endregion Jobs
@@ -451,29 +414,6 @@ namespace Content.Client.Preferences.UI
_tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
_antagPreferences = new List<AntagPreferenceSelector>();
foreach (var antag in prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag);
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
IsDirty = true;
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
IsDirty = true;
};
}
#endregion Antags
#region Traits
@@ -493,7 +433,7 @@ namespace Content.Client.Preferences.UI
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, preference);
IsDirty = true;
SetDirty();
};
}
}
@@ -526,7 +466,7 @@ namespace Content.Client.Preferences.UI
#region FlavorText
if (_configurationManager.GetCVar(CCVars.FlavorText))
if (configurationManager.GetCVar(CCVars.FlavorText))
{
var flavorText = new FlavorText.FlavorText();
_tabContainer.AddChild(flavorText);
@@ -543,22 +483,14 @@ namespace Content.Client.Preferences.UI
_previewRotateLeftButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
_previewRotateRightButton.OnPressed += _ =>
{
_previewRotation = _previewRotation.TurnCcw();
_needUpdatePreview = true;
SetPreviewRotation(_previewRotation);
};
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
#endregion Dummy
#endregion Left
@@ -568,6 +500,13 @@ namespace Content.Client.Preferences.UI
LoadServerData();
}
ShowClothes.OnToggled += args =>
{
var lobby = UserInterfaceManager.GetUIController<LobbyUIController>();
lobby.SetClothes(args.Pressed);
SetDirty();
};
preferencesManager.OnServerDataLoaded += LoadServerData;
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
@@ -575,27 +514,69 @@ namespace Content.Client.Preferences.UI
UpdateSpeciesGuidebookIcon();
IsDirty = false;
controller.UpdateProfile();
}
private void SetDirty()
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadCharacterUI();
IsDirty = true;
}
private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
{
var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var page = "Species";
var page = DefaultSpeciesGuidebook;
if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
page = species;
if (_prototypeManager.TryIndex<GuideEntryPrototype>("Species", out var guideRoot))
if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
{
var dict = new Dictionary<string, GuideEntry> { { "Species", guideRoot } };
var dict = new Dictionary<string, GuideEntry>();
dict.Add(DefaultSpeciesGuidebook, guideRoot);
//TODO: Don't close the guidebook if its already open, just go to the correct page
guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
}
}
private void ToggleClothes(BaseButton.ButtonEventArgs obj)
private void OnDummyUpdate(EntityUid value)
{
RebuildSpriteView();
_previewSpriteView.SetEntity(value);
}
private void UpdateAntagRequirements()
{
_antagList.DisposeAllChildren();
_antagPreferences.Clear();
var btnGroup = new ButtonGroup();
foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
{
if (!antag.SetPreference)
continue;
var selector = new AntagPreferenceSelector(antag, btnGroup)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
_antagList.AddChild(selector);
_antagPreferences.Add(selector);
if (selector.Disabled)
{
Profile = Profile?.WithAntagPreference(antag.ID, false);
SetDirty();
}
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithAntagPreference(antag.ID, preference);
SetDirty();
};
}
}
private void UpdateRoleRequirements()
@@ -656,10 +637,19 @@ namespace Content.Client.Preferences.UI
.Where(job => job.SetPreference)
.ToArray();
Array.Sort(jobs, JobUIComparer.Instance);
var jobLoadoutGroup = new ButtonGroup();
foreach (var job in jobs)
{
var selector = new JobPrioritySelector(job, _prototypeManager);
RoleLoadout? loadout = null;
// Clone so we don't modify the underlying loadout.
Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
loadout = loadout?.Clone();
var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
{
Margin = new Thickness(3f, 3f, 3f, 0f),
};
if (!_requirements.IsAllowed(job, out var reason))
{
@@ -669,10 +659,15 @@ namespace Content.Client.Preferences.UI
category.AddChild(selector);
_jobPriorities.Add(selector);
selector.LoadoutUpdated += args =>
{
Profile = Profile?.WithLoadout(args);
SetDirty();
};
selector.PriorityChanged += priority =>
{
Profile = Profile?.WithJobPriority(job.ID, priority);
IsDirty = true;
foreach (var jobSelector in _jobPriorities)
{
@@ -688,6 +683,8 @@ namespace Content.Client.Preferences.UI
Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
}
}
SetDirty();
};
}
@@ -705,7 +702,7 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithFlavorText(content);
IsDirty = true;
SetDirty();
}
private void OnMarkingChange(MarkingSet markings)
@@ -714,20 +711,12 @@ namespace Content.Client.Preferences.UI
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
_needUpdatePreview = true;
IsDirty = true;
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadProfile();
}
private void OnMarkingColorChange(List<Marking> markings)
{
if (Profile is null)
return;
Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings));
IsDirty = true;
}
private void OnSkinColorOnValueChanged()
{
if (Profile is null) return;
@@ -779,6 +768,9 @@ namespace Content.Client.Preferences.UI
}
IsDirty = true;
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.UpdateProfile(Profile);
controller.ReloadProfile();
}
protected override void Dispose(bool disposing)
@@ -787,39 +779,28 @@ namespace Content.Client.Preferences.UI
if (!disposing)
return;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy.Value);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.PreviewDummyUpdated -= OnDummyUpdate;
_requirements.Updated -= UpdateAntagRequirements;
_requirements.Updated -= UpdateRoleRequirements;
_preferencesManager.OnServerDataLoaded -= LoadServerData;
}
private void RebuildSpriteView()
{
var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
if (_previewDummy != null)
_entMan.DeleteEntity(_previewDummy!.Value);
_previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
_previewSpriteView.SetEntity(_previewDummy);
_needUpdatePreview = true;
}
private void LoadServerData()
public void LoadServerData()
{
Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
UpdateAntagRequirements();
UpdateRoleRequirements();
UpdateControls();
_needUpdatePreview = true;
ShowClothes.Pressed = true;
}
private void SetAge(int newAge)
{
Profile = Profile?.WithAge(newAge);
IsDirty = true;
SetDirty();
}
private void SetSex(Sex newSex)
@@ -841,21 +822,21 @@ namespace Content.Client.Preferences.UI
UpdateGenderControls();
CMarkings.SetSex(newSex);
UpdateTTSVoicesControls(); //WD-EDIT
IsDirty = true;
SetDirty();
}
//WD-EDIT
private void SetVoice(string newVoice)
{
Profile = Profile?.WithVoice(newVoice);
IsDirty = true;
SetDirty();
}
//WD-EDIT
private void SetGender(Gender newGender)
{
Profile = Profile?.WithGender(newGender);
IsDirty = true;
SetDirty();
}
private void SetSpecies(string newSpecies)
@@ -864,65 +845,53 @@ namespace Content.Client.Preferences.UI
OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
UpdateSexControls(); // update sex for new species
RebuildSpriteView(); // they might have different inv so we need a new dummy
UpdateSpeciesGuidebookIcon();
UpdateBodyTypes();
IsDirty = true;
_needUpdatePreview = true;
SetDirty();
UpdatePreview();
}
private void SetName(string newName)
{
Profile = Profile?.WithName(newName);
IsDirty = true;
SetDirty();
}
private void SetClownName(string newName)
{
Profile = Profile?.WithClownName(newName);
IsDirty = true;
SetDirty();
}
private void SetMimeName(string newName)
{
Profile = Profile?.WithMimeName(newName);
IsDirty = true;
SetDirty();
}
private void SetBorgName(string newName)
{
Profile = Profile?.WithBorgName(newName);
IsDirty = true;
}
private void SetClothing(ClothingPreference newClothing)
{
Profile = Profile?.WithClothingPreference(newClothing);
IsDirty = true;
}
private void SetBackpack(BackpackPreference newBackpack)
{
Profile = Profile?.WithBackpackPreference(newBackpack);
IsDirty = true;
SetDirty();
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
{
Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
IsDirty = true;
SetDirty();
}
public void Save()
{
IsDirty = false;
if (Profile != null)
{
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
_needUpdatePreview = true;
}
if (Profile == null)
return;
_preferencesManager.UpdateCharacter(Profile, CharacterSlot);
OnProfileChanged?.Invoke(Profile, CharacterSlot);
// Reset profile to default.
UserInterfaceManager.GetUIController<LobbyUIController>().UpdateProfile();
}
private void OnBodyTypeSelected(OptionButton.ItemSelectedEventArgs args)
@@ -954,7 +923,7 @@ namespace Content.Client.Preferences.UI
}
CBodyTypesButton.Select(_bodyTypesList.FindIndex(x => x.ID == Profile.BodyType));
IsDirty = true;
SetDirty();
}
private bool IsDirty
@@ -963,7 +932,6 @@ namespace Content.Client.Preferences.UI
set
{
_isDirty = value;
_needUpdatePreview = true;
UpdateSaveButton();
}
}
@@ -1086,7 +1054,7 @@ namespace Content.Client.Preferences.UI
if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
return;
var style = speciesProto.GuideBookIcon;
const string style = "SpeciesInfoDefault";
SpeciesInfoButton.StyleClasses.Add(style);
}
@@ -1128,26 +1096,6 @@ namespace Content.Client.Preferences.UI
_genderButton.SelectId((int) Profile.Gender);
}
private void UpdateClothingControls()
{
if (Profile == null)
{
return;
}
_clothingButton.SelectId((int) Profile.Clothing);
}
private void UpdateBackpackControls()
{
if (Profile == null)
{
return;
}
_backpackButton.SelectId((int) Profile.Backpack);
}
private void UpdateSpawnPriorityControls()
{
if (Profile == null)
@@ -1266,13 +1214,13 @@ namespace Content.Client.Preferences.UI
if (Profile is null)
return;
var humanoid = _entMan.System<HumanoidAppearanceSystem>();
humanoid.LoadProfile(_previewDummy!.Value, Profile);
UserInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
SetPreviewRotation(_previewRotation);
}
if (ShowClothes.Pressed)
LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile);
_previewSpriteView.OverrideDirection = (Direction) ((int) _previewRotation % 4 * 2);
private void SetPreviewRotation(Direction direction)
{
_previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
}
public void UpdateControls()
@@ -1286,17 +1234,16 @@ namespace Content.Client.Preferences.UI
UpdateGenderControls();
UpdateSkinColor();
UpdateSpecies();
UpdateClothingControls();
UpdateBackpackControls();
UpdateSpawnPriorityControls();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
UpdateLoadouts();
UpdateRoleRequirements();
UpdateJobPriorities();
UpdateAntagPreferences();
UpdateTraitPreferences();
UpdateMarkings();
RebuildSpriteView();
UpdateHairPickers();
UpdateCMarkingsHair();
UpdateCMarkingsFacialHair();
@@ -1387,143 +1334,11 @@ namespace Content.Client.Preferences.UI
}
}
private abstract class RequirementsSelector<T> : Control
private void UpdateLoadouts()
{
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private StripeBack _lockStripe;
private Label _requirementsLabel;
protected RequirementsSelector(T proto)
foreach (var prioritySelector in _jobPriorities)
{
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
_requirementsLabel = new Label
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack
{
Visible = false,
HorizontalExpand = true,
MouseFilter = MouseFilterMode.Stop,
Children =
{
_requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
var container = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
};
if (icon != null)
container.AddChild(icon);
container.AddChild(titleLabel);
container.AddChild(Options);
container.AddChild(_lockStripe);
AddChild(container);
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90
};
}
}
private sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
: base(proto)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
prioritySelector.CloseLoadout();
}
}
@@ -1548,41 +1363,6 @@ namespace Content.Client.Preferences.UI
}
}
private sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
{
// 0 is yes and 1 is no
public bool Preference
{
get => Options.SelectedValue == 0;
set => Options.Select((value && !Disabled) ? 0 : 1);
}
public event Action<bool>? PreferenceChanged;
public AntagPreferenceSelector(AntagPrototype proto)
: base(proto)
{
Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
var items = new[]
{
("humanoid-profile-editor-antag-preference-yes-button", 0),
("humanoid-profile-editor-antag-preference-no-button", 1)
};
var title = Loc.GetString(proto.Name);
var description = Loc.GetString(proto.Objective);
Setup(items, title, 250, description);
// immediately lock requirements if they arent met.
// another function checks Disabled after creating the selector so this has to be done now
var requirements = IoCManager.Resolve<JobRequirementsManager>();
if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
{
LockRequirements(reason);
}
}
}
private sealed class TraitPreferenceSelector : Control
{
public TraitPrototype Trait { get; }

View File

@@ -0,0 +1,46 @@
using System.Numerics;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
public sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
{
public JobPriority Priority
{
get => (JobPriority) Options.SelectedValue;
set => Options.SelectByValue((int) value);
}
public event Action<JobPriority>? PriorityChanged;
public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
: base(proto, btnGroup)
{
Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
var items = new[]
{
("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
};
var icon = new TextureRect
{
TextureScale = new Vector2(2, 2),
VerticalAlignment = VAlignment.Center
};
var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
icon.Texture = jobIcon.Icon.Frame0();
Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
}
}

View File

@@ -0,0 +1,15 @@
<BoxContainer Name="Container" xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Orientation="Horizontal"
HorizontalExpand="True"
MouseFilter="Ignore"
Margin="0 0 0 5">
<Button Name="SelectButton" ToggleMode="True" Margin="0 0 5 0" HorizontalExpand="True"/>
<PanelContainer SetSize="64 64" HorizontalAlignment="Right">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<SpriteView Name="Sprite" Scale="4 4" MouseFilter="Stop"/>
</PanelContainer>
</BoxContainer>

View File

@@ -0,0 +1,74 @@
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutContainer : BoxContainer
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
private readonly EntityUid? _entity;
public Button Select => SelectButton;
public LoadoutContainer(ProtoId<ItemLoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
SelectButton.Disabled = disabled;
if (disabled && reason != null)
{
var tooltip = new Tooltip();
tooltip.SetMessage(reason);
SelectButton.TooltipSupplier = _ => tooltip;
}
if (_protoManager.TryIndex(proto, out var loadProto))
{
var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
if (ent != null)
{
_entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
Sprite.SetEntity(_entity);
var spriteTooltip = new Tooltip();
spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
Sprite.TooltipSupplier = _ => spriteTooltip;
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_entManager.DeleteEntity(_entity);
}
public bool Pressed
{
get => SelectButton.Pressed;
set => SelectButton.Pressed = value;
}
public string? Text
{
get => SelectButton.Text;
set => SelectButton.Text = value;
}
}

View File

@@ -0,0 +1,10 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Vertical">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
</PanelContainer>
<!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
<Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
<BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
</BoxContainer>

View File

@@ -0,0 +1,93 @@
using System.Linq;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutGroupContainer : BoxContainer
{
private readonly LoadoutGroupPrototype _groupProto;
public event Action<ProtoId<ItemLoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<ItemLoadoutPrototype>>? OnLoadoutUnpressed;
public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
_groupProto = groupProto;
RefreshLoadouts(loadout, session, collection);
}
/// <summary>
/// Updates button availabilities and buttons.
/// </summary>
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
var protoMan = collection.Resolve<IPrototypeManager>();
var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
RestrictionsContainer.DisposeAllChildren();
if (_groupProto.MinLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-min-limit", ("count", _groupProto.MinLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (_groupProto.MaxLimit > 0)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-max-limit", ("count", _groupProto.MaxLimit)),
Margin = new Thickness(5, 0, 5, 5),
});
}
if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
{
RestrictionsContainer.AddChild(new Label()
{
Text = Loc.GetString("loadouts-points-limit", ("count", loadout.Points.Value), ("max", roleProto.Points.Value)),
Margin = new Thickness(5, 0, 5, 5),
});
}
LoadoutsContainer.DisposeAllChildren();
// Didn't use options because this is more robust in future.
var selected = loadout.SelectedLoadouts[_groupProto.ID];
foreach (var loadoutProto in _groupProto.Loadouts)
{
if (!protoMan.TryIndex(loadoutProto, out var loadProto))
continue;
var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
var pressed = matchingLoadout != null;
var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
loadoutContainer.Select.Pressed = pressed;
loadoutContainer.Text = loadoutSystem.GetName(loadProto);
loadoutContainer.Select.OnPressed += args =>
{
if (args.Button.Pressed)
OnLoadoutPressed?.Invoke(loadoutProto);
else
OnLoadoutUnpressed?.Invoke(loadoutProto);
};
LoadoutsContainer.AddChild(loadoutContainer);
}
}
}

View File

@@ -0,0 +1,10 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="800 800"
MinSize="800 64">
<VerticalTabContainer Name="LoadoutGroupsContainer"
VerticalExpand="True"
HorizontalExpand="True">
</VerticalTabContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,60 @@
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Preferences.UI;
[GenerateTypedNameReferences]
public sealed partial class LoadoutWindow : FancyWindow
{
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<ItemLoadoutPrototype>>? OnLoadoutPressed;
public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<ItemLoadoutPrototype>>? OnLoadoutUnpressed;
private List<LoadoutGroupContainer> _groups = new();
public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
{
RobustXamlLoader.Load(this);
var protoManager = collection.Resolve<IPrototypeManager>();
foreach (var group in proto.Groups)
{
if (!protoManager.TryIndex(group, out var groupProto))
continue;
var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
_groups.Add(container);
container.OnLoadoutPressed += args =>
{
OnLoadoutPressed?.Invoke(group, args);
};
container.OnLoadoutUnpressed += args =>
{
OnLoadoutUnpressed?.Invoke(group, args);
};
}
}
public override void Close()
{
base.Close();
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(null);
}
public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
{
foreach (var group in _groups)
{
group.RefreshLoadouts(loadout, session, collection);
}
}
}

View File

@@ -0,0 +1,222 @@
using System.Numerics;
using Content.Client.Lobby;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Clothing;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Content.Shared.Roles;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.Preferences.UI;
public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototype
{
private ButtonGroup _loadoutGroup;
public T Proto { get; }
public bool Disabled => _lockStripe.Visible;
protected readonly RadioOptions<int> Options;
private readonly StripeBack _lockStripe;
private LoadoutWindow? _loadoutWindow;
private RoleLoadout? _loadout;
/// <summary>
/// Raised if a loadout has been updated.
/// </summary>
public event Action<RoleLoadout>? LoadoutUpdated;
protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
{
_loadoutGroup = loadoutGroup;
Proto = proto;
Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
{
FirstButtonStyle = StyleBase.ButtonOpenRight,
ButtonStyle = StyleBase.ButtonOpenBoth,
LastButtonStyle = StyleBase.ButtonOpenLeft,
HorizontalExpand = true,
};
//Override default radio option button width
Options.GenerateItem = GenerateButton;
Options.OnItemSelected += args => Options.Select(args.Id);
var requirementsLabel = new Label()
{
Text = Loc.GetString("role-timer-locked"),
Visible = true,
HorizontalAlignment = HAlignment.Center,
StyleClasses = {StyleBase.StyleClassLabelSubText},
};
_lockStripe = new StripeBack()
{
Visible = false,
HorizontalExpand = true,
HasMargins = false,
MouseFilter = MouseFilterMode.Stop,
Children =
{
requirementsLabel
}
};
// Setup must be called after
}
/// <summary>
/// Actually adds the controls, must be called in the inheriting class' constructor.
/// </summary>
protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
{
_loadout = loadout;
foreach (var (text, value) in items)
{
Options.AddItem(Loc.GetString(text), value);
}
var titleLabel = new Label()
{
Margin = new Thickness(5f, 0, 5f, 0),
Text = title,
MinSize = new Vector2(titleSize, 0),
MouseFilter = MouseFilterMode.Stop,
ToolTip = description
};
if (icon != null)
AddChild(icon);
AddChild(titleLabel);
AddChild(Options);
AddChild(_lockStripe);
var loadoutWindowBtn = new Button()
{
Text = Loc.GetString("loadout-window"),
HorizontalAlignment = HAlignment.Right,
Group = _loadoutGroup,
Margin = new Thickness(3f, 0f, 0f, 0f),
};
var collection = IoCManager.Instance!;
var protoManager = collection.Resolve<IPrototypeManager>();
// If no loadout found then disabled button
if (!protoManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(Proto.ID)))
{
loadoutWindowBtn.Disabled = true;
}
// else
else
{
var session = collection.Resolve<IPlayerManager>().LocalSession!;
// TODO: Most of lobby state should be a uicontroller
// trying to handle all this shit is a big-ass mess.
// Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
loadoutWindowBtn.OnPressed += args =>
{
if (args.Button.Pressed)
{
// We only create a loadout when necessary to avoid unnecessary DB entries.
_loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
_loadout.SetDefault(protoManager);
_loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
{
Title = Loc.GetString(Proto.ID + "-loadout"),
};
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
// If it's a job preview then refresh it.
if (Proto is JobPrototype jobProto)
{
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.SetDummyJob(jobProto);
}
_loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
{
if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
return;
_loadout.EnsureValid(session, collection);
_loadoutWindow.RefreshLoadouts(_loadout, session, collection);
var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
controller.ReloadProfile();
LoadoutUpdated?.Invoke(_loadout);
};
_loadoutWindow.OpenCenteredLeft();
_loadoutWindow.OnClose += () =>
{
loadoutWindowBtn.Pressed = false;
_loadoutWindow?.Dispose();
_loadoutWindow = null;
};
}
else
{
CloseLoadout();
}
};
}
AddChild(loadoutWindowBtn);
}
public void CloseLoadout()
{
_loadoutWindow?.Close();
_loadoutWindow?.Dispose();
_loadoutWindow = null;
}
public void LockRequirements(FormattedMessage requirements)
{
var tooltip = new Tooltip();
tooltip.SetMessage(requirements);
_lockStripe.TooltipSupplier = _ => tooltip;
_lockStripe.Visible = true;
Options.Visible = false;
}
// TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
public void UnlockRequirements()
{
_lockStripe.Visible = false;
Options.Visible = true;
}
private Button GenerateButton(string text, int value)
{
return new Button
{
Text = text,
MinWidth = 90,
HorizontalExpand = true,
};
}
}

View File

@@ -233,7 +233,7 @@ public sealed partial class GeneralStationRecordConsoleWindow : DefaultWindow
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name, profile);
var itemType = gear.GetGear(slot.Name);
if (inventorySystem.TryUnequip(dummy, slot.Name, out var unequippedItem, true, true))
{
entityManager.DeleteEntity(unequippedItem.Value);

View File

@@ -17,7 +17,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
private string _windowName = Loc.GetString("store-ui-default-title");
[ViewVariables]
private string _search = "";
private string _search = string.Empty;
[ViewVariables]
private HashSet<ListingData> _listings = new();
@@ -41,7 +41,7 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
_menu.OnCategoryButtonPressed += (_, category) =>
{
_menu.CurrentCategory = category;
SendMessage(new StoreRequestUpdateInterfaceMessage());
_menu?.UpdateListing();
};
_menu.OnWithdrawAttempt += (_, type, amount) =>
@@ -49,11 +49,6 @@ public sealed class StoreBoundUserInterface : BoundUserInterface
SendMessage(new StoreRequestWithdrawMessage(type, amount));
};
_menu.OnRefreshButtonPressed += (_) =>
{
SendMessage(new StoreRequestUpdateInterfaceMessage());
};
_menu.SearchTextUpdated += (_, search) =>
{
_search = search.Trim().ToLowerInvariant();

View File

@@ -15,6 +15,7 @@
Margin="0,0,4,0"
MinSize="48 48"
Stretch="KeepAspectCentered" />
<Control MinWidth="5"/>
<RichTextLabel Name="StoreItemDescription" />
</BoxContainer>
</BoxContainer>

View File

@@ -1,25 +1,91 @@
using Content.Client.GameTicking.Managers;
using Content.Shared.Store;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Graphics;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Store.Ui;
[GenerateTypedNameReferences]
public sealed partial class StoreListingControl : Control
{
public StoreListingControl(string itemName, string itemDescription,
string price, bool canBuy, Texture? texture = null)
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IEntityManager _entity = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly ClientGameTicker _ticker;
private readonly ListingData _data;
private readonly bool _hasBalance;
private readonly string _price;
public StoreListingControl(ListingData data, string price, bool hasBalance, Texture? texture = null)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
StoreItemName.Text = itemName;
StoreItemDescription.SetMessage(itemDescription);
_ticker = _entity.System<ClientGameTicker>();
StoreItemBuyButton.Text = price;
StoreItemBuyButton.Disabled = !canBuy;
_data = data;
_hasBalance = hasBalance;
_price = price;
StoreItemName.Text = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype);
StoreItemDescription.SetMessage(ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(_data, _prototype));
UpdateBuyButtonText();
StoreItemBuyButton.Disabled = !CanBuy();
StoreItemTexture.Texture = texture;
}
private bool CanBuy()
{
if (!_hasBalance)
return false;
var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
if (_data.RestockTime > stationTime)
return false;
return true;
}
private void UpdateBuyButtonText()
{
var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
if (_data.RestockTime > stationTime)
{
var timeLeftToBuy = stationTime - _data.RestockTime;
StoreItemBuyButton.Text = timeLeftToBuy.Duration().ToString(@"mm\:ss");
}
else
{
StoreItemBuyButton.Text = _price;
}
}
private void UpdateName()
{
var name = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(_data, _prototype);
var stationTime = _timing.CurTime.Subtract(_ticker.RoundStartTimeSpan);
if (_data.RestockTime > stationTime)
{
name += Loc.GetString("store-ui-button-out-of-stock");
}
StoreItemName.Text = name;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdateBuyButtonText();
UpdateName();
StoreItemBuyButton.Disabled = !CanBuy();
}
}

View File

@@ -12,11 +12,6 @@
HorizontalAlignment="Left"
Access="Public"
HorizontalExpand="True" />
<Button
Name="RefreshButton"
MinWidth="64"
HorizontalAlignment="Right"
Text="Refresh" />
<Button
Name="WithdrawButton"
MinWidth="64"

View File

@@ -1,6 +1,5 @@
using System.Linq;
using Content.Client.Actions;
using Content.Client.GameTicking.Managers;
using Content.Client.Message;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
@@ -11,7 +10,6 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Store.Ui;
@@ -20,9 +18,6 @@ public sealed partial class StoreMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
private readonly ClientGameTicker _gameTicker;
private StoreWithdrawWindow? _withdrawWindow;
@@ -30,21 +25,19 @@ public sealed partial class StoreMenu : DefaultWindow
public event Action<BaseButton.ButtonEventArgs, ListingData>? OnListingButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string>? OnCategoryButtonPressed;
public event Action<BaseButton.ButtonEventArgs, string, int>? OnWithdrawAttempt;
public event Action<BaseButton.ButtonEventArgs>? OnRefreshButtonPressed;
public event Action<BaseButton.ButtonEventArgs>? OnRefundAttempt;
public Dictionary<string, FixedPoint2> Balance = new();
public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance = new();
public string CurrentCategory = string.Empty;
private List<ListingData> _cachedListings = new();
public StoreMenu(string name)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_gameTicker = _entitySystem.GetEntitySystem<ClientGameTicker>();
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
RefreshButton.OnButtonDown += OnRefreshButtonDown;
RefundButton.OnButtonDown += OnRefundButtonDown;
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
@@ -52,12 +45,12 @@ public sealed partial class StoreMenu : DefaultWindow
Window.Title = name;
}
public void UpdateBalance(Dictionary<string, FixedPoint2> balance)
public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance)
{
Balance = balance;
var currency = balance.ToDictionary(type =>
(type.Key, type.Value), type => _prototypeManager.Index<CurrencyPrototype>(type.Key));
(type.Key, type.Value), type => _prototypeManager.Index(type.Key));
var balanceStr = string.Empty;
foreach (var ((_, amount), proto) in currency)
@@ -75,12 +68,18 @@ public sealed partial class StoreMenu : DefaultWindow
disabled = false;
}
WithdrawButton.Visible = !disabled;
WithdrawButton.Disabled = disabled;
}
public void UpdateListing(List<ListingData> listings)
{
var sorted = listings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
_cachedListings = listings;
UpdateListing();
}
public void UpdateListing()
{
var sorted = _cachedListings.OrderBy(l => l.Priority).ThenBy(l => l.Cost.Values.Sum());
// should probably chunk these out instead. to-do if this clogs the internet tubes.
// maybe read clients prototypes instead?
@@ -96,12 +95,6 @@ public sealed partial class StoreMenu : DefaultWindow
TraitorFooter.Visible = visible;
}
private void OnRefreshButtonDown(BaseButton.ButtonEventArgs args)
{
OnRefreshButtonPressed?.Invoke(args);
}
private void OnWithdrawButtonDown(BaseButton.ButtonEventArgs args)
{
// check if window is already open
@@ -129,10 +122,8 @@ public sealed partial class StoreMenu : DefaultWindow
if (!listing.Categories.Contains(CurrentCategory))
return;
var listingName = ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager);
var listingDesc = ListingLocalisationHelpers.GetLocalisedDescriptionOrEntityDescription(listing, _prototypeManager);
var listingPrice = listing.Cost;
var canBuy = CanBuyListing(Balance, listingPrice);
var hasBalance = HasListingPrice(Balance, listingPrice);
var spriteSys = _entityManager.EntitySysManager.GetEntitySystem<SpriteSystem>();
@@ -154,21 +145,8 @@ public sealed partial class StoreMenu : DefaultWindow
texture = spriteSys.Frame0(action.Icon);
}
}
var listingInStock = ListingInStock(listing);
if (listingInStock != GetListingPriceString(listing))
{
listingName += " (Out of stock)";
canBuy = false;
}
// WD START
if (listing.SaleAmount > 0)
listingName += $" [СКИДКА] ({listing.SaleAmount}%!)";
else if (listing.OldCost.Count > 0)
listingName += " [Скидка закончилась]";
// WD END
var newListing = new StoreListingControl(listingName, listingDesc, listingInStock, canBuy, texture);
var newListing = new StoreListingControl(listing, GetListingPriceString(listing), hasBalance, texture);
if (listing.SaleAmount > 0) // WD
newListing.StoreItemBuyButton.AddStyleClass("ButtonColorRed");
@@ -179,25 +157,7 @@ public sealed partial class StoreMenu : DefaultWindow
StoreListingsContainer.AddChild(newListing);
}
/// <summary>
/// Return time until available or the cost.
/// </summary>
/// <param name="listing"></param>
/// <returns></returns>
public string ListingInStock(ListingData listing)
{
var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
TimeSpan restockTimeSpan = TimeSpan.FromMinutes(listing.RestockTime);
if (restockTimeSpan > stationTime)
{
var timeLeftToBuy = stationTime - restockTimeSpan;
return timeLeftToBuy.Duration().ToString(@"mm\:ss");
}
return GetListingPriceString(listing);
}
public bool CanBuyListing(Dictionary<string, FixedPoint2> currency, Dictionary<string, FixedPoint2> price)
public bool HasListingPrice(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> currency, Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> price)
{
foreach (var type in price)
{
@@ -219,7 +179,7 @@ public sealed partial class StoreMenu : DefaultWindow
{
foreach (var (type, amount) in listing.Cost)
{
var currency = _prototypeManager.Index<CurrencyPrototype>(type);
var currency = _prototypeManager.Index(type);
text += Loc.GetString("store-ui-price-display", ("amount", amount),
("currency", Loc.GetString(currency.DisplayName, ("amount", amount))));
}
@@ -240,7 +200,7 @@ public sealed partial class StoreMenu : DefaultWindow
{
foreach (var cat in listing.Categories)
{
var proto = _prototypeManager.Index<StoreCategoryPrototype>(cat);
var proto = _prototypeManager.Index(cat);
if (!allCategories.Contains(proto))
allCategories.Add(proto);
}
@@ -259,12 +219,17 @@ public sealed partial class StoreMenu : DefaultWindow
if (allCategories.Count < 1)
return;
var group = new ButtonGroup();
foreach (var proto in allCategories)
{
var catButton = new StoreCategoryButton
{
Text = Loc.GetString(proto.Name),
Id = proto.ID
Id = proto.ID,
Pressed = proto.ID == CurrentCategory,
Group = group,
ToggleMode = true,
StyleClasses = { "OpenBoth" }
};
catButton.OnPressed += args => OnCategoryButtonPressed?.Invoke(args, catButton.Id);
@@ -280,7 +245,7 @@ public sealed partial class StoreMenu : DefaultWindow
public void UpdateRefund(bool allowRefund)
{
RefundButton.Disabled = !allowRefund;
RefundButton.Visible = allowRefund;
}
private sealed class StoreCategoryButton : Button

View File

@@ -28,12 +28,12 @@ public sealed partial class StoreWithdrawWindow : DefaultWindow
IoCManager.InjectDependencies(this);
}
public void CreateCurrencyButtons(Dictionary<string, FixedPoint2> balance)
public void CreateCurrencyButtons(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance)
{
_validCurrencies.Clear();
foreach (var currency in balance)
{
if (!_prototypeManager.TryIndex<CurrencyPrototype>(currency.Key, out var proto))
if (!_prototypeManager.TryIndex(currency.Key, out var proto))
continue;
_validCurrencies.Add(currency.Value, proto);

View File

@@ -8,20 +8,20 @@ public sealed class JukeboxBUI : BoundUserInterface
{
[Dependency] private readonly EntityManager _entityManager = default!;
private readonly JukeboxMenu? _window;
private readonly WhiteJukeboxMenu? _window;
public JukeboxBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
var sharedPopupSystem = _entityManager.System<SharedPopupSystem>();
if (!_entityManager.TryGetComponent(owner, out JukeboxComponent? jukeboxComponent))
if (!_entityManager.TryGetComponent(owner, out WhiteJukeboxComponent? jukeboxComponent))
{
sharedPopupSystem.PopupEntity("Тут нет JukeboxComponent, звоните кодерам", owner);
sharedPopupSystem.PopupEntity("Тут нет WhiteJukeboxComponent, звоните кодерам", owner);
return;
}
_window = new JukeboxMenu(owner, jukeboxComponent);
_window = new WhiteJukeboxMenu(owner, jukeboxComponent);
_window.RepeatButton.OnToggled += OnRepeatButtonToggled;
_window.StopButton.OnPressed += OnStopButtonPressed;
_window.EjectButton.OnPressed += OnEjectButtonPressed;

View File

@@ -25,7 +25,7 @@ public sealed class JukeboxSystem : EntitySystem
private const CollisionGroup CollisionMask = CollisionGroup.Impassable;
private readonly Dictionary<JukeboxComponent, JukeboxAudio> _playingJukeboxes = new();
private readonly Dictionary<WhiteJukeboxComponent, JukeboxAudio> _playingJukeboxes = new();
private float _jukeboxVolume;
@@ -35,7 +35,7 @@ public sealed class JukeboxSystem : EntitySystem
_cfg.OnValueChanged(WhiteCVars.JukeboxVolume, JukeboxVolumeChanged, true);
SubscribeLocalEvent<JukeboxComponent, ComponentRemove>(OnComponentRemoved);
SubscribeLocalEvent<WhiteJukeboxComponent, ComponentRemove>(OnComponentRemoved);
SubscribeNetworkEvent<RoundRestartCleanupEvent>(OnRoundRestart);
SubscribeNetworkEvent<TickerJoinLobbyEvent>(JoinLobby);
SubscribeNetworkEvent<JukeboxStopPlaying>(OnStopPlaying);
@@ -57,7 +57,7 @@ public sealed class JukeboxSystem : EntitySystem
CleanUp();
}
private void OnComponentRemoved(EntityUid uid, JukeboxComponent component, ComponentRemove args)
private void OnComponentRemoved(EntityUid uid, WhiteJukeboxComponent component, ComponentRemove args)
{
if (!_playingJukeboxes.TryGetValue(component, out var playingData)) return;
@@ -68,7 +68,7 @@ public sealed class JukeboxSystem : EntitySystem
private void OnStopPlaying(JukeboxStopPlaying ev)
{
if (!ev.JukeboxUid.HasValue) return;
if (!TryComp<JukeboxComponent>(GetEntity(ev.JukeboxUid), out var jukeboxComponent)) return;
if (!TryComp<WhiteJukeboxComponent>(GetEntity(ev.JukeboxUid), out var jukeboxComponent)) return;
if (!_playingJukeboxes.TryGetValue(jukeboxComponent, out var jukeboxAudio)) return;
@@ -76,7 +76,7 @@ public sealed class JukeboxSystem : EntitySystem
_playingJukeboxes.Remove(jukeboxComponent);
}
public void RequestSongToPlay(EntityUid jukebox, JukeboxComponent component, JukeboxSong jukeboxSong)
public void RequestSongToPlay(EntityUid jukebox, WhiteJukeboxComponent component, JukeboxSong jukeboxSong)
{
if (!_resource.TryGetResource<AudioResource>((ResPath) jukeboxSong.SongPath!, out var songResource))
{
@@ -108,7 +108,7 @@ public sealed class JukeboxSystem : EntitySystem
private void ProcessJukeboxes()
{
var jukeboxes = EntityQueryEnumerator<JukeboxComponent, TransformComponent>();
var jukeboxes = EntityQueryEnumerator<WhiteJukeboxComponent, TransformComponent>();
var player = _playerManager.LocalEntity!.Value;
var playerXform = Comp<TransformComponent>(player);
@@ -173,7 +173,7 @@ public sealed class JukeboxSystem : EntitySystem
private void SetRolloffAndOcclusion(
EntityUid player,
EntityUid jukebox,
JukeboxComponent jukeboxComponent,
WhiteJukeboxComponent jukeboxComponent,
JukeboxAudio jukeboxAudio)
{
var jukeboxWorldPosition = _transform.GetWorldPosition(jukebox);
@@ -197,7 +197,7 @@ public sealed class JukeboxSystem : EntitySystem
EntityUid jukebox,
EntityUid player,
JukeboxAudio jukeboxAudio,
JukeboxComponent jukeboxComponent)
WhiteJukeboxComponent jukeboxComponent)
{
jukeboxAudio.PlayingStream.StopPlaying();
@@ -221,7 +221,7 @@ public sealed class JukeboxSystem : EntitySystem
EntityUid jukebox,
EntityUid player,
JukeboxAudio jukeboxAudio,
JukeboxComponent jukeboxComponent)
WhiteJukeboxComponent jukeboxComponent)
{
if (!jukeboxComponent.Playing)
{
@@ -247,7 +247,7 @@ public sealed class JukeboxSystem : EntitySystem
}
}
private JukeboxAudio? TryCreateStream(EntityUid jukebox, EntityUid player, JukeboxComponent jukeboxComponent)
private JukeboxAudio? TryCreateStream(EntityUid jukebox, EntityUid player, WhiteJukeboxComponent jukeboxComponent)
{
if (jukeboxComponent.PlayingSongData == null) return null!;

View File

@@ -9,18 +9,18 @@ using Robust.Shared.Timing;
namespace Content.Client._White.Jukebox;
[GenerateTypedNameReferences]
public sealed partial class JukeboxMenu : DefaultWindow
public sealed partial class WhiteJukeboxMenu : DefaultWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly JukeboxSystem _jukeboxSystem;
private readonly EntityUid _jukeboxEntity;
private readonly JukeboxComponent _component;
private readonly WhiteJukeboxComponent _component;
private readonly List<JukeboxSongEntry> _defaultSongsEntries = new() { };
private readonly List<JukeboxSongEntry> _tapeSongsEntries = new() { };
public JukeboxMenu(EntityUid jukeboxEntity, JukeboxComponent component)
public WhiteJukeboxMenu(EntityUid jukeboxEntity, WhiteJukeboxComponent component)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);

View File

@@ -19,7 +19,7 @@ public sealed partial class MindTests
await using var pair = await PoolManager.GetServerClient(settings);
// Client is connected with a valid entity & mind
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
// Delete **everything**
@@ -28,6 +28,12 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
foreach (var ent in pair.Client.EntMan.GetEntities())
{
Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent));
}
Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
// Create a new map.
@@ -36,7 +42,7 @@ public sealed partial class MindTests
await pair.RunTicksSync(5);
// Client is not attached to anything
Assert.That(pair.Client.Player?.ControlledEntity, Is.Null);
Assert.That(pair.Client.AttachedEntity, Is.Null);
Assert.That(pair.PlayerData?.Mind, Is.Null);
// Attempt to ghost
@@ -45,9 +51,9 @@ public sealed partial class MindTests
await pair.RunTicksSync(10);
// Client should be attached to a ghost placed on the new map.
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
await pair.CleanReturnAsync();

View File

@@ -0,0 +1,44 @@
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Preferences;
[TestFixture]
[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
public sealed class LoadoutTests
{
/// <summary>
/// Checks that an empty loadout still spawns with default gear and not naked.
/// </summary>
[Test]
public async Task TestEmptyLoadout()
{
var pair = await PoolManager.GetServerClient(new PoolSettings()
{
Dirty = true,
});
var server = pair.Server;
var entManager = server.ResolveDependency<IEntityManager>();
// Check that an empty role loadout spawns gear
var stationSystem = entManager.System<StationSpawningSystem>();
var testMap = await pair.CreateTestMap();
// That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
var profile = new HumanoidCharacterProfile();
profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
{
// Sue me, there's so much involved in setting up jobs
Prototype = "CargoTechnician"
}, profile, station: null);
await pair.CleanReturnAsync();
}
}

View File

@@ -4,6 +4,8 @@ using Content.Server.Database;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Configuration;
@@ -58,8 +60,6 @@ namespace Content.IntegrationTests.Tests.Preferences
Color.Beige,
new ()
),
ClothingPreference.Jumpskirt,
BackpackPreference.Backpack,
SpawnPriorityPreference.None,
new Dictionary<string, JobPriority>
{
@@ -67,7 +67,8 @@ namespace Content.IntegrationTests.Tests.Preferences
},
PreferenceUnavailableMode.StayInLobby,
new List<string> (),
new List<string>()
new List<string>(),
new Dictionary<string, RoleLoadout>()
);
}

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldDefaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
migrationBuilder.Sql("UPDATE round SET start_date = NULL WHERE start_date = '-Infinity';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "timestamp with time zone",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class Retards : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_id = table.Column<int>(type: "integer", nullable: false),
role_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false),
group_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loa~",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false),
loadout_name = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group~",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@@ -762,11 +762,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("text")
.HasColumnName("backpack");
b.Property<string>("BodyType")
.IsRequired()
.HasColumnType("text")
@@ -782,11 +777,6 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("text")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("text")
.HasColumnName("clothing");
b.Property<string>("ClownName")
.IsRequired()
.HasColumnType("text")
@@ -884,6 +874,84 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_loadout_group_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("profile_role_loadout_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("ProfileId")
.HasColumnType("integer")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("text")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -897,10 +965,8 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("integer")
.HasColumnName("server_id");
b.Property<DateTime>("StartDate")
.ValueGeneratedOnAdd()
b.Property<DateTime?>("StartDate")
.HasColumnType("timestamp with time zone")
.HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.HasColumnName("start_date");
b.HasKey("Id")
@@ -1581,6 +1647,42 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1793,9 +1895,21 @@ namespace Content.Server.Database.Migrations.Postgres
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

View File

@@ -0,0 +1,38 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "TEXT",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldDefaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified));
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "round",
type: "TEXT",
nullable: false,
defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified),
oldClrType: typeof(DateTime),
oldType: "TEXT",
oldNullable: true);
}
}
}

View File

@@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class FixRoundStartDateNullability2 : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// This needs to be its own separate migration,
// because EF Core re-arranges the order of the commands if it's a single migration...
// (only relevant for SQLite since it needs cursed shit to do ALTER COLUMN)
migrationBuilder.Sql("UPDATE round SET start_date = NULL WHERE start_date = '0001-01-01 00:00:00';");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,125 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class Retards : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "backpack",
table: "profile");
migrationBuilder.DropColumn(
name: "clothing",
table: "profile");
migrationBuilder.CreateTable(
name: "profile_role_loadout",
columns: table => new
{
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_id = table.Column<int>(type: "INTEGER", nullable: false),
role_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
table.ForeignKey(
name: "FK_profile_role_loadout_profile_profile_id",
column: x => x.profile_id,
principalTable: "profile",
principalColumn: "profile_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout_group",
columns: table => new
{
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false),
group_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
table.ForeignKey(
name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id",
column: x => x.profile_role_loadout_id,
principalTable: "profile_role_loadout",
principalColumn: "profile_role_loadout_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "profile_loadout",
columns: table => new
{
profile_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false),
loadout_name = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
table.ForeignKey(
name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group_id",
column: x => x.profile_loadout_group_id,
principalTable: "profile_loadout_group",
principalColumn: "profile_loadout_group_id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_profile_loadout_group_id",
table: "profile_loadout",
column: "profile_loadout_group_id");
migrationBuilder.CreateIndex(
name: "IX_profile_loadout_group_profile_role_loadout_id",
table: "profile_loadout_group",
column: "profile_role_loadout_id");
migrationBuilder.CreateIndex(
name: "IX_profile_role_loadout_profile_id",
table: "profile_role_loadout",
column: "profile_id");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "backpack",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "clothing",
table: "profile",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.DropTable(
name: "profile_loadout");
migrationBuilder.DropTable(
name: "profile_loadout_group");
migrationBuilder.DropTable(
name: "profile_role_loadout");
}
}
}

View File

@@ -713,11 +713,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("age");
b.Property<string>("Backpack")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("backpack");
b.Property<string>("BodyType")
.IsRequired()
.HasColumnType("TEXT")
@@ -733,11 +728,6 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT")
.HasColumnName("char_name");
b.Property<string>("Clothing")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("clothing");
b.Property<string>("ClownName")
.IsRequired()
.HasColumnType("TEXT")
@@ -835,6 +825,78 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("profile", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_id");
b.Property<string>("LoadoutName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("loadout_name");
b.Property<int>("ProfileLoadoutGroupId")
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.HasKey("Id")
.HasName("PK_profile_loadout");
b.HasIndex("ProfileLoadoutGroupId");
b.ToTable("profile_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_loadout_group_id");
b.Property<string>("GroupName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("group_name");
b.Property<int>("ProfileRoleLoadoutId")
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.HasKey("Id")
.HasName("PK_profile_loadout_group");
b.HasIndex("ProfileRoleLoadoutId");
b.ToTable("profile_loadout_group", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("profile_role_loadout_id");
b.Property<int>("ProfileId")
.HasColumnType("INTEGER")
.HasColumnName("profile_id");
b.Property<string>("RoleName")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("role_name");
b.HasKey("Id")
.HasName("PK_profile_role_loadout");
b.HasIndex("ProfileId");
b.ToTable("profile_role_loadout", (string)null);
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Property<int>("Id")
@@ -846,10 +908,8 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("INTEGER")
.HasColumnName("server_id");
b.Property<DateTime>("StartDate")
.ValueGeneratedOnAdd()
b.Property<DateTime?>("StartDate")
.HasColumnType("TEXT")
.HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
.HasColumnName("start_date");
b.HasKey("Id")
@@ -1510,6 +1570,42 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Preference");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
{
b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
.WithMany("Loadouts")
.HasForeignKey("ProfileLoadoutGroupId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
b.Navigation("ProfileLoadoutGroup");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
.WithMany("Groups")
.HasForeignKey("ProfileRoleLoadoutId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
b.Navigation("ProfileRoleLoadout");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.HasOne("Content.Server.Database.Profile", "Profile")
.WithMany("Loadouts")
.HasForeignKey("ProfileId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired()
.HasConstraintName("FK_profile_role_loadout_profile_profile_id");
b.Navigation("Profile");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.HasOne("Content.Server.Database.Server", "Server")
@@ -1722,9 +1818,21 @@ namespace Content.Server.Database.Migrations.Sqlite
b.Navigation("Jobs");
b.Navigation("Loadouts");
b.Navigation("Traits");
});
modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
{
b.Navigation("Loadouts");
});
modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
{
b.Navigation("Groups");
});
modelBuilder.Entity("Content.Server.Database.Round", b =>
{
b.Navigation("AdminLogs");

View File

@@ -57,8 +57,26 @@ namespace Content.Server.Database
.IsUnique();
modelBuilder.Entity<Trait>()
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
.HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
.IsUnique();
modelBuilder.Entity<ProfileRoleLoadout>()
.HasOne(e => e.Profile)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileId)
.IsRequired();
modelBuilder.Entity<ProfileLoadoutGroup>()
.HasOne(e => e.ProfileRoleLoadout)
.WithMany(e => e.Groups)
.HasForeignKey(e => e.ProfileRoleLoadoutId)
.IsRequired();
modelBuilder.Entity<ProfileLoadout>()
.HasOne(e => e.ProfileLoadoutGroup)
.WithMany(e => e.Loadouts)
.HasForeignKey(e => e.ProfileLoadoutGroupId)
.IsRequired();
modelBuilder.Entity<Job>()
.HasIndex(j => j.ProfileId);
@@ -119,10 +137,6 @@ namespace Content.Server.Database
modelBuilder.Entity<Round>()
.HasIndex(round => round.StartDate);
modelBuilder.Entity<Round>()
.Property(round => round.StartDate)
.HasDefaultValue(default(DateTime));
modelBuilder.Entity<AdminLogPlayer>()
.HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
@@ -360,13 +374,13 @@ namespace Content.Server.Database
public string FacialHairColor { get; set; } = null!;
public string EyeColor { get; set; } = null!;
public string SkinColor { get; set; } = null!;
public string Clothing { get; set; } = null!;
public string Backpack { get; set; } = null!;
public int SpawnPriority { get; set; } = 0;
public List<Job> Jobs { get; } = new();
public List<Antag> Antags { get; } = new();
public List<Trait> Traits { get; } = new();
public List<ProfileRoleLoadout> Loadouts { get; } = new();
[Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
public int PreferenceId { get; set; }
@@ -410,6 +424,79 @@ namespace Content.Server.Database
public string TraitName { get; set; } = null!;
}
#region Loadouts
/// <summary>
/// Corresponds to a single role's loadout inside the DB.
/// </summary>
public class ProfileRoleLoadout
{
public int Id { get; set; }
public int ProfileId { get; set; }
public Profile Profile { get; set; } = null!;
/// <summary>
/// The corresponding role prototype on the profile.
/// </summary>
public string RoleName { get; set; } = string.Empty;
/// <summary>
/// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
/// </summary>
public List<ProfileLoadoutGroup> Groups { get; set; } = new();
}
/// <summary>
/// Corresponds to a loadout group prototype with the specified loadouts attached.
/// </summary>
public class ProfileLoadoutGroup
{
public int Id { get; set; }
public int ProfileRoleLoadoutId { get; set; }
/// <summary>
/// The corresponding RoleLoadout that owns this.
/// </summary>
public ProfileRoleLoadout ProfileRoleLoadout { get; set; } = null!;
/// <summary>
/// The corresponding group prototype.
/// </summary>
public string GroupName { get; set; } = string.Empty;
/// <summary>
/// Selected loadout prototype. Null if none is set.
/// May get validated at runtime and updated to to the default.
/// </summary>
public List<ProfileLoadout> Loadouts { get; set; } = new();
}
/// <summary>
/// Corresponds to a selected loadout.
/// </summary>
public class ProfileLoadout
{
public int Id { get; set; }
public int ProfileLoadoutGroupId { get; set; }
public ProfileLoadoutGroup ProfileLoadoutGroup { get; set; } = null!;
/// <summary>
/// Corresponding loadout prototype.
/// </summary>
public string LoadoutName { get; set; } = string.Empty;
/*
* Insert extra data here like custom descriptions or colors or whatever.
*/
}
#endregion
public enum DbPreferenceUnavailableMode
{
// These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.
@@ -514,7 +601,7 @@ namespace Content.Server.Database
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime? StartDate { get; set; }
public List<Player> Players { get; set; } = default!;
@@ -901,8 +988,35 @@ namespace Content.Server.Database
public byte[] Data { get; set; } = default!;
}
// Note: this interface isn't used by the game, but it *is* used by SS14.Admin.
// Don't remove! Or face the consequences!
public interface IAdminRemarksCommon
{
public int Id { get; }
public int? RoundId { get; }
public Round? Round { get; }
public Guid? PlayerUserId { get; }
public Player? Player { get; }
public TimeSpan PlaytimeAtNote { get; }
public string Message { get; }
public Player? CreatedBy { get; }
public DateTime CreatedAt { get; }
public Player? LastEditedBy { get; }
public DateTime? LastEditedAt { get; }
public DateTime? ExpirationTime { get; }
public bool Deleted { get; }
}
[Index(nameof(PlayerUserId))]
public class AdminNote
public class AdminNote : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
@@ -936,7 +1050,7 @@ namespace Content.Server.Database
}
[Index(nameof(PlayerUserId))]
public class AdminWatchlist
public class AdminWatchlist : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }
@@ -967,7 +1081,7 @@ namespace Content.Server.Database
}
[Index(nameof(PlayerUserId))]
public class AdminMessage
public class AdminMessage : IAdminRemarksCommon
{
[Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; }

View File

@@ -97,7 +97,7 @@ namespace Content.Server.Administration.Commands
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = startingGear.GetGear(slot.Name, profile);
var gearStr = startingGear.GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;

View File

@@ -24,6 +24,14 @@ public sealed partial class AdvertiseComponent : Component
[DataField]
public int MaximumWait { get; private set; } = 10 * 60;
/// <summary>
/// If true, the delay before the first advertisement (at MapInit) will ignore <see cref="MinimumWait"/>
/// and instead be rolled between 0 and <see cref="MaximumWait"/>. This only applies to the initial delay;
/// <see cref="MinimumWait"/> will be respected after that.
/// </summary>
[DataField]
public bool Prewarm = true;
/// <summary>
/// The identifier for the advertisements pack prototype.
/// </summary>

View File

@@ -37,13 +37,14 @@ public sealed class AdvertiseSystem : EntitySystem
private void OnMapInit(EntityUid uid, AdvertiseComponent advert, MapInitEvent args)
{
RandomizeNextAdvertTime(advert);
var prewarm = advert.Prewarm;
RandomizeNextAdvertTime(advert, prewarm);
_nextCheckTime = MathHelper.Min(advert.NextAdvertisementTime, _nextCheckTime);
}
private void RandomizeNextAdvertTime(AdvertiseComponent advert)
private void RandomizeNextAdvertTime(AdvertiseComponent advert, bool prewarm = false)
{
var minDuration = Math.Max(1, advert.MinimumWait);
var minDuration = prewarm ? 0 : Math.Max(1, advert.MinimumWait);
var maxDuration = Math.Max(minDuration, advert.MaximumWait);
var waitDuration = TimeSpan.FromSeconds(_random.Next(minDuration, maxDuration));

View File

@@ -12,6 +12,7 @@ namespace Content.Server.Atmos.EntitySystems
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public override void Initialize()
{
@@ -59,12 +60,14 @@ namespace Content.Server.Atmos.EntitySystems
var gridId = xform.GridUid;
var coords = xform.Coordinates;
var tilePos = grid.TileIndicesFor(coords);
var tilePos = _mapSystem.TileIndicesFor(gridId.Value, grid, coords);
// Update and invalidate new position.
airtight.LastPosition = (gridId.Value, tilePos);
InvalidatePosition(gridId.Value, tilePos);
var airtightEv = new AirtightChanged(uid, airtight, (gridId.Value, tilePos));
RaiseLocalEvent(uid, ref airtightEv, true);
}
private void OnAirtightReAnchor(EntityUid uid, AirtightComponent airtight, ref ReAnchorEvent args)
@@ -74,6 +77,9 @@ namespace Content.Server.Atmos.EntitySystems
// Update and invalidate new position.
airtight.LastPosition = (gridId, args.TilePos);
InvalidatePosition(gridId, args.TilePos);
var airtightEv = new AirtightChanged(uid, airtight, (gridId, args.TilePos));
RaiseLocalEvent(uid, ref airtightEv, true);
}
}
@@ -153,6 +159,5 @@ namespace Content.Server.Atmos.EntitySystems
}
[ByRefEvent]
public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight,
(EntityUid Grid, Vector2i Tile) Position);
public readonly record struct AirtightChanged(EntityUid Entity, AirtightComponent Airtight, (EntityUid Grid, Vector2i Tile) Position);
}

View File

@@ -132,7 +132,7 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
private void OnDisabledMessage(EntityUid uid, GasAnalyzerComponent component, GasAnalyzerDisableMessage message)
{
if (message.Session.AttachedEntity is not {Valid: true})
if (message.Session.AttachedEntity is not { Valid: true })
return;
DisableAnalyzer(uid, component);
}
@@ -169,7 +169,7 @@ namespace Content.Server.Atmos.EntitySystems
// Check if position is out of range => don't update and disable
if (!component.LastPosition.Value.InRange(EntityManager, _transform, userPos, SharedInteractionSystem.InteractionRange))
{
if(component.User is { } userId && component.Enabled)
if (component.User is { } userId && component.Enabled)
_popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
DisableAnalyzer(uid, component, component.User);
return false;
@@ -182,13 +182,13 @@ namespace Content.Server.Atmos.EntitySystems
var tileMixture = _atmo.GetContainingMixture(uid, true);
if (tileMixture != null)
{
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Pressure, tileMixture.Temperature,
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), tileMixture.Volume, tileMixture.Pressure, tileMixture.Temperature,
GenerateGasEntryArray(tileMixture)));
}
else
{
// No gases were found
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f));
gasMixList.Add(new GasMixEntry(Loc.GetString("gas-analyzer-window-environment-tab-label"), 0f, 0f, 0f));
}
var deviceFlipped = false;
@@ -209,8 +209,8 @@ namespace Content.Server.Atmos.EntitySystems
{
foreach (var mixes in ev.GasMixtures)
{
if(mixes.Value != null)
gasMixList.Add(new GasMixEntry(mixes.Key, mixes.Value.Pressure, mixes.Value.Temperature, GenerateGasEntryArray(mixes.Value)));
if (mixes.Item2 != null)
gasMixList.Add(new GasMixEntry(mixes.Item1, mixes.Item2.Volume, mixes.Item2.Pressure, mixes.Item2.Temperature, GenerateGasEntryArray(mixes.Item2)));
}
deviceFlipped = ev.DeviceFlipped;
@@ -223,7 +223,16 @@ namespace Content.Server.Atmos.EntitySystems
foreach (var pair in node.Nodes)
{
if (pair.Value is PipeNode pipeNode)
gasMixList.Add(new GasMixEntry(pair.Key, pipeNode.Air.Pressure, pipeNode.Air.Temperature, GenerateGasEntryArray(pipeNode.Air)));
{
// check if the volume is zero for some reason so we don't divide by zero
if (pipeNode.Air.Volume == 0f)
continue;
// only display the gas in the analyzed pipe element, not the whole system
var pipeAir = pipeNode.Air.Clone();
pipeAir.Multiply(pipeNode.Volume / pipeNode.Air.Volume);
pipeAir.Volume = pipeNode.Volume;
gasMixList.Add(new GasMixEntry(pair.Key, pipeAir.Volume, pipeAir.Pressure, pipeAir.Temperature, GenerateGasEntryArray(pipeAir)));
}
}
}
}
@@ -286,9 +295,9 @@ namespace Content.Server.Atmos.EntitySystems
public sealed class GasAnalyzerScanEvent : EntityEventArgs
{
/// <summary>
/// Key is the mix name (ex "pipe", "inlet", "filter"), value is the pipe direction and GasMixture. Add all mixes that should be reported when scanned.
/// The string is for the name (ex "pipe", "inlet", "filter"), GasMixture for the corresponding gas mix. Add all mixes that should be reported when scanned.
/// </summary>
public Dictionary<string, GasMixture?>? GasMixtures;
public List<(string, GasMixture?)>? GasMixtures;
/// <summary>
/// If the device is flipped. Flipped is defined as when the inline input is 90 degrees CW to the side input

View File

@@ -359,7 +359,8 @@ namespace Content.Server.Atmos.EntitySystems
/// </summary>
private void OnAnalyzed(EntityUid uid, GasTankComponent component, GasAnalyzerScanEvent args)
{
args.GasMixtures = new Dictionary<string, GasMixture?> { {Name(uid), component.Air} };
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), component.Air));
}
private void OnGasTankPrice(EntityUid uid, GasTankComponent component, ref PriceCalculationEvent args)

View File

@@ -73,7 +73,7 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
if (filter.FilteredGas.HasValue)
{
var filteredOut = new GasMixture() {Temperature = removed.Temperature};
var filteredOut = new GasMixture() { Temperature = removed.Temperature };
filteredOut.SetMoles(filter.FilteredGas.Value, removed.GetMoles(filter.FilteredGas.Value));
removed.SetMoles(filter.FilteredGas.Value, 0f);
@@ -180,17 +180,30 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
/// </summary>
private void OnFilterAnalyzed(EntityUid uid, GasFilterComponent component, GasAnalyzerScanEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
return;
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures ??= new Dictionary<string, GasMixture?>();
if(_nodeContainer.TryGetNode(nodeContainer, component.InletName, out PipeNode? inlet))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-inlet"), inlet.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.FilterName, out PipeNode? filterNode))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-filter"), filterNode.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.OutletName, out PipeNode? outlet))
args.GasMixtures.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, component.InletName, out PipeNode? inlet) && inlet.Air.Volume != 0f)
{
var inletAirLocal = inlet.Air.Clone();
inletAirLocal.Multiply(inlet.Volume / inlet.Air.Volume);
inletAirLocal.Volume = inlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-inlet"), inletAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.FilterName, out PipeNode? filterNode) && filterNode.Air.Volume != 0f)
{
var filterNodeAirLocal = filterNode.Air.Clone();
filterNodeAirLocal.Multiply(filterNode.Volume / filterNode.Air.Volume);
filterNodeAirLocal.Volume = filterNode.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-filter"), filterNodeAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f)
{
var outletAirLocal = outlet.Air.Clone();
outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume);
outletAirLocal.Volume = outlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal));
}
args.DeviceFlipped = inlet != null && filterNode != null && inlet.CurrentPipeDirection.ToDirection() == filterNode.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
}

View File

@@ -205,19 +205,31 @@ namespace Content.Server.Atmos.Piping.Trinary.EntitySystems
/// </summary>
private void OnMixerAnalyzed(EntityUid uid, GasMixerComponent component, GasAnalyzerScanEvent args)
{
if (!EntityManager.TryGetComponent(uid, out NodeContainerComponent? nodeContainer))
return;
args.GasMixtures ??= new List<(string, GasMixture?)>();
var gasMixDict = new Dictionary<string, GasMixture?>();
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, component.InletOneName, out PipeNode? inletOne) && inletOne.Air.Volume != 0f)
{
var inletOneAirLocal = inletOne.Air.Clone();
inletOneAirLocal.Multiply(inletOne.Volume / inletOne.Air.Volume);
inletOneAirLocal.Volume = inletOne.Volume;
args.GasMixtures.Add(($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOneAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.InletTwoName, out PipeNode? inletTwo) && inletTwo.Air.Volume != 0f)
{
var inletTwoAirLocal = inletTwo.Air.Clone();
inletTwoAirLocal.Multiply(inletTwo.Volume / inletTwo.Air.Volume);
inletTwoAirLocal.Volume = inletTwo.Volume;
args.GasMixtures.Add(($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwoAirLocal));
}
if (_nodeContainer.TryGetNode(uid, component.OutletName, out PipeNode? outlet) && outlet.Air.Volume != 0f)
{
var outletAirLocal = outlet.Air.Clone();
outletAirLocal.Multiply(outlet.Volume / outlet.Air.Volume);
outletAirLocal.Volume = outlet.Volume;
args.GasMixtures.Add((Loc.GetString("gas-analyzer-window-text-outlet"), outletAirLocal));
}
if(_nodeContainer.TryGetNode(nodeContainer, component.InletOneName, out PipeNode? inletOne))
gasMixDict.Add($"{inletOne.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletOne.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.InletTwoName, out PipeNode? inletTwo))
gasMixDict.Add($"{inletTwo.CurrentPipeDirection} {Loc.GetString("gas-analyzer-window-text-inlet")}", inletTwo.Air);
if(_nodeContainer.TryGetNode(nodeContainer, component.OutletName, out PipeNode? outlet))
gasMixDict.Add(Loc.GetString("gas-analyzer-window-text-outlet"), outlet.Air);
args.GasMixtures = gasMixDict;
args.DeviceFlipped = inletOne != null && inletTwo != null && inletOne.CurrentPipeDirection.ToDirection() == inletTwo.CurrentPipeDirection.ToDirection().GetClockwise90Degrees();
}
}

View File

@@ -303,9 +303,17 @@ public sealed class GasCanisterSystem : EntitySystem
/// <summary>
/// Returns the gas mixture for the gas analyzer
/// </summary>
private void OnAnalyzed(EntityUid uid, GasCanisterComponent component, GasAnalyzerScanEvent args)
private void OnAnalyzed(EntityUid uid, GasCanisterComponent canisterComponent, GasAnalyzerScanEvent args)
{
args.GasMixtures = new Dictionary<string, GasMixture?> { {Name(uid), component.Air} };
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), canisterComponent.Air));
// if a tank is inserted show it on the analyzer as well
if (canisterComponent.GasTankSlot.Item != null)
{
var tank = canisterComponent.GasTankSlot.Item.Value;
var tankComponent = Comp<GasTankComponent>(tank);
args.GasMixtures.Add((Name(tank), tankComponent.Air));
}
}
/// <summary>

View File

@@ -80,7 +80,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
}
var timeDelta = args.dt;
var timeDelta = args.dt;
var pressureDelta = timeDelta * vent.TargetPressureChange;
if (vent.PumpDirection == VentPumpDirection.Releasing && pipe.Air.Pressure > 0)
@@ -292,7 +292,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
/// </summary>
private void OnAnalyzed(EntityUid uid, GasVentPumpComponent component, GasAnalyzerScanEvent args)
{
var gasMixDict = new Dictionary<string, GasMixture?>();
args.GasMixtures ??= new List<(string, GasMixture?)>();
// these are both called pipe, above it switches using this so I duplicated that...?
var nodeName = component.PumpDirection switch
@@ -301,10 +301,14 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
VentPumpDirection.Siphoning => component.Outlet,
_ => throw new ArgumentOutOfRangeException()
};
if (_nodeContainer.TryGetNode(uid, nodeName, out PipeNode? pipe))
gasMixDict.Add(nodeName, pipe.Air);
args.GasMixtures = gasMixDict;
// multiply by volume fraction to make sure to send only the gas inside the analyzed pipe element, not the whole pipe system
if (_nodeContainer.TryGetNode(uid, nodeName, out PipeNode? pipe) && pipe.Air.Volume != 0f)
{
var pipeAirLocal = pipe.Air.Clone();
pipeAirLocal.Multiply(pipe.Volume / pipe.Air.Volume);
pipeAirLocal.Volume = pipe.Volume;
args.GasMixtures.Add((nodeName, pipeAirLocal));
}
}
private void OnWeldChanged(EntityUid uid, GasVentPumpComponent component, ref WeldableChangedEvent args)

View File

@@ -151,10 +151,8 @@ namespace Content.Server.Atmos.Portable
/// </summary>
private void OnScrubberAnalyzed(EntityUid uid, PortableScrubberComponent component, GasAnalyzerScanEvent args)
{
args.GasMixtures ??= new Dictionary<string, GasMixture?> { { Name(uid), component.Air } };
// If it's connected to a port, include the port side
if (_nodeContainer.TryGetNode(uid, component.PortName, out PipeNode? port))
args.GasMixtures.Add(component.PortName, port.Air);
args.GasMixtures ??= new List<(string, GasMixture?)>();
args.GasMixtures.Add((Name(uid), component.Air));
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.Mobs;
namespace Content.Server.Audio;

View File

@@ -0,0 +1,152 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Audio.Jukebox;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Components;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
using JukeboxComponent = Content.Shared.Audio.Jukebox.JukeboxComponent;
namespace Content.Server.Audio.Jukebox;
public sealed class JukeboxSystem : SharedJukeboxSystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<JukeboxComponent, JukeboxSelectedMessage>(OnJukeboxSelected);
SubscribeLocalEvent<JukeboxComponent, JukeboxPlayingMessage>(OnJukeboxPlay);
SubscribeLocalEvent<JukeboxComponent, JukeboxPauseMessage>(OnJukeboxPause);
SubscribeLocalEvent<JukeboxComponent, JukeboxStopMessage>(OnJukeboxStop);
SubscribeLocalEvent<JukeboxComponent, JukeboxSetTimeMessage>(OnJukeboxSetTime);
SubscribeLocalEvent<JukeboxComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<JukeboxComponent, ComponentShutdown>(OnComponentShutdown);
SubscribeLocalEvent<JukeboxComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnComponentInit(EntityUid uid, JukeboxComponent component, ComponentInit args)
{
if (HasComp<ApcPowerReceiverComponent>(uid))
{
TryUpdateVisualState(uid, component);
}
}
private void OnJukeboxPlay(EntityUid uid, JukeboxComponent component, ref JukeboxPlayingMessage args)
{
if (Exists(component.AudioStream))
{
Audio.SetState(component.AudioStream, AudioState.Playing);
}
else
{
component.AudioStream = Audio.Stop(component.AudioStream);
if (string.IsNullOrEmpty(component.SelectedSongId) ||
!_protoManager.TryIndex(component.SelectedSongId, out var jukeboxProto))
{
return;
}
component.AudioStream = Audio.PlayPvs(jukeboxProto.Path, uid, AudioParams.Default.WithMaxDistance(10f))?.Entity;
Dirty(uid, component);
}
}
private void OnJukeboxPause(Entity<JukeboxComponent> ent, ref JukeboxPauseMessage args)
{
Audio.SetState(ent.Comp.AudioStream, AudioState.Paused);
}
private void OnJukeboxSetTime(EntityUid uid, JukeboxComponent component, JukeboxSetTimeMessage args)
{
var offset = (args.Session.Channel.Ping * 1.5f) / 1000f;
Audio.SetPlaybackPosition(component.AudioStream, args.SongTime + offset);
}
private void OnPowerChanged(Entity<JukeboxComponent> entity, ref PowerChangedEvent args)
{
TryUpdateVisualState(entity);
if (!this.IsPowered(entity.Owner, EntityManager))
{
Stop(entity);
}
}
private void OnJukeboxStop(Entity<JukeboxComponent> entity, ref JukeboxStopMessage args)
{
Stop(entity);
}
private void Stop(Entity<JukeboxComponent> entity)
{
Audio.SetState(entity.Comp.AudioStream, AudioState.Stopped);
Dirty(entity);
}
private void OnJukeboxSelected(EntityUid uid, JukeboxComponent component, JukeboxSelectedMessage args)
{
if (!Audio.IsPlaying(component.AudioStream))
{
component.SelectedSongId = args.SongId;
DirectSetVisualState(uid, JukeboxVisualState.Select);
component.Selecting = true;
component.AudioStream = Audio.Stop(component.AudioStream);
}
Dirty(uid, component);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<JukeboxComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.Selecting)
{
comp.SelectAccumulator += frameTime;
if (comp.SelectAccumulator >= 0.5f)
{
comp.SelectAccumulator = 0f;
comp.Selecting = false;
TryUpdateVisualState(uid, comp);
}
}
}
}
private void OnComponentShutdown(EntityUid uid, JukeboxComponent component, ComponentShutdown args)
{
component.AudioStream = Audio.Stop(component.AudioStream);
}
private void DirectSetVisualState(EntityUid uid, JukeboxVisualState state)
{
_appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, state);
}
private void TryUpdateVisualState(EntityUid uid, JukeboxComponent? jukeboxComponent = null)
{
if (!Resolve(uid, ref jukeboxComponent))
return;
var finalState = JukeboxVisualState.On;
if (!this.IsPowered(uid, EntityManager))
{
finalState = JukeboxVisualState.Off;
}
_appearanceSystem.SetData(uid, JukeboxVisuals.VisualState, finalState);
}
}

View File

@@ -1,11 +1,16 @@
using System.Globalization;
using System.Linq;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.Inventory;
using Content.Server.Popups;
using Content.Server.Chat.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
using Content.Shared.StationRecords;
using Content.Shared.UserInterface;
using Content.Shared.Access.Systems;
using Content.Shared.Bed.Cryostorage;
@@ -32,6 +37,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly ClimbSystem _climb = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
@@ -40,6 +46,7 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly StationJobsSystem _stationJobs = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
@@ -163,26 +170,30 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
{
var comp = ent.Comp;
var cryostorageEnt = ent.Comp.Cryostorage;
var station = _station.GetOwningStation(ent);
var name = Name(ent.Owner);
if (!TryComp<CryostorageComponent>(cryostorageEnt, out var cryostorageComponent))
return;
// if we have a session, we use that to add back in all the job slots the player had.
if (userId != null)
{
foreach (var station in _station.GetStationsSet())
foreach (var uniqueStation in _station.GetStationsSet())
{
if (!TryComp<StationJobsComponent>(station, out var stationJobs))
if (!TryComp<StationJobsComponent>(uniqueStation, out var stationJobs))
continue;
if (!_stationJobs.TryGetPlayerJobs(station, userId.Value, out var jobs, stationJobs))
if (!_stationJobs.TryGetPlayerJobs(uniqueStation, userId.Value, out var jobs, stationJobs))
continue;
foreach (var job in jobs)
{
_stationJobs.TryAdjustJobSlot(station, job, 1, clamp: true);
_stationJobs.TryAdjustJobSlot(uniqueStation, job, 1, clamp: true);
}
_stationJobs.TryRemovePlayerJobs(station, userId.Value, stationJobs);
_stationJobs.TryRemovePlayerJobs(uniqueStation, userId.Value, stationJobs);
}
}
@@ -203,12 +214,36 @@ public sealed class CryostorageSystem : SharedCryostorageSystem
_gameTicker.OnGhostAttempt(mind.Value, false);
}
}
comp.AllowReEnteringBody = false;
_transform.SetParent(ent, PausedMap.Value);
cryostorageComponent.StoredPlayers.Add(ent);
Dirty(ent, comp);
UpdateCryostorageUIState((cryostorageEnt.Value, cryostorageComponent));
AdminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent):player} was entered into cryostorage inside of {ToPrettyString(cryostorageEnt.Value)}");
if (!TryComp<StationRecordsComponent>(station, out var stationRecords))
return;
var jobName = Loc.GetString("earlyleave-cryo-job-unknown");
var recordId = _stationRecords.GetRecordByName(station.Value, name);
if (recordId != null)
{
var key = new StationRecordKey(recordId.Value, station.Value);
if (_stationRecords.TryGetRecord<GeneralStationRecord>(key, out var entry, stationRecords))
jobName = entry.JobTitle;
_stationRecords.RemoveRecord(key, stationRecords);
}
_chatSystem.DispatchStationAnnouncement(station.Value,
Loc.GetString(
"earlyleave-cryo-announcement",
("character", name),
("job", CultureInfo.CurrentCulture.TextInfo.ToTitleCase(jobName))
), Loc.GetString("earlyleave-cryo-sender"),
playDefaultSound: false
);
}
private void HandleCryostorageReconnection(Entity<CryostorageContainedComponent> entity)

View File

@@ -185,9 +185,16 @@ namespace Content.Server.Cargo.Systems
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
_audio.PlayPvs(component.ConfirmSound, uid);
ConsolePopup(args.Session,
Loc.GetString("cargo-console-trade-station",
("destination", MetaData(tradeDestination.Value).EntityName)));
var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id");
var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id");
var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast",
("productName", Loc.GetString(order.ProductName)),
("orderAmount", order.OrderQuantity),
("approverName", approverName),
("approverJob", approverJob),
("cost", cost));
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
ConsolePopup(args.Session, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(tradeDestination.Value).EntityName)));
// Log order approval
_adminLogger.Add(LogType.Action, LogImpact.Low,
@@ -332,8 +339,7 @@ namespace Content.Server.Cargo.Systems
private static CargoOrderData GetOrderData(CargoConsoleAddOrderMessage args, CargoProductPrototype cargoProduct,
int id)
{
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Cost, args.Amount, args.Requester,
args.Reason);
return new CargoOrderData(id, cargoProduct.Product, cargoProduct.Name, cargoProduct.Cost, args.Amount, args.Requester, args.Reason);
}
public static int GetOutstandingOrderCount(StationCargoOrderDatabaseComponent component)
@@ -382,6 +388,7 @@ namespace Content.Server.Cargo.Systems
public bool AddAndApproveOrder(
EntityUid dbUid,
string spawnId,
string name,
int cost,
int qty,
string sender,
@@ -394,7 +401,7 @@ namespace Content.Server.Cargo.Systems
DebugTools.Assert(_protoMan.HasIndex<EntityPrototype>(spawnId));
// Make an order
var id = GenerateOrderId(component);
var order = new CargoOrderData(id, spawnId, cost, qty, sender, description);
var order = new CargoOrderData(id, spawnId, name, cost, qty, sender, description);
// Approve it now
order.SetApproverData(dest, sender);

View File

@@ -180,7 +180,7 @@ public sealed partial class CargoSystem
// We won't be able to fit the whole order on, so make one
// which represents the space we do have left:
var reducedOrder = new CargoOrderData(order.OrderId,
order.ProductId, order.Price, spaceRemaining, order.Requester, order.Reason);
order.ProductId, order.ProductName, order.Price, spaceRemaining, order.Requester, order.Reason);
orders.Add(reducedOrder);
}
else

View File

@@ -10,6 +10,7 @@ using Content.Server._White.Economy;
using Content.Server.GameTicking;
using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
@@ -46,6 +47,7 @@ public sealed partial class CargoSystem : SharedCargoSystem
[Dependency] private readonly StationSystem _station = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly RadioSystem _radio = default!;
[Dependency] private readonly BankCardSystem _bankCard = default!; // WD
[Dependency] private readonly IConfigurationManager _cfgManager = default!; // WD
[Dependency] private readonly IMapManager _mapManager = default!; // WD

View File

@@ -149,12 +149,12 @@ public sealed partial class SolutionContainerSystem : SharedSolutionContainerSys
var relation = new ContainedSolutionComponent() { Container = container.Owner, ContainerName = name };
AddComp(uid, relation);
MetaData.SetEntityName(uid, $"solution - {name}");
ContainerSystem.Insert(uid, container, force: true);
return (uid, solution, relation);
}
#region Event Handlers
private void OnMapInit(Entity<SolutionContainerManagerComponent> entity, ref MapInitEvent args)

View File

@@ -3,7 +3,7 @@ using Content.Server.StationRecords.Systems;
using Content.Shared.CriminalRecords;
using Content.Shared.Security;
using Content.Shared.StationRecords;
using Robust.Shared.Timing;
using Content.Server.GameTicking;
namespace Content.Server.CriminalRecords.Systems;
@@ -17,7 +17,7 @@ namespace Content.Server.CriminalRecords.Systems;
/// </summary>
public sealed class CriminalRecordsSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
public override void Initialize()
@@ -71,7 +71,7 @@ public sealed class CriminalRecordsSystem : EntitySystem
/// </summary>
public bool TryAddHistory(StationRecordKey key, string line)
{
var entry = new CrimeHistory(_timing.CurTime, line);
var entry = new CrimeHistory(_ticker.RoundDuration(), line);
return TryAddHistory(key, entry);
}

View File

@@ -123,6 +123,6 @@ public sealed record PlayerRecord(
IPAddress LastSeenAddress,
ImmutableArray<byte>? HWId);
public sealed record RoundRecord(int Id, DateTimeOffset StartDate, ServerRecord Server);
public sealed record RoundRecord(int Id, DateTimeOffset? StartDate, ServerRecord Server);
public sealed record ServerRecord(int Id, string Name);

View File

@@ -13,6 +13,8 @@ using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Preferences.Loadouts.Effects;
using Microsoft.EntityFrameworkCore;
using Robust.Shared.Enums;
using Robust.Shared.Network;
@@ -42,6 +44,10 @@ namespace Content.Server.Database
.Include(p => p.Profiles).ThenInclude(h => h.Jobs)
.Include(p => p.Profiles).ThenInclude(h => h.Antags)
.Include(p => p.Profiles).ThenInclude(h => h.Traits)
.Include(p => p.Profiles)
.ThenInclude(h => h.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSingleQuery()
.SingleOrDefaultAsync(p => p.UserId == userId.UserId);
@@ -90,6 +96,9 @@ namespace Content.Server.Database
.Include(p => p.Jobs)
.Include(p => p.Antags)
.Include(p => p.Traits)
.Include(p => p.Loadouts)
.ThenInclude(l => l.Groups)
.ThenInclude(group => group.Loadouts)
.AsSplitQuery()
.SingleOrDefault(h => h.Slot == slot);
@@ -183,13 +192,6 @@ namespace Content.Server.Database
sex = sexVal;
var bodyType = profile.BodyType;
var clothing = ClothingPreference.Jumpsuit;
if (Enum.TryParse<ClothingPreference>(profile.Clothing, true, out var clothingVal))
clothing = clothingVal;
var backpack = BackpackPreference.Backpack;
if (Enum.TryParse<BackpackPreference>(profile.Backpack, true, out var backpackVal))
backpack = backpackVal;
var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
@@ -217,6 +219,27 @@ namespace Content.Server.Database
}
}
var loadouts = new Dictionary<string, RoleLoadout>();
foreach (var role in profile.Loadouts)
{
var loadout = new RoleLoadout(role.RoleName);
foreach (var group in role.Groups)
{
var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
foreach (var profLoadout in group.Loadouts)
{
groupLoadouts.Add(new Loadout()
{
Prototype = profLoadout.LoadoutName,
});
}
}
loadouts[role.RoleName] = loadout;
}
return new HumanoidCharacterProfile(
profile.CharacterName,
profile.ClownName,
@@ -239,13 +262,12 @@ namespace Content.Server.Database
Color.FromHex(profile.SkinColor),
markings
),
clothing,
backpack,
spawnPriority,
jobs,
(PreferenceUnavailableMode) profile.PreferenceUnavailable,
antags.ToList(),
traits.ToList()
traits.ToList(),
loadouts
);
}
@@ -277,8 +299,6 @@ namespace Content.Server.Database
profile.FacialHairColor = appearance.FacialHairColor.ToHex();
profile.EyeColor = appearance.EyeColor.ToHex();
profile.SkinColor = appearance.SkinColor.ToHex();
profile.Clothing = humanoid.Clothing.ToString();
profile.Backpack = humanoid.Backpack.ToString();
profile.SpawnPriority = (int) humanoid.SpawnPriority;
profile.Markings = markings;
profile.Slot = slot;
@@ -304,6 +324,36 @@ namespace Content.Server.Database
.Select(t => new Trait { TraitName = t })
);
profile.Loadouts.Clear();
foreach (var (role, loadouts) in humanoid.Loadouts)
{
var dz = new ProfileRoleLoadout()
{
RoleName = role,
};
foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
{
var profileGroup = new ProfileLoadoutGroup()
{
GroupName = group,
};
foreach (var loadout in groupLoadouts)
{
profileGroup.Loadouts.Add(new ProfileLoadout()
{
LoadoutName = loadout.Prototype,
});
}
dz.Groups.Add(profileGroup);
}
profile.Loadouts.Add(dz);
}
return profile;
}
@@ -748,7 +798,7 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync(cancel);
}
public virtual async Task<int> AddNewRound(Server server, params Guid[] playerIds)
public async Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
await using var db = await GetDb();

View File

@@ -471,34 +471,6 @@ namespace Content.Server.Database
return (admins.Select(p => (p.a, p.LastSeenUserName)).ToArray(), adminRanks)!;
}
public override async Task<int> AddNewRound(Server server, params Guid[] playerIds)
{
await using var db = await GetDb();
var players = await db.DbContext.Player
.Where(player => playerIds.Contains(player.UserId))
.ToListAsync();
var nextId = 1;
if (await db.DbContext.Round.AnyAsync())
{
nextId = db.DbContext.Round.Max(round => round.Id) + 1;
}
var round = new Round
{
Id = nextId,
Players = players,
ServerId = server.Id
};
db.DbContext.Round.Add(round);
await db.DbContext.SaveChangesAsync();
return round.Id;
}
protected override IQueryable<AdminLog> StartAdminLogsQuery(ServerDbContext db, LogFilter? filter = null)
{
IQueryable<AdminLog> query = db.AdminLog;

View File

@@ -1,5 +1,4 @@
using Content.Shared.Body.Components;
using Content.Shared.Inventory;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@@ -17,12 +16,12 @@ public sealed partial class BurnBodyBehavior : IThresholdBehavior
var inventorySystem = system.EntityManager.System<InventorySystem>();
var sharedPopupSystem = system.EntityManager.System<SharedPopupSystem>();
if (system.EntityManager.TryGetComponent<InventoryComponent>(bodyId, out var comp))
if (!system.EntityManager.TryGetComponent<InventoryComponent>(bodyId, out var comp))
return;
foreach (var item in inventorySystem.GetHandOrInventoryEntities(bodyId))
{
foreach (var item in inventorySystem.GetHandOrInventoryEntities(bodyId))
{
transformSystem.DropNextTo(item, bodyId);
}
transformSystem.DropNextTo(item, bodyId);
}
sharedPopupSystem.PopupCoordinates(Loc.GetString("bodyburn-text-others", ("name", bodyId)), transformSystem.GetMoverCoordinates(bodyId), PopupType.LargeCaution);

View File

@@ -86,13 +86,13 @@ public sealed partial class ElectrifiedComponent : Component
[DataField("shockVolume")]
public float ShockVolume = 20;
[DataField]
public float Probability = 1f;
// WD EDIT START
[DataField]
public bool IgnoreInsulation;
public EntityUid? Caster;
// WD EDIT END
[DataField]
public float Probability = 1f;
}

View File

@@ -103,7 +103,7 @@ namespace Content.Server.Forensics
private void OnAfterInteract(EntityUid uid, CleansForensicsComponent component, AfterInteractEvent args)
{
if (args.Handled)
if (args.Handled || !args.CanReach)
return;
if (!TryComp<ForensicsComponent>(args.Target, out var forensicsComp))

View File

@@ -737,7 +737,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
_humanoid.LoadProfile(mob, profile);
var gear = _prototypeManager.Index(spawnDetails.GearProto);
_stationSpawning.EquipStartingGear(mob, gear, profile);
_stationSpawning.EquipStartingGear(mob, gear);
_npcFaction.RemoveFaction(mob, "NanoTrasen", false);
_npcFaction.AddFaction(mob, "Syndicate");

View File

@@ -249,7 +249,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
_mindSystem.TransferTo(newMind, mob);
var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
_stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
_stationSpawningSystem.EquipStartingGear(mob, pirateGear);
_npcFaction.RemoveFaction(mob, EnemyFactionId, false);
_npcFaction.AddFaction(mob, PirateFactionId);

View File

@@ -165,7 +165,7 @@ namespace Content.Server.Hands.Systems
continue;
}
QueueDel(hand.HeldEntity.Value);
TryDrop(args.PullerUid, hand, handsComp: component);
break;
}
}

View File

@@ -62,6 +62,7 @@ public class IdentitySystem : SharedIdentitySystem
{
var ident = Spawn(null, Transform(uid).Coordinates);
_metaData.SetEntityName(ident, "identity");
QueueIdentityUpdate(uid);
_container.Insert(ident, component.IdentityEntitySlot);
}

Some files were not shown because too many files have changed in this diff Show More